[
  {
    "path": ".gitattributes",
    "content": "* text=auto eol=lf\n*.sh text eol=lf\n*.bat text eol=crlf\n*.png binary\n*.xcf binary\n*.properties linguist-generated\n\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"gradle\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n  - package-ecosystem: github-actions\n    directory: /\n    schedule:\n      interval: \"daily\"\n"
  },
  {
    "path": ".gitignore",
    "content": ".gradle/\nbuild/\n.idea/*\n!.idea/codeStyles\n!.idea/inspectionProfiles\nlib/\ndev.properties\nextensions.txt\ndev_storage\nlocal/\nlocal*/\nlocal_*/\n.vs\n.vscode\nobj\nout\nbin\n.DS_Store\nComponentsGenerated.wxs\n!dist/javafx/**/lib\n!dist/javafx/**/bin\nxcuserdata/\n*.dylib\nproject.xcworkspace\ntranslations_patch\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement via [hello@xpipe.io](mailto:hello@xpipe.io).\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations."
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Development\n\nAny contribution is welcomed!\nThere are no real formal contribution guidelines right now, they will maybe come later.\n\n## Repository Structure\n\n- [core](core) - Shared core classes of the XPipe Java API, XPipe extensions, and the XPipe daemon implementation.\n  This mainly concerns API classes not a lot of implementation.\n- [beacon](beacon) - The XPipe beacon component is responsible for handling all communications between the XPipe\n  daemon and the client applications, for example APIs and the CLI\n- [app](app) - Contains the XPipe daemon implementation and the XPipe desktop application\n- [dist](dist) - Tools to create a distributable package of XPipe\n- [ext](ext) - Available XPipe extensions. Essentially every concrete feature implementation is implemented as an extension\n\n## Development Setup\n\nYou need to have JDK for Java 25 installed to compile the project.\nIf you are on Linux or macOS, you can easily accomplish that by using [SDKMAN](https://sdkman.io/) and running\n```bash\ncurl -s \"https://get.sdkman.io\" | bash\n. \"$HOME/.sdkman/bin/sdkman-init.sh\"\nsdk install java 25.0.2-graalce\nsdk default java 25.0.2-graalce\n```\n\nOn Windows, you have to manually install a JDK, e.g. from [Adoptium](https://adoptium.net/temurin/releases/?version=25).\n\nYou can configure a few development options in the file `app/dev.properties` which will be automatically generated when gradle is first run.\n\nYou need to have an up-to-date version of XPipe installed on your local system in order to properly\nrun XPipe in a development environment.\nThis is due to the fact that some components are only included in the release version and not in this repository.\nXPipe is able to automatically detect your local installation and fetch the required\ncomponents from it when it is run in a development environment.\n\nTo disable the local installation check, you can set the property `io.xpipe.app.locator.disableInstallationVersionCheck=false` in the file `app/dev.properties`. This allows it to start up even if there are version mismatches. Some things might not work as expected though. You can also use a local PTB installation instead of the stable release version by setting the property `io.xpipe.app.locator.usePtbInstallation=true` in the file `app/dev.properties`.\n\nNote that in case the current master branch is ahead of the latest release, it might happen that there are some incompatibilities when loading data from your local XPipe installation.\nYou should therefore always check out the matching version tag for your local repository and local XPipe installation.\nYou can find the available version tags at https://github.com/xpipe-io/xpipe/tags.\nSo for example if you currently have XPipe `21.0` installed, you should run `git reset --hard 21.0` first to properly compile against it.\n\n## Building and Running\n\nYou can use the gradle wrapper to build and run the project:\n- `gradlew app:run` will run the desktop application. You can set various useful properties in `app/build.gradle`\n- `gradlew clean dist` will create a distributable production version in `dist/build/dist/base`.\n- `gradlew <project>:test` will run the tests of the specified project.\n\nYou are also able to properly debug the built production application:\n- The `dist/build/dist/base/app/scripts/xpiped_debug` script will launch the application in debug mode and with a console attached to it\n\n## Modularity and IDEs\n\nAll XPipe components target [Java 25](https://openjdk.java.net/projects/jdk/25/) and make full use of the Java Module System (JPMS).\nAll components are modularized, including all their dependencies.\nIn case a dependency is (sadly) not modularized yet, module information is manually added using [extra-java-module-info](https://github.com/gradlex-org/extra-java-module-info).\nFurther, note that as this is a pretty complicated Java project that fully utilizes modularity,\nmany IDEs still have problems building this project properly.\n\nFor example, you can't build this project in eclipse or vscode as it will complain about missing modules.\nThe tested and recommended IDE is IntelliJ.\nWhen setting up the project in IntelliJ, make sure that the correct JDK (Java 25)\nis selected both for the project and for gradle itself.\n\n## Contributing guide\n\nEspecially when starting out, it might be a good idea to start with easy tasks first. Here's a selection of suitable common tasks that are very easy to implement:\n\n### Interacting via the HTTP API\n\nYou can create clients that communicate with the XPipe daemon via its HTTP API.\nTo get started, see the [OpenAPI spec](https://docs.xpipe.io/api).\n\n### Implementing support for a new editor\n\nAll code for handling external editors can be found [here](https://github.com/xpipe-io/xpipe/blob/master/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java). There you will find plenty of working examples that you can use as a base for your own implementation.\n\n### Implementing support for a new terminal\n\nAll code for handling external terminals can be found [here](https://github.com/xpipe-io/xpipe/blob/master/app/src/main/java/io/xpipe/app/terminal/). There you will find plenty of working examples that you can use as a base for your own implementation.\n\n### Adding more context menu actions in the file browser\n\nIn case you want to implement your own actions for certain file types in the file browser, you can easily do so. You can find most existing actions [here](https://github.com/xpipe-io/xpipe/tree/master/ext/base/src/main/java/io/xpipe/ext/base/browser) to get some inspiration.\nOnce you created your custom classes, you have to register them in your module info, just like [here](https://github.com/xpipe-io/xpipe/blob/master/ext/base/src/main/java/module-info.java).\n\n### Implementing custom actions for the connection hub\n\nAll actions that you can perform for certain connections in the connection overview tab are implemented using an [Action API](https://github.com/xpipe-io/xpipe/blob/master/app/src/main/java/io/xpipe/app/ext/ActionProvider.java). You can find a sample implementation [here](https://github.com/xpipe-io/xpipe/blob/master/ext/base/src/main/java/io/xpipe/ext/base/action/SampleAction.java) and many common action implementations [here](https://github.com/xpipe-io/xpipe/tree/master/ext/base/src/main/java/io/xpipe/ext/base/action).\n\n### Adding more predefined scripts\n\nYou can add custom script definitions [here](https://github.com/xpipe-io/xpipe/tree/master/ext/base/src/main/java/io/xpipe/ext/base/script/PredefinedScriptStore.java) and [here](https://github.com/xpipe-io/xpipe/tree/master/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts).\n\n### Adding more file icons for specific types\n\nYou can register file types [here](https://github.com/xpipe-io/xpipe/blob/master/app/src/main/resources/io/xpipe/app/resources/file_list.txt) and add the respective icons [here](https://github.com/xpipe-io/xpipe/tree/master/app/src/main/resources/io/xpipe/app/resources/img/browser).\n\nThe existing file list and icons are taken from the [vscode-icons](https://github.com/vscode-icons/vscode-icons) project. Due to limitations in the file definition list compatibility, some file types might not be listed by their proper extension and are therefore not being applied correctly even though the images and definitions exist already.\n\n### Implementing something else\n\nif you want to work on something that was not listed here, you can still do so of course. You can reach out on the [Discord server](https://discord.gg/8y89vS8cRb) to discuss any development plans and get you started.\n\n### Adding translations\n\nSee the [translation guide](/lang) for details.\n"
  },
  {
    "path": "LICENSE.md",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n   Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n   stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n   that You distribute, all copyright, patent, trademark, and\n   attribution notices from the Source form of the Work,\n   excluding those notices that do not pertain to any part of\n   the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n   distribution, then any Derivative Works that You distribute must\n   include a readable copy of the attribution notices contained\n   within such NOTICE file, excluding those notices that do not\n   pertain to any part of the Derivative Works, in at least one\n   of the following places: within a NOTICE text file distributed\n   as part of the Derivative Works; within the Source form or\n   documentation, if provided along with the Derivative Works; or,\n   within a display generated by the Derivative Works, if and\n   wherever such third-party notices normally appear. The contents\n   of the NOTICE file are for informational purposes only and\n   do not modify the License. You may add Your own attribution\n   notices within Derivative Works that You distribute, alongside\n   or as an addendum to the NOTICE text from the Work, provided\n   that such additional attribution notices cannot be construed\n   as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf\n   of any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\nCopyright 2023 Christopher Schnick\nCopyright 2023 XPipe UG (haftungsbeschränkt)\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n    <a href=\"https://xpipe.io\" target=\"_blank\" rel=\"noopener\">\n        <img src=\"https://github.com/xpipe-io/.github/raw/main/img/banner.png\" alt=\"XPipe Banner\" />\n    </a>\n</p>\n\n<h1></h1>\n\n## About\n\nXPipe is a connection hub that allows you to access your entire server infrastructure from your local desktop. It works on top of your installed command-line programs like SSH, docker, or others, and does not require any setup on your remote systems. It integrates with your favourite text editors, terminals, shells, VNC/RDP clients, password managers, and command-line tools. The platform is designed to be extensible, allowing anyone to add easily support for more tools or to implement custom functionality through a modular extension system.\n\nIt currently supports:\n\n- [SSH](https://docs.xpipe.io/guide/ssh) connections, config files, and tunnels\n- [Docker](https://docs.xpipe.io/guide/docker) + compose, [Podman](https://docs.xpipe.io/guide/podman), [LXD](https://docs.xpipe.io/guide/lxc), and [incus](https://docs.xpipe.io/guide/lxc) containers\n- [Proxmox PVE](https://docs.xpipe.io/guide/proxmox), [Hyper-V](https://docs.xpipe.io/guide/hyperv), [KVM](https://docs.xpipe.io/guide/kvm), and [VMware Player/Workstation/Fusion](https://docs.xpipe.io/guide/vmware) virtual machines\n- [Tailscale](https://docs.xpipe.io/guide/tailscale), [Netbird](https://docs.xpipe.io/guide/netbird), and [Teleport](https://docs.xpipe.io/guide/teleport) connections\n- [AWS](https://docs.xpipe.io/guide/aws) and [Hetzner Cloud](https://docs.xpipe.io/guide/hcloud) servers\n- [RDP](https://docs.xpipe.io/guide/rdp) and [VNC](https://docs.xpipe.io/guide/vnc) connections\n- Windows Subsystem for Linux, Cygwin, and MSYS2 environments\n- [Kubernetes](https://docs.xpipe.io/guide/kubernetes) clusters, pods, and containers\n- [Powershell Remote Sessions](https://docs.xpipe.io/guide/pssession)\n\n---\n\n<div align=\"center\">\n    <a href=\"https://docs.xpipe.io/guide/ssh\" target=\"_blank\" rel=\"noopener\">\n        <img src=\"https://github.com/xpipe-io/.github/raw/main/icons/ssh.svg\" width=40 height=40 />\n    </a>\n    <a href=\"#\"><img width=10 /></a>\n    <a href=\"https://docs.xpipe.io/guide/docker\" target=\"_blank\" rel=\"noopener\">\n        <img src=\"https://github.com/xpipe-io/.github/raw/main/icons/docker.svg\" width=40 height=40 />\n    </a>\n    <a href=\"#\"><img width=10 /></a>\n    <a href=\"https://docs.xpipe.io/guide/docker#compose\" target=\"_blank\" rel=\"noopener\">\n        <img src=\"https://github.com/xpipe-io/.github/raw/main/icons/compose.png\" width=40 height=40 />\n    </a>\n    <a href=\"#\"><img width=10 /></a>\n    <a href=\"https://docs.xpipe.io/guide/lxc\" target=\"_blank\" rel=\"noopener\">\n        <img src=\"https://github.com/xpipe-io/.github/raw/main/icons/lxd.svg\" width=40 height=40 />\n    </a>\n    <a href=\"#\"><img width=10 /></a>\n    <a href=\"https://docs.xpipe.io/guide/podman\" target=\"_blank\" rel=\"noopener\">\n        <img src=\"https://github.com/xpipe-io/.github/raw/main/icons/podman.svg\" width=40 height=40 />\n    </a>\n    <a href=\"#\"><img width=10 /></a>\n    <a href=\"https://docs.xpipe.io/guide/aws\" target=\"_blank\" rel=\"noopener\">\n        <img src=\"https://github.com/xpipe-io/.github/raw/main/icons/aws.svg\" width=40 height=40 />\n    </a>\n    <a href=\"#\"><img width=10 /></a>\n    <a href=\"https://docs.xpipe.io/guide/kubernetes\" target=\"_blank\" rel=\"noopener\">\n        <img src=\"https://github.com/xpipe-io/.github/raw/main/icons/k8s.svg\" width=40 height=40 />\n    </a>\n    <a href=\"#\"><img width=10 /></a>\n    <a href=\"https://docs.xpipe.io/guide/proxmox\" target=\"_blank\" rel=\"noopener\">\n        <img src=\"https://github.com/xpipe-io/.github/raw/main/icons/proxmox.svg\" width=40 height=40 />\n    </a>\n    <a href=\"#\"><img width=10 /></a>\n    <a href=\"https://docs.xpipe.io/guide/vmware\" target=\"_blank\" rel=\"noopener\">\n        <img src=\"https://github.com/xpipe-io/.github/raw/main/icons/vmware.svg\" width=40 height=40 />\n    </a>\n    <a href=\"#\"><img width=10 /></a>\n    <a href=\"https://docs.xpipe.io/guide/kvm\" target=\"_blank\" rel=\"noopener\">\n        <img src=\"https://github.com/xpipe-io/.github/raw/main/icons/virsh.png\" width=40 height=40 />\n    </a>\n    <a href=\"#\"><img width=10 /></a>\n    <a href=\"https://docs.xpipe.io/guide/tailscale\" target=\"_blank\" rel=\"noopener\">\n        <img src=\"https://github.com/xpipe-io/.github/raw/main/icons/tailscale.svg\" width=40 height=40 />\n    </a>\n    <a href=\"#\"><img width=10 /></a>\n    <a href=\"https://docs.xpipe.io/guide/netbird\" target=\"_blank\" rel=\"noopener\">\n        <img src=\"https://github.com/xpipe-io/.github/raw/main/icons/netbird.svg\" width=40 height=40 />\n    </a>\n    <a href=\"#\"><img width=10 /></a>\n    <a href=\"https://docs.xpipe.io/guide/hcloud\" target=\"_blank\" rel=\"noopener\">\n        <img src=\"https://github.com/xpipe-io/.github/raw/main/icons/hetzner.svg\" width=40 height=40 />\n    </a>\n    <a href=\"#\"><img width=10 /></a>\n    <a href=\"https://docs.xpipe.io/guide/teleport\" target=\"_blank\" rel=\"noopener\">\n        <img src=\"https://github.com/xpipe-io/.github/raw/main/icons/teleport.png\" width=40 height=40 />\n    </a>\n</div>\n\n## Connection hub\n\n- Easily establish and manage connections to remote systems from a central hub interface\n- Organize all your connections in hierarchical categories to maintain an overview over hundreds of connections.\n- Create custom shell login environments to instantly jump into a properly set up shell for every use case\n- Quickly perform various commonly used actions like starting/stopping systems, establishing tunnels, and more\n- Create desktop shortcuts and macros that automatically open remote connections in your terminal without having to open any GUI\n\n![Connection hub](https://github.com/xpipe-io/.github/raw/main/img/hub_shadow.png)\n\n## File browser\n\n- Interact with the file system of any remote system using a workflow optimized for professionals\n- Utilize your entire arsenal of locally installed programs to open and edit remote files\n- Dynamically elevate sessions with sudo when required without having to restart the session\n- Seamlessly transfer files from and to your system desktop environment\n- Work and perform transfers on multiple systems at the same time with the built-in tabbed multitasking\n- Quickly open a terminal session into any directory in your favourite terminal emulator\n- Customize every action through the scripting system\n\n![Browser](https://github.com/xpipe-io/.github/raw/main/img/browser_shadow.png)\n\n## Terminal launcher\n\n- Launches you into a shell session in your favourite terminal with one click. Automatically fills password prompts and more\n- Comes with support for all commonly used terminal emulators across all operating systems\n- Supports opening custom terminal emulators as well via a custom command-line spec\n- Works with all command shells such as bash, zsh, fish, cmd, PowerShell, and more, locally and remote\n- Integrates with multiplexers like tmux and zellij, plus prompts like starship and oh-my-zsh\n- Supports opening multiple sessions in split terminal pane views\n- Connects to a system while the terminal is still starting up, allowing for faster connections than otherwise possible\n\n![Terminal](https://github.com/xpipe-io/.github/raw/main/img/terminal_shadow.png)\n\n## Versatile scripting system\n\n- Create reusable simple shell scripts, templates, and groups to run on connected remote systems\n- Automatically make your scripts available in the PATH on any remote system without any setup\n- Setup shell init environments for connections to fully customize your work environment for every purpose\n- Open custom shells and custom remote connections by providing your own commands\n- Use custom scripts in the file browser \n\n![scripts](https://github.com/xpipe-io/.github/raw/main/img/scripts_shadow.png)\n\n## And much more\n\n- You can synchronize your vault across multiple systems and share it with other team members via your own self-hosted git repository\n- All data is stored exclusively on your systems in a cryptographically secure vault. You can also choose to increase security by using a custom master passphrase for further encryption\n- XPipe is able to retrieve secrets automatically from your installed password manager and doesn't have store secrets itself\n- There are no servers involved, all your information stays on your systems. The XPipe application does not send any personal or sensitive information to outside services\n- XPipe has an integrated MCP server that you can enable. This allows you to easily use all of XPipe's features from an AI agent\n- Run coherent desktop applications remotely via the uniform desktop application system in XPipe for RDP, VNC, and X11 forwards\n- Securely tunnel and automatically open remote services with one click with the services integration\n\n# Downloads\n\nNote that this is a desktop application that should be run on your local desktop workstation, not on any server or containers. It will be able to connect to your server infrastructure from there.\n\nFor a full reference and instructions, see the [installation docs](https://docs.xpipe.io/guide/installation) and [managed installation docs](https://docs.xpipe.io/guide/managed-installation).\n\n## Windows\n\nInstallers are the easiest way to get started and come with an optional automatic update functionality:\n\n- [Windows .msi Installer (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-windows-x86_64.msi)\n- [Windows .msi Installer (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-windows-arm64.msi)\n\nIf you don't like installers, you can also use a portable version that is packaged as an archive:\n\n- [Windows .zip Portable (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-windows-x86_64.zip)\n- [Windows .zip Portable (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-windows-arm64.zip)\n\nAlternatively, you can also use the following package managers:\n- [choco](https://community.chocolatey.org/packages/xpipe) to install it with `choco install xpipe`.\n- [winget](https://github.com/microsoft/winget-cli) to install it with `winget install xpipe-io.xpipe --source winget`.\n- [scoop](https://github.com/microsoft/winget-cli) to install it with `scoop install extras/xpipe`.\n\n## macOS\n\nInstallers are the easiest way to get started and come with an optional automatic update functionality:\n\n- [MacOS .pkg Installer (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-macos-x86_64.pkg)\n- [MacOS .pkg Installer (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-macos-arm64.pkg)\n\nIf you don't like installers, you can also use a portable version that is packaged as an archive:\n\n- [MacOS .dmg Portable (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-macos-x86_64.dmg)\n- [MacOS .dmg Portable (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-macos-arm64.dmg)\n\nAlternatively, you can also use [Homebrew](https://github.com/xpipe-io/homebrew-tap) to install XPipe with `brew install --cask xpipe-io/tap/xpipe`.\n\n## Linux\n\nYou can install XPipe the fastest by pasting the installation command into your terminal. This will perform the setup automatically.\nThe script supports installation via `apt`, `dnf`, `yum`, `zypper`, `rpm`, and `pacman` on Linux:\n\n```\nbash <(curl -sL https://github.com/xpipe-io/xpipe/raw/master/get-xpipe.sh)\n```\n\nOf course, there are also other installation methods available.\n\n### Debian-based distros\n\nThe following debian installers are available:\n\n- [Linux .deb Installer (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-linux-x86_64.deb)\n- [Linux .deb Installer (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-linux-arm64.deb)\n\nNote that you should use apt to install the package with `sudo apt install <file>` as other package managers, for example dpkg,\nare not able to resolve and install any dependency packages.\n\n### RHEL-based distros\n\nThe rpm releases are signed with the GPG key https://xpipe.io/signatures/crschnick.asc.\nYou can import it via `rpm --import https://xpipe.io/signatures/crschnick.asc` to allow your rpm-based package manager to verify the release signature. \n\nThe following rpm installers are available:\n\n- [Linux .rpm Installer (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-linux-x86_64.rpm)\n- [Linux .rpm Installer (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-linux-arm64.rpm)\n\n### Arch\n\nThere is an official [AUR package](https://aur.archlinux.org/packages/xpipe) available that you can either install manually or via an AUR helper such as with `yay -S xpipe`.\n\n### AppImages\n\nAlternatively, there are also AppImages available. These can be useful if you are using an immutable distro.\n\n- [Linux .AppImage Portable (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-linux-x86_64.AppImage)\n- [Linux .AppImage Portable (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-linux-arm64.AppImage)\n\n### NixOS\n\nThere's an official [xpipe nixpkg](https://search.nixos.org/packages?channel=unstable&show=xpipe&from=0&size=50&sort=relevance&type=packages&query=xpipe) available that you can install with `nix-env -iA nixos.xpipe` on x86_64 Linux systems. This package is however usually not up to date.\n\nThere is also a custom repository that contains the latest up-to-date release flakes for Linux and macOS systems: https://github.com/xpipe-io/nixpkg.\n\n### Tarball\n\nIn case you prefer to use an archive version that you can extract anywhere, you can use these:\n\n- [Linux .tar.gz Portable (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-linux-x86_64.tar.gz)\n- [Linux .tar.gz Portable (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-linux-arm64.tar.gz)\n\n### Docker container\n\nXPipe is a desktop application first and foremost. It requires a full desktop environment to function with various installed applications such as terminals, editors, shells, CLI tools, and more. So there is no true web-based interface for XPipe.\n\nSince it might make sense however to access your XPipe environment from the web, there is also a so-called webtop docker container image for XPipe. [XPipe Webtop](https://github.com/xpipe-io/xpipe-webtop) is a web-based desktop environment that can be run in a container and accessed from a browser via KasmVNC. The desktop environment comes with XPipe and various terminals and editors preinstalled and configured. This image is also available for Kasm Workspaces in the [XPipe Kasm Registry](https://github.com/xpipe-io/kasm-registry).\n\n# Further information\n\n## Contributing\n\nSee [CONTRIBUTING.md](/CONTRIBUTING.md) for details.\n\n<img src=\"https://contrib.rocks/image?repo=xpipe-io/xpipe\" alt=\"contrib.rocks image\" />\n\n## Open source model\n\nXPipe follows an open core model, which essentially means that the main application is open source while certain other components are not. This mainly concerns the features only available in the homelab/professional plan and the shell handling library implementation. Furthermore, some CI pipelines and tests that run on private servers are also not included in the open repository.\n\nThe distributed XPipe application consists out of two parts:\n- The open-source core that you can find this repository. It is licensed under the [Apache License 2.0](/LICENSE.md).\n- The closed-source extensions, mostly for homelab/professional plan features, which are not included in this repository\n\nAdditional features are available in the homelab/professional plan. For more details see https://xpipe.io/pricing.\nIf your enterprise puts great emphasis on having access to the full source code, there are also full source-available enterprise options available.\n\n## Documentation\n\nYou can find the documentation at https://docs.xpipe.io.\n\n## Discord\n\n[![Discord](https://discordapp.com/api/guilds/979695018782646285/widget.png?style=banner2)](https://discord.gg/8y89vS8cRb)\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security \n\nDue to its nature, XPipe has to handle a lot of sensitive information. Therefore, the security, integrity, and privacy of your data has topmost priority.\n\nMore information about the security approach of the XPipe application can be found on the documentation website at https://docs.xpipe.io/reference/security.\n\nYou can report security vulnerabilities in this GitHub repository in a confidential manner. We will get back to you as soon as possible if you do.\n"
  },
  {
    "path": "app/build.gradle",
    "content": "plugins {\n    id 'application'\n    id 'jvm-test-suite'\n    id 'java-library'\n}\n\nrepositories {\n    mavenCentral()\n}\n\napply from: \"$rootDir/gradle/gradle_scripts/java.gradle\"\napply from: \"$rootDir/gradle/gradle_scripts/javafx.gradle\"\napply from: \"$rootDir/gradle/gradle_scripts/jna.gradle\"\napply from: \"$rootDir/gradle/gradle_scripts/lombok.gradle\"\n\nconfigurations {\n    implementation.extendsFrom(javafx)\n    api.extendsFrom(jna)\n}\n\ndependencies {\n    api project(':core')\n    api project(':beacon')\n\n    compileOnly 'org.hamcrest:hamcrest:3.0'\n    compileOnly 'org.junit.jupiter:junit-jupiter-api:5.14.2'\n    compileOnly 'org.junit.jupiter:junit-jupiter-params:5.14.2'\n\n    api 'com.vladsch.flexmark:flexmark:0.64.8'\n    api 'com.vladsch.flexmark:flexmark-util:0.64.8'\n    api 'com.vladsch.flexmark:flexmark-util-options:0.64.8'\n    api 'com.vladsch.flexmark:flexmark-util-data:0.64.8'\n    api 'com.vladsch.flexmark:flexmark-util-ast:0.64.8'\n    api 'com.vladsch.flexmark:flexmark-util-builder:0.64.8'\n    api 'com.vladsch.flexmark:flexmark-util-sequence:0.64.8'\n    api 'com.vladsch.flexmark:flexmark-util-misc:0.64.8'\n    api 'com.vladsch.flexmark:flexmark-util-dependency:0.64.8'\n    api 'com.vladsch.flexmark:flexmark-util-collection:0.64.8'\n    api 'com.vladsch.flexmark:flexmark-util-format:0.64.8'\n    api 'com.vladsch.flexmark:flexmark-util-html:0.64.8'\n    api 'com.vladsch.flexmark:flexmark-util-visitor:0.64.8'\n    api 'com.vladsch.flexmark:flexmark-ext-tables:0.64.8'\n    api 'com.vladsch.flexmark:flexmark-ext-gfm-strikethrough:0.64.8'\n    api 'com.vladsch.flexmark:flexmark-ext-gfm-tasklist:0.64.8'\n    api 'com.vladsch.flexmark:flexmark-ext-footnotes:0.64.8'\n    api 'com.vladsch.flexmark:flexmark-ext-definition:0.64.8'\n    api 'com.vladsch.flexmark:flexmark-ext-anchorlink:0.64.8'\n    api 'com.vladsch.flexmark:flexmark-ext-yaml-front-matter:0.64.8'\n    api 'com.vladsch.flexmark:flexmark-ext-toc:0.64.8'\n\n    api ('io.modelcontextprotocol.sdk:mcp-core:0.17.2')  {\n        exclude group: \"com.ethlo.time\", module: \"itu\"\n    }\n    api ('io.modelcontextprotocol.sdk:mcp-json:0.17.2')  {\n        exclude group: \"com.ethlo.time\", module: \"itu\"\n    }\n    api ('io.modelcontextprotocol.sdk:mcp-json-jackson2:0.17.2')  {\n        exclude group: \"com.ethlo.time\", module: \"itu\"\n        exclude group: \"com.fasterxml.jackson.dataformat\", module: \"jackson-dataformat-yaml\"\n    }\n\n    api \"io.projectreactor:reactor-core:3.7.9\"\n    api \"org.reactivestreams:reactive-streams:1.0.4\"\n    api (\"com.networknt:json-schema-validator:1.5.8\") {\n        exclude group: \"com.ethlo.time\", module: \"itu\"\n        exclude group: \"com.fasterxml.jackson.dataformat\", module: \"jackson-dataformat-yaml\"\n    }\n\n    api \"com.github.weisj:jsvg:1.7.2\"\n    api 'io.xpipe:vernacular:1.16'\n    api 'org.bouncycastle:bcprov-jdk18on:1.83'\n    api 'info.picocli:picocli:4.7.7'\n    api 'org.apache.commons:commons-lang3:3.20.0'\n    api 'io.sentry:sentry:8.20.0'\n    api 'commons-io:commons-io:2.21.0'\n    api \"com.fasterxml.jackson.core:jackson-databind:2.21.0\"\n    api \"com.fasterxml.jackson.core:jackson-annotations:2.21\"\n    api \"com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.21.0\"\n    api \"org.kordamp.ikonli:ikonli-material2-pack:12.4.0\"\n    api \"org.kordamp.ikonli:ikonli-materialdesign2-pack:12.4.0\"\n    api 'org.kordamp.ikonli:ikonli-bootstrapicons-pack:12.4.0'\n    api \"org.kordamp.ikonli:ikonli-javafx:12.4.0\"\n    api \"org.slf4j:slf4j-api:2.0.17\"\n    api \"org.slf4j:slf4j-jdk-platform-logging:2.0.17\"\n    api 'io.xpipe:modulefs:0.1.8'\n    api 'net.synedra:validatorfx:0.4.2'\n    api files(\"$rootDir/gradle/gradle_scripts/atlantafx-base-2.0.2.jar\")\n\n    api(\"org.int4.fx:fx-values:0.4\")\n    api files(\"$rootDir/gradle/gradle_scripts/fx-builders-1.0.0-SNAPSHOT.jar\")\n}\n\napply from: \"$rootDir/gradle/gradle_scripts/local_junit_suite.gradle\"\n\ndef extensionJarDepList = project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)).toList()\njar {\n    finalizedBy(extensionJarDepList)\n}\n\napplication {\n    mainModule = groupName + '.app'\n    mainClass = groupName + '.app.Main'\n    applicationDefaultJvmArgs = jvmRunArgs\n}\n\nrun {\n    systemProperty propertyName('useVirtualThreads'), 'false'\n    systemProperty propertyName('mode'), 'gui'\n    systemProperty propertyName('writeLogs'), \"true\"\n    systemProperty propertyName('writeSysOut'), \"true\"\n    systemProperty propertyName('developerMode'), \"true\"\n    systemProperty propertyName('logLevel'), \"trace\"\n    systemProperty propertyName('fullVersion'), fullVersion\n    systemProperty propertyName('staging'), isStage\n\n    // Apply passed xpipe properties\n    for (final def e in System.getProperties().entrySet()) {\n        if (e.getKey().toString().contains(snakeProductName)) {\n            systemProperty e.getKey().toString(), e.getValue()\n        }\n    }\n\n    workingDir = rootDir\n    jvmArgs += ['-XX:+EnableDynamicAgentLoading']\n\n    def exts = files(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0].outputs.files.singleFile).toList())\n    classpath += exts\n\n    dependsOn(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0]).toList())\n}\n\ntasks.register('runAttachedDebugger', JavaExec) {\n    workingDir = rootDir\n    classpath = run.classpath\n    mainModule = groupName + '.app'\n    mainClass = groupName + '.app.Main'\n    modularity.inferModulePath = true\n    jvmArgs += jvmRunArgs\n    jvmArgs += List.of(\n            \"-javaagent:${System.getProperty(\"user.home\")}/.attachme/attachme-agent-1.2.9.jar=port:7857,host:localhost\".toString(),\n            \"-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0\"\n    )\n    jvmArgs += ['-XX:+EnableDynamicAgentLoading']\n    systemProperties run.systemProperties\n\n    def exts = files(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0].outputs.files.singleFile).toList())\n    classpath += exts\n    dependsOn(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0]).toList())\n\n}\n\nprocessResources {\n    doLast {\n        def cssFiles = fileTree(dir: \"$sourceSets.main.output.resourcesDir/io/xpipe/app/resources/style\")\n        cssFiles.include \"**/*.css\"\n        cssFiles.each { css ->\n            providers.javaexec {\n                workingDir = projectDir\n                jvmArgs += [\"--module-path=${configurations.javafx.asFileTree.asPath},\", \"--add-modules=javafx.graphics\"]\n                mainClass = \"com.sun.javafx.css.parser.Css2Bin\"\n                args css\n            }.result.get()\n\n            delete css\n        }\n    }\n\n    doLast {\n        def resourcesDir = new File(sourceSets.main.output.resourcesDir, \"io/xpipe/app/resources/third-party\")\n        resourcesDir.mkdirs()\n        copy {\n            from \"$rootDir/dist/licenses\"\n            into resourcesDir\n        }\n    }\n}\n\ndistTar {\n    enabled = false\n}\n\ndistZip {\n    enabled = false\n}\n\nassembleDist {\n    enabled = false\n}"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/Main.java",
    "content": "package io.xpipe.app;\n\nimport io.xpipe.app.core.AppNames;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.core.mode.AppOperationMode;\n\npublic class Main {\n\n    static void main(String[] args) {\n        if (args.length == 1 && args[0].equals(\"version\")) {\n            AppProperties.init(args);\n            System.out.println(AppProperties.get().getVersion());\n            return;\n        }\n\n        // Since this is not marked as a console application, it will not print anything when you run it in a console on\n        // Windows\n        if (args.length == 1 && args[0].equals(\"--help\")) {\n            System.out.printf(\"\"\"\n                              The daemon executable %s does not accept any command-line arguments.\n\n                              For a reference on how to use xpipe from the command-line, take a look at https://docs.xpipe.io/cli.\n                              %n\"\"\", AppNames.ofCurrent().getExecutableName());\n            return;\n        }\n\n        AppOperationMode.init(args);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/action/AbstractAction.java",
    "content": "package io.xpipe.app.action;\n\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.util.DataStoreFormatter;\nimport io.xpipe.app.util.LicensedFeature;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport lombok.experimental.SuperBuilder;\n\nimport java.util.*;\nimport java.util.function.Consumer;\n\n@SuperBuilder\npublic abstract class AbstractAction {\n\n    private static final Set<AbstractAction> active = new HashSet<>();\n    private static boolean closed;\n    private static Consumer<AbstractAction> pick;\n\n    public static synchronized void expectPick() {\n        if (pick != null) {\n            return;\n        }\n\n        var show = !AppCache.getBoolean(\"pickIntroductionShown\", false);\n        if (show) {\n            var modal = ModalOverlay.of(\"actionPickerTitle\", AppDialog.dialogTextKey(\"actionPickerDescription\"));\n            modal.addButton(ModalButton.ok());\n            modal.showAndWait();\n            AppCache.update(\"pickIntroductionShown\", true);\n        }\n\n        AppLayoutModel.get().getQueueEntries().add(queueEntry);\n        pick = action -> {\n            if (action instanceof SerializableAction) {\n                cancelPick();\n                var modal = ModalOverlay.of(\"actionShortcuts\", new ActionPickComp(action).prefWidth(600));\n                modal.show();\n            }\n        };\n    }\n\n    private static final AppLayoutModel.QueueEntry queueEntry = new AppLayoutModel.QueueEntry(\n            AppI18n.observable(\"cancelActionPicker\"), new LabelGraphic.IconGraphic(\"mdal-cancel_presentation\"), () -> {\n                cancelPick();\n                return true;\n            });\n\n    public static synchronized void cancelPick() {\n        AppLayoutModel.get().getQueueEntries().remove(queueEntry);\n        pick = null;\n    }\n\n    public static void reset() {\n        closed = true;\n        for (int i = 50; i > 0; i--) {\n            synchronized (active) {\n                var count = active.size();\n                if (count == 0) {\n                    break;\n                }\n            }\n\n            // Wait 5s max\n            ThreadHelper.sleep(100);\n        }\n\n        synchronized (active) {\n            for (AbstractAction abstractAction : active) {\n                TrackEvent.info(\"Action has not quit after timeout: \" + abstractAction.toString());\n            }\n        }\n    }\n\n    public boolean executeSync() {\n        if (closed) {\n            return false;\n        }\n\n        synchronized (AbstractAction.class) {\n            if (pick != null) {\n                TrackEvent.withTrace(\"Picked action\").tags(toDisplayMap()).handle();\n                pick.accept(this);\n                pick = null;\n                return false;\n            }\n        }\n\n        return executeSyncImpl(true);\n    }\n\n    public void executeAsync() {\n        if (closed) {\n            return;\n        }\n\n        synchronized (AbstractAction.class) {\n            if (pick != null) {\n                TrackEvent.withTrace(\"Picked action\").tags(toDisplayMap()).handle();\n                pick.accept(this);\n                pick = null;\n                return;\n            }\n        }\n\n        ThreadHelper.runAsync(() -> {\n            executeSyncImpl(true);\n        });\n    }\n\n    public boolean executeSyncImpl(boolean confirm) {\n        if (confirm && !ActionConfirmation.confirmAction(this)) {\n            return false;\n        }\n\n        if (closed) {\n            return false;\n        }\n\n        checkLicense();\n\n        synchronized (active) {\n            active.add(this);\n        }\n\n        TrackEvent.withTrace(\"Starting action execution\").tags(toDisplayMap()).handle();\n\n        try {\n            beforeExecute();\n        } catch (Throwable t) {\n            ErrorEventFactory.fromThrowable(t).handle();\n            return false;\n        }\n\n        try {\n            executeImpl();\n            return true;\n        } catch (Throwable t) {\n            ErrorEventFactory.fromThrowable(t).handle();\n            return false;\n        } finally {\n            afterExecute();\n            synchronized (active) {\n                active.remove(this);\n            }\n        }\n    }\n\n    public String getId() {\n        return getProvider().getId();\n    }\n\n    public String getDisplayName() {\n        var id = getId();\n        return id != null ? DataStoreFormatter.camelCaseToName(id) : \"?\";\n    }\n\n    public ActionProvider getProvider() {\n        var clazz = getClass();\n        var enc = clazz.getEnclosingClass();\n        if (enc == null) {\n            throw new IllegalStateException(\"No enclosing instance of \" + clazz);\n        }\n        return ActionProvider.ALL.stream()\n                .filter(actionProvider -> actionProvider.getClass().equals(enc))\n                .findFirst()\n                .orElseThrow(IllegalStateException::new);\n    }\n\n    public String getShortcutName() {\n        return getDisplayName();\n    }\n\n    public abstract void executeImpl() throws Exception;\n\n    protected void beforeExecute() throws Exception {}\n\n    public boolean isMutation() {\n        return false;\n    }\n\n    public boolean forceConfirmation() {\n        return false;\n    }\n\n    public LicensedFeature getLicensedFeature() {\n        return null;\n    }\n\n    protected void checkLicense() {\n        var feature = getLicensedFeature();\n        if (feature != null) {\n            feature.throwIfUnsupported();\n        }\n    }\n\n    protected void afterExecute() {}\n\n    public abstract Map<String, String> toDisplayMap();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/action/ActionConfigComp.java",
    "content": "package io.xpipe.app.action;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.hub.action.BatchStoreAction;\nimport io.xpipe.app.hub.action.MultiStoreAction;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.comp.StoreChoiceComp;\nimport io.xpipe.app.hub.comp.StoreListChoiceComp;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.property.*;\nimport javafx.collections.FXCollections;\nimport javafx.scene.layout.Region;\n\npublic class ActionConfigComp extends SimpleRegionBuilder {\n\n    private final Property<AbstractAction> action;\n\n    public ActionConfigComp(Property<AbstractAction> action) {\n        this.action = action;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var options = new OptionsBuilder();\n        options.nameAndDescription(\"actionStore\")\n                .addComp(createChooser())\n                .nameAndDescription(\"actionStores\")\n                .addComp(createMultiChooser());\n        options.nameAndDescription(\"actionConfiguration\").addComp(createTextArea());\n        return options.build();\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private BaseRegionBuilder<?, ?> createMultiChooser() {\n        var listProp = new SimpleListProperty<DataStoreEntryRef<DataStore>>(FXCollections.observableArrayList());\n        if (action.getValue() instanceof BatchStoreAction<?> ba) {\n            listProp.setAll(((BatchStoreAction<DataStore>) ba).getRefs());\n        } else if (action.getValue() instanceof MultiStoreAction<?> ma) {\n            listProp.setAll(((MultiStoreAction<DataStore>) ma).getRefs());\n        } else {\n            listProp.clear();\n        }\n\n        listProp.addListener((obs, o, n) -> {\n            if (action.getValue() instanceof BatchStoreAction<?> ba) {\n                action.setValue(((BatchStoreAction<DataStore>) ba).withRefs(n));\n            } else if (action.getValue() instanceof MultiStoreAction<?> ma) {\n                action.setValue(((MultiStoreAction<DataStore>) ma).withRefs(n));\n            }\n        });\n\n        var choice = new StoreListChoiceComp<>(\n                listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory());\n        choice.hide(listProp.emptyProperty());\n        choice.maxHeight(450);\n        return choice;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private BaseRegionBuilder<?, ?> createChooser() {\n        var singleProp = new SimpleObjectProperty<DataStoreEntryRef<DataStore>>();\n        var s = action.getValue() instanceof StoreAction<?> sa ? sa.getRef() : null;\n        singleProp.set((DataStoreEntryRef<DataStore>) s);\n\n        singleProp.addListener((obs, o, n) -> {\n            if (action.getValue() instanceof StoreAction<?> sa && n != null) {\n                action.setValue(sa.withRef(n.asNeeded()));\n            }\n        });\n\n        var choice = new StoreChoiceComp<>(\n                null,\n                singleProp,\n                DataStore.class,\n                ref -> true,\n                StoreViewState.get().getAllConnectionsCategory());\n        choice.hide(singleProp.isNull());\n        return choice;\n    }\n\n    private BaseRegionBuilder<?, ?> createTextArea() {\n        var config = new SimpleStringProperty();\n        var s = action.getValue() instanceof SerializableAction sa ? sa.toConfigNode() : null;\n        config.set(s != null && s.size() > 0 ? s.toPrettyString() : null);\n\n        config.addListener((obs, o, n) -> {\n            if (action.getValue() instanceof SerializableAction aa && n != null) {\n                var with = aa.withConfigString(n);\n                if (with.isPresent()) {\n                    action.setValue(with.get());\n                }\n            }\n        });\n\n        var area = new IntegratedTextAreaComp(config, false, \"action\", new SimpleStringProperty(\"json\"));\n        area.hide(config.isNull());\n        return area;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/action/ActionConfirmComp.java",
    "content": "package io.xpipe.app.action;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.ScrollComp;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.hub.action.BatchStoreAction;\nimport io.xpipe.app.hub.action.MultiStoreAction;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.comp.StoreListChoiceComp;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.property.SimpleListProperty;\nimport javafx.collections.FXCollections;\nimport javafx.scene.control.Label;\nimport javafx.scene.layout.ColumnConstraints;\nimport javafx.scene.layout.GridPane;\nimport javafx.scene.layout.Region;\n\nimport java.util.List;\nimport java.util.Map;\n\npublic class ActionConfirmComp extends SimpleRegionBuilder {\n\n    private final AbstractAction action;\n\n    public ActionConfirmComp(AbstractAction action) {\n        this.action = action;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var options = new OptionsBuilder();\n        var plural = action instanceof BatchStoreAction<?> || action instanceof MultiStoreAction<?>;\n        options.nameAndDescription(plural ? \"actionConnections\" : \"actionConnection\")\n                .addComp(createList());\n        options.nameAndDescription(\"actionConfiguration\").addComp(createTable());\n        var scroll = new ScrollComp(options.buildComp());\n        return scroll.build();\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private BaseRegionBuilder<?, ?> createList() {\n        var listProp = new SimpleListProperty<DataStoreEntryRef<DataStore>>(FXCollections.observableArrayList());\n        if (action instanceof BatchStoreAction<?> ba) {\n            listProp.setAll(((BatchStoreAction<DataStore>) ba).getRefs());\n        } else if (action instanceof MultiStoreAction<?> ma) {\n            listProp.setAll(((MultiStoreAction<DataStore>) ma).getRefs());\n        } else if (action instanceof StoreAction<?> sa) {\n            listProp.setAll(List.of(sa.getRef().asNeeded()));\n        }\n\n        var choice = new StoreListChoiceComp<>(\n                listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory());\n        choice.maxHeight(450);\n        choice.setEditable(false);\n        choice.hide(listProp.emptyProperty());\n        return choice;\n    }\n\n    private BaseRegionBuilder<?, ?> createTable() {\n        var map = action.toDisplayMap();\n        return RegionBuilder.of(() -> {\n            var grid = new GridPane();\n            grid.setHgap(11);\n            grid.setVgap(2);\n            grid.getColumnConstraints().add(new ColumnConstraints(120, 120, 150));\n            var row = 0;\n            for (Map.Entry<String, String> e : map.entrySet()) {\n                var name = new Label(e.getKey());\n                var value = new Label(e.getValue());\n                value.setWrapText(true);\n                grid.add(name, 0, row);\n                grid.add(value, 1, row);\n                row++;\n            }\n            return grid;\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/action/ActionConfirmation.java",
    "content": "package io.xpipe.app.action;\n\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\n\nimport javafx.beans.property.SimpleBooleanProperty;\n\nimport java.util.List;\n\npublic class ActionConfirmation {\n\n    public static boolean confirmAction(AbstractAction action) {\n        if (!action.forceConfirmation() && (!action.isMutation() || !confirmAllModifications(action))) {\n            return true;\n        }\n\n        var ok = new SimpleBooleanProperty(false);\n        var modal = ModalOverlay.of(\"confirmAction\", new ActionConfirmComp(action).prefWidth(550));\n        modal.addButton(ModalButton.cancel());\n        modal.addButton(ModalButton.ok(() -> ok.set(true)));\n        modal.showAndWait();\n        return ok.get();\n    }\n\n    private static boolean confirmAllModifications(AbstractAction action) {\n        var context = getContext(action);\n        return context.stream().anyMatch(dataStoreEntry -> {\n            var config = DataStorage.get().getEffectiveCategoryConfig(dataStoreEntry);\n            return config.getConfirmAllModifications() != null && config.getConfirmAllModifications();\n        });\n    }\n\n    private static List<DataStoreEntry> getContext(AbstractAction action) {\n        if (action instanceof StoreContextAction ca) {\n            return ca.getStoreEntryContext();\n        }\n\n        return List.of();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/action/ActionJacksonMapper.java",
    "content": "package io.xpipe.app.action;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.hub.action.*;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.core.JacksonMapper;\nimport io.xpipe.core.UuidHelper;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.DeserializationFeature;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.JsonNodeFactory;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\n\nimport java.util.ArrayList;\n\npublic class ActionJacksonMapper {\n\n    @SuppressWarnings(\"unchecked\")\n    public static <T extends AbstractAction> T parse(JsonNode tree) throws JsonProcessingException {\n        if (!tree.isObject()) {\n            return null;\n        }\n\n        var id = tree.get(\"id\");\n        if (id == null || !id.isTextual()) {\n            return null;\n        }\n\n        var provider = ActionProvider.ALL.stream()\n                .filter(actionProvider -> id.textValue().equals(actionProvider.getId()))\n                .findFirst();\n        if (provider.isEmpty()) {\n            return null;\n        }\n\n        var clazz = provider.get().getActionClass();\n        if (clazz.isEmpty()) {\n            return null;\n        }\n\n        var object = (ObjectNode) tree;\n        var ref = tree.get(\"ref\");\n\n        var mapper = JacksonMapper.newMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);\n\n        if (ref != null && !ref.isArray() && StoreAction.class.isAssignableFrom(clazz.get())) {\n            validateRef(provider.get(), ref.asText());\n            var action = mapper.treeToValue(tree, clazz.get());\n            return (T) action;\n        }\n\n        var makeBatch = ref != null && ref.isArray() && !MultiStoreAction.class.isAssignableFrom(clazz.get());\n        if (makeBatch) {\n            if (ref.size() == 0) {\n                return null;\n            }\n\n            var batchActions = new ArrayList<StoreAction<DataStore>>();\n            object.remove(\"ref\");\n            for (JsonNode batchRef : ref) {\n                validateRef(provider.get(), batchRef.asText());\n                object.set(\"ref\", batchRef);\n                var action = mapper.treeToValue(object, clazz.get());\n                batchActions.add((StoreAction<DataStore>) action);\n            }\n            return (T) BatchStoreAction.builder().actions(batchActions).build();\n        }\n\n        var makeMulti = ref != null && ref.isArray() && MultiStoreAction.class.isAssignableFrom(clazz.get());\n        if (makeMulti) {\n            validateRef(provider.get(), ref.asText());\n            object.remove(\"ref\");\n            object.set(\"refs\", ref);\n            var action = mapper.treeToValue(object, clazz.get());\n            return (T) action;\n        }\n\n        return null;\n    }\n\n    private static void validateRef(ActionProvider provider, String ref) {\n        var uuid = UuidHelper.parse(ref);\n        if (uuid.isEmpty()) {\n            throw ErrorEventFactory.expected(new IllegalArgumentException(\"Invalid store id: \" + ref));\n        }\n\n        var entry = DataStorage.get().getStoreEntryIfPresent(uuid.get());\n        if (entry.isEmpty()) {\n            throw ErrorEventFactory.expected(new IllegalArgumentException(\"Store not found for id: \" + ref));\n        }\n\n        if (!entry.get().getValidity().isUsable()) {\n            throw ErrorEventFactory.expected(new IllegalArgumentException(\n                    \"Store \" + DataStorage.get().getStorePath(entry.get()) + \" is incomplete\"));\n        }\n\n        if (provider instanceof HubLeafProvider<?> l\n                && (!l.getApplicableClass()\n                                .isAssignableFrom(entry.get().getStore().getClass())\n                        || !l.isApplicable(entry.get().ref()))) {\n            throw ErrorEventFactory.expected(new IllegalArgumentException(\n                    \"Store \" + DataStorage.get().getStorePath(entry.get()) + \" is not applicable for action type\"));\n        }\n\n        if (provider instanceof BatchHubProvider<?> h\n                && (!h.getApplicableClass()\n                                .isAssignableFrom(entry.get().getStore().getClass())\n                        || !h.isActive(entry.get().ref())\n                        || !h.isApplicable(entry.get().ref()))) {\n            throw ErrorEventFactory.expected(new IllegalArgumentException(\n                    \"Store \" + DataStorage.get().getStorePath(entry.get()) + \" is not applicable for action type\"));\n        }\n    }\n\n    public static ObjectNode write(AbstractAction value) {\n        if (value instanceof BatchStoreAction<?> b) {\n            var arrayNode = JsonNodeFactory.instance.arrayNode();\n            b.getActions().stream()\n                    .map(a -> {\n                        var tree = (ObjectNode) JacksonMapper.getDefault().valueToTree(a);\n                        return tree.get(\"ref\");\n                    })\n                    .forEach(n -> arrayNode.add(n));\n            var tree = (ObjectNode)\n                    JacksonMapper.getDefault().valueToTree(b.getActions().getFirst());\n            tree.set(\"ref\", arrayNode);\n            tree.put(\"id\", b.getActions().getFirst().getId());\n            return tree;\n        }\n\n        var tree = (ObjectNode) JacksonMapper.getDefault().valueToTree(value);\n        var treeCopy = JsonNodeFactory.instance.objectNode();\n        treeCopy.put(\"id\", value.getId());\n        tree.properties().forEach(p -> {\n            treeCopy.set(p.getKey(), p.getValue());\n        });\n\n        if (value instanceof MultiStoreAction<?> m) {\n            var refs = treeCopy.get(\"refs\");\n            treeCopy.remove(\"refs\");\n            treeCopy.set(\"ref\", refs);\n            treeCopy.put(\"id\", m.getId());\n            return treeCopy;\n        }\n\n        return treeCopy;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/action/ActionPickComp.java",
    "content": "package io.xpipe.app.action;\n\nimport io.xpipe.app.comp.base.ModalOverlayContentComp;\nimport io.xpipe.app.comp.base.ScrollComp;\nimport io.xpipe.app.platform.OptionsBuilder;\n\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.scene.layout.Region;\n\npublic class ActionPickComp extends ModalOverlayContentComp {\n\n    private final AbstractAction action;\n\n    public ActionPickComp(AbstractAction action) {\n        this.action = action;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var prop = new SimpleObjectProperty<>(action);\n        var top = new ActionConfigComp(prop);\n        var bottom = new ActionShortcutComp(prop, () -> {\n            getModalOverlay().close();\n        });\n        var options = new OptionsBuilder().addComp(top).addComp(bottom);\n        var scroll = new ScrollComp(options.buildComp());\n        return scroll.build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/action/ActionProvider.java",
    "content": "package io.xpipe.app.action;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.core.ModuleLayerLoader;\n\nimport java.util.*;\n\npublic interface ActionProvider {\n\n    List<ActionProvider> ALL = new ArrayList<>();\n\n    static void initProviders() {\n        TrackEvent.trace(\"Starting action provider initialization\");\n        for (ActionProvider actionProvider : ALL) {\n            try {\n                actionProvider.init();\n\n                // For debugging\n                //                if (actionProvider instanceof HubLeafProvider<?>) {\n                //                    actionProvider.getActionClass().orElseThrow();\n                //                }\n                //                if (actionProvider instanceof HubBranchProvider<?> b) {\n                //                    for (HubMenuItemProvider<?> child : b.getChildren(null)) {\n                //                        if (ALL.stream().noneMatch(a -> a.getClass().equals(child.getClass()))) {\n                //                            System.out.println(child.getClass());\n                //                        }\n                //                    }\n                //                }\n\n            } catch (Throwable t) {\n                ErrorEventFactory.fromThrowable(t).handle();\n            }\n        }\n        TrackEvent.trace(\"Finished action provider initialization\");\n    }\n\n    default void init() {}\n\n    default String getId() {\n        return null;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    default Optional<Class<? extends AbstractAction>> getActionClass() {\n        var child = Arrays.stream(getClass().getDeclaredClasses())\n                .filter(aClass -> AbstractAction.class.isAssignableFrom(aClass))\n                .findFirst()\n                .map(aClass -> (Class<? extends AbstractAction>) aClass);\n        return child.isPresent() ? Optional.of(child.get()) : Optional.empty();\n    }\n\n    class Loader implements ModuleLayerLoader {\n\n        @Override\n        public void init(ModuleLayer layer) {\n            ALL.addAll(ServiceLoader.load(layer, ActionProvider.class).stream()\n                    .sorted(Comparator.comparing(p -> p.type().getModule().getName()))\n                    .map(p -> p.get())\n                    .toList());\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/action/ActionShortcutComp.java",
    "content": "package io.xpipe.app.action;\n\nimport io.xpipe.app.beacon.AppBeaconServer;\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.ButtonComp;\nimport io.xpipe.app.comp.base.InputGroupComp;\nimport io.xpipe.app.comp.base.TextFieldComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.platform.ClipboardHelper;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.update.AppDistributionType;\nimport io.xpipe.app.util.*;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.scene.layout.Region;\n\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.List;\n\npublic class ActionShortcutComp extends SimpleRegionBuilder {\n\n    private final Property<AbstractAction> action;\n    private final Runnable onCreateMacro;\n\n    public ActionShortcutComp(Property<AbstractAction> action, Runnable onCreateMacro) {\n        this.action = action;\n        this.onCreateMacro = onCreateMacro;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var options = new OptionsBuilder();\n        options.nameAndDescription(\"actionDesktopShortcut\").addComp(createDesktopComp());\n        options.name(AppDistributionType.get().isSupportsUrls() ? \"actionUrlShortcut\" : \"actionUrlShortcutDisabled\");\n        options.description(\n                AppDistributionType.get().isSupportsUrls()\n                        ? AppI18n.observable(\"actionUrlShortcutDescription\")\n                        : AppI18n.observable(\n                                \"actionUrlShortcutDisabledDescription\",\n                                AppDistributionType.get().toTranslatedString().getValue()));\n        options.addComp(createUrlComp()).disable(!AppDistributionType.get().isSupportsUrls());\n        options.nameAndDescription(\"actionApiCall\").addComp(createApiComp());\n        return options.build();\n    }\n\n    private BaseRegionBuilder<?, ?> createUrlComp() {\n        var url = new SimpleStringProperty();\n        action.subscribe((v) -> {\n            var s = ActionUrls.toUrl(v);\n            PlatformThread.runLaterIfNeeded(() -> {\n                url.set(s);\n            });\n        });\n\n        var copyButton = new ButtonComp(null, new FontIcon(\"mdi2c-clipboard-multiple-outline\"), () -> {\n                    ClipboardHelper.copyUrl(url.getValue());\n                })\n                .describe(d -> d.nameKey(\"copyUrl\"));\n        var field = new TextFieldComp(url);\n        field.apply(struc -> struc.setEditable(false));\n        var group = new InputGroupComp(List.of(field, copyButton));\n        group.setMainReference(field);\n        group.hide(Bindings.isNull(url));\n        return group;\n    }\n\n    private BaseRegionBuilder<?, ?> createDesktopComp() {\n        var url = BindingsHelper.map(action, abstractAction -> ActionUrls.toUrl(abstractAction));\n        var name = new SimpleStringProperty();\n        action.subscribe((v) -> {\n            var s = v.getShortcutName();\n            PlatformThread.runLaterIfNeeded(() -> {\n                name.set(s);\n            });\n        });\n        var copyButton = new ButtonComp(null, new FontIcon(\"mdi2f-file-move-outline\"), () -> {\n                    ThreadHelper.runFailableAsync(() -> {\n                        var file = DesktopShortcuts.createOpen(\n                                name.getValue(),\n                                \"open \\\"\" + url.getValue() + \"\\\" -d \\\"\" + AppProperties.get().getDataDir() + \"\\\"\",\n                                null);\n                        DesktopHelper.browseFileInDirectory(file);\n                    });\n                })\n                .describe(d -> d.nameKey(\"createShortcut\"));\n        var field = new TextFieldComp(name);\n        var group = new InputGroupComp(List.of(field, copyButton));\n        group.setMainReference(field);\n        group.hide(BindingsHelper.map(action, v -> !(v instanceof SerializableAction)));\n        return group;\n    }\n\n    private BaseRegionBuilder<?, ?> createApiComp() {\n        var url = \"curl -X POST \\\"http://localhost:\" + AppBeaconServer.get().getPort() + \"/action\\\" ...\";\n        var text = AppI18n.observable(\"actionApiUrl\", url);\n        var prop = new SimpleStringProperty();\n        prop.bind(text);\n\n        var copyButton = new ButtonComp(null, new FontIcon(\"mdi2c-clipboard-multiple-outline\"), () -> {\n                    if (action.getValue() instanceof SerializableAction sa) {\n                        ClipboardHelper.copyUrl(sa.toNode().toPrettyString());\n                    }\n                })\n                .describe(d -> d.nameKey(\"copyBody\"));\n        var field = new TextFieldComp(prop, true);\n        field.apply(struc -> struc.setEditable(false));\n        var group = new InputGroupComp(List.of(field, copyButton));\n        group.setMainReference(field);\n        group.hide(BindingsHelper.map(action, v -> !(v instanceof SerializableAction)));\n        return group;\n    }\n\n    @SuppressWarnings(\"unused\")\n    private BaseRegionBuilder<?, ?> createMacroComp() {\n        var button = new ButtonComp(\n                AppI18n.observable(\"createMacro\"), new FontIcon(\"mdi2c-clipboard-multiple-outline\"), onCreateMacro);\n        return button;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/action/ActionUrls.java",
    "content": "package io.xpipe.app.action;\n\nimport io.xpipe.core.JacksonMapper;\nimport io.xpipe.core.SecretValue;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport lombok.SneakyThrows;\n\nimport java.net.URLDecoder;\nimport java.net.URLEncoder;\nimport java.nio.charset.StandardCharsets;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic class ActionUrls {\n\n    private static String encodeValue(String value) {\n        return URLEncoder.encode(value, StandardCharsets.UTF_8);\n    }\n\n    private static List<String> nodeToString(JsonNode node) {\n        if (node.isTextual()) {\n            return List.of(encodeValue(node.asText()));\n        }\n\n        if (node.isArray()) {\n            var list = new ArrayList<String>();\n            for (JsonNode c : node) {\n                var r = nodeToString(c);\n                if (r.size() == 1) {\n                    list.add(r.getFirst());\n                }\n            }\n            return list;\n        }\n\n        var enc = SecretValue.toBase64e(node.toPrettyString().getBytes(StandardCharsets.UTF_8));\n        return List.of(\"~\" + enc);\n    }\n\n    @SneakyThrows\n    public static String toUrl(AbstractAction action) {\n        if (!(action instanceof SerializableAction sa)) {\n            return null;\n        }\n\n        var json = sa.toNode();\n        var parsed =\n                JacksonMapper.getDefault().treeToValue(json, new TypeReference<LinkedHashMap<String, JsonNode>>() {});\n\n        Map<String, List<String>> requestParams = new LinkedHashMap<>();\n        for (Map.Entry<String, JsonNode> e : parsed.entrySet()) {\n            var value = nodeToString(e.getValue());\n            requestParams.put(e.getKey(), value);\n        }\n\n        String encodedURL = requestParams.keySet().stream()\n                .map(key -> {\n                    var vals = requestParams.get(key);\n                    return vals.stream().map(s -> key + \"=\" + s).collect(Collectors.joining(\"&\"));\n                })\n                .collect(Collectors.joining(\"&\", \"xpipe://action?\", \"\"));\n        return encodedURL;\n    }\n\n    public static Optional<AbstractAction> parse(String queryString) throws Exception {\n        var query = splitQuery(queryString);\n\n        var id = query.get(\"id\");\n        if (id == null || id.size() != 1) {\n            return Optional.empty();\n        }\n\n        var provider = ActionProvider.ALL.stream()\n                .filter(actionProvider -> id.getFirst().equals(actionProvider.getId()))\n                .findFirst();\n        if (provider.isEmpty()) {\n            return Optional.empty();\n        }\n\n        var clazz = provider.get().getActionClass();\n        if (clazz.isEmpty()) {\n            return Optional.empty();\n        }\n\n        if (!SerializableAction.class.isAssignableFrom(clazz.get())) {\n            return Optional.empty();\n        }\n\n        var stores = query.get(\"ref\");\n        if (stores == null || stores.isEmpty()) {\n            return Optional.empty();\n        }\n\n        var fixedMap = new LinkedHashMap<String, Object>();\n        for (var entry : query.entrySet()) {\n            var list = new ArrayList<>();\n            for (String s : entry.getValue()) {\n                if (s.startsWith(\"~\")) {\n                    var json = SecretValue.fromBase64e(s.substring(1));\n                    var node = JacksonMapper.getDefault().readTree(json);\n                    list.add(node);\n                } else {\n                    list.add(s);\n                }\n            }\n\n            var unwrapped = list.size() == 1 ? list.getFirst() : list;\n            fixedMap.put(entry.getKey(), unwrapped);\n        }\n\n        var json = (ObjectNode) JacksonMapper.getDefault().valueToTree(fixedMap);\n        var instance = ActionJacksonMapper.parse(json);\n        return Optional.ofNullable(instance);\n    }\n\n    private static Map<String, List<String>> splitQuery(String query) {\n        if (query == null || query.isBlank()) {\n            return Collections.emptyMap();\n        }\n\n        return Arrays.stream(query.split(\"&\"))\n                .map(ActionUrls::splitQueryParameter)\n                .collect(Collectors.groupingBy(\n                        AbstractMap.SimpleImmutableEntry::getKey,\n                        LinkedHashMap::new,\n                        Collectors.mapping(Map.Entry::getValue, Collectors.toList())));\n    }\n\n    private static AbstractMap.SimpleImmutableEntry<String, String> splitQueryParameter(String it) {\n        final int idx = it.indexOf(\"=\");\n        final String key = idx > 0 ? it.substring(0, idx) : it;\n        final String value = idx > 0 && it.length() > idx + 1 ? it.substring(idx + 1) : null;\n        return new AbstractMap.SimpleImmutableEntry<>(\n                URLDecoder.decode(key, StandardCharsets.UTF_8),\n                value != null ? URLDecoder.decode(value, StandardCharsets.UTF_8) : null);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/action/LauncherUrlProvider.java",
    "content": "package io.xpipe.app.action;\n\nimport java.net.URI;\n\npublic interface LauncherUrlProvider extends ActionProvider {\n\n    String getScheme();\n\n    AbstractAction createAction(URI uri) throws Exception;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/action/SerializableAction.java",
    "content": "package io.xpipe.app.action;\n\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.util.DataStoreFormatter;\nimport io.xpipe.core.JacksonMapper;\nimport io.xpipe.core.UuidHelper;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport lombok.experimental.SuperBuilder;\n\nimport java.util.*;\nimport java.util.stream.Collectors;\n\n@SuperBuilder\npublic abstract class SerializableAction extends AbstractAction {\n\n    public String toString() {\n        return toNode().toPrettyString();\n    }\n\n    public ObjectNode toNode() {\n        var json = ActionJacksonMapper.write(this);\n        return json;\n    }\n\n    public ObjectNode toConfigNode() {\n        var json = toNode();\n        json.remove(\"ref\");\n        json.remove(\"refs\");\n        return json;\n    }\n\n    public Optional<? extends SerializableAction> withConfigString(String configString) {\n        try {\n            var tree = (ObjectNode) JacksonMapper.getDefault().readTree(configString);\n            tree.put(\"id\", getId());\n            SerializableAction action = ActionJacksonMapper.parse(tree);\n            return Optional.ofNullable(action);\n        } catch (Exception ex) {\n            return Optional.empty();\n        }\n    }\n\n    @Override\n    public Map<String, String> toDisplayMap() {\n        var node = toConfigNode();\n\n        var map = new LinkedHashMap<String, String>();\n        map.put(\"Action\", getDisplayName());\n        for (Map.Entry<String, JsonNode> property : node.properties()) {\n            if (property.getKey().equals(\"id\")) {\n                continue;\n            }\n\n            var name = DataStoreFormatter.camelCaseToName(property.getKey());\n            name = Arrays.stream(name.split(\" \"))\n                    .filter(s -> !s.equals(\"Store\"))\n                    .collect(Collectors.joining(\" \"));\n\n            if (property.getValue().isTextual()) {\n                var value = property.getValue().textValue();\n                var uuid = UuidHelper.parse(value);\n                if (uuid.isPresent()) {\n                    var refName = DataStorage.get()\n                            .getStoreEntryIfPresent(uuid.get())\n                            .map(e -> e.getName())\n                            .or(() -> {\n                                return DataStorage.get()\n                                        .getStoreCategoryIfPresent(uuid.get())\n                                        .map(c -> c.getName());\n                            });\n                    map.put(name, refName.orElse(value));\n                } else {\n                    map.put(name, value);\n                }\n            } else if (property.getValue().isArray()) {\n                var list = new ArrayList<String>();\n                for (JsonNode jsonNode : property.getValue()) {\n                    var s = jsonNode.asText();\n                    if (!s.isEmpty()) {\n                        list.add(s);\n                    }\n                }\n\n                if (!list.isEmpty()) {\n                    map.put(name, String.join(\"\\n\", list));\n                }\n            } else if (property.getValue().isBoolean()) {\n                map.put(name, property.getValue().booleanValue() ? \"Yes\" : \"No\");\n            } else {\n                var value = property.getValue().asText();\n                map.put(name, value);\n            }\n        }\n        return map;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/action/StoreContextAction.java",
    "content": "package io.xpipe.app.action;\n\nimport io.xpipe.app.storage.DataStoreEntry;\n\nimport java.util.List;\n\npublic interface StoreContextAction {\n\n    List<DataStoreEntry> getStoreEntryContext();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/action/XPipeUrlProvider.java",
    "content": "package io.xpipe.app.action;\n\nimport java.net.URI;\n\npublic class XPipeUrlProvider implements LauncherUrlProvider {\n\n    @Override\n    public String getScheme() {\n        return \"xpipe\";\n    }\n\n    @Override\n    public AbstractAction createAction(URI uri) throws Exception {\n        var a = uri.getHost();\n        if (!\"action\".equals(a)) {\n            return null;\n        }\n\n        var query = uri.getQuery();\n        var action = ActionUrls.parse(query);\n        return action.orElse(null);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/AppBeaconCache.java",
    "content": "package io.xpipe.app.beacon;\n\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.beacon.BeaconClientException;\n\nimport lombok.Value;\n\nimport java.util.HashSet;\nimport java.util.Set;\nimport java.util.UUID;\n\n@Value\npublic class AppBeaconCache {\n\n    Set<BeaconShellSession> shellSessions = new HashSet<>();\n\n    public BeaconShellSession getShellSession(UUID uuid) throws BeaconClientException {\n        var found = shellSessions.stream()\n                .filter(beaconShellSession ->\n                        beaconShellSession.getEntry().getUuid().equals(uuid))\n                .findFirst();\n        if (found.isEmpty()) {\n            throw new BeaconClientException(\"No active shell session known for id \" + uuid);\n        }\n        return found.get();\n    }\n\n    public BeaconShellSession getOrStart(DataStoreEntryRef<ShellStore> ref) throws Exception {\n        var existing = AppBeaconServer.get().getCache().getShellSessions().stream()\n                .filter(beaconShellSession -> beaconShellSession.getEntry().equals(ref.get()))\n                .findFirst();\n        var control = (existing.isPresent()\n                ? existing.get().getControl()\n                : ref.getStore().standaloneControl().start());\n        control.setNonInteractive();\n        control.start();\n\n        if (existing.isEmpty()) {\n            AppBeaconServer.get().getCache().getShellSessions().add(new BeaconShellSession(ref.get(), control));\n        }\n\n        return new BeaconShellSession(ref.get(), control);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java",
    "content": "package io.xpipe.app.beacon;\n\nimport io.xpipe.app.beacon.mcp.AppMcpServer;\nimport io.xpipe.app.core.AppLocalTemp;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.beacon.BeaconConfig;\nimport io.xpipe.beacon.BeaconInterface;\nimport io.xpipe.core.OsType;\n\nimport com.sun.net.httpserver.HttpExchange;\nimport com.sun.net.httpserver.HttpServer;\nimport lombok.Getter;\n\nimport java.io.IOException;\nimport java.net.HttpURLConnection;\nimport java.net.Inet4Address;\nimport java.net.InetSocketAddress;\nimport java.nio.file.Files;\nimport java.nio.file.attribute.PosixFilePermissions;\nimport java.util.*;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.TimeUnit;\n\npublic class AppBeaconServer {\n\n    private static AppBeaconServer INSTANCE;\n\n    @Getter\n    private final int port;\n\n    @Getter\n    private final Set<BeaconSession> sessions = new HashSet<>();\n\n    @Getter\n    private final AppBeaconCache cache = new AppBeaconCache();\n\n    private boolean running;\n    private ExecutorService executor;\n    private HttpServer server;\n\n    @Getter\n    private String localAuthSecret;\n\n    private AppBeaconServer(int port) {\n        this.port = port;\n    }\n\n    public static void setupPort() {\n        int port = BeaconConfig.getUsedPort();\n        INSTANCE = new AppBeaconServer(port);\n    }\n\n    public static void init() {\n        try {\n            INSTANCE.initAuthSecret();\n            INSTANCE.start();\n            TrackEvent.withInfo(\"Started http server\")\n                    .tag(\"port\", INSTANCE.getPort())\n                    .build()\n                    .handle();\n        } catch (Exception ex) {\n            // Not terminal!\n            // We can still continue without the running server\n            ErrorEventFactory.fromThrowable(\"Unable to start local http server on port \" + INSTANCE.getPort(), ex)\n                    .documentationLink(DocumentationLink.BEACON_PORT_BIND)\n                    .build()\n                    .handle();\n        }\n    }\n\n    public static void reset() {\n        if (INSTANCE != null) {\n            INSTANCE.stop();\n            INSTANCE.deleteAuthSecret();\n            for (BeaconShellSession ss : INSTANCE.getCache().getShellSessions()) {\n                try {\n                    ss.getControl().close();\n                } catch (Exception ex) {\n                    ErrorEventFactory.fromThrowable(ex).omit().expected().handle();\n                }\n            }\n            INSTANCE = null;\n        }\n    }\n\n    public static AppBeaconServer get() {\n        return INSTANCE;\n    }\n\n    public void addSession(BeaconSession session) {\n        this.sessions.add(session);\n    }\n\n    private void stop() {\n        if (!running) {\n            return;\n        }\n\n        running = false;\n        server.stop(0);\n        executor.shutdown();\n        try {\n            executor.awaitTermination(30, TimeUnit.SECONDS);\n        } catch (InterruptedException ignored) {\n        }\n    }\n\n    private void initAuthSecret() throws IOException {\n        var file = BeaconConfig.getLocalBeaconAuthFile();\n        // Create and set temp dir permissions for Linux\n        AppLocalTemp.getLocalTempDataDirectory();\n        var id = UUID.randomUUID().toString();\n        Files.writeString(file, id);\n        if (OsType.ofLocal() != OsType.WINDOWS) {\n            Files.setPosixFilePermissions(file, PosixFilePermissions.fromString(\"rw-rw----\"));\n        }\n        localAuthSecret = id;\n    }\n\n    private void deleteAuthSecret() {\n        var file = BeaconConfig.getLocalBeaconAuthFile();\n        try {\n            Files.delete(file);\n        } catch (IOException ignored) {\n        }\n    }\n\n    private void start() throws IOException {\n        executor = Executors.newFixedThreadPool(5, r -> {\n            Thread t = Executors.defaultThreadFactory().newThread(r);\n            t.setDaemon(true);\n            t.setName(\"http handler\");\n            t.setUncaughtExceptionHandler((t1, e) -> {\n                ErrorEventFactory.fromThrowable(e).handle();\n            });\n            return t;\n        });\n        server = HttpServer.create(\n                new InetSocketAddress(Inet4Address.getByAddress(new byte[] {0x7f, 0x00, 0x00, 0x01}), port), 10);\n        BeaconInterface.getAll().forEach(beaconInterface -> {\n            var handler = new BeaconRequestHandler<>(beaconInterface);\n            server.createContext(beaconInterface.getPath(), exchange -> {\n                if (!handleCorsHeaders(exchange)) {\n                    handler.handle(exchange);\n                }\n            });\n        });\n        server.setExecutor(executor);\n\n        server.createContext(\"/\", exchange -> {\n            if (!handleCorsHeaders(exchange)) {\n                handleCatchAll(exchange);\n            }\n        });\n\n        server.createContext(\"/mcp\", exchange -> {\n            if (!handleCorsHeaders(exchange)) {\n                var mcpServer = AppMcpServer.get();\n                if (mcpServer != null) {\n                    mcpServer.createHttpHandler().handle(exchange);\n                }\n            }\n        });\n\n        server.start();\n        running = true;\n    }\n\n    private boolean handleCorsHeaders(HttpExchange exchange) throws IOException {\n        exchange.getResponseHeaders()\n                .add(\"Origin\", \"http://localhost:\" + AppBeaconServer.get().getPort());\n        exchange.getResponseHeaders().add(\"Vary\", \"Origin\");\n        exchange.getResponseHeaders().add(\"Access-Control-Allow-Origin\", \"*\");\n        exchange.getResponseHeaders().add(\"Access-Control-Allow-Credentials\", \"true\");\n        exchange.getResponseHeaders().add(\"Access-Control-Allow-Headers\", \"*\");\n        exchange.getResponseHeaders().add(\"Access-Control-Allow-Methods\", \"*\");\n        if (exchange.getRequestMethod().equals(\"OPTIONS\")) {\n            exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, -1);\n            return true;\n        } else {\n            return false;\n        }\n    }\n\n    private void handleCatchAll(HttpExchange exchange) throws IOException {\n        exchange.getResponseHeaders().add(\"Location\", DocumentationLink.API.getLink());\n        exchange.sendResponseHeaders(HttpURLConnection.HTTP_SEE_OTHER, 0);\n        exchange.close();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java",
    "content": "package io.xpipe.app.beacon;\n\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.beacon.*;\nimport io.xpipe.core.JacksonMapper;\n\nimport com.sun.net.httpserver.HttpExchange;\nimport com.sun.net.httpserver.HttpHandler;\nimport lombok.SneakyThrows;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Arrays;\n\npublic class BeaconRequestHandler<T> implements HttpHandler {\n\n    private final BeaconInterface<T> beaconInterface;\n\n    public BeaconRequestHandler(BeaconInterface<T> beaconInterface) {\n        this.beaconInterface = beaconInterface;\n    }\n\n    @Override\n    public void handle(HttpExchange exchange) {\n        if (AppOperationMode.isInShutdown() && !beaconInterface.acceptInShutdown()) {\n            writeError(exchange, new BeaconClientErrorResponse(\"Daemon is currently in shutdown\"), 400);\n            return;\n        }\n\n        if (beaconInterface.requiresCompletedStartup()) {\n            while (AppOperationMode.isInStartup()) {\n                ThreadHelper.sleep(100);\n            }\n        }\n\n        if (beaconInterface.requiresEnabledApi()\n                && !AppPrefs.get().enableHttpApi().get()) {\n            var ex = new BeaconServerException(\"HTTP API is not enabled in the settings menu\");\n            writeError(exchange, ex, 403);\n            return;\n        }\n\n        if (!AppPrefs.get().disableApiAuthentication().get() && beaconInterface.requiresAuthentication()) {\n            var auth = exchange.getRequestHeaders().getFirst(\"Authorization\");\n            if (auth == null) {\n                writeError(exchange, new BeaconClientErrorResponse(\"Missing Authorization header\"), 401);\n                return;\n            }\n\n            var token = auth.replace(\"Bearer \", \"\");\n            var session = AppBeaconServer.get().getSessions().stream()\n                    .filter(s -> s.getToken().equals(token))\n                    .findFirst()\n                    .orElse(null);\n            if (session == null) {\n                writeError(exchange, new BeaconClientErrorResponse(\"Unknown token\"), 403);\n                return;\n            }\n        }\n\n        handleAuthenticatedRequest(exchange);\n    }\n\n    private void handleAuthenticatedRequest(HttpExchange exchange) {\n        T object;\n        Object response;\n        try {\n            if (beaconInterface.readRawRequestBody()) {\n                object = createDefaultRequest(beaconInterface);\n            } else {\n                try (InputStream is = exchange.getRequestBody()) {\n                    var read = is.readAllBytes();\n                    var rawDataRequestClass = beaconInterface.getRequestClass().getDeclaredFields().length == 1\n                            && beaconInterface\n                                    .getRequestClass()\n                                    .getDeclaredFields()[0]\n                                    .getType()\n                                    .equals(byte[].class);\n                    if (!new String(read, StandardCharsets.US_ASCII).strip().startsWith(\"{\") && rawDataRequestClass) {\n                        object = createRawDataRequest(beaconInterface, read);\n                    } else {\n                        var tree = JacksonMapper.getDefault().readTree(read);\n                        TrackEvent.trace(\"Parsed raw request:\\n\" + tree.toPrettyString());\n                        var emptyRequestClass = tree.isEmpty()\n                                && beaconInterface.getRequestClass().getDeclaredFields().length == 0;\n                        object = emptyRequestClass\n                                ? createDefaultRequest(beaconInterface)\n                                : JacksonMapper.getDefault().treeToValue(tree, beaconInterface.getRequestClass());\n                        TrackEvent.trace(\"Parsed request object:\\n\" + object);\n                    }\n                }\n            }\n\n            var sync = beaconInterface.getSynchronizationObject();\n            if (sync != null) {\n                synchronized (sync) {\n                    response = beaconInterface.handle(exchange, object);\n                }\n            } else {\n                response = beaconInterface.handle(exchange, object);\n            }\n        } catch (BeaconClientException clientException) {\n            ErrorEventFactory.fromThrowable(clientException).omit().expected().handle();\n            writeError(exchange, new BeaconClientErrorResponse(clientException.getMessage()), 400);\n            return;\n        } catch (BeaconServerException serverException) {\n            var cause = serverException.getCause() != null ? serverException.getCause() : serverException;\n            var event = ErrorEventFactory.fromThrowable(cause).omit().handle();\n            var link = event.getLink();\n            writeError(exchange, new BeaconServerErrorResponse(cause, link), 500);\n            return;\n        } catch (IOException ex) {\n            // Handle serialization errors as normal exceptions and other IO exceptions as assuming that the connection\n            // is broken\n            if (!ex.getClass().getName().contains(\"jackson\")) {\n                ErrorEventFactory.fromThrowable(ex).omit().expected().handle();\n            } else {\n                ErrorEventFactory.fromThrowable(ex).omit().expected().handle();\n                // Make deserialization error message more readable\n                var message = ex.getMessage()\n                        .replace(\"$RequestBuilder\", \"\")\n                        .replace(\"Exchange$Request\", \"Request\")\n                        .replace(\"at [Source: UNKNOWN; byte offset: #UNKNOWN]\", \"\")\n                        .replaceAll(\"(\\\\w+) is marked non-null but is null\", \"field $1 is missing from object\")\n                        .strip();\n                writeError(exchange, new BeaconClientErrorResponse(message), 400);\n            }\n            return;\n        } catch (Throwable other) {\n            var event = ErrorEventFactory.fromThrowable(other).omit().expected().handle();\n            var link = event.getLink();\n            writeError(exchange, new BeaconServerErrorResponse(other, link), 500);\n            return;\n        }\n\n        try {\n            var emptyResponseClass = beaconInterface.getResponseClass().getDeclaredFields().length == 0;\n            if (!emptyResponseClass && response != null) {\n                TrackEvent.trace(\"Sending response:\\n\" + response);\n                TrackEvent.trace(\"Sending raw response:\\n\"\n                        + JacksonMapper.getCensored().valueToTree(response).toPrettyString());\n                var bytes = JacksonMapper.getDefault()\n                        .valueToTree(response)\n                        .toPrettyString()\n                        .getBytes(StandardCharsets.UTF_8);\n                exchange.sendResponseHeaders(200, bytes.length);\n                try (OutputStream os = exchange.getResponseBody()) {\n                    os.write(bytes);\n                }\n            } else {\n                exchange.sendResponseHeaders(200, -1);\n            }\n        } catch (IOException ioException) {\n            // The exchange implementation might have already sent a response manually\n            if (!\"headers already sent\".equals(ioException.getMessage())) {\n                ErrorEventFactory.fromThrowable(ioException).omit().expected().handle();\n            }\n        } catch (Throwable other) {\n            var event = ErrorEventFactory.fromThrowable(other).handle();\n            var link = event.getLink();\n            writeError(exchange, new BeaconServerErrorResponse(other, link), 500);\n        }\n    }\n\n    private void writeError(HttpExchange exchange, Object errorMessage, int code) {\n        try {\n            var bytes =\n                    JacksonMapper.getDefault().writeValueAsString(errorMessage).getBytes(StandardCharsets.UTF_8);\n            exchange.sendResponseHeaders(code, bytes.length);\n            try (OutputStream os = exchange.getResponseBody()) {\n                os.write(bytes);\n            }\n        } catch (IOException ex) {\n            ErrorEventFactory.fromThrowable(ex).omit().expected().handle();\n        }\n    }\n\n    @SneakyThrows\n    @SuppressWarnings(\"unchecked\")\n    private <REQ> REQ createDefaultRequest(BeaconInterface<?> beaconInterface) {\n        var c = beaconInterface.getRequestClass().getDeclaredMethod(\"builder\");\n        c.setAccessible(true);\n        var b = c.invoke(null);\n        var m = b.getClass().getDeclaredMethod(\"build\");\n        m.setAccessible(true);\n        return (REQ) beaconInterface.getRequestClass().cast(m.invoke(b));\n    }\n\n    @SneakyThrows\n    @SuppressWarnings(\"unchecked\")\n    private <REQ> REQ createRawDataRequest(BeaconInterface<?> beaconInterface, byte[] s) {\n        var c = beaconInterface.getRequestClass().getDeclaredMethod(\"builder\");\n        c.setAccessible(true);\n\n        var b = c.invoke(null);\n        var setMethod = Arrays.stream(b.getClass().getDeclaredMethods())\n                .filter(method -> method.getParameterCount() == 1\n                        && method.getParameters()[0].getType().equals(byte[].class))\n                .findFirst()\n                .orElseThrow();\n        setMethod.invoke(b, (Object) s);\n\n        var m = b.getClass().getDeclaredMethod(\"build\");\n        m.setAccessible(true);\n        return (REQ) beaconInterface.getRequestClass().cast(m.invoke(b));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/BeaconSession.java",
    "content": "package io.xpipe.app.beacon;\n\nimport io.xpipe.beacon.BeaconClientInformation;\n\nimport lombok.Value;\n\n@Value\npublic class BeaconSession {\n\n    BeaconClientInformation clientInformation;\n    String token;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/BeaconShellSession.java",
    "content": "package io.xpipe.app.beacon;\n\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.storage.DataStoreEntry;\n\nimport lombok.Value;\n\n@Value\npublic class BeaconShellSession {\n\n    DataStoreEntry entry;\n    ShellControl control;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/BlobManager.java",
    "content": "package io.xpipe.app.beacon;\n\nimport io.xpipe.app.core.AppLocalTemp;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.beacon.BeaconClientException;\n\nimport org.apache.commons.io.FileUtils;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.ConcurrentHashMap;\n\npublic class BlobManager {\n\n    private static final Path TEMP = AppLocalTemp.getLocalTempDataDirectory(\"blob\");\n    private static BlobManager INSTANCE;\n    private final Map<UUID, byte[]> memoryBlobs = new ConcurrentHashMap<>();\n    private final Map<UUID, Path> fileBlobs = new ConcurrentHashMap<>();\n\n    public static BlobManager get() {\n        return INSTANCE;\n    }\n\n    public static void init() {\n        INSTANCE = new BlobManager();\n        try {\n            FileUtils.forceMkdir(TEMP.toFile());\n            try {\n                // Remove old files in dir\n                FileUtils.cleanDirectory(TEMP.toFile());\n            } catch (IOException ignored) {\n            }\n        } catch (IOException e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n        }\n    }\n\n    public static void reset() {\n        try {\n            FileUtils.cleanDirectory(TEMP.toFile());\n        } catch (IOException ignored) {\n        }\n        INSTANCE = null;\n    }\n\n    public Path newBlobFile() throws IOException {\n        var file = TEMP.resolve(UUID.randomUUID().toString());\n        FileUtils.forceMkdir(file.getParent().toFile());\n        return file;\n    }\n\n    public void store(UUID uuid, byte[] blob) {\n        memoryBlobs.put(uuid, blob);\n    }\n\n    public void store(UUID uuid, InputStream blob) throws IOException {\n        var file = TEMP.resolve(uuid.toString());\n        try (var fileOut = Files.newOutputStream(file)) {\n            blob.transferTo(fileOut);\n        }\n        fileBlobs.put(uuid, file);\n    }\n\n    public InputStream getBlob(UUID uuid) throws Exception {\n        var memory = memoryBlobs.get(uuid);\n        if (memory != null) {\n            return new ByteArrayInputStream(memory);\n        }\n\n        var found = fileBlobs.get(uuid);\n        if (found == null) {\n            throw new BeaconClientException(\"No saved data known for id \" + uuid);\n        }\n\n        return Files.newInputStream(found);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/ActionExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.action.ActionJacksonMapper;\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.beacon.BeaconClientException;\nimport io.xpipe.beacon.api.ActionExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\n\npublic class ActionExchangeImpl extends ActionExchange {\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) throws Exception {\n        var action = ActionJacksonMapper.parse(msg.getAction());\n        if (action == null) {\n            throw new BeaconClientException(\"Unable to parse action into known schema\");\n        }\n\n        if (!checkPermission()) {\n            return Response.builder().build();\n        }\n\n        action.executeSyncImpl(msg.isConfirm());\n        return Response.builder().build();\n    }\n\n    private boolean checkPermission() {\n        var cache = AppCache.getBoolean(\"externalActionPermitted\", false);\n        if (cache) {\n            return true;\n        }\n\n        var r = AppDialog.confirm(\"externalAction\");\n        if (r) {\n            AppCache.update(\"externalActionPermitted\", true);\n        }\n        return r;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.secret.SecretManager;\nimport io.xpipe.app.secret.SecretQueryState;\nimport io.xpipe.app.terminal.TerminalView;\nimport io.xpipe.app.util.*;\nimport io.xpipe.beacon.BeaconClientException;\nimport io.xpipe.beacon.api.AskpassExchange;\nimport io.xpipe.core.InPlaceSecretValue;\n\nimport javafx.beans.property.SimpleStringProperty;\n\nimport com.sun.net.httpserver.HttpExchange;\n\nimport java.time.Duration;\n\npublic class AskpassExchangeImpl extends AskpassExchange {\n\n    @Override\n    public boolean requiresCompletedStartup() {\n        return false;\n    }\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {\n        // SSH auth with a smartcard will prompt to confirm user presence\n        // Maybe we can show some dialog for this in the future\n        if (msg.getPrompt() != null && msg.getPrompt().toLowerCase().contains(\"confirm user presence\")) {\n            var shown = AppLayoutModel.get().getQueueEntries().stream().anyMatch(queueEntry -> msg.getPrompt()\n                    .equals(queueEntry.getName().getValue()));\n            if (!shown) {\n                var qe = new AppLayoutModel.QueueEntry(\n                        new SimpleStringProperty(msg.getPrompt()),\n                        new LabelGraphic.IconGraphic(\"mdi2f-fingerprint\"),\n                        () -> true);\n                AppLayoutModel.get().getQueueEntries().add(qe);\n                GlobalTimer.delay(\n                        () -> {\n                            AppLayoutModel.get().getQueueEntries().remove(qe);\n                        },\n                        Duration.ofSeconds(10));\n            }\n            return Response.builder().value(InPlaceSecretValue.of(\"\")).build();\n        }\n\n        var prompt = msg.getPrompt();\n        // sudo-rs uses a different prefix which we don't really need\n        prompt = prompt.replace(\"[sudo: authenticate]\", \"[sudo]\");\n\n        if (msg.getRequest() == null) {\n            var r = AskpassAlert.queryRaw(prompt, null, true);\n            return Response.builder()\n                    .value(r.getState() == SecretQueryState.NORMAL ? r.getSecret() : InPlaceSecretValue.of(\"\"))\n                    .build();\n        }\n\n        var found = msg.getSecretId() != null\n                ? SecretManager.getProgress(msg.getRequest(), msg.getSecretId())\n                : SecretManager.getProgress(msg.getRequest());\n        if (found.isEmpty()) {\n            throw new BeaconClientException(\"Unknown askpass request\");\n        }\n\n        var p = found.get();\n        var secret = p.process(prompt);\n        if (p.getState() != SecretQueryState.NORMAL) {\n            var ex = new BeaconClientException(SecretQueryState.toErrorMessage(p.getState()));\n            ErrorEventFactory.preconfigure(ErrorEventFactory.fromThrowable(ex).ignore());\n            throw ex;\n        }\n        focusTerminalIfNeeded(msg.getPid());\n        return Response.builder().value(secret.inPlace()).build();\n    }\n\n    @Override\n    public boolean requiresEnabledApi() {\n        return false;\n    }\n\n    private void focusTerminalIfNeeded(long pid) {\n        if (TerminalView.get() == null) {\n            return;\n        }\n\n        var found = TerminalView.get().findSession(pid);\n        if (found.isEmpty()) {\n            return;\n        }\n\n        var term = TerminalView.get().getTerminalInstances().stream()\n                .filter(instance -> instance.equals(found.get().getTerminal()))\n                .findFirst();\n        if (term.isEmpty()) {\n            return;\n        }\n        TerminalView.focus(term.get());\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/CategoryAddExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreCategory;\nimport io.xpipe.beacon.BeaconClientException;\nimport io.xpipe.beacon.api.CategoryAddExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\n\npublic class CategoryAddExchangeImpl extends CategoryAddExchange {\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) throws Throwable {\n        if (DataStorage.get().getStoreCategoryIfPresent(msg.getParent()).isEmpty()) {\n            throw new BeaconClientException(\"Parent category with id \" + msg.getParent() + \" does not exist\");\n        }\n\n        var found = DataStorage.get().getStoreCategories().stream()\n                .filter(dataStoreCategory -> msg.getParent().equals(dataStoreCategory.getParentCategory())\n                        && msg.getName().equals(dataStoreCategory.getName()))\n                .findAny();\n        if (found.isPresent()) {\n            return Response.builder().category(found.get().getUuid()).build();\n        }\n\n        var cat = DataStoreCategory.createNew(msg.getParent(), msg.getName());\n        DataStorage.get().addStoreCategory(cat);\n        return Response.builder().category(cat.getUuid()).build();\n    }\n\n    @Override\n    public Object getSynchronizationObject() {\n        return DataStorage.get();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/CategoryInfoExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.beacon.BeaconClientException;\nimport io.xpipe.beacon.api.CategoryInfoExchange;\nimport io.xpipe.core.JacksonMapper;\n\nimport com.sun.net.httpserver.HttpExchange;\n\nimport java.util.ArrayList;\nimport java.util.UUID;\n\npublic class CategoryInfoExchangeImpl extends CategoryInfoExchange {\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {\n        var list = new ArrayList<InfoResponse>();\n        for (UUID uuid : msg.getCategories()) {\n            var cat = DataStorage.get()\n                    .getStoreCategoryIfPresent(uuid)\n                    .orElseThrow(() -> new BeaconClientException(\"Unknown category: \" + uuid));\n\n            var name = DataStorage.get().getStorePath(cat);\n\n            var apply = InfoResponse.builder()\n                    .lastModified(cat.getLastModified())\n                    .lastUsed(cat.getLastUsed())\n                    .category(cat.getUuid())\n                    .parentCategory(cat.getParentCategory())\n                    .name(name)\n                    .config(JacksonMapper.getDefault().valueToTree(cat.getConfig()))\n                    .build();\n            list.add(apply);\n        }\n        return Response.builder().infos(list).build();\n    }\n\n    @Override\n    public Object getSynchronizationObject() {\n        return DataStorage.get();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/CategoryQueryExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStorageQuery;\nimport io.xpipe.beacon.api.CategoryQueryExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\n\npublic class CategoryQueryExchangeImpl extends CategoryQueryExchange {\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) {\n        var found = DataStorageQuery.queryCategory(msg.getFilter());\n        return Response.builder()\n                .found(found.stream().map(entry -> entry.getUuid()).toList())\n                .build();\n    }\n\n    @Override\n    public Object getSynchronizationObject() {\n        return DataStorage.get();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/CategoryRemoveExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreCategory;\nimport io.xpipe.beacon.BeaconClientException;\nimport io.xpipe.beacon.api.CategoryRemoveExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\n\nimport java.util.ArrayList;\nimport java.util.UUID;\n\npublic class CategoryRemoveExchangeImpl extends CategoryRemoveExchange {\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {\n        var toRemove = new ArrayList<DataStoreCategory>();\n        for (UUID uuid : msg.getCategories()) {\n            var cat = DataStorage.get()\n                    .getStoreCategoryIfPresent(uuid)\n                    .orElseThrow(() -> new BeaconClientException(\"Unknown category: \" + uuid));\n\n            if (!DataStorage.get().canDeleteStoreCategory(cat)) {\n                throw new BeaconClientException(\"Cannot delete category: \" + cat.getName());\n            }\n\n            toRemove.add(cat);\n        }\n\n        for (DataStoreCategory cat : toRemove) {\n            DataStorage.get().deleteStoreCategory(cat, msg.isRemoveChildrenCategories(), msg.isRemoveContents());\n        }\n\n        return Response.builder().build();\n    }\n\n    @Override\n    public Object getSynchronizationObject() {\n        return DataStorage.get();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.ValidationException;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.beacon.BeaconClientException;\nimport io.xpipe.beacon.api.ConnectionAddExchange;\nimport io.xpipe.core.JacksonMapper;\n\nimport com.sun.net.httpserver.HttpExchange;\n\npublic class ConnectionAddExchangeImpl extends ConnectionAddExchange {\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) throws Throwable {\n        var store = JacksonMapper.getDefault().treeToValue(msg.getData(), DataStore.class);\n        if (store == null) {\n            throw new BeaconClientException(\"Unable to parse store data into valid store\");\n        }\n\n        var found = DataStorage.get().getStoreEntryIfPresent(store, false);\n        if (found.isEmpty()) {\n            found = DataStorage.get().getStoreEntryIfPresent(msg.getName());\n        }\n\n        if (found.isPresent()) {\n            DataStorage.get().updateEntryStore(found.get(), store);\n            return Response.builder().connection(found.get().getUuid()).build();\n        }\n\n        if (msg.getCategory() != null\n                && DataStorage.get()\n                        .getStoreCategoryIfPresent(msg.getCategory())\n                        .isEmpty()) {\n            throw new BeaconClientException(\"Category with id \" + msg.getCategory() + \" does not exist\");\n        }\n\n        var entry = DataStoreEntry.createNew(msg.getName(), store);\n        if (msg.getCategory() != null) {\n            entry.setCategoryUuid(msg.getCategory());\n        }\n        try {\n            DataStorage.get().addStoreEntryInProgress(entry);\n            if (msg.getValidate()) {\n                entry.validateOrThrow();\n            }\n        } catch (Throwable ex) {\n            if (ex instanceof ValidationException) {\n                ErrorEventFactory.expected(ex);\n            } else if (ex instanceof StackOverflowError) {\n                // Cycles in connection graphs can fail hard but are expected\n                ErrorEventFactory.expected(ex);\n            }\n            throw ex;\n        } finally {\n            DataStorage.get().removeStoreEntryInProgress(entry);\n        }\n        DataStorage.get().addStoreEntryIfNotPresent(entry);\n\n        // Explicitly assign category\n        if (msg.getCategory() != null) {\n            DataStorage.get()\n                    .moveEntryToCategory(\n                            entry,\n                            DataStorage.get()\n                                    .getStoreCategoryIfPresent(msg.getCategory())\n                                    .orElseThrow());\n        }\n\n        return Response.builder().connection(entry.getUuid()).build();\n    }\n\n    @Override\n    public Object getSynchronizationObject() {\n        return DataStorage.get();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/ConnectionInfoExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.beacon.BeaconClientException;\nimport io.xpipe.beacon.api.ConnectionInfoExchange;\nimport io.xpipe.core.StorePath;\n\nimport com.sun.net.httpserver.HttpExchange;\nimport org.apache.commons.lang3.ClassUtils;\n\nimport java.util.ArrayList;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\n\npublic class ConnectionInfoExchangeImpl extends ConnectionInfoExchange {\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {\n        var list = new ArrayList<InfoResponse>();\n        for (UUID uuid : msg.getConnections()) {\n            var e = DataStorage.get()\n                    .getStoreEntryIfPresent(uuid)\n                    .orElseThrow(() -> new BeaconClientException(\"Unknown connection: \" + uuid));\n\n            var names = DataStorage.get()\n                    .getStorePath(DataStorage.get()\n                            .getStoreCategoryIfPresent(e.getCategoryUuid())\n                            .orElseThrow())\n                    .getNames();\n            var cat = new StorePath(names.subList(1, names.size()));\n            var cache = e.getStoreCache().entrySet().stream()\n                    .filter(kv -> {\n                        return kv.getValue() != null\n                                && (ClassUtils.isPrimitiveOrWrapper(\n                                                kv.getValue().getClass())\n                                        || kv.getValue() instanceof String);\n                    })\n                    .collect(Collectors.toMap(\n                            stringObjectEntry -> stringObjectEntry.getKey(),\n                            stringObjectEntry -> stringObjectEntry.getValue()));\n\n            var apply = InfoResponse.builder()\n                    .lastModified(e.getLastModified())\n                    .lastUsed(e.getLastUsed())\n                    .connection(e.getUuid())\n                    .category(cat)\n                    .name(DataStorage.get().getStorePath(e))\n                    .rawData(e.getStore())\n                    .usageCategory(e.getProvider().getUsageCategory())\n                    .type(e.getProvider().getId())\n                    .state(e.getStorePersistentState() != null ? e.getStorePersistentState() : new Object())\n                    .cache(cache)\n                    .build();\n            list.add(apply);\n        }\n        return Response.builder().infos(list).build();\n    }\n\n    @Override\n    public Object getSynchronizationObject() {\n        return DataStorage.get();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/ConnectionQueryExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStorageQuery;\nimport io.xpipe.beacon.api.ConnectionQueryExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\n\npublic class ConnectionQueryExchangeImpl extends ConnectionQueryExchange {\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) {\n        var found =\n                DataStorageQuery.queryEntry(msg.getCategoryFilter(), msg.getConnectionFilter(), msg.getTypeFilter());\n        return Response.builder()\n                .found(found.stream().map(entry -> entry.getUuid()).toList())\n                .build();\n    }\n\n    @Override\n    public Object getSynchronizationObject() {\n        return DataStorage.get();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRefreshExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.ext.FixedHierarchyStore;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.beacon.BeaconClientException;\nimport io.xpipe.beacon.api.ConnectionRefreshExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\n\npublic class ConnectionRefreshExchangeImpl extends ConnectionRefreshExchange {\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) throws Throwable {\n        var e = DataStorage.get()\n                .getStoreEntryIfPresent(msg.getConnection())\n                .orElseThrow(() -> new BeaconClientException(\"Unknown connection: \" + msg.getConnection()));\n        if (e.getStore() instanceof FixedHierarchyStore) {\n            DataStorage.get().refreshChildren(e, true);\n        } else {\n            e.validateOrThrow();\n        }\n        return Response.builder().build();\n    }\n\n    @Override\n    public Object getSynchronizationObject() {\n        return DataStorage.get();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRemoveExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.beacon.BeaconClientException;\nimport io.xpipe.beacon.api.ConnectionRemoveExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\n\nimport java.util.ArrayList;\nimport java.util.UUID;\n\npublic class ConnectionRemoveExchangeImpl extends ConnectionRemoveExchange {\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {\n        var entries = new ArrayList<DataStoreEntry>();\n        for (UUID uuid : msg.getConnections()) {\n            var e = DataStorage.get()\n                    .getStoreEntryIfPresent(uuid)\n                    .orElseThrow(() -> new BeaconClientException(\"Unknown connection: \" + uuid));\n            entries.add(e);\n        }\n        DataStorage.get().deleteWithChildren(entries.toArray(DataStoreEntry[]::new));\n        return Response.builder().build();\n    }\n\n    @Override\n    public Object getSynchronizationObject() {\n        return DataStorage.get();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/DaemonFocusExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.core.window.AppMainWindow;\nimport io.xpipe.beacon.api.DaemonFocusExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\n\npublic class DaemonFocusExchangeImpl extends DaemonFocusExchange {\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) throws Throwable {\n        if (AppOperationMode.isInStartup()) {\n            return Response.builder().build();\n        }\n\n        if (AppOperationMode.GUI.isSupported()) {\n            AppOperationMode.switchToSyncOrThrow(AppOperationMode.GUI);\n        }\n\n        var w = AppMainWindow.get();\n        if (w != null) {\n            w.focus();\n        }\n        return Response.builder().build();\n    }\n\n    @Override\n    public boolean requiresEnabledApi() {\n        return false;\n    }\n\n    @Override\n    public boolean requiresCompletedStartup() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.beacon.BeaconClientException;\nimport io.xpipe.beacon.api.DaemonModeExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\n\npublic class DaemonModeExchangeImpl extends DaemonModeExchange {\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {\n        var mode = AppOperationMode.map(msg.getMode());\n        if (!mode.isSupported()) {\n            throw new BeaconClientException(\"Unsupported mode: \" + msg.getMode().getDisplayName()\n                    + \". Supported: \"\n                    + String.join(\n                            \", \",\n                            AppOperationMode.getAll().stream()\n                                    .filter(AppOperationMode::isSupported)\n                                    .map(AppOperationMode::getId)\n                                    .toList()));\n        }\n\n        AppOperationMode.switchToSyncIfPossible(mode);\n        return DaemonModeExchange.Response.builder().usedMode(msg.getMode()).build();\n    }\n\n    @Override\n    public boolean requiresEnabledApi() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.core.AppOpenArguments;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.platform.PlatformInit;\nimport io.xpipe.beacon.BeaconServerException;\nimport io.xpipe.beacon.api.DaemonOpenExchange;\nimport io.xpipe.core.OsType;\n\nimport com.sun.net.httpserver.HttpExchange;\n\npublic class DaemonOpenExchangeImpl extends DaemonOpenExchange {\n\n    private int openCounter = 0;\n\n    @Override\n    public boolean requiresCompletedStartup() {\n        return false;\n    }\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) throws BeaconServerException {\n        if (msg.getArguments().isEmpty()) {\n            try {\n                // At this point we are already loading this on another thread\n                // so this call will only perform the waiting\n                PlatformInit.init(true);\n            } catch (Throwable t) {\n                throw new BeaconServerException(t);\n            }\n\n            // The open command is used as a default opener on Linux\n            // We don't want to overwrite the default startup mode\n            if (OsType.ofLocal() == OsType.LINUX && openCounter++ == 0) {\n                return Response.builder().build();\n            }\n\n            AppOperationMode.switchToAsync(AppOperationMode.GUI);\n        } else {\n            AppOpenArguments.handle(msg.getArguments());\n        }\n        return Response.builder().build();\n    }\n\n    @Override\n    public boolean requiresEnabledApi() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/DaemonStatusExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.beacon.api.DaemonStatusExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\n\npublic class DaemonStatusExchangeImpl extends DaemonStatusExchange {\n\n    @Override\n    public boolean requiresCompletedStartup() {\n        return false;\n    }\n\n    @Override\n    public Object handle(HttpExchange exchange, Request body) {\n        String mode;\n        if (AppOperationMode.get() == null) {\n            mode = \"none\";\n        } else {\n            mode = AppOperationMode.get().getId();\n        }\n\n        return Response.builder().mode(mode).build();\n    }\n\n    @Override\n    public boolean requiresEnabledApi() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/DaemonStopExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.beacon.api.DaemonStopExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\n\npublic class DaemonStopExchangeImpl extends DaemonStopExchange {\n\n    @Override\n    public boolean requiresCompletedStartup() {\n        return false;\n    }\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) {\n        ThreadHelper.runAsync(() -> {\n            ThreadHelper.sleep(1000);\n            AppOperationMode.close();\n        });\n        return Response.builder().success(true).build();\n    }\n\n    @Override\n    public boolean requiresEnabledApi() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/DaemonVersionExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.core.AppVersion;\nimport io.xpipe.app.util.LicenseProvider;\nimport io.xpipe.beacon.api.DaemonVersionExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\n\npublic class DaemonVersionExchangeImpl extends DaemonVersionExchange {\n\n    @Override\n    public boolean requiresCompletedStartup() {\n        return false;\n    }\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) {\n        var jvmVersion = System.getProperty(\"java.vm.vendor\") + \" \" + System.getProperty(\"java.vm.name\") + \" (\"\n                + System.getProperty(\"java.vm.version\") + \")\";\n        var version = AppProperties.get().getVersion();\n        return Response.builder()\n                .version(version)\n                .canonicalVersion(AppVersion.parse(version)\n                        .map(appVersion -> appVersion.toString())\n                        .orElse(\"?\"))\n                .buildVersion(AppProperties.get().getBuild())\n                .jvmVersion(jvmVersion)\n                .plan(LicenseProvider.get().getLicenseId())\n                .build();\n    }\n\n    @Override\n    public boolean requiresEnabledApi() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/FsBlobExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.beacon.BlobManager;\nimport io.xpipe.beacon.api.FsBlobExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\nimport lombok.SneakyThrows;\n\nimport java.util.UUID;\n\npublic class FsBlobExchangeImpl extends FsBlobExchange {\n\n    @Override\n    @SneakyThrows\n    public Object handle(HttpExchange exchange, Request msg) {\n        var id = UUID.randomUUID();\n\n        var size = exchange.getRequestBody().available();\n        if (size > 100_000_000) {\n            BlobManager.get().store(id, exchange.getRequestBody());\n        } else {\n            BlobManager.get().store(id, exchange.getRequestBody().readAllBytes());\n        }\n        return Response.builder().blob(id).build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/FsReadExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.beacon.AppBeaconServer;\nimport io.xpipe.app.beacon.BlobManager;\nimport io.xpipe.app.ext.ConnectionFileSystem;\nimport io.xpipe.app.util.FixedSizeInputStream;\nimport io.xpipe.beacon.BeaconClientException;\nimport io.xpipe.beacon.api.FsReadExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\nimport lombok.SneakyThrows;\n\nimport java.io.BufferedInputStream;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\n\npublic class FsReadExchangeImpl extends FsReadExchange {\n\n    @Override\n    @SneakyThrows\n    public Object handle(HttpExchange exchange, Request msg) {\n        var shell = AppBeaconServer.get().getCache().getShellSession(msg.getConnection());\n        var fs = new ConnectionFileSystem(shell.getControl());\n\n        if (!fs.fileExists(msg.getPath())) {\n            throw new BeaconClientException(\"File does not exist\");\n        }\n\n        var size = fs.getFileSize(msg.getPath());\n        if (size > 100_000_000) {\n            var file = BlobManager.get().newBlobFile();\n            try (var in = fs.openInput(msg.getPath())) {\n                var fixedIn = new FixedSizeInputStream(new BufferedInputStream(in), size);\n                try (var fileOut = Files.newOutputStream(file)) {\n                    fixedIn.transferTo(fileOut);\n                }\n                in.transferTo(OutputStream.nullOutputStream());\n            }\n\n            exchange.sendResponseHeaders(200, size);\n            try (var fileIn = Files.newInputStream(file);\n                    var out = exchange.getResponseBody()) {\n                fileIn.transferTo(out);\n            }\n        } else {\n            byte[] bytes;\n            try (var in = fs.openInput(msg.getPath())) {\n                var fixedIn = new FixedSizeInputStream(new BufferedInputStream(in), size);\n                bytes = fixedIn.readAllBytes();\n                in.transferTo(OutputStream.nullOutputStream());\n            }\n            exchange.sendResponseHeaders(200, bytes.length);\n            try (var out = exchange.getResponseBody()) {\n                out.write(bytes);\n            }\n        }\n        return Response.builder().build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.beacon.AppBeaconServer;\nimport io.xpipe.app.beacon.BlobManager;\nimport io.xpipe.app.process.ScriptHelper;\nimport io.xpipe.beacon.api.FsScriptExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\nimport lombok.SneakyThrows;\n\nimport java.nio.charset.StandardCharsets;\n\npublic class FsScriptExchangeImpl extends FsScriptExchange {\n\n    @Override\n    @SneakyThrows\n    public Object handle(HttpExchange exchange, Request msg) {\n        var shell = AppBeaconServer.get().getCache().getShellSession(msg.getConnection());\n        String data;\n        try (var in = BlobManager.get().getBlob(msg.getBlob())) {\n            data = new String(in.readAllBytes(), StandardCharsets.UTF_8);\n        }\n        data = shell.getControl().getShellDialect().prepareScriptContent(shell.getControl(), data);\n        var file = ScriptHelper.createExecScript(shell.getControl(), data);\n        return Response.builder().path(file).build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/FsWriteExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.beacon.AppBeaconServer;\nimport io.xpipe.app.beacon.BlobManager;\nimport io.xpipe.app.ext.ConnectionFileSystem;\nimport io.xpipe.beacon.api.FsWriteExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\nimport lombok.SneakyThrows;\n\npublic class FsWriteExchangeImpl extends FsWriteExchange {\n\n    @Override\n    @SneakyThrows\n    public Object handle(HttpExchange exchange, Request msg) {\n        var shell = AppBeaconServer.get().getCache().getShellSession(msg.getConnection());\n        var fs = new ConnectionFileSystem(shell.getControl());\n        try (var in = BlobManager.get().getBlob(msg.getBlob());\n                var os = fs.openOutput(msg.getPath(), in.available())) {\n            in.transferTo(os);\n        }\n        return Response.builder().build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/HandshakeExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.beacon.AppBeaconServer;\nimport io.xpipe.app.beacon.BeaconSession;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.beacon.BeaconAuthMethod;\nimport io.xpipe.beacon.BeaconClientException;\nimport io.xpipe.beacon.api.HandshakeExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\n\nimport java.util.UUID;\n\npublic class HandshakeExchangeImpl extends HandshakeExchange {\n\n    @Override\n    public boolean requiresCompletedStartup() {\n        return false;\n    }\n\n    @Override\n    public Object handle(HttpExchange exchange, Request request) throws BeaconClientException {\n        if (!checkAuth(request.getAuth())) {\n            throw new BeaconClientException(\"Authentication failed\");\n        }\n\n        TrackEvent.withTrace(\"Handshake request received\")\n                .tag(\"client\", request.getClient().toDisplayString())\n                .handle();\n\n        var session = new BeaconSession(request.getClient(), UUID.randomUUID().toString());\n        AppBeaconServer.get().addSession(session);\n        return Response.builder().sessionToken(session.getToken()).build();\n    }\n\n    @Override\n    public boolean requiresEnabledApi() {\n        return false;\n    }\n\n    private boolean checkAuth(BeaconAuthMethod authMethod) {\n        if (authMethod instanceof BeaconAuthMethod.Local local) {\n            var c = local.getAuthFileContent().strip();\n            return AppBeaconServer.get().getLocalAuthSecret().equals(c);\n        }\n\n        if (authMethod instanceof BeaconAuthMethod.ApiKey key) {\n            var c = key.getKey().strip();\n            return AppPrefs.get().apiKey().get().equals(c);\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/SecretDecryptExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.storage.DataStorageSecret;\nimport io.xpipe.beacon.BeaconClientException;\nimport io.xpipe.beacon.api.SecretDecryptExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\n\nimport java.io.IOException;\n\npublic class SecretDecryptExchangeImpl extends SecretDecryptExchange {\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException {\n        var secret = DataStorageSecret.deserialize(msg.getEncrypted());\n        if (secret == null) {\n            throw new BeaconClientException(\"Unable to parse secret\");\n        }\n\n        return Response.builder().decrypted(new String(secret.getSecret())).build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/SecretEncryptExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.storage.DataStorageSecret;\nimport io.xpipe.beacon.api.SecretEncryptExchange;\nimport io.xpipe.core.InPlaceSecretValue;\n\nimport com.sun.net.httpserver.HttpExchange;\n\npublic class SecretEncryptExchangeImpl extends SecretEncryptExchange {\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) {\n        var secret = DataStorageSecret.ofCurrentSecret(InPlaceSecretValue.of(msg.getValue()));\n        return Response.builder().encrypted(secret.serialize(true)).build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/ShellExecExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.beacon.AppBeaconServer;\nimport io.xpipe.beacon.api.ShellExecExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\nimport lombok.SneakyThrows;\n\nimport java.util.concurrent.atomic.AtomicReference;\n\npublic class ShellExecExchangeImpl extends ShellExecExchange {\n\n    @Override\n    @SneakyThrows\n    public Object handle(HttpExchange exchange, Request msg) {\n        var existing = AppBeaconServer.get().getCache().getShellSession(msg.getConnection());\n        AtomicReference<String> out = new AtomicReference<>();\n        AtomicReference<String> err = new AtomicReference<>();\n        long exitCode;\n        try (var command = existing.getControl().command(msg.getCommand()).start()) {\n            var r = command.readStdoutAndStderr();\n            out.set(r[0]);\n            err.set(r[1]);\n            command.close();\n            exitCode = command.getExitCode();\n        }\n        return Response.builder()\n                .stdout(out.get())\n                .stderr(err.get())\n                .exitCode(exitCode)\n                .build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.beacon.AppBeaconServer;\nimport io.xpipe.app.beacon.BeaconShellSession;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.beacon.BeaconClientException;\nimport io.xpipe.beacon.api.ShellStartExchange;\nimport io.xpipe.core.JacksonMapper;\n\nimport com.sun.net.httpserver.HttpExchange;\nimport lombok.SneakyThrows;\n\npublic class ShellStartExchangeImpl extends ShellStartExchange {\n\n    @Override\n    @SneakyThrows\n    public Object handle(HttpExchange exchange, Request msg) {\n        var e = DataStorage.get()\n                .getStoreEntryIfPresent(msg.getConnection())\n                .orElseThrow(() -> new BeaconClientException(\"Unknown connection\"));\n        if (!(e.getStore() instanceof ShellStore s)) {\n            throw new BeaconClientException(\"Not a shell connection\");\n        }\n\n        var existing = AppBeaconServer.get().getCache().getShellSessions().stream()\n                .filter(beaconShellSession -> beaconShellSession.getEntry().equals(e))\n                .findFirst();\n        var control = (existing.isPresent()\n                ? existing.get().getControl()\n                : s.standaloneControl().start());\n        control.setNonInteractive();\n        control.start();\n\n        var d = control.getShellDialect().getDumbMode();\n        if (!d.supportsAnyPossibleInteraction()) {\n            control.close();\n            d.throwIfUnsupported();\n        }\n\n        if (existing.isEmpty()) {\n            AppBeaconServer.get().getCache().getShellSessions().add(new BeaconShellSession(e, control));\n        }\n        var ttyState =\n                JacksonMapper.getDefault().valueToTree(control.getTtyState()).asText();\n        return Response.builder()\n                .shellDialect(control.getShellDialect().getId())\n                .osType(control.getOsType())\n                .osName(control.getOsName())\n                .temp(control.getSystemTemporaryDirectory())\n                .ttyState(ttyState)\n                .build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/ShellStopExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.beacon.AppBeaconServer;\nimport io.xpipe.beacon.api.ShellStopExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\nimport lombok.SneakyThrows;\n\npublic class ShellStopExchangeImpl extends ShellStopExchange {\n\n    @Override\n    @SneakyThrows\n    public Object handle(HttpExchange exchange, Request msg) {\n        var e = AppBeaconServer.get().getCache().getShellSession(msg.getConnection());\n        e.getControl().close();\n        AppBeaconServer.get().getCache().getShellSessions().remove(e);\n        return Response.builder().build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/SshLaunchExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.process.ShellDialects;\nimport io.xpipe.app.terminal.TerminalLauncherManager;\nimport io.xpipe.beacon.api.SshLaunchExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\n\nimport java.util.List;\n\npublic class SshLaunchExchangeImpl extends SshLaunchExchange {\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) throws Exception {\n        if (\"echo $SHELL\".equals(msg.getArguments())) {\n            return Response.builder().command(List.of(\"echo\", \"/bin/bash\")).build();\n        }\n\n        var usedDialect = ShellDialects.getStartableDialects().stream()\n                .filter(dialect -> dialect.getExecutableName().equalsIgnoreCase(msg.getArguments()))\n                .findFirst();\n        if (msg.getArguments() != null\n                && usedDialect.isEmpty()\n                && !msg.getArguments().contains(\"SSH_ORIGINAL_COMMAND\")) {\n            return Response.builder().command(List.of()).build();\n        }\n\n        // There are sometimes multiple requests by a terminal client (e.g. Termius)\n        // This might fail sometimes, but it is expected\n        var r = TerminalLauncherManager.sshLaunchExchange();\n        var c = ProcessControlProvider.get()\n                .getEffectiveLocalDialect()\n                .getOpenScriptCommand(r.toString())\n                .buildBaseParts(null);\n        return Response.builder().command(c).build();\n    }\n\n    @Override\n    public boolean requiresEnabledApi() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/TerminalExternalLaunchExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStorageQuery;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.terminal.TerminalLauncherManager;\nimport io.xpipe.beacon.BeaconClientException;\nimport io.xpipe.beacon.BeaconServerException;\nimport io.xpipe.beacon.api.TerminalExternalLaunchExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\n\nimport java.util.List;\n\npublic class TerminalExternalLaunchExchangeImpl extends TerminalExternalLaunchExchange {\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException, BeaconServerException {\n        var found = DataStorageQuery.queryUserInput(msg.getConnection());\n        if (found.isEmpty()) {\n            throw new BeaconClientException(\"No connection found for input \" + msg.getConnection());\n        }\n\n        if (found.size() > 1) {\n            throw new BeaconClientException(\"Multiple connections found: \"\n                    + found.stream().map(DataStoreEntry::getName).toList());\n        }\n\n        var e = found.getFirst();\n        var isShell = e.getStore() instanceof ShellStore;\n        if (!isShell) {\n            throw new BeaconClientException(\n                    \"Connection \" + DataStorage.get().getStorePath(e).toString() + \" is not a shell connection\");\n        }\n\n        if (!checkPermission()) {\n            return Response.builder().command(List.of()).build();\n        }\n\n        var r = TerminalLauncherManager.externalExchange(e.ref(), msg.getArguments());\n        return Response.builder().command(r).build();\n    }\n\n    @Override\n    public boolean requiresEnabledApi() {\n        return false;\n    }\n\n    @Override\n    public Object getSynchronizationObject() {\n        return DataStorage.get();\n    }\n\n    private boolean checkPermission() {\n        var cache = AppCache.getBoolean(\"externalLaunchPermitted\", false);\n        if (cache) {\n            return true;\n        }\n\n        var r = AppDialog.confirm(\"externalLaunch\");\n        if (r) {\n            AppCache.update(\"externalLaunchPermitted\", true);\n        }\n        return r;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/TerminalLaunchExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.terminal.TerminalLauncherManager;\nimport io.xpipe.beacon.BeaconClientException;\nimport io.xpipe.beacon.BeaconServerException;\nimport io.xpipe.beacon.api.TerminalLaunchExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\n\npublic class TerminalLaunchExchangeImpl extends TerminalLaunchExchange {\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException, BeaconServerException {\n        var r = TerminalLauncherManager.launchExchange(msg.getRequest());\n        return Response.builder().targetFile(r).build();\n    }\n\n    @Override\n    public boolean requiresEnabledApi() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/TerminalPrepareExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.beacon.api.TerminalPrepareExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\n\npublic class TerminalPrepareExchangeImpl extends TerminalPrepareExchange {\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) {\n        var term = AppPrefs.get().terminalType().getValue();\n        var unicode = term.supportsUnicode();\n        var escapes = term.supportsEscapes();\n        return Response.builder()\n                .supportsUnicode(unicode)\n                .supportsEscapeSequences(escapes)\n                .build();\n    }\n\n    @Override\n    public boolean requiresEnabledApi() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/TerminalRegisterExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.terminal.TerminalLauncherManager;\nimport io.xpipe.app.terminal.TerminalView;\nimport io.xpipe.beacon.BeaconClientException;\nimport io.xpipe.beacon.api.TerminalRegisterExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\n\npublic class TerminalRegisterExchangeImpl extends TerminalRegisterExchange {\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {\n        TerminalView.get().open(msg.getRequest(), msg.getPid());\n        TerminalLauncherManager.registerPid(msg.getRequest(), msg.getPid());\n        return Response.builder().build();\n    }\n\n    @Override\n    public boolean requiresEnabledApi() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/impl/TerminalWaitExchangeImpl.java",
    "content": "package io.xpipe.app.beacon.impl;\n\nimport io.xpipe.app.terminal.TerminalLauncherManager;\nimport io.xpipe.beacon.BeaconServerException;\nimport io.xpipe.beacon.api.TerminalWaitExchange;\n\nimport com.sun.net.httpserver.HttpExchange;\n\npublic class TerminalWaitExchangeImpl extends TerminalWaitExchange {\n\n    @Override\n    public Object handle(HttpExchange exchange, Request msg) throws BeaconServerException {\n        TerminalLauncherManager.waitExchange(msg.getRequest());\n        return Response.builder().build();\n    }\n\n    @Override\n    public boolean requiresEnabledApi() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/mcp/AppMcpServer.java",
    "content": "package io.xpipe.app.beacon.mcp;\n\nimport io.xpipe.app.core.AppNames;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.sun.net.httpserver.HttpExchange;\nimport com.sun.net.httpserver.HttpHandler;\nimport io.modelcontextprotocol.common.McpTransportContext;\nimport io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;\nimport io.modelcontextprotocol.server.McpServerFeatures;\nimport io.modelcontextprotocol.server.McpSyncServer;\nimport io.modelcontextprotocol.spec.HttpHeaders;\nimport io.modelcontextprotocol.spec.McpSchema;\nimport lombok.SneakyThrows;\nimport lombok.Value;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\n@Value\npublic class AppMcpServer {\n\n    private static AppMcpServer INSTANCE;\n\n    McpSyncServer mcpSyncServer;\n    HttpStreamableServerTransportProvider transportProvider;\n    List<McpServerFeatures.SyncToolSpecification> readOnlyTools;\n    List<McpServerFeatures.SyncToolSpecification> mutationTools;\n\n    public static AppMcpServer get() {\n        return INSTANCE;\n    }\n\n    @SneakyThrows\n    public static void init() {\n        var transportProvider = new HttpStreamableServerTransportProvider(\n                new JacksonMcpJsonMapper(new ObjectMapper()),\n                \"/mcp\",\n                false,\n                (serverRequest) -> McpTransportContext.EMPTY,\n                null);\n\n        McpSyncServer syncServer = io.modelcontextprotocol.server.McpServer.sync(transportProvider)\n                .serverInfo(AppNames.ofCurrent().getName(), AppProperties.get().getVersion())\n                .capabilities(McpSchema.ServerCapabilities.builder()\n                        .resources(false, false)\n                        .tools(true)\n                        .prompts(false)\n                        .build())\n                .instructions(AppPrefs.get().mcpAdditionalContext().getValue())\n                .build();\n\n        var readOnlyTools = new ArrayList<McpServerFeatures.SyncToolSpecification>();\n        readOnlyTools.add(McpTools.help());\n        readOnlyTools.add(McpTools.listSystems());\n        readOnlyTools.add(McpTools.readFile());\n        readOnlyTools.add(McpTools.listFiles());\n        readOnlyTools.add(McpTools.findFile());\n        readOnlyTools.add(McpTools.getFileInfo());\n\n        var mutationTools = new ArrayList<McpServerFeatures.SyncToolSpecification>();\n        mutationTools.add(McpTools.openTerminal());\n        mutationTools.add(McpTools.openTerminalInline());\n        mutationTools.add(McpTools.createFile());\n        mutationTools.add(McpTools.writeFile());\n        mutationTools.add(McpTools.createDirectory());\n        mutationTools.add(McpTools.runCommand());\n        mutationTools.add(McpTools.runScript());\n        mutationTools.add(McpTools.toggleState());\n\n        for (McpServerFeatures.SyncToolSpecification readOnlyTool : readOnlyTools) {\n            syncServer.addTool(readOnlyTool);\n        }\n\n        var toolsAdded = new AtomicBoolean();\n        AppPrefs.get().enableMcpMutationTools().subscribe(value -> {\n            for (var mutationTool : mutationTools) {\n                if (value) {\n                    syncServer.addTool(mutationTool);\n                } else if (toolsAdded.get()) {\n                    syncServer.removeTool(mutationTool.tool().name());\n                }\n            }\n            if (value) {\n                toolsAdded.set(true);\n            }\n            syncServer.notifyToolsListChanged();\n        });\n\n        INSTANCE = new AppMcpServer(syncServer, transportProvider, readOnlyTools, mutationTools);\n    }\n\n    public static void reset() {\n        INSTANCE.mcpSyncServer.close();\n        INSTANCE = null;\n    }\n\n    public HttpHandler createHttpHandler() {\n        return new HttpHandler() {\n\n            @Override\n            public void handle(HttpExchange exchange) throws IOException {\n                try (exchange) {\n                    if (AppPrefs.get() == null) {\n                        transportProvider.sendError(exchange, 503, \"Not initialized\");\n                        return;\n                    }\n\n                    if (!AppPrefs.get().enableMcpServer().get()) {\n                        transportProvider.sendError(exchange, 403, \"MCP server is not enabled in the settings menu\");\n                        if (exchange.getRequestMethod().equals(\"POST\")) {\n                            ThreadHelper.runAsync(() -> {\n                                ErrorEventFactory.fromMessage(\n                                                \"An external request was made to the XPipe MCP server, however the MCP server is not enabled in the\"\n                                                        + \" settings menu\")\n                                        .expected()\n                                        .handle();\n                            });\n                        }\n                        return;\n                    }\n\n                    if (exchange.getRequestMethod().equals(\"GET\")\n                            && exchange.getRequestHeaders().getFirst(HttpHeaders.MCP_SESSION_ID) == null) {\n                        var msg = \"Session ID required in mcp-session-id header.\"\n                                + \" Check whether you are using the streamable HTTP transport and not something else like SSE.\";\n                        transportProvider.sendError(exchange, 400, msg);\n                        ThreadHelper.runAsync(() -> {\n                            ErrorEventFactory.fromMessage(msg).expected().handle();\n                        });\n                        return;\n                    }\n\n                    if (!AppPrefs.get().disableApiAuthentication().get()) {\n                        var apiKey = exchange.getRequestHeaders().getFirst(\"Authorization\");\n                        if (apiKey == null) {\n                            transportProvider.sendError(exchange, 403, \"Header Authorization is not set\");\n                            if (exchange.getRequestMethod().equals(\"POST\")) {\n                                ThreadHelper.runAsync(() -> {\n                                    ErrorEventFactory.fromMessage(\n                                                    \"An external request was made to the XPipe MCP server without the header Authorization set. \"\n                                                            + \"Please configure your MCP client with the Bearer API token you can find the API \"\n                                                            + \"settings menu\")\n                                            .expected()\n                                            .handle();\n                                });\n                            }\n                            return;\n                        }\n\n                        var correct = apiKey.replace(\"Bearer \", \"\")\n                                .equals(AppPrefs.get().apiKey().get());\n                        if (!correct) {\n                            transportProvider.sendError(exchange, 403, \"Invalid API key\");\n                            if (exchange.getRequestMethod().equals(\"POST\")) {\n                                ThreadHelper.runAsync(() -> {\n                                    ErrorEventFactory.fromMessage(\n                                                    \"The Authorization header sent by the MCP client is not correct\")\n                                            .expected()\n                                            .handle();\n                                });\n                            }\n                            return;\n                        }\n                    }\n\n                    if (exchange.getRequestMethod().equals(\"GET\")) {\n                        transportProvider.doGet(exchange);\n                    } else if (exchange.getRequestMethod().equals(\"POST\")) {\n                        transportProvider.doPost(exchange);\n                    } else if (exchange.getRequestMethod().equals(\"DELETE\")) {\n                        transportProvider.doDelete(exchange);\n                    } else {\n                        transportProvider.doOther(exchange);\n                    }\n                }\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/mcp/HttpStreamableServerTransportProvider.java",
    "content": "/*\n * Copyright 2024-2024 the original author or authors.\n */\n\npackage io.xpipe.app.beacon.mcp;\n\nimport io.xpipe.app.issue.TrackEvent;\n\nimport com.sun.net.httpserver.HttpExchange;\nimport io.modelcontextprotocol.common.McpTransportContext;\nimport io.modelcontextprotocol.json.McpJsonMapper;\nimport io.modelcontextprotocol.json.TypeRef;\nimport io.modelcontextprotocol.server.McpTransportContextExtractor;\nimport io.modelcontextprotocol.spec.*;\nimport io.modelcontextprotocol.util.Assert;\nimport io.modelcontextprotocol.util.KeepAliveScheduler;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\nimport java.io.*;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.locks.ReentrantLock;\n\npublic class HttpStreamableServerTransportProvider implements McpStreamableServerTransportProvider {\n\n    public static final String MESSAGE_EVENT_TYPE = \"message\";\n\n    public static final String UTF_8 = \"UTF-8\";\n    public static final String APPLICATION_JSON = \"application/json\";\n    public static final String TEXT_EVENT_STREAM = \"text/event-stream\";\n    public static final String FAILED_TO_SEND_ERROR_RESPONSE = \"Failed to send error response: {}\";\n    private static final Logger logger = LoggerFactory.getLogger(HttpStreamableServerTransportProvider.class);\n\n    private static final String ACCEPT = \"Accept\";\n\n    private final String mcpEndpoint;\n\n    private final boolean disallowDelete;\n\n    private final McpJsonMapper jsonMapper;\n\n    private final ConcurrentHashMap<String, McpStreamableServerSession> sessions = new ConcurrentHashMap<>();\n\n    private final McpTransportContextExtractor<HttpExchange> contextExtractor;\n    private McpStreamableServerSession.Factory sessionFactory;\n\n    private volatile boolean isClosing = false;\n\n    private KeepAliveScheduler keepAliveScheduler;\n\n    HttpStreamableServerTransportProvider(\n            McpJsonMapper jsonMapper,\n            String mcpEndpoint,\n            boolean disallowDelete,\n            McpTransportContextExtractor<HttpExchange> contextExtractor,\n            Duration keepAliveInterval) {\n        Assert.notNull(jsonMapper, \"ObjectMapper must not be null\");\n        Assert.notNull(mcpEndpoint, \"MCP endpoint must not be null\");\n        Assert.notNull(contextExtractor, \"Context extractor must not be null\");\n\n        this.jsonMapper = jsonMapper;\n        this.mcpEndpoint = mcpEndpoint;\n        this.disallowDelete = disallowDelete;\n        this.contextExtractor = contextExtractor;\n\n        if (keepAliveInterval != null) {\n\n            this.keepAliveScheduler = KeepAliveScheduler.builder(\n                            () -> (isClosing) ? Flux.empty() : Flux.fromIterable(sessions.values()))\n                    .initialDelay(keepAliveInterval)\n                    .interval(keepAliveInterval)\n                    .build();\n\n            this.keepAliveScheduler.start();\n        }\n    }\n\n    public List<String> protocolVersions() {\n        return List.of(\n                ProtocolVersions.MCP_2024_11_05, ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);\n    }\n\n    @Override\n    public void setSessionFactory(McpStreamableServerSession.Factory sessionFactory) {\n        this.sessionFactory = sessionFactory;\n    }\n\n    @Override\n    public Mono<Void> notifyClients(String method, Object params) {\n        if (this.sessions.isEmpty()) {\n            logger.debug(\"No active sessions to broadcast message to\");\n            return Mono.empty();\n        }\n\n        logger.debug(\"Attempting to broadcast message to {} active sessions\", this.sessions.size());\n\n        return Mono.fromRunnable(() -> {\n            this.sessions.values().parallelStream().forEach(session -> {\n                try {\n                    session.sendNotification(method, params).block();\n                } catch (Exception e) {\n                    logger.error(\"Failed to send message to session {}: {}\", session.getId(), e.getMessage());\n                }\n            });\n        });\n    }\n\n    /**\n     * Initiates a graceful shutdown of the transport.\n     *\n     * @return A Mono that completes when all cleanup operations are finished\n     */\n    @Override\n    public Mono<Void> closeGracefully() {\n        return Mono.fromRunnable(() -> {\n                    this.isClosing = true;\n                    logger.debug(\"Initiating graceful shutdown with {} active sessions\", this.sessions.size());\n\n                    this.sessions.values().parallelStream().forEach(session -> {\n                        try {\n                            session.closeGracefully().block();\n                        } catch (Exception e) {\n                            logger.error(\"Failed to close session {}: {}\", session.getId(), e.getMessage());\n                        }\n                    });\n\n                    this.sessions.clear();\n                    logger.debug(\"Graceful shutdown completed\");\n                })\n                .then()\n                .doOnSuccess(v -> {\n                    sessions.clear();\n                    logger.debug(\"Graceful shutdown completed\");\n                    if (this.keepAliveScheduler != null) {\n                        this.keepAliveScheduler.shutdown();\n                    }\n                });\n    }\n\n    public void doGet(HttpExchange exchange) throws IOException {\n\n        String requestURI = exchange.getRequestURI().toString();\n        if (!requestURI.endsWith(mcpEndpoint)) {\n            sendError(exchange, 404, null);\n            return;\n        }\n\n        if (this.isClosing) {\n            sendError(exchange, 503, \"Server is shutting down\");\n            return;\n        }\n\n        List<String> badRequestErrors = new ArrayList<>();\n\n        String accept = exchange.getRequestHeaders().getFirst(ACCEPT);\n        if (accept == null || !accept.contains(TEXT_EVENT_STREAM)) {\n            badRequestErrors.add(\"text/event-stream required in Accept header\");\n        }\n\n        String sessionId = exchange.getRequestHeaders().getFirst(HttpHeaders.MCP_SESSION_ID);\n\n        if (sessionId == null || sessionId.isBlank()) {\n            badRequestErrors.add(\"Session ID required in mcp-session-id header\");\n        }\n\n        if (!badRequestErrors.isEmpty()) {\n            String combinedMessage = String.join(\"; \", badRequestErrors);\n            this.sendError(exchange, 400, combinedMessage);\n            return;\n        }\n\n        McpStreamableServerSession session = this.sessions.get(sessionId);\n\n        if (session == null) {\n            sendError(exchange, 404, null);\n            return;\n        }\n\n        logger.debug(\"Handling GET request for session: {}\", sessionId);\n\n        McpTransportContext transportContext = this.contextExtractor.extract(exchange);\n\n        try {\n            exchange.getResponseHeaders().add(\"Content-Type\", TEXT_EVENT_STREAM);\n            exchange.getResponseHeaders().add(\"Content-Encoding\", UTF_8);\n            exchange.getResponseHeaders().add(\"Cache-Control\", \"no-cache\");\n            exchange.getResponseHeaders().add(\"Connection\", \"keep-alive\");\n            exchange.getResponseHeaders().add(\"Access-Control-Allow-Origin\", \"*\");\n            exchange.sendResponseHeaders(200, 0);\n\n            var writer = new PrintWriter(exchange.getResponseBody());\n            HttpServletStreamableMcpSessionTransport sessionTransport =\n                    new HttpServletStreamableMcpSessionTransport(sessionId, exchange, writer);\n\n            // Check if this is a replay request\n            if (exchange.getRequestHeaders().getFirst(HttpHeaders.LAST_EVENT_ID) != null) {\n                String lastId = exchange.getRequestHeaders().getFirst(HttpHeaders.LAST_EVENT_ID);\n\n                try {\n                    session.replay(lastId)\n                            .contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext))\n                            .toIterable()\n                            .forEach(message -> {\n                                try {\n                                    sessionTransport\n                                            .sendMessage(message)\n                                            .contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext))\n                                            .block();\n                                } catch (Exception e) {\n                                    logger.error(\"Failed to replay message: {}\", e.getMessage());\n                                    exchange.close();\n                                }\n                            });\n                } catch (Exception e) {\n                    logger.error(\"Failed to replay messages: {}\", e.getMessage());\n                    exchange.close();\n                }\n            }\n        } catch (Exception e) {\n            logger.error(\"Failed to handle GET request for session {}: {}\", sessionId, e.getMessage());\n            sendError(exchange, 500, null);\n        }\n    }\n\n    public void sendError(HttpExchange exchange, int code, String message) throws IOException {\n        var b = message != null ? message.getBytes(StandardCharsets.UTF_8) : new byte[0];\n        exchange.getResponseHeaders().add(\"Content-Encoding\", UTF_8);\n        exchange.sendResponseHeaders(code, b.length != 0 ? b.length : -1);\n        try (OutputStream os = exchange.getResponseBody()) {\n            os.write(b);\n        }\n\n        TrackEvent.error(\"MCP server error \" + code + \": \" + message);\n    }\n\n    public void doPost(HttpExchange exchange) throws IOException {\n\n        String requestURI = exchange.getRequestURI().toString();\n        if (!requestURI.endsWith(mcpEndpoint)) {\n            sendError(exchange, 404, null);\n            return;\n        }\n\n        if (this.isClosing) {\n            sendError(exchange, 503, \"Server is shutting down\");\n            return;\n        }\n\n        List<String> badRequestErrors = new ArrayList<>();\n\n        String accept = exchange.getRequestHeaders().getFirst(ACCEPT);\n        if (accept == null || !accept.contains(TEXT_EVENT_STREAM)) {\n            badRequestErrors.add(\"text/event-stream required in Accept header\");\n        }\n        if (accept == null || !accept.contains(APPLICATION_JSON)) {\n            badRequestErrors.add(\"application/json required in Accept header\");\n        }\n\n        McpTransportContext transportContext = this.contextExtractor.extract(exchange);\n\n        try {\n            var body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);\n\n            McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body);\n\n            // Handle initialization request\n            if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest\n                    && jsonrpcRequest.method().equals(McpSchema.METHOD_INITIALIZE)) {\n                if (!badRequestErrors.isEmpty()) {\n                    String combinedMessage = String.join(\"; \", badRequestErrors);\n                    this.sendError(exchange, 400, combinedMessage);\n                    return;\n                }\n\n                McpSchema.InitializeRequest initializeRequest =\n                        jsonMapper.convertValue(jsonrpcRequest.params(), new TypeRef<>() {});\n                McpStreamableServerSession.McpStreamableServerSessionInit init =\n                        this.sessionFactory.startSession(initializeRequest);\n                this.sessions.put(init.session().getId(), init.session());\n\n                try {\n                    McpSchema.InitializeResult initResult = init.initResult().block();\n\n                    String jsonResponse = jsonMapper.writeValueAsString(new McpSchema.JSONRPCResponse(\n                            McpSchema.JSONRPC_VERSION, jsonrpcRequest.id(), initResult, null));\n                    var jsonBytes = jsonResponse.getBytes(StandardCharsets.UTF_8);\n\n                    exchange.getResponseHeaders().add(\"Content-Type\", APPLICATION_JSON);\n                    exchange.getResponseHeaders().add(\"Content-Encoding\", UTF_8);\n                    exchange.getResponseHeaders()\n                            .add(HttpHeaders.MCP_SESSION_ID, init.session().getId());\n                    exchange.sendResponseHeaders(200, jsonBytes.length);\n                    exchange.getResponseBody().write(jsonBytes);\n                    return;\n                } catch (Exception e) {\n                    logger.error(\"Failed to initialize session: {}\", e.getMessage());\n                    this.sendError(exchange, 500, \"Failed to initialize session: \" + e.getMessage());\n                    return;\n                }\n            }\n\n            String sessionId = exchange.getRequestHeaders().getFirst(HttpHeaders.MCP_SESSION_ID);\n\n            if (sessionId == null || sessionId.isBlank()) {\n                badRequestErrors.add(\"Session ID required in mcp-session-id header\");\n            }\n\n            if (!badRequestErrors.isEmpty()) {\n                String combinedMessage = String.join(\"; \", badRequestErrors);\n                this.sendError(exchange, 400, combinedMessage);\n                return;\n            }\n\n            McpStreamableServerSession session = this.sessions.get(sessionId);\n\n            if (session == null) {\n                this.sendError(exchange, 404, \"Session not found: \" + sessionId + \". Was the session not refreshed?\");\n                return;\n            }\n\n            if (message instanceof McpSchema.JSONRPCResponse jsonrpcResponse) {\n                session.accept(jsonrpcResponse)\n                        .contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext))\n                        .block();\n                exchange.sendResponseHeaders(200, -1);\n            } else if (message instanceof McpSchema.JSONRPCNotification jsonrpcNotification) {\n                session.accept(jsonrpcNotification)\n                        .contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext))\n                        .block();\n                exchange.sendResponseHeaders(202, -1);\n            } else if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) {\n                // For streaming responses, we need to return SSE\n                exchange.getResponseHeaders().add(\"Content-Type\", TEXT_EVENT_STREAM);\n                exchange.getResponseHeaders().add(\"Content-Encoding\", UTF_8);\n                exchange.getResponseHeaders().add(\"Cache-Control\", \"no-cache\");\n                exchange.getResponseHeaders().add(\"Connection\", \"keep-alive\");\n                exchange.getResponseHeaders().add(\"Access-Control-Allow-Origin\", \"*\");\n                exchange.sendResponseHeaders(200, 0);\n\n                var writer = new PrintWriter(exchange.getResponseBody());\n\n                HttpServletStreamableMcpSessionTransport sessionTransport =\n                        new HttpServletStreamableMcpSessionTransport(sessionId, exchange, writer);\n\n                try {\n                    session.responseStream(jsonrpcRequest, sessionTransport)\n                            .contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext))\n                            .block();\n                } catch (Exception e) {\n                    logger.error(\"Failed to handle request stream: {}\", e.getMessage());\n                    exchange.close();\n                }\n            } else {\n                this.sendError(exchange, 500, \"Unknown message type\");\n            }\n        } catch (IllegalArgumentException | IOException e) {\n            logger.error(\"Failed to deserialize message: {}\", e.getMessage());\n            this.sendError(exchange, 400, \"Invalid message format: \" + e.getMessage());\n        } catch (Exception e) {\n            logger.error(\"Error handling message: {}\", e.getMessage());\n            try {\n                this.sendError(exchange, 500, \"Error processing message: \" + e.getMessage());\n            } catch (IOException ex) {\n                logger.error(FAILED_TO_SEND_ERROR_RESPONSE, ex.getMessage());\n                sendError(exchange, 500, \"Error processing message\");\n            }\n        }\n    }\n\n    public void doOther(HttpExchange exchange) throws IOException {\n        sendError(exchange, 405, \"Unsupported HTTP method: \" + exchange.getRequestMethod());\n    }\n\n    protected void doDelete(HttpExchange exchange) throws IOException {\n\n        String requestURI = exchange.getRequestURI().toString();\n        if (!requestURI.endsWith(mcpEndpoint)) {\n            sendError(exchange, 404, null);\n            return;\n        }\n\n        if (this.isClosing) {\n            sendError(exchange, 503, \"Server is shutting down\");\n            return;\n        }\n\n        if (this.disallowDelete) {\n            sendError(exchange, 405, null);\n            return;\n        }\n\n        McpTransportContext transportContext = this.contextExtractor.extract(exchange);\n\n        if (exchange.getRequestHeaders().getFirst(HttpHeaders.MCP_SESSION_ID) == null) {\n            sendError(exchange, 400, \"Session ID required in mcp-session-id header\");\n            return;\n        }\n\n        String sessionId = exchange.getRequestHeaders().getFirst(HttpHeaders.MCP_SESSION_ID);\n        McpStreamableServerSession session = this.sessions.get(sessionId);\n\n        if (session == null) {\n            sendError(exchange, 404, null);\n            return;\n        }\n\n        try {\n            session.delete()\n                    .contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext))\n                    .block();\n            this.sessions.remove(sessionId);\n            exchange.sendResponseHeaders(200, -1);\n        } catch (Exception e) {\n            logger.error(\"Failed to delete session {}: {}\", sessionId, e.getMessage());\n            try {\n                sendError(exchange, 500, e.getMessage());\n            } catch (IOException ex) {\n                logger.error(FAILED_TO_SEND_ERROR_RESPONSE, ex.getMessage());\n                sendError(exchange, 500, \"Error deleting session\");\n            }\n        }\n    }\n\n    private void sendEvent(PrintWriter writer, String eventType, String data, String id) throws IOException {\n        if (id != null) {\n            writer.write(\"id: \" + id + \"\\n\");\n        }\n        writer.write(\"event: \" + eventType + \"\\n\");\n        writer.write(\"data: \" + data + \"\\n\\n\");\n        writer.flush();\n\n        if (writer.checkError()) {\n            throw new IOException(\"Client disconnected\");\n        }\n    }\n\n    private class HttpServletStreamableMcpSessionTransport implements McpStreamableServerTransport {\n\n        private final String sessionId;\n\n        private final HttpExchange exchange;\n\n        private final PrintWriter writer;\n        private final ReentrantLock lock = new ReentrantLock();\n        private volatile boolean closed = false;\n\n        HttpServletStreamableMcpSessionTransport(String sessionId, HttpExchange exchange, PrintWriter writer) {\n            this.sessionId = sessionId;\n            this.exchange = exchange;\n            this.writer = writer;\n            logger.debug(\"Streamable session transport {} initialized with SSE writer\", sessionId);\n        }\n\n        @Override\n        public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {\n            return jsonMapper.convertValue(data, typeRef);\n        }\n\n        @Override\n        public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message, String messageId) {\n            return Mono.fromRunnable(() -> {\n                if (this.closed) {\n                    logger.debug(\"Attempted to send message to closed session: {}\", this.sessionId);\n                    return;\n                }\n\n                lock.lock();\n                try {\n                    if (this.closed) {\n                        logger.debug(\"Session {} was closed during message send attempt\", this.sessionId);\n                        return;\n                    }\n\n                    String jsonText = jsonMapper.writeValueAsString(message);\n                    HttpStreamableServerTransportProvider.this.sendEvent(\n                            writer, MESSAGE_EVENT_TYPE, jsonText, messageId != null ? messageId : this.sessionId);\n                    logger.debug(\"Message sent to session {} with ID {}\", this.sessionId, messageId);\n                } catch (Exception e) {\n                    var clientDisconnected = \"Client disconnected\".equals(e.getMessage());\n                    if (!clientDisconnected) {\n                        logger.error(\"Failed to send message to session {}: {}\", this.sessionId, e.getMessage());\n                        HttpStreamableServerTransportProvider.this.sessions.remove(this.sessionId);\n                        exchange.close();\n                    }\n                } finally {\n                    lock.unlock();\n                }\n            });\n        }\n\n        @Override\n        public void close() {\n            lock.lock();\n            try {\n                if (this.closed) {\n                    logger.debug(\"Session transport {} already closed\", this.sessionId);\n                    return;\n                }\n\n                this.closed = true;\n\n                // HttpServletStreamableServerTransportProvider.this.sessions.remove(this.sessionId);\n                exchange.close();\n                logger.debug(\"Successfully completed async context for session {}\", sessionId);\n            } catch (Exception e) {\n                logger.warn(\"Failed to complete async context for session {}: {}\", sessionId, e.getMessage());\n            } finally {\n                lock.unlock();\n            }\n        }\n\n        @Override\n        public Mono<Void> closeGracefully() {\n            return Mono.fromRunnable(() -> {\n                HttpServletStreamableMcpSessionTransport.this.close();\n            });\n        }\n\n        @Override\n        public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {\n            return sendMessage(message, null);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/mcp/McpSchemaFiles.java",
    "content": "package io.xpipe.app.beacon.mcp;\n\nimport io.xpipe.core.JacksonMapper;\n\nimport io.modelcontextprotocol.spec.McpSchema;\n\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\n\npublic class McpSchemaFiles {\n\n    public static String load(String name) throws IOException {\n        try (var in = McpSchemaFiles.class.getResourceAsStream(\"/io/xpipe/app/resources/mcp/\" + name)) {\n            return new String(in.readAllBytes(), StandardCharsets.UTF_8);\n        }\n    }\n\n    public static McpSchema.Tool loadTool(String name) throws IOException {\n        var s = load(name);\n        return JacksonMapper.getDefault().readValue(s, McpSchema.Tool.class);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/mcp/McpToolHandler.java",
    "content": "package io.xpipe.app.beacon.mcp;\n\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStorageQuery;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.beacon.BeaconClientException;\nimport io.xpipe.core.FilePath;\n\nimport io.modelcontextprotocol.server.McpSyncServerExchange;\nimport io.modelcontextprotocol.spec.McpSchema;\nimport lombok.SneakyThrows;\n\nimport java.util.Optional;\nimport java.util.function.BiFunction;\n\npublic interface McpToolHandler\n        extends BiFunction<McpSyncServerExchange, McpSchema.CallToolRequest, McpSchema.CallToolResult> {\n\n    static McpToolHandler of(McpToolHandler t) {\n        return t;\n    }\n\n    @Override\n    @SneakyThrows\n    default McpSchema.CallToolResult apply(\n            McpSyncServerExchange mcpSyncServerExchange, McpSchema.CallToolRequest callToolRequest) {\n        var req = new ToolRequest(mcpSyncServerExchange, callToolRequest);\n        try {\n            return handle(req);\n        } catch (BeaconClientException e) {\n            ErrorEventFactory.fromThrowable(e).expected().omit().handle();\n            return McpSchema.CallToolResult.builder()\n                    .addTextContent(e.getMessage())\n                    .isError(true)\n                    .build();\n        } catch (Throwable e) {\n            ErrorEventFactory.fromThrowable(e).omit().handle();\n            return McpSchema.CallToolResult.builder()\n                    .addTextContent(e.getMessage())\n                    .isError(true)\n                    .build();\n        }\n    }\n\n    McpSchema.CallToolResult handle(ToolRequest request) throws Exception;\n\n    class ToolRequest {\n\n        protected final McpSyncServerExchange exchange;\n        protected final McpSchema.CallToolRequest request;\n\n        public ToolRequest(McpSyncServerExchange exchange, McpSchema.CallToolRequest request) {\n            this.exchange = exchange;\n            this.request = request;\n        }\n\n        public McpSchema.CallToolRequest getRawRequest() {\n            return request;\n        }\n\n        public Optional<String> getOptionalStringArgument(String key) {\n            var o = request.arguments().get(key);\n            if (o == null) {\n                return Optional.empty();\n            }\n\n            if (!(o instanceof String s) || s.isBlank()) {\n                return Optional.empty();\n            }\n\n            return Optional.of(s);\n        }\n\n        public String getStringArgument(String key) throws BeaconClientException {\n            var o = request.arguments().get(key);\n            if (o == null) {\n                throw new BeaconClientException(\"Missing argument for key \" + key);\n            }\n\n            if (!(o instanceof String s) || s.isBlank()) {\n                throw new BeaconClientException(\"Invalid argument for key \" + key);\n            }\n\n            return s;\n        }\n\n        public Optional<Boolean> getOptionalBooleanArgument(String key) {\n            var o = request.arguments().get(key);\n            if (o == null) {\n                return Optional.empty();\n            }\n\n            if (!(o instanceof Boolean b)) {\n                return Optional.empty();\n            }\n\n            return Optional.of(b);\n        }\n\n        public boolean getBooleanArgument(String key) throws BeaconClientException {\n            var o = request.arguments().get(key);\n            if (o == null) {\n                throw new BeaconClientException(\"Missing argument for key \" + key);\n            }\n\n            if (!(o instanceof Boolean b)) {\n                throw new BeaconClientException(\"Invalid argument for key \" + key);\n            }\n\n            return b;\n        }\n\n        public FilePath getFilePath(String key) throws BeaconClientException {\n            var s = getStringArgument(key);\n            var path = FilePath.parse(s);\n            if (path == null) {\n                throw new BeaconClientException(\"Invalid argument for key \" + key);\n            }\n            return path;\n        }\n\n        public DataStoreEntryRef<?> getDataStoreRef(String name) throws BeaconClientException {\n            var found = DataStorageQuery.queryUserInput(name);\n            if (found.isEmpty()) {\n                throw new BeaconClientException(\"No connection found for input \" + name);\n            }\n\n            if (found.size() > 1) {\n                throw new BeaconClientException(\"Multiple connections found: \"\n                        + found.stream().map(DataStoreEntry::getName).toList());\n            }\n\n            var e = found.getFirst();\n            return e.ref();\n        }\n\n        public DataStoreEntryRef<ShellStore> getShellStoreRef(String name, boolean mutation) throws BeaconClientException {\n            var ref = getDataStoreRef(name);\n            var isShell = ref.getStore() instanceof ShellStore;\n            if (!isShell) {\n                throw new BeaconClientException(\"Connection \"\n                        + DataStorage.get().getStorePath(ref.get()).toString() + \" is not a shell connection\");\n            }\n\n            var disableMutation = DataStorage.get().getEffectiveCategoryConfig(ref.get()).getDontAllowScripts();\n            if (mutation && disableMutation != null && disableMutation) {\n                throw new BeaconClientException(\"Modifications to connection \"\n                        + DataStorage.get().getStorePath(ref.get()).toString() + \" is disabled by the category setting\");\n            }\n\n            return ref.asNeeded();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/beacon/mcp/McpTools.java",
    "content": "package io.xpipe.app.beacon.mcp;\n\nimport io.xpipe.app.beacon.AppBeaconServer;\nimport io.xpipe.app.core.AppExtensionManager;\nimport io.xpipe.app.core.AppNames;\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.process.ScriptHelper;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.TerminalInitScriptConfig;\nimport io.xpipe.app.process.WorkingDirectoryFunction;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStorageQuery;\nimport io.xpipe.app.terminal.TerminalLaunch;\nimport io.xpipe.app.util.CommandDialog;\nimport io.xpipe.beacon.BeaconClientException;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.JacksonMapper;\n\nimport com.fasterxml.jackson.databind.node.JsonNodeFactory;\nimport io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;\nimport io.modelcontextprotocol.server.McpServerFeatures;\nimport io.modelcontextprotocol.spec.McpSchema;\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\n\npublic final class McpTools {\n\n    public static McpServerFeatures.SyncToolSpecification help() throws IOException {\n        var tool = McpSchemaFiles.loadTool(\"help.json\");\n        return McpServerFeatures.SyncToolSpecification.builder()\n                .tool(tool)\n                .callHandler(McpToolHandler.of((req) -> {\n                    var ro = AppMcpServer.get().getReadOnlyTools().stream()\n                            .filter(syncToolSpecification ->\n                                    !syncToolSpecification.tool().name().equals(\"help\"))\n                            .toList();\n                    var mu = AppMcpServer.get().getMutationTools();\n\n                    var roList = ro.stream()\n                            .map(syncToolSpecification ->\n                                    \"- \" + syncToolSpecification.tool().name() + \": \"\n                                            + syncToolSpecification.tool().description())\n                            .collect(Collectors.joining(\"\\n\"));\n                    var muList = mu.stream()\n                            .map(syncToolSpecification ->\n                                    \"- \" + syncToolSpecification.tool().name() + \": \"\n                                            + syncToolSpecification.tool().description())\n                            .collect(Collectors.joining(\"\\n\"));\n\n                    var text = \"\"\"\n                               The XPipe MCP server offers the following read-only tools:\n                               %s\n                               These tools will not modify anything on your system and are safe to use.\n\n                               You can also enable the following potentially destructive tools in the settings menu:\n                               %s\n                               These tools can perform write operations and other actions that might be potentially destructive.\n                               \"\"\".formatted(roList, muList);\n\n                    return McpSchema.CallToolResult.builder()\n                            .addTextContent(text)\n                            .build();\n                }))\n                .build();\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class ConnectionResource {\n        @NonNull\n        String name;\n\n        @NonNull\n        String path;\n\n        String information;\n\n        String notes;\n    }\n\n    public static McpServerFeatures.SyncToolSpecification listSystems() throws IOException {\n        var tool = McpSchemaFiles.loadTool(\"list_systems.json\");\n        return McpServerFeatures.SyncToolSpecification.builder()\n                .tool(tool)\n                .callHandler(McpToolHandler.of((req) -> {\n                    var filter = req.getOptionalStringArgument(\"filter\");\n                    var entries = filter.isPresent()\n                            ? DataStorageQuery.queryUserInput(filter.get())\n                            : DataStorage.get().getStoreEntries();\n\n                    var list = new ArrayList<ConnectionResource>();\n                    for (var e : entries) {\n                        if (!e.getValidity().isUsable()) {\n                            continue;\n                        }\n\n                        if (!e.getProvider().includeInConnectionCount()) {\n                            continue;\n                        }\n\n                        var section = StoreViewState.get().getSectionForWrapper(StoreViewState.get().getEntryWrapper(e));\n                        var info = section.isPresent() ? e.getProvider().informationString(section.get()).getValue() : null;\n\n                        var r = ConnectionResource.builder()\n                                .name(e.getName())\n                                .path(DataStorage.get().getStorePath(e).toString())\n                                .information(info)\n                                .notes(e.getNotes())\n                                .build();\n                        list.add(r);\n                    }\n\n                    var json = JsonNodeFactory.instance.arrayNode();\n                    for (var e : list) {\n                        json.add(JacksonMapper.getDefault().valueToTree(e));\n                    }\n\n                    var object = JsonNodeFactory.instance.objectNode();\n                    object.set(\"found\", json);\n\n                    return McpSchema.CallToolResult.builder()\n                            .structuredContent(\n                                    new JacksonMcpJsonMapper(JacksonMapper.getDefault()),\n                                    JacksonMapper.getDefault().writeValueAsString(object))\n                            .build();\n                }))\n                .build();\n    }\n\n    public static McpServerFeatures.SyncToolSpecification readFile() throws IOException {\n        var tool = McpSchemaFiles.loadTool(\"read_file.json\");\n        return McpServerFeatures.SyncToolSpecification.builder()\n                .tool(tool)\n                .callHandler(McpToolHandler.of((req) -> {\n                    var path = req.getFilePath(\"path\");\n                    var system = req.getStringArgument(\"system\");\n                    var shellStore = req.getShellStoreRef(system, false);\n                    var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);\n                    var fs = new ConnectionFileSystem(shellSession.getControl());\n\n                    if (!fs.fileExists(path)) {\n                        throw new BeaconClientException(\"File \" + path + \" does not exist\");\n                    }\n\n                    try (var in = fs.openInput(path)) {\n                        var b = in.readAllBytes();\n                        var s = new String(b, StandardCharsets.UTF_8);\n                        return McpSchema.CallToolResult.builder()\n                                .addTextContent(s)\n                                .build();\n                    }\n                }))\n                .build();\n    }\n\n    public static McpServerFeatures.SyncToolSpecification listFiles() throws IOException {\n        var tool = McpSchemaFiles.loadTool(\"list_files.json\");\n        return McpServerFeatures.SyncToolSpecification.builder()\n                .tool(tool)\n                .callHandler(McpToolHandler.of((req) -> {\n                    var path = req.getFilePath(\"path\");\n                    var system = req.getStringArgument(\"system\");\n                    var recursive = req.getOptionalBooleanArgument(\"recursive\").orElse(false);\n                    var shellStore = req.getShellStoreRef(system, false);\n                    var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);\n                    var fs = new ConnectionFileSystem(shellSession.getControl());\n\n                    if (!fs.directoryExists(path)) {\n                        throw new BeaconClientException(\"Directory \" + path + \" does not exist\");\n                    }\n\n                    try (var stream = recursive ? fs.listFilesRecursively(fs, path).stream() : fs.listFiles(fs, path)) {\n                        var list = stream.toList();\n                        var builder = McpSchema.CallToolResult.builder();\n                        for (FileEntry e : list) {\n                            builder.addTextContent(e.getPath().toString());\n                        }\n                        return builder.build();\n                    }\n                }))\n                .build();\n    }\n\n    public static McpServerFeatures.SyncToolSpecification findFile() throws IOException {\n        var tool = McpSchemaFiles.loadTool(\"find_file.json\");\n        return McpServerFeatures.SyncToolSpecification.builder()\n                .tool(tool)\n                .callHandler(McpToolHandler.of((req) -> {\n                    var path = req.getFilePath(\"path\");\n                    var system = req.getStringArgument(\"system\");\n                    var recursive = req.getOptionalBooleanArgument(\"recursive\").orElse(false);\n                    var pattern = req.getStringArgument(\"name\");\n                    var shellStore = req.getShellStoreRef(system, false);\n                    var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);\n                    var fs = new ConnectionFileSystem(shellSession.getControl());\n\n                    if (!fs.directoryExists(path)) {\n                        throw new BeaconClientException(\"Directory \" + path + \" does not exist\");\n                    }\n\n                    var regex = Pattern.compile(DataStorageQuery.toRegex(pattern));\n                    try (var stream = recursive ? fs.listFilesRecursively(fs, path).stream() : fs.listFiles(fs, path)) {\n                        var list = stream.toList();\n                        var builder = McpSchema.CallToolResult.builder();\n                        list.stream()\n                                .filter(fileEntry -> regex.matcher(\n                                                fileEntry.getPath().toString())\n                                        .find())\n                                .forEach(fileEntry -> {\n                                    builder.addTextContent(fileEntry.getPath().toString());\n                                });\n                        return builder.build();\n                    }\n                }))\n                .build();\n    }\n\n    public static McpServerFeatures.SyncToolSpecification getFileInfo() throws IOException {\n        var tool = McpSchemaFiles.loadTool(\"get_file_info.json\");\n        return McpServerFeatures.SyncToolSpecification.builder()\n                .tool(tool)\n                .callHandler(McpToolHandler.of((req) -> {\n                    var path = req.getFilePath(\"path\");\n                    var system = req.getStringArgument(\"system\");\n                    var shellStore = req.getShellStoreRef(system, false);\n                    var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);\n                    var fs = new ConnectionFileSystem(shellSession.getControl());\n\n                    if (!fs.fileExists(path) && !fs.directoryExists(path)) {\n                        throw new BeaconClientException(\"Path \" + path + \" does not exist\");\n                    }\n\n                    var entry = fs.getFileInfo(path);\n                    if (entry.isEmpty()) {\n                        throw new BeaconClientException(\"Path \" + path + \" does not exist\");\n                    }\n\n                    var map = new LinkedHashMap<String, Object>();\n                    map.put(\"path\", entry.get().getPath().toString());\n                    map.put(\"size\", entry.get().getSize());\n                    if (entry.get().getInfo() instanceof FileInfo.Unix u) {\n                        map.put(\"permissions\", u.getPermissions());\n                        map.put(\"user\", u.getUser());\n                        map.put(\"group\", u.getGroup());\n                    } else if (entry.get().getInfo() instanceof FileInfo.Windows w) {\n                        map.put(\"attributes\", w.getAttributes());\n                    }\n                    map.put(\"type\", entry.get().getKind().toString().toLowerCase());\n                    map.put(\"date\", entry.get().getDate().toString());\n                    map.entrySet().removeIf(e -> e.getValue() == null);\n\n                    return McpSchema.CallToolResult.builder()\n                            .structuredContent(map)\n                            .build();\n                }))\n                .build();\n    }\n\n    public static McpServerFeatures.SyncToolSpecification createFile() throws IOException {\n        var tool = McpSchemaFiles.loadTool(\"create_file.json\");\n        return McpServerFeatures.SyncToolSpecification.builder()\n                .tool(tool)\n                .callHandler(McpToolHandler.of((req) -> {\n                    var path = req.getFilePath(\"path\");\n                    var system = req.getStringArgument(\"system\");\n                    var shellStore = req.getShellStoreRef(system, true);\n                    var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);\n                    var fs = new ConnectionFileSystem(shellSession.getControl());\n\n                    if (fs.fileExists(path)) {\n                        throw new BeaconClientException(\"File \" + path + \" does already exist\");\n                    }\n\n                    fs.touch(path);\n\n                    if (req.getRawRequest().arguments().containsKey(\"content\")) {\n                        var s = req.getRawRequest().arguments().get(\"content\").toString();\n                        var b = s.getBytes(StandardCharsets.UTF_8);\n                        try (var out = fs.openOutput(path, b.length)) {\n                            out.write(b);\n                        }\n                    }\n\n                    return McpSchema.CallToolResult.builder()\n                            .addTextContent(\"File created successfully\")\n                            .build();\n                }))\n                .build();\n    }\n\n    public static McpServerFeatures.SyncToolSpecification writeFile() throws IOException {\n        var tool = McpSchemaFiles.loadTool(\"write_file.json\");\n        return McpServerFeatures.SyncToolSpecification.builder()\n                .tool(tool)\n                .callHandler(McpToolHandler.of((req) -> {\n                    var path = req.getFilePath(\"path\");\n                    var system = req.getStringArgument(\"system\");\n                    var content = req.getStringArgument(\"content\");\n                    var shellStore = req.getShellStoreRef(system, true);\n                    var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);\n                    var fs = new ConnectionFileSystem(shellSession.getControl());\n\n                    var b = content.getBytes(StandardCharsets.UTF_8);\n                    try (var out = fs.openOutput(path, b.length)) {\n                        out.write(b);\n                    }\n\n                    return McpSchema.CallToolResult.builder()\n                            .addTextContent(\"File written successfully\")\n                            .build();\n                }))\n                .build();\n    }\n\n    public static McpServerFeatures.SyncToolSpecification createDirectory() throws IOException {\n        var tool = McpSchemaFiles.loadTool(\"create_directory.json\");\n        return McpServerFeatures.SyncToolSpecification.builder()\n                .tool(tool)\n                .callHandler(McpToolHandler.of((req) -> {\n                    var path = req.getFilePath(\"path\");\n                    var system = req.getStringArgument(\"system\");\n                    var shellStore = req.getShellStoreRef(system, true);\n                    var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);\n                    var fs = new ConnectionFileSystem(shellSession.getControl());\n\n                    if (fs.fileExists(path)) {\n                        throw new BeaconClientException(\"Directory \" + path + \" does already exist\");\n                    }\n\n                    fs.mkdirs(path);\n\n                    return McpSchema.CallToolResult.builder()\n                            .addTextContent(\"Directory created successfully\")\n                            .build();\n                }))\n                .build();\n    }\n\n    public static McpServerFeatures.SyncToolSpecification runCommand() throws IOException {\n        var tool = McpSchemaFiles.loadTool(\"run_command.json\");\n        return McpServerFeatures.SyncToolSpecification.builder()\n                .tool(tool)\n                .callHandler(McpToolHandler.of((req) -> {\n                    var command = req.getStringArgument(\"command\");\n                    var system = req.getStringArgument(\"system\");\n                    var shellStore = req.getShellStoreRef(system, true);\n                    var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);\n\n                    var out = ProcessControlProvider.get().executeMcpCommand(shellSession.getControl(), command);\n                    var formatted = CommandDialog.formatOutput(out);\n\n                    return McpSchema.CallToolResult.builder()\n                            .addTextContent(formatted)\n                            .build();\n                }))\n                .build();\n    }\n\n    public static McpServerFeatures.SyncToolSpecification runScript() throws IOException {\n        var tool = McpSchemaFiles.loadTool(\"run_script.json\");\n        return McpServerFeatures.SyncToolSpecification.builder()\n                .tool(tool)\n                .callHandler(McpToolHandler.of((req) -> {\n                    var system = req.getStringArgument(\"system\");\n                    var script = req.getDataStoreRef(\"script\");\n                    var directory = req.getFilePath(\"directory\");\n                    var arguments = req.getStringArgument(\"arguments\");\n\n                    var shellStore = req.getShellStoreRef(system, true);\n                    var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);\n\n                    var clazz = Class.forName(\n                            AppExtensionManager.getInstance()\n                                    .getExtendedLayer()\n                                    .findModule(AppNames.extModuleName(\"base\"))\n                                    .orElseThrow(),\n                            AppNames.extModuleName(\"base\") + \".script.ScriptStore\");\n                    var method = clazz.getDeclaredMethod(\"assembleScriptChain\", ShellControl.class);\n                    var command = (String) method.invoke(script.getStore(), shellSession.getControl());\n                    var scriptFile = ScriptHelper.createExecScript(shellSession.getControl(), command);\n                    var out = shellSession\n                            .getControl()\n                            .command(shellSession\n                                            .getControl()\n                                            .getShellDialect()\n                                            .runScriptCommand(shellSession.getControl(), scriptFile.toString())\n                                    + arguments)\n                            .withWorkingDirectory(directory)\n                            .readStdoutOrThrow();\n                    var formatted = CommandDialog.formatOutput(out);\n\n                    return McpSchema.CallToolResult.builder()\n                            .addTextContent(formatted)\n                            .build();\n                }))\n                .build();\n    }\n\n    public static McpServerFeatures.SyncToolSpecification openTerminal() throws IOException {\n        var tool = McpSchemaFiles.loadTool(\"open_terminal.json\");\n        return McpServerFeatures.SyncToolSpecification.builder()\n                .tool(tool)\n                .callHandler(McpToolHandler.of((req) -> {\n                    var system = req.getStringArgument(\"system\");\n                    var directory = req.getOptionalStringArgument(\"directory\");\n                    var shellStore = req.getShellStoreRef(system, true);\n                    var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);\n\n                    TerminalLaunch.builder()\n                            .entry(shellStore.get())\n                            .directory(FilePath.of(directory.orElse(null)))\n                            .command(shellSession.getControl())\n                            .launch();\n\n                    return McpSchema.CallToolResult.builder()\n                            .addTextContent(\"Terminal is launching\")\n                            .build();\n                }))\n                .build();\n    }\n\n    public static McpServerFeatures.SyncToolSpecification openTerminalInline() throws IOException {\n        var tool = McpSchemaFiles.loadTool(\"open_terminal_inline.json\");\n        return McpServerFeatures.SyncToolSpecification.builder()\n                .tool(tool)\n                .callHandler(McpToolHandler.of((req) -> {\n                    var system = req.getStringArgument(\"system\");\n                    var directory = req.getOptionalStringArgument(\"directory\");\n                    var shellStore = req.getShellStoreRef(system, true);\n                    var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);\n\n                    var script = shellSession\n                            .getControl()\n                            .prepareTerminalOpen(\n                                    TerminalInitScriptConfig.ofName(\n                                            shellStore.get().getName()),\n                                    directory.isPresent()\n                                            ? WorkingDirectoryFunction.fixed(FilePath.parse(directory.get()))\n                                            : WorkingDirectoryFunction.none());\n\n                    var json = JsonNodeFactory.instance.objectNode();\n                    json.put(\"command\", script);\n                    return McpSchema.CallToolResult.builder()\n                            .structuredContent(JacksonMapper.getDefault().writeValueAsString(json))\n                            .build();\n                }))\n                .build();\n    }\n\n    public static McpServerFeatures.SyncToolSpecification toggleState() throws IOException {\n        var tool = McpSchemaFiles.loadTool(\"toggle_state.json\");\n        return McpServerFeatures.SyncToolSpecification.builder()\n                .tool(tool)\n                .callHandler(McpToolHandler.of((req) -> {\n                    var system = req.getStringArgument(\"system\");\n                    var state = req.getBooleanArgument(\"state\");\n                    var ref = req.getDataStoreRef(system);\n\n                    if (!(ref.getStore() instanceof SingletonSessionStore<?> singletonSessionStore)) {\n                        throw new BeaconClientException(\"Not a toggleable connection\");\n                    }\n                    if (state) {\n                        singletonSessionStore.startSessionIfNeeded();\n                    } else {\n                        singletonSessionStore.stopSessionIfNeeded();\n                    }\n\n                    return McpSchema.CallToolResult.builder()\n                            .addTextContent(\"Connection state set to \" + state)\n                            .build();\n                }))\n                .build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/BrowserAbstractSessionModel.java",
    "content": "package io.xpipe.app.browser;\n\nimport io.xpipe.app.util.BooleanScope;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.beans.property.BooleanProperty;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.collections.FXCollections;\nimport javafx.collections.ObservableList;\n\nimport lombok.Getter;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@Getter\npublic class BrowserAbstractSessionModel<T extends BrowserSessionTab> {\n\n    protected final ObservableList<T> sessionEntries = FXCollections.observableArrayList();\n    protected final Property<T> selectedEntry = new SimpleObjectProperty<>();\n    protected final BooleanProperty busy = new SimpleBooleanProperty();\n\n    public void closeAsync(BrowserSessionTab e) {\n        ThreadHelper.runAsync(() -> {\n            // This is a bit ugly\n            // If we die on tab init, wait a bit with closing to avoid removal while it is still being inited/added\n            ThreadHelper.sleep(100);\n            closeSync(e);\n        });\n    }\n\n    public void openSync(T e, BooleanProperty externalBusy) throws Exception {\n        try (var ignored =\n                new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {\n            e.init();\n            // Prevent multiple calls from interfering with each other\n            synchronized (this) {\n                sessionEntries.add(e);\n                // The tab pane doesn't automatically select new tabs\n                selectedEntry.setValue(e);\n            }\n        }\n    }\n\n    public void closeSync(BrowserSessionTab e) {\n        e.close();\n        synchronized (BrowserAbstractSessionModel.this) {\n            this.sessionEntries.remove(e);\n        }\n    }\n\n    public List<T> getSessionEntriesSnapshot() {\n        synchronized (this) {\n            return new ArrayList<>(sessionEntries);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java",
    "content": "package io.xpipe.app.browser;\n\nimport io.xpipe.app.browser.file.BrowserConnectionListComp;\nimport io.xpipe.app.browser.file.BrowserConnectionListFilterComp;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabComp;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.ext.FileSystemStore;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.hub.comp.StoreEntryWrapper;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.platform.InputHelper;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.FileReference;\nimport io.xpipe.app.util.ObservableSubscriber;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.FilePath;\n\nimport javafx.beans.property.BooleanProperty;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.collections.ListChangeListener;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\nimport javafx.scene.shape.Rectangle;\n\nimport java.util.List;\nimport java.util.function.BiConsumer;\nimport java.util.function.Consumer;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\n\npublic class BrowserFileChooserSessionComp extends ModalOverlayContentComp {\n\n    private final BrowserFileChooserSessionModel model;\n    private final Predicate<DataStoreEntry> filter;\n\n    public BrowserFileChooserSessionComp(BrowserFileChooserSessionModel model, Predicate<DataStoreEntry> filter) {\n        this.model = model;\n        this.filter = filter;\n    }\n\n    public static void open(\n            Supplier<DataStoreEntryRef<? extends FileSystemStore>> store,\n            Supplier<FilePath> initialPath,\n            Consumer<FileReference> file,\n            boolean save,\n            boolean directory,\n            Predicate<DataStoreEntry> filter) {\n        var model = new BrowserFileChooserSessionModel(directory);\n        model.setOnFinish(fileStores -> {\n            file.accept(fileStores.size() > 0 ? fileStores.getFirst() : null);\n        });\n        var comp = new BrowserFileChooserSessionComp(model, filter)\n                .style(\"browser\")\n                .style(\"chooser\");\n        var selection = new SimpleStringProperty();\n        model.getFileSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {\n            selection.set(\n                    c.getList().size() > 0\n                            ? c.getList().getFirst().getRawFileEntry().getPath().toString()\n                            : null);\n        });\n        var selectionField = new TextFieldComp(selection);\n        selectionField.apply(struc -> {\n            struc.setEditable(false);\n            AppFontSizes.base(struc);\n        });\n        selectionField.style(\"chooser-selection\");\n        selectionField.hgrow();\n        var modal = ModalOverlay.of(save ? \"saveFileTitle\" : \"openFileTitle\", comp);\n        modal.setRequireCloseButtonForClose(true);\n        modal.addButtonBarComp(selectionField);\n        modal.addButton(new ModalButton(\"select\", () -> model.finishChooser(), true, true));\n        modal.show();\n        ThreadHelper.runAsync(() -> {\n            model.openFileSystemAsync(store.get(), null, (sc) -> initialPath.get(), model.getBusy());\n        });\n    }\n\n    @Override\n    protected void setModalOverlay(ModalOverlay modalOverlay) {\n        super.setModalOverlay(modalOverlay);\n        if (modalOverlay == null) {\n            model.closeFileSystem();\n        }\n    }\n\n    @Override\n    protected Region createSimple() {\n        Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {\n            return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore)\n                    && storeEntryWrapper.getEntry().getValidity().isUsable()\n                    && filter.test(storeEntryWrapper.getEntry());\n        };\n        BiConsumer<StoreEntryWrapper, BooleanProperty> action = (w, busy) -> {\n            ThreadHelper.runFailableAsync(() -> {\n                var entry = w.getEntry();\n                if (!entry.getValidity().isUsable()) {\n                    return;\n                }\n\n                // Don't open same system again\n                var current = model.getSelectedEntry().getValue();\n                if (current != null && entry.ref().equals(current.getEntry())) {\n                    return;\n                }\n\n                if (entry.getStore() instanceof ShellStore) {\n                    model.openFileSystemAsync(entry.ref(), null, null, busy);\n                }\n            });\n        };\n\n        var category = new SimpleObjectProperty<>(\n                StoreViewState.get().getActiveCategory().getValue());\n        var filter = new SimpleStringProperty();\n        var filterTrigger = new ObservableSubscriber();\n        var bookmarkTopBar = new BrowserConnectionListFilterComp(filterTrigger, category, filter);\n        var bookmarksList = new BrowserConnectionListComp(\n                BindingsHelper.map(\n                        model.getSelectedEntry(), v -> v != null ? v.getEntry().get() : null),\n                applicable,\n                action,\n                category,\n                filter);\n        var bookmarksContainer = new StackComp(List.of(bookmarksList)).style(\"bookmarks-container\");\n        bookmarksContainer\n                .apply(struc -> {\n                    var rec = new Rectangle();\n                    rec.widthProperty().bind(struc.widthProperty());\n                    rec.heightProperty().bind(struc.heightProperty());\n                    rec.setArcHeight(7);\n                    rec.setArcWidth(7);\n                    struc.getChildren().getFirst().setClip(rec);\n                })\n                .vgrow();\n\n        var stack = RegionBuilder.of(() -> {\n            var s = new StackPane();\n            model.getSelectedEntry().subscribe(selected -> {\n                PlatformThread.runLaterIfNeeded(() -> {\n                    if (selected != null) {\n                        s.getChildren().setAll(new BrowserFileSystemTabComp(selected, false).build());\n                    } else {\n                        s.getChildren().clear();\n                    }\n                });\n            });\n            InputHelper.onKeyCombination(\n                    s, new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN), false, keyEvent -> {\n                        filterTrigger.trigger();\n                        keyEvent.consume();\n                    });\n            return s;\n        });\n\n        var vertical = new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer)).style(\"left\");\n        var splitPane = new LeftSplitPaneComp(vertical, stack)\n                .withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())\n                .applyStructure(struc -> {\n                    struc.getLeft().setMinWidth(200);\n                    struc.getLeft().setMaxWidth(500);\n                });\n        splitPane.disable(model.getBusy());\n        return splitPane.prefHeight(2000).build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionModel.java",
    "content": "package io.xpipe.app.browser;\n\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.ext.FileSystem;\nimport io.xpipe.app.ext.FileSystemStore;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.BooleanScope;\nimport io.xpipe.app.util.FileReference;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.FailableFunction;\nimport io.xpipe.core.FilePath;\n\nimport javafx.beans.property.BooleanProperty;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.collections.FXCollections;\nimport javafx.collections.ListChangeListener;\nimport javafx.collections.ObservableList;\n\nimport lombok.Getter;\nimport lombok.Setter;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Consumer;\n\n@Getter\npublic class BrowserFileChooserSessionModel extends BrowserAbstractSessionModel<BrowserFileSystemTabModel> {\n\n    private final ObservableList<BrowserEntry> fileSelection = FXCollections.observableArrayList();\n    private final boolean directory;\n\n    @Setter\n    private Consumer<List<FileReference>> onFinish;\n\n    public BrowserFileChooserSessionModel(boolean directory) {\n        this.directory = directory;\n        selectedEntry.addListener((observable, oldValue, newValue) -> {\n            if (newValue == null) {\n                fileSelection.clear();\n                return;\n            }\n\n            fileSelection.setAll(newValue.getFileList().getSelection());\n            newValue.getFileList().getSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {\n                fileSelection.setAll(newValue.getFileList().getSelection());\n            });\n        });\n    }\n\n    public void finishChooser() {\n        var chosen = new ArrayList<>(\n                fileSelection.stream().map(be -> be.getRawFileEntry().getPath()).toList());\n\n        synchronized (BrowserFileChooserSessionModel.this) {\n            var open = selectedEntry.getValue();\n            if (open != null) {\n                if (chosen.isEmpty() && directory) {\n                    var current = open.getCurrentDirectory();\n                    if (current != null) {\n                        chosen.add(current.getPath());\n                    }\n                }\n\n                ThreadHelper.runAsync(() -> {\n                    open.close();\n                });\n            }\n        }\n\n        var stores = chosen.stream()\n                .map(entry -> new FileReference(selectedEntry.getValue().getEntry(), entry))\n                .toList();\n        onFinish.accept(stores);\n    }\n\n    public void closeFileSystem() {\n        synchronized (BrowserFileChooserSessionModel.this) {\n            var open = selectedEntry.getValue();\n            if (open != null) {\n                ThreadHelper.runAsync(() -> {\n                    open.close();\n                });\n            }\n        }\n    }\n\n    public void openFileSystemAsync(\n            DataStoreEntryRef<? extends FileSystemStore> store,\n            FailableFunction<DataStoreEntryRef<FileSystemStore>, FileSystem, Exception> customFileSystemFactory,\n            FailableFunction<BrowserFileSystemTabModel, FilePath, Exception> path,\n            BooleanProperty externalBusy) {\n        if (store == null) {\n            return;\n        }\n\n        ThreadHelper.runFailableAsync(() -> {\n            BrowserFileSystemTabModel model;\n\n            try (var ignored =\n                    new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {\n                model = new BrowserFileSystemTabModel(\n                        this,\n                        store,\n                        customFileSystemFactory != null\n                                ? customFileSystemFactory\n                                : ref -> ref.getStore().createFileSystem());\n                model.init();\n                // Prevent multiple calls from interfering with each other\n                synchronized (BrowserFileChooserSessionModel.this) {\n                    selectedEntry.setValue(model);\n                    sessionEntries.add(model);\n                }\n\n                if (path != null) {\n                    var initialPath = path.apply(model);\n                    if (initialPath != null) {\n                        model.initWithGivenDirectory(initialPath.toDirectory());\n                        return;\n                    }\n                }\n                model.initWithDefaultDirectory();\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/BrowserFullSessionComp.java",
    "content": "package io.xpipe.app.browser;\n\nimport io.xpipe.app.browser.file.BrowserConnectionListComp;\nimport io.xpipe.app.browser.file.BrowserConnectionListFilterComp;\nimport io.xpipe.app.browser.file.BrowserTransferComp;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.core.window.AppMainWindow;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.hub.comp.StoreEntryWrapper;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.platform.InputHelper;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.util.ObservableSubscriber;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.BooleanProperty;\nimport javafx.beans.property.SimpleDoubleProperty;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.geometry.Insets;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\nimport javafx.scene.layout.AnchorPane;\nimport javafx.scene.layout.Region;\nimport javafx.scene.shape.Rectangle;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.function.BiConsumer;\nimport java.util.function.Predicate;\n\npublic class BrowserFullSessionComp extends SimpleRegionBuilder {\n\n    private final BrowserFullSessionModel model;\n\n    public BrowserFullSessionComp(BrowserFullSessionModel model) {\n        this.model = model;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var filterTrigger = new ObservableSubscriber();\n        var left = RegionBuilder.of(() -> createLeftSide(filterTrigger));\n\n        var leftSplit = new SimpleDoubleProperty();\n        var rightSplit = new SimpleDoubleProperty();\n        var tabs = new BrowserSessionTabsComp(model, leftSplit, rightSplit);\n        tabs.apply(struc -> {\n            struc.setViewOrder(1);\n            struc.setPickOnBounds(false);\n            AnchorPane.setTopAnchor(struc, 0.0);\n            AnchorPane.setBottomAnchor(struc, 0.0);\n            AnchorPane.setLeftAnchor(struc, 0.0);\n            AnchorPane.setRightAnchor(struc, 0.0);\n        });\n\n        left.apply(struc -> {\n            struc.paddingProperty()\n                    .bind(Bindings.createObjectBinding(\n                            () -> new Insets(tabs.getHeaderHeight().get(), 0, 0, 0), tabs.getHeaderHeight()));\n        });\n        var loadingIndicator = new LoadingIconComp(model.getBusy(), AppFontSizes::xxxl)\n                .apply(struc -> {\n                    AnchorPane.setTopAnchor(struc, 0.0);\n                    AnchorPane.setRightAnchor(struc, 0.0);\n                })\n                .style(\"tab-loading-indicator\");\n\n        var pinnedStack = createSplitStack(rightSplit, tabs);\n\n        var loadingStack = new AnchorComp(List.of(tabs, pinnedStack, loadingIndicator));\n        loadingStack.apply(struc -> struc.setPickOnBounds(false));\n        var delayedStack = new DelayedInitComp(\n                left, () -> StoreViewState.get() != null && StoreViewState.get().isInitialized());\n        delayedStack.hide(AppMainWindow.get().getStage().widthProperty().lessThan(1000));\n        var splitPane = new LeftSplitPaneComp(delayedStack, loadingStack)\n                .withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())\n                .withOnDividerChange(d -> {\n                    if (d > 0.0) {\n                        AppLayoutModel.get().getSavedState().setBrowserConnectionsWidth(d);\n                    }\n                    leftSplit.set(d);\n                });\n        splitPane.applyStructure(struc -> {\n            struc.getLeft().setMinWidth(200);\n            struc.getLeft().setMaxWidth(500);\n            struc.get().setPickOnBounds(false);\n        });\n\n        splitPane.apply(struc -> {\n            struc.skinProperty().subscribe(newValue -> {\n                if (newValue != null) {\n                    Platform.runLater(() -> {\n                        struc.getChildrenUnmodifiable().forEach(node -> {\n                            node.setClip(null);\n                            node.setPickOnBounds(false);\n                        });\n                        struc.lookupAll(\".split-pane-divider\").forEach(node -> node.setViewOrder(-1));\n                    });\n                }\n            });\n        });\n        splitPane.apply(struc -> {\n            InputHelper.onKeyCombination(\n                    struc, new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN), false, keyEvent -> {\n                        filterTrigger.trigger();\n                        keyEvent.consume();\n                    });\n        });\n        splitPane.style(\"browser\");\n        var r = splitPane.build();\n        return r;\n    }\n\n    private Region createLeftSide(ObservableSubscriber filterTrigger) {\n        Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {\n            if (!storeEntryWrapper.getEntry().getValidity().isUsable()) {\n                return false;\n            }\n\n            if (storeEntryWrapper.getEntry().getStore() instanceof ShellStore) {\n                return true;\n            }\n\n            return storeEntryWrapper.getEntry().getProvider().launchBrowser(model, storeEntryWrapper.getEntry(), null)\n                    != null;\n        };\n        BiConsumer<StoreEntryWrapper, BooleanProperty> action = (w, busy) -> {\n            var entry = w.getEntry();\n            if (!entry.getValidity().isUsable()) {\n                return;\n            }\n\n            var a = entry.getProvider().launchBrowser(model, entry, busy);\n            if (a != null) {\n                ThreadHelper.runFailableAsync(() -> {\n                    a.run();\n                });\n            }\n        };\n\n        var category = new SimpleObjectProperty<>(\n                StoreViewState.get().getActiveCategory().getValue());\n        var filter = new SimpleStringProperty();\n        var bookmarkTopBar = new BrowserConnectionListFilterComp(filterTrigger, category, filter);\n        var bookmarksList = new BrowserConnectionListComp(\n                BindingsHelper.map(\n                        model.getSelectedEntry(),\n                        v -> v instanceof BrowserStoreSessionTab<?> st\n                                ? st.getEntry().get()\n                                : null),\n                applicable,\n                action,\n                category,\n                filter);\n        var bookmarksContainer = new StackComp(List.of(bookmarksList)).style(\"bookmarks-container\");\n        bookmarksContainer\n                .apply(struc -> {\n                    var rec = new Rectangle();\n                    rec.widthProperty().bind(struc.widthProperty());\n                    rec.heightProperty().bind(struc.heightProperty());\n                    rec.setArcHeight(11);\n                    rec.setArcWidth(11);\n                    struc.getChildren().getFirst().setClip(rec);\n                })\n                .vgrow();\n        var localDownloadStage = new BrowserTransferComp(model.getLocalTransfersStage())\n                .hide(Bindings.createBooleanBinding(\n                        () -> {\n                            if (model.getSessionEntries().size() == 0) {\n                                return true;\n                            }\n\n                            return false;\n                        },\n                        model.getSessionEntries(),\n                        model.getSelectedEntry()));\n        localDownloadStage.prefHeight(200);\n        localDownloadStage.maxHeight(200);\n        var vertical = new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer, localDownloadStage)).style(\"left\");\n        return vertical.build();\n    }\n\n    private StackComp createSplitStack(SimpleDoubleProperty rightSplit, BrowserSessionTabsComp tabs) {\n        var cache = new HashMap<BrowserSessionTab, Region>();\n        var splitStack = new StackComp(List.of());\n        splitStack.apply(struc -> struc.setPickOnBounds(false));\n        splitStack.apply(struc -> {\n            model.getEffectiveRightTab().subscribe((newValue) -> {\n                PlatformThread.runLaterIfNeeded(() -> {\n                    var all = model.getAllTabs();\n                    cache.keySet().removeIf(browserSessionTab -> !all.contains(browserSessionTab));\n\n                    if (newValue == null) {\n                        struc.getChildren().clear();\n                        return;\n                    }\n\n                    var cached = cache.containsKey(newValue);\n                    if (!cached) {\n                        cache.put(newValue, newValue.comp().build());\n                    }\n                    var r = cache.get(newValue);\n                    struc.getChildren().clear();\n                    struc.getChildren().add(r);\n\n                    struc.setMinWidth(rightSplit.get());\n                    struc.setPrefWidth(rightSplit.get());\n                    struc.setMaxWidth(rightSplit.get());\n                });\n            });\n\n            rightSplit.addListener((observable, oldValue, newValue) -> {\n                struc.setMinWidth(newValue.doubleValue());\n                struc.setPrefWidth(newValue.doubleValue());\n                struc.setMaxWidth(newValue.doubleValue());\n            });\n\n            var clip = new Rectangle();\n            clip.widthProperty().bind(struc.widthProperty());\n            clip.heightProperty().bind(struc.heightProperty());\n            struc.setClip(clip);\n\n            AnchorPane.setBottomAnchor(struc, 0.0);\n            AnchorPane.setRightAnchor(struc, 0.0);\n            tabs.getHeaderHeight().subscribe(number -> {\n                AnchorPane.setTopAnchor(struc, number.doubleValue());\n            });\n        });\n        return splitStack;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/BrowserFullSessionModel.java",
    "content": "package io.xpipe.app.browser;\n\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.file.BrowserHistorySavedState;\nimport io.xpipe.app.browser.file.BrowserHistoryTabModel;\nimport io.xpipe.app.browser.file.BrowserTransferModel;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.ext.FileSystem;\nimport io.xpipe.app.ext.FileSystemStore;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.BooleanScope;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.FailableFunction;\nimport io.xpipe.core.FilePath;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.BooleanProperty;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.FXCollections;\nimport javafx.collections.ListChangeListener;\nimport javafx.collections.ObservableMap;\n\nimport lombok.Getter;\n\nimport java.util.*;\n\n@Getter\npublic class BrowserFullSessionModel extends BrowserAbstractSessionModel<BrowserSessionTab> {\n\n    public static final BrowserFullSessionModel DEFAULT = new BrowserFullSessionModel();\n    private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this);\n    private final Property<Boolean> draggingFiles = new SimpleBooleanProperty();\n    private final Property<BrowserSessionTab> globalPinnedTab = new SimpleObjectProperty<>();\n    private final ObservableMap<BrowserSessionTab, BrowserSessionTab> splits = FXCollections.observableHashMap();\n    private final ObservableValue<BrowserSessionTab> effectiveRightTab = createEffectiveRightTab();\n    private final SequencedSet<BrowserSessionTab> previousTabs = new LinkedHashSet<>();\n\n    public BrowserFullSessionModel() {\n        sessionEntries.addListener((ListChangeListener<? super BrowserSessionTab>) c -> {\n            var v = globalPinnedTab.getValue();\n            if (v != null && !c.getList().contains(v)) {\n                globalPinnedTab.setValue(null);\n            }\n\n            splits.keySet().removeIf(browserSessionTab -> !c.getList().contains(browserSessionTab));\n        });\n\n        selectedEntry.addListener((observable, oldValue, newValue) -> {\n            if (newValue != null) {\n                previousTabs.remove(newValue);\n                previousTabs.add(newValue);\n            }\n        });\n    }\n\n    public static void init() throws Exception {\n        DEFAULT.openSync(new BrowserHistoryTabModel(DEFAULT), null);\n        if (AppPrefs.get().pinLocalMachineOnStartup().get()) {\n            var tab = new BrowserFileSystemTabModel(\n                    DEFAULT, DataStorage.get().local().ref(), ref -> ref.getStore()\n                            .createFileSystem());\n            try {\n                DEFAULT.openSync(tab, null);\n                DEFAULT.pinTab(tab);\n            } catch (Exception ex) {\n                // Don't fail startup if this operation fails\n                ErrorEventFactory.fromThrowable(ex).handle();\n            }\n        }\n    }\n\n    private ObservableValue<BrowserSessionTab> createEffectiveRightTab() {\n        return Bindings.createObjectBinding(\n                () -> {\n                    var current = selectedEntry.getValue();\n                    if (current == null) {\n                        return null;\n                    }\n\n                    if (!current.isCloseable()) {\n                        return null;\n                    }\n\n                    var split = splits.get(current);\n                    if (split != null) {\n                        return split;\n                    }\n\n                    var global = globalPinnedTab.getValue();\n                    if (global == null) {\n                        return null;\n                    }\n\n                    if (global == selectedEntry.getValue()) {\n                        return null;\n                    }\n\n                    return global;\n                },\n                globalPinnedTab,\n                selectedEntry,\n                splits);\n    }\n\n    public Set<BrowserSessionTab> getAllTabs() {\n        var set = new HashSet<BrowserSessionTab>();\n        set.addAll(sessionEntries);\n        set.addAll(splits.values());\n        if (globalPinnedTab.getValue() != null) {\n            set.add(globalPinnedTab.getValue());\n        }\n        return set;\n    }\n\n    public void splitTab(BrowserSessionTab tab, BrowserSessionTab split) {\n        if (splits.containsKey(tab)) {\n            return;\n        }\n\n        splits.put(tab, split);\n        ThreadHelper.runFailableAsync(() -> {\n            split.init();\n        });\n    }\n\n    public void unsplitTab(BrowserSessionTab tab) {\n        if (splits.values().remove(tab)) {\n            ThreadHelper.runFailableAsync(() -> {\n                tab.close();\n            });\n        }\n    }\n\n    public void pinTab(BrowserSessionTab tab) {\n        if (tab.equals(globalPinnedTab.getValue())) {\n            return;\n        }\n\n        globalPinnedTab.setValue(tab);\n\n        var previousOthers = previousTabs.stream()\n                .filter(browserSessionTab -> browserSessionTab != tab && browserSessionTab.isCloseable())\n                .toList();\n        if (previousOthers.size() > 0) {\n            var prev = previousOthers.getLast();\n            getSelectedEntry().setValue(prev);\n        }\n    }\n\n    public void unpinTab() {\n        ThreadHelper.runFailableAsync(() -> {\n            globalPinnedTab.setValue(null);\n        });\n    }\n\n    public void restoreState(BrowserHistorySavedState state) {\n        ThreadHelper.runAsync(() -> {\n            var l = new ArrayList<>(state.getEntries());\n            l.forEach(e -> {\n                restoreStateAsync(e, null);\n                // Don't try to run everything in parallel as that can be taxing\n                ThreadHelper.sleep(1000);\n            });\n        });\n    }\n\n    public void restoreStateAsync(BrowserHistorySavedState.Entry e, BooleanProperty busy) {\n        var storageEntry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());\n        storageEntry.ifPresent(entry -> {\n            openFileSystemAsync(entry.ref(), null, model -> e.getPath(), busy);\n        });\n    }\n\n    public void reset() {\n        synchronized (BrowserFullSessionModel.this) {\n            if (globalPinnedTab.getValue() != null) {\n                globalPinnedTab.setValue(null);\n            }\n\n            var all = new ArrayList<>(sessionEntries);\n            for (var o : all) {\n                // Don't close busy connections gracefully\n                // as we otherwise might lock up\n                if (!o.canImmediatelyClose()) {\n                    continue;\n                }\n\n                // Prevent blocking of shutdown\n                closeAsync(o);\n            }\n            if (all.size() > 0) {\n                ThreadHelper.sleep(1000);\n            }\n        }\n\n        // Delete all files\n        localTransfersStage.clear(true);\n    }\n\n    public void openFileSystemAsync(\n            DataStoreEntryRef<? extends FileSystemStore> store,\n            FailableFunction<DataStoreEntryRef<FileSystemStore>, FileSystem, Exception> customFileSystemFactory,\n            FailableFunction<BrowserFileSystemTabModel, FilePath, Exception> path,\n            BooleanProperty externalBusy) {\n        if (store == null) {\n            return;\n        }\n\n        ThreadHelper.runFailableAsync(() -> {\n            openFileSystemSync(store, customFileSystemFactory, path, externalBusy, true);\n        });\n    }\n\n    public BrowserFileSystemTabModel openFileSystemSync(\n            DataStoreEntryRef<? extends FileSystemStore> store,\n            FailableFunction<DataStoreEntryRef<FileSystemStore>, FileSystem, Exception> customFileSystemFactory,\n            FailableFunction<BrowserFileSystemTabModel, FilePath, Exception> path,\n            BooleanProperty externalBusy,\n            boolean select)\n            throws Exception {\n        BrowserFileSystemTabModel model;\n        try (var ignored =\n                new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {\n            try (var ignored2 = new BooleanScope(busy).exclusive().start()) {\n                model = new BrowserFileSystemTabModel(\n                        this,\n                        store,\n                        customFileSystemFactory != null\n                                ? customFileSystemFactory\n                                : ref -> ref.getStore().createFileSystem());\n                model.init();\n                // Prevent multiple calls from interfering with each other\n                synchronized (BrowserFullSessionModel.this) {\n                    sessionEntries.add(model);\n                    if (select) {\n                        AppLayoutModel.get().selectBrowser();\n                        // The tab pane doesn't automatically select new tabs\n                        selectedEntry.setValue(model);\n                    }\n                }\n            }\n        }\n        if (path != null) {\n            var applied = path.apply(model);\n            if (applied != null) {\n                model.initWithGivenDirectory(applied.toDirectory());\n            } else {\n                model.initWithDefaultDirectory();\n            }\n        } else {\n            model.initWithDefaultDirectory();\n        }\n        return model;\n    }\n\n    @Override\n    public void closeSync(BrowserSessionTab e) {\n        var split = splits.get(e);\n        if (split != null) {\n            split.close();\n        }\n        previousTabs.remove(e);\n        super.closeSync(e);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/BrowserSessionTab.java",
    "content": "package io.xpipe.app.browser;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.storage.DataStoreColor;\n\nimport javafx.beans.property.BooleanProperty;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.Getter;\n\n@Getter\npublic abstract class BrowserSessionTab {\n\n    protected final BooleanProperty busy = new SimpleBooleanProperty();\n    protected final BrowserAbstractSessionModel<?> browserModel;\n    protected final Property<BrowserSessionTab> splitTab = new SimpleObjectProperty<>();\n\n    public BrowserSessionTab(BrowserAbstractSessionModel<?> browserModel) {\n        this.browserModel = browserModel;\n    }\n\n    public abstract BaseRegionBuilder<?, ?> comp();\n\n    public abstract boolean canImmediatelyClose();\n\n    public abstract void init() throws Exception;\n\n    public abstract void close();\n\n    public abstract ObservableValue<String> getName();\n\n    public abstract String getIcon();\n\n    public abstract DataStoreColor getColor();\n\n    public boolean isCloseable() {\n        return true;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/BrowserSessionTabsComp.java",
    "content": "package io.xpipe.app.browser;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.LoadingIconComp;\nimport io.xpipe.app.comp.base.PrettyImageHelper;\nimport io.xpipe.app.comp.base.StackComp;\nimport io.xpipe.app.core.App;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.MenuHelper;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.util.BooleanScope;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.DoubleProperty;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleDoubleProperty;\nimport javafx.beans.value.ObservableDoubleValue;\nimport javafx.collections.ListChangeListener;\nimport javafx.css.PseudoClass;\nimport javafx.geometry.Insets;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.*;\nimport javafx.scene.control.skin.TabPaneSkin;\nimport javafx.scene.input.*;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\n\nimport atlantafx.base.theme.Styles;\nimport lombok.Getter;\n\nimport java.util.*;\n\nimport static atlantafx.base.theme.Styles.DENSE;\nimport static atlantafx.base.theme.Styles.toggleStyleClass;\nimport static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS;\n\npublic class BrowserSessionTabsComp extends SimpleRegionBuilder {\n\n    private final BrowserFullSessionModel model;\n    private final ObservableDoubleValue leftPadding;\n    private final DoubleProperty rightPadding;\n\n    @Getter\n    private final DoubleProperty headerHeight;\n\n    public BrowserSessionTabsComp(\n            BrowserFullSessionModel model, ObservableDoubleValue leftPadding, DoubleProperty rightPadding) {\n        this.model = model;\n        this.leftPadding = leftPadding;\n        this.rightPadding = rightPadding;\n        this.headerHeight = new SimpleDoubleProperty();\n    }\n\n    private static void setupKeyEvents(TabPane tabs) {\n        tabs.addEventHandler(KeyEvent.KEY_PRESSED, keyEvent -> {\n            var current = tabs.getSelectionModel().getSelectedItem();\n            if (current == null) {\n                return;\n            }\n\n            if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(keyEvent)) {\n                tabs.getTabs().remove(current);\n                keyEvent.consume();\n                return;\n            }\n\n            if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN)\n                    .match(keyEvent)) {\n                tabs.getTabs().clear();\n                keyEvent.consume();\n            }\n\n            if (keyEvent.getCode().isFunctionKey()) {\n                var start = KeyCode.F1.getCode();\n                var index = keyEvent.getCode().getCode() - start;\n                if (index < tabs.getTabs().size()) {\n                    tabs.getSelectionModel().select(index);\n                    keyEvent.consume();\n                    return;\n                }\n            }\n\n            var forward = new KeyCodeCombination(KeyCode.TAB, KeyCombination.SHORTCUT_DOWN);\n            if (forward.match(keyEvent)) {\n                var next = (tabs.getSelectionModel().getSelectedIndex() + 1)\n                        % tabs.getTabs().size();\n                tabs.getSelectionModel().select(next);\n                keyEvent.consume();\n                return;\n            }\n\n            var back = new KeyCodeCombination(KeyCode.TAB, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN);\n            if (back.match(keyEvent)) {\n                var previous = (tabs.getTabs().size() + tabs.getSelectionModel().getSelectedIndex() - 1)\n                        % tabs.getTabs().size();\n                tabs.getSelectionModel().select(previous);\n                keyEvent.consume();\n            }\n        });\n    }\n\n    public Region createSimple() {\n        var tabs = createTabPane();\n        var topBackground = RegionBuilder.hspacer().style(\"top-spacer\").build();\n        leftPadding.subscribe(number -> {\n            StackPane.setMargin(topBackground, new Insets(0, 0, 0, -number.doubleValue() - 3));\n        });\n        var stack = new StackPane(topBackground, tabs);\n        stack.setAlignment(Pos.TOP_CENTER);\n        topBackground.prefHeightProperty().bind(headerHeight);\n        topBackground.maxHeightProperty().bind(topBackground.prefHeightProperty());\n        topBackground.prefWidthProperty().bind(tabs.widthProperty());\n        return stack;\n    }\n\n    private TabPane createTabPane() {\n        var tabs = new TabPane();\n        tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);\n        tabs.setTabMinWidth(Region.USE_PREF_SIZE);\n        tabs.setTabMaxWidth(400);\n        tabs.setTabClosingPolicy(ALL_TABS);\n        tabs.setSkin(new TabPaneSkin(tabs));\n        Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING);\n        toggleStyleClass(tabs, DENSE);\n\n        setupCustomStyle(tabs);\n        // Sync to guarantee that no external changes are made during this\n        synchronized (model) {\n            setupTabEntries(tabs);\n        }\n        setupKeyEvents(tabs);\n\n        return tabs;\n    }\n\n    private void setupTabEntries(TabPane tabs) {\n        var map = new HashMap<BrowserSessionTab, Tab>();\n\n        // Restore state\n        model.getSessionEntries().forEach(v -> {\n            var t = createTab(tabs, v);\n            map.put(v, t);\n            tabs.getTabs().add(t);\n        });\n        tabs.getSelectionModel()\n                .select(model.getSessionEntries()\n                        .indexOf(model.getSelectedEntry().getValue()));\n\n        // Used for ignoring changes by the tabpane when new tabs are added. We want to perform the selections manually!\n        var addingTab = new SimpleBooleanProperty();\n\n        // Handle selection from platform\n        tabs.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {\n            if (addingTab.get()) {\n                return;\n            }\n\n            if (newValue == null) {\n                model.getSelectedEntry().setValue(null);\n                return;\n            }\n\n            var source = map.entrySet().stream()\n                    .filter(openFileSystemModelTabEntry ->\n                            openFileSystemModelTabEntry.getValue().equals(newValue))\n                    .findAny()\n                    .map(Map.Entry::getKey)\n                    .orElse(null);\n            model.getSelectedEntry().setValue(source);\n        });\n\n        // Handle selection from model\n        model.getSelectedEntry().addListener((observable, oldValue, newValue) -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                if (newValue == null) {\n                    tabs.getSelectionModel().select(null);\n                    return;\n                }\n\n                var toSelect = map.entrySet().stream()\n                        .filter(openFileSystemModelTabEntry ->\n                                openFileSystemModelTabEntry.getKey().equals(newValue))\n                        .findAny()\n                        .map(Map.Entry::getValue)\n                        .orElse(null);\n                if (toSelect == null || !tabs.getTabs().contains(toSelect)) {\n                    tabs.getSelectionModel().select(null);\n                    return;\n                }\n\n                tabs.getSelectionModel().select(toSelect);\n                Platform.runLater(() -> {\n                    toSelect.getContent().requestFocus();\n                });\n            });\n        });\n\n        model.getSessionEntries().addListener((ListChangeListener<? super BrowserSessionTab>) c -> {\n            while (c.next()) {\n                for (var r : c.getRemoved()) {\n                    PlatformThread.runLaterIfNeeded(() -> {\n                        var t = map.remove(r);\n                        tabs.getTabs().remove(t);\n                    });\n                }\n\n                for (var a : c.getAddedSubList()) {\n                    PlatformThread.runLaterIfNeeded(() -> {\n                        try (var ignored = new BooleanScope(addingTab).start()) {\n                            var t = createTab(tabs, a);\n                            map.put(a, t);\n                            tabs.getTabs().add(t);\n                        }\n                    });\n                }\n            }\n        });\n\n        tabs.getTabs().addListener((ListChangeListener<? super Tab>) c -> {\n            while (c.next()) {\n                for (var r : c.getRemoved()) {\n                    var source = map.entrySet().stream()\n                            .filter(openFileSystemModelTabEntry ->\n                                    openFileSystemModelTabEntry.getValue().equals(r))\n                            .findAny()\n                            .orElse(null);\n\n                    // Only handle close events that are triggered from the platform\n                    if (source == null) {\n                        continue;\n                    }\n\n                    model.closeAsync(source.getKey());\n                }\n            }\n        });\n    }\n\n    private void setupCustomStyle(TabPane tabs) {\n        tabs.skinProperty().subscribe(newValue -> {\n            if (newValue != null) {\n                Platform.runLater(() -> {\n                    tabs.setClip(null);\n                    tabs.setPickOnBounds(false);\n                    tabs.lookupAll(\".tab-header-area\").forEach(node -> {\n                        node.setClip(null);\n                        node.setPickOnBounds(false);\n\n                        var r = (Region) node;\n                        r.prefHeightProperty().bind(r.maxHeightProperty());\n                        r.setMinHeight(Region.USE_PREF_SIZE);\n                    });\n                    tabs.lookupAll(\".headers-region\").forEach(node -> {\n                        node.setPickOnBounds(false);\n\n                        var r = (Region) node;\n                        r.prefHeightProperty().bind(r.maxHeightProperty());\n                        r.setMinHeight(Region.USE_PREF_SIZE);\n                    });\n\n                    Region headerArea = (Region) tabs.lookup(\".tab-header-area\");\n                    headerArea\n                            .paddingProperty()\n                            .bind(Bindings.createObjectBinding(\n                                    () -> {\n                                        var w = App.getApp().getStage().getWidth();\n                                        if (w >= 1000) {\n                                            return new Insets(2, 0, 4, -leftPadding.get() + 3);\n                                        } else {\n                                            return new Insets(2, 0, 4, -leftPadding.get() - 4);\n                                        }\n                                    },\n                                    App.getApp().getStage().widthProperty(),\n                                    leftPadding));\n                    tabs.paddingProperty()\n                            .bind(Bindings.createObjectBinding(\n                                    () -> {\n                                        var w = App.getApp().getStage().getWidth();\n                                        if (w >= 1000) {\n                                            return new Insets(0, 0, 0, -5);\n                                        } else {\n                                            return new Insets(0, 0, 0, 5);\n                                        }\n                                    },\n                                    App.getApp().getStage().widthProperty()));\n                    headerHeight.bind(headerArea.heightProperty());\n                });\n            }\n        });\n    }\n\n    private ContextMenu createContextMenu(TabPane tabs, Tab tab, BrowserSessionTab tabModel) {\n        var cm = MenuHelper.createContextMenu();\n\n        if (tabModel.isCloseable()) {\n            var unpin = MenuHelper.createMenuItem(LabelGraphic.none(), \"unpinTab\");\n            unpin.visibleProperty()\n                    .bind(PlatformThread.sync(Bindings.createBooleanBinding(\n                            () -> {\n                                return model.getGlobalPinnedTab().getValue() != null\n                                        && model.getGlobalPinnedTab().getValue().equals(tabModel);\n                            },\n                            model.getGlobalPinnedTab())));\n            unpin.setOnAction(event -> {\n                model.unpinTab();\n                event.consume();\n            });\n            cm.getItems().add(unpin);\n\n            var pin = MenuHelper.createMenuItem(LabelGraphic.none(), \"pinTab\");\n            pin.visibleProperty()\n                    .bind(PlatformThread.sync(Bindings.createBooleanBinding(\n                            () -> {\n                                return model.getGlobalPinnedTab().getValue() == null;\n                            },\n                            model.getGlobalPinnedTab())));\n            pin.setOnAction(event -> {\n                model.pinTab(tabModel);\n                event.consume();\n            });\n            cm.getItems().add(pin);\n        }\n\n        var select = MenuHelper.createMenuItem(LabelGraphic.none(), \"selectTab\");\n        select.acceleratorProperty()\n                .bind(Bindings.createObjectBinding(\n                        () -> {\n                            var start = KeyCode.F1.getCode();\n                            var index = tabs.getTabs().indexOf(tab);\n                            var keyCode = Arrays.stream(KeyCode.values())\n                                    .filter(code -> code.getCode() == start + index)\n                                    .findAny()\n                                    .orElse(null);\n                            return keyCode != null ? new KeyCodeCombination(keyCode) : null;\n                        },\n                        tabs.getTabs()));\n        select.setOnAction(event -> {\n            tabs.getSelectionModel().select(tab);\n            event.consume();\n        });\n        cm.getItems().add(select);\n\n        cm.getItems().add(new SeparatorMenuItem());\n\n        var close = MenuHelper.createMenuItem(LabelGraphic.none(), \"closeTab\");\n        close.setAccelerator(new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN));\n        close.setOnAction(event -> {\n            if (tab.isClosable()) {\n                tabs.getTabs().remove(tab);\n            }\n            event.consume();\n        });\n        cm.getItems().add(close);\n\n        var closeOthers = MenuHelper.createMenuItem(LabelGraphic.none(), \"closeOtherTabs\");\n        closeOthers.setOnAction(event -> {\n            tabs.getTabs()\n                    .removeAll(tabs.getTabs().stream()\n                            .filter(t -> t != tab && t.isClosable())\n                            .toList());\n            event.consume();\n        });\n        cm.getItems().add(closeOthers);\n\n        var closeLeft = MenuHelper.createMenuItem(LabelGraphic.none(), \"closeLeftTabs\");\n        closeLeft.setOnAction(event -> {\n            var index = tabs.getTabs().indexOf(tab);\n            tabs.getTabs()\n                    .removeAll(tabs.getTabs().stream()\n                            .filter(t -> tabs.getTabs().indexOf(t) < index && t.isClosable())\n                            .toList());\n            event.consume();\n        });\n        cm.getItems().add(closeLeft);\n\n        var closeRight = MenuHelper.createMenuItem(LabelGraphic.none(), \"closeRightTabs\");\n        closeRight.setOnAction(event -> {\n            var index = tabs.getTabs().indexOf(tab);\n            tabs.getTabs()\n                    .removeAll(tabs.getTabs().stream()\n                            .filter(t -> tabs.getTabs().indexOf(t) > index && t.isClosable())\n                            .toList());\n            event.consume();\n        });\n        cm.getItems().add(closeRight);\n\n        var closeAll = MenuHelper.createMenuItem(LabelGraphic.none(), \"closeAllTabs\");\n        closeAll.setAccelerator(\n                new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN));\n        closeAll.setOnAction(event -> {\n            tabs.getTabs()\n                    .removeAll(\n                            tabs.getTabs().stream().filter(t -> t.isClosable()).toList());\n            event.consume();\n        });\n        cm.getItems().add(closeAll);\n\n        return cm;\n    }\n\n    private Tab createTab(TabPane tabs, BrowserSessionTab tabModel) {\n        var tab = new Tab();\n        if (tabModel.isCloseable()) {\n            tab.setContextMenu(createContextMenu(tabs, tab, tabModel));\n        }\n\n        tab.setClosable(tabModel.isCloseable());\n        // Prevent closing while busy\n        tab.setOnCloseRequest(event -> {\n            if (!tabModel.canImmediatelyClose()) {\n                event.consume();\n            }\n        });\n\n        if (tabModel.getIcon() != null) {\n            var loading = new LoadingIconComp(tabModel.getBusy(), AppFontSizes::base);\n            loading.prefWidth(16);\n            loading.prefHeight(16);\n\n            var image = tabModel.getIcon();\n            var logo = PrettyImageHelper.ofFixedSizeSquare(image, 16);\n            logo.apply(struc -> {\n                struc.opacityProperty()\n                        .bind(PlatformThread.sync(Bindings.createDoubleBinding(\n                                () -> {\n                                    return !tabModel.getBusy().get() ? 1.0 : 0.15;\n                                },\n                                tabModel.getBusy())));\n            });\n\n            var stack = new StackComp(List.of(logo, loading));\n            tab.setGraphic(stack.build());\n        }\n\n        if (tabModel.getBrowserModel() instanceof BrowserFullSessionModel sessionModel) {\n            var global = PlatformThread.sync(sessionModel.getGlobalPinnedTab());\n            tab.textProperty()\n                    .bind(Bindings.createStringBinding(\n                            () -> {\n                                var n = tabModel.getName().getValue();\n                                return (AppPrefs.get().censorMode().get() ? \"*\".repeat(n.length()) : n)\n                                        + (global.getValue() == tabModel ? \" (\" + AppI18n.get(\"pinned\") + \")\" : \"\");\n                            },\n                            tabModel.getName(),\n                            global,\n                            AppI18n.activeLanguage(),\n                            AppPrefs.get().censorMode()));\n        } else {\n            tab.textProperty().bind(tabModel.getName());\n        }\n\n        BaseRegionBuilder<?, ?> comp = tabModel.comp();\n        var compRegion = comp.build();\n\n        var empty = new StackPane();\n        empty.setMinWidth(180);\n        empty.widthProperty().addListener((observable, oldValue, newValue) -> {\n            if (tabModel.isCloseable() && tabs.getSelectionModel().getSelectedItem() == tab) {\n                rightPadding.setValue(newValue.doubleValue());\n            }\n        });\n\n        var split = new SplitPane(compRegion);\n        if (tabModel.isCloseable()) {\n            split.getItems().add(empty);\n        }\n        tabs.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {\n            if (tabModel.isCloseable() && newValue == tab) {\n                rightPadding.setValue(empty.getWidth());\n            }\n        });\n        model.getEffectiveRightTab().subscribe(browserSessionTab -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                if (browserSessionTab != null && split.getItems().size() > 1) {\n                    split.getItems().set(1, empty);\n                } else if (browserSessionTab != null && split.getItems().size() == 1) {\n                    split.getItems().add(empty);\n                } else if (browserSessionTab == null && split.getItems().size() > 1) {\n                    split.getItems().remove(1);\n                }\n            });\n        });\n        tab.setContent(split);\n\n        var id = UUID.randomUUID().toString();\n        tab.setId(id);\n\n        tabs.skinProperty().subscribe(newValue -> {\n            if (newValue != null) {\n                Platform.runLater(() -> {\n                    Label l = (Label) tabs.lookup(\"#\" + id + \" .tab-label\");\n                    l.setGraphicTextGap(7);\n                    var w = l.maxWidthProperty();\n                    l.minWidthProperty().bind(w);\n                    l.prefWidthProperty().bind(w);\n                    if (!tabModel.isCloseable()) {\n                        l.pseudoClassStateChanged(PseudoClass.getPseudoClass(\"static\"), true);\n                    }\n\n                    var close = (StackPane) tabs.lookup(\"#\" + id + \" .tab-close-button\");\n                    close.setPrefWidth(30);\n\n                    StackPane c = (StackPane) tabs.lookup(\"#\" + id + \" .tab-container\");\n                    c.getStyleClass().add(\"color-box\");\n                    var color = tabModel.getColor();\n                    if (color != null) {\n                        c.getStyleClass().add(color.getId());\n                    } else {\n                        c.getStyleClass().add(\"gray\");\n                    }\n                    c.addEventHandler(DragEvent.DRAG_ENTERED, de -> {\n                        // Prevent switch when dragging local files into app\n                        if (tabModel.isCloseable() && !de.getDragboard().hasContent(DataFormat.FILES)) {\n                            Platform.runLater(() -> tabs.getSelectionModel().select(tab));\n                            de.consume();\n                        }\n                    });\n                });\n            }\n        });\n\n        return tab;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/BrowserStoreSessionTab.java",
    "content": "package io.xpipe.app.browser;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreColor;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.Getter;\n\n@Getter\npublic abstract class BrowserStoreSessionTab<T extends DataStore> extends BrowserSessionTab {\n\n    protected final DataStoreEntryRef<? extends T> entry;\n    private final String name;\n\n    public BrowserStoreSessionTab(BrowserAbstractSessionModel<?> browserModel, DataStoreEntryRef<? extends T> entry) {\n        super(browserModel);\n        this.entry = entry;\n        this.name = DataStorage.get().getStoreEntryDisplayName(entry.get());\n    }\n\n    public abstract BaseRegionBuilder<?, ?> comp();\n\n    public abstract boolean canImmediatelyClose();\n\n    public abstract void init() throws Exception;\n\n    public abstract void close();\n\n    @Override\n    public ObservableValue<String> getName() {\n        return new SimpleStringProperty(name);\n    }\n\n    @Override\n    public String getIcon() {\n        return entry.get().getEffectiveIconFile();\n    }\n\n    @Override\n    public DataStoreColor getColor() {\n        return DataStorage.get().getEffectiveColor(entry.get());\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java",
    "content": "package io.xpipe.app.browser.action;\n\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.browser.BrowserStoreSessionTab;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.ext.FileSystemStore;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.core.FilePath;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport lombok.Getter;\nimport lombok.experimental.SuperBuilder;\n\nimport java.util.List;\n\n@SuperBuilder\npublic abstract class BrowserAction extends StoreAction<FileSystemStore> {\n\n    protected final List<FilePath> files;\n\n    @JsonIgnore\n    @Getter\n    protected BrowserFileSystemTabModel model;\n\n    @JsonIgnore\n    private List<BrowserEntry> entries;\n\n    @Override\n    protected void beforeExecute() throws Exception {\n        AppLayoutModel.get().selectBrowser();\n\n        if (model == null) {\n            var found = BrowserFullSessionModel.DEFAULT.getAllTabs().stream()\n                    .filter(t -> t instanceof BrowserStoreSessionTab<?> bs\n                            && bs.getEntry().equals(ref))\n                    .findFirst();\n            if (found.isPresent()) {\n                model = (BrowserFileSystemTabModel) found.get();\n                var target = getTargetDirectory(model);\n                model.cdSync(target.toString());\n                model.startIfNeeded();\n            } else {\n                model = BrowserFullSessionModel.DEFAULT.openFileSystemSync(\n                        ref.asNeeded(),\n                        null,\n                        model -> {\n                            return getTargetDirectory(model);\n                        },\n                        null,\n                        true);\n            }\n\n            validateAutomatedAction();\n        }\n\n        model.getBusy().set(true);\n\n        // Restart in case we exited\n        model.getFileSystem().reinitIfNeeded();\n    }\n\n    @Override\n    protected void afterExecute() {\n        model.getBusy().set(false);\n    }\n\n    private void validateAutomatedAction() throws Exception {\n        var bap = (BrowserActionProvider) getProvider();\n        if (!bap.isApplicable(getModel(), getEntries())) {\n            throw ErrorEventFactory.expected(\n                    new IllegalArgumentException(\"Selection is not applicable for action type\"));\n        }\n\n        if (files != null) {\n            for (var f : files) {\n                if (!model.getFileSystem().fileExists(f)\n                        && !model.getFileSystem().directoryExists(f)) {\n                    throw ErrorEventFactory.expected(new IllegalArgumentException(\"Target \" + f + \" does not exist\"));\n                }\n            }\n        }\n    }\n\n    private FilePath getTargetDirectory(BrowserFileSystemTabModel model) throws Exception {\n        var isFile = model.getFileSystem().fileExists(files.getFirst());\n        if (isFile) {\n            return files.getFirst().getParent();\n        } else {\n            var dir = files.getFirst();\n            if (!model.getFileSystem().directoryExists(dir)) {\n                throw ErrorEventFactory.expected(\n                        new IllegalArgumentException(\"File or directory does not exist: \" + dir));\n            }\n            return dir;\n        }\n    }\n\n    public List<BrowserEntry> getEntries() {\n        if (entries != null) {\n            return entries;\n        }\n\n        entries = files.stream()\n                .map(filePath -> {\n                    var be = model.getFileList().getAll().getValue().stream()\n                            .filter(browserEntry ->\n                                    browserEntry.getRawFileEntry().getPath().equals(filePath))\n                            .findFirst();\n                    if (be.isPresent()) {\n                        return be.get();\n                    }\n\n                    var current = model.getCurrentDirectory();\n                    if (current != null && filePath.equals(current.getPath())) {\n                        return new BrowserEntry(current, model.getFileList());\n                    }\n\n                    return null;\n                })\n                .filter(browserEntry -> browserEntry != null)\n                .toList();\n        return entries;\n    }\n\n    public abstract static class BrowserActionBuilder<C extends BrowserAction, B extends BrowserActionBuilder<C, B>>\n            extends StoreActionBuilder<FileSystemStore, C, B> {\n\n        public void initEntries(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            ref(model.getEntry().asNeeded());\n            model(model);\n            files(entries.stream()\n                    .map(browserEntry -> browserEntry.getRawFileEntry().getPath())\n                    .toList());\n            entries(entries);\n        }\n\n        public void initFiles(BrowserFileSystemTabModel model, List<FilePath> entries) {\n            ref(model.getEntry().asNeeded());\n            model(model);\n            files(entries);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/BrowserActionProvider.java",
    "content": "package io.xpipe.app.browser.action;\n\nimport io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\n\nimport java.util.List;\n\npublic interface BrowserActionProvider extends ActionProvider {\n\n    default boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return true;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/BrowserActionProviders.java",
    "content": "package io.xpipe.app.browser.action;\n\nimport io.xpipe.app.action.ActionProvider;\n\npublic class BrowserActionProviders {\n\n    public static BrowserActionProvider forClass(Class<? extends BrowserActionProvider> clazz) {\n        return (BrowserActionProvider) ActionProvider.ALL.stream()\n                .filter(actionProvider -> actionProvider.getClass().equals(clazz))\n                .findFirst()\n                .orElseThrow();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/impl/ApplyFileEditActionProvider.java",
    "content": "package io.xpipe.app.browser.action.impl;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.browser.file.BrowserFileInput;\nimport io.xpipe.app.browser.file.BrowserFileOutput;\nimport io.xpipe.app.storage.DataStorage;\n\nimport lombok.NonNull;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\npublic class ApplyFileEditActionProvider implements ActionProvider {\n\n    @Override\n    public String getId() {\n        return \"applyFileEdit\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends AbstractAction {\n\n        @NonNull\n        String target;\n\n        @NonNull\n        BrowserFileInput input;\n\n        @NonNull\n        BrowserFileOutput output;\n\n        @Override\n        public void executeImpl() throws Exception {\n            output.beforeTransfer();\n            try (var out = output.open()) {\n                input.open().transferTo(out);\n            }\n            try {\n                output.onFinish();\n            } finally {\n                input.onFinish();\n            }\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n\n        @Override\n        public Map<String, String> toDisplayMap() {\n            var map = new LinkedHashMap<String, String>();\n            map.put(\"Action\", getDisplayName());\n\n            var system = output.target();\n            if (system.isPresent()) {\n                map.put(\"System\", DataStorage.get().getStoreEntryDisplayName(system.get()));\n            }\n\n            map.put(\"File\", target);\n\n            return map;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/impl/BrowseInNativeManagerActionProvider.java",
    "content": "package io.xpipe.app.browser.action.impl;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.util.DesktopHelper;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\n\npublic class BrowseInNativeManagerActionProvider implements BrowserActionProvider {\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        if (model.getFileSystem().getShell().isEmpty()) {\n            return false;\n        }\n\n        return model.getFileSystem()\n                .getShell()\n                .orElseThrow()\n                .getLocalSystemAccess()\n                .supportsFileSystemAccess();\n    }\n\n    @Override\n    public String getId() {\n        return \"browseInNativeFileManager\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        @Override\n        public void executeImpl() throws Exception {\n            ShellControl sc = model.getFileSystem().getShell().orElseThrow();\n            for (BrowserEntry entry : getEntries()) {\n                var e = entry.getRawFileEntry().getPath();\n                var localFile = sc.getLocalSystemAccess().translateToLocalSystemPath(e);\n                DesktopHelper.browseFileInDirectory(localFile.asLocalPath());\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/impl/ChgrpActionProvider.java",
    "content": "package io.xpipe.app.browser.action.impl;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\n\nimport lombok.NonNull;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\n\npublic class ChgrpActionProvider implements BrowserActionProvider {\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return model.getFileSystem().supportsChgrp();\n    }\n\n    @Override\n    public String getId() {\n        return \"chgrp\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        @NonNull\n        private final String group;\n\n        private final boolean recursive;\n\n        @Override\n        public void executeImpl() throws Exception {\n            for (BrowserEntry entry : getEntries()) {\n                model.getFileSystem().chgrp(entry.getRawFileEntry().getPath(), group, recursive);\n            }\n            model.refreshBrowserEntriesSync(getEntries());\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/impl/ChmodActionProvider.java",
    "content": "package io.xpipe.app.browser.action.impl;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\n\nimport lombok.NonNull;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\n\npublic class ChmodActionProvider implements BrowserActionProvider {\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return model.getFileSystem().supportsChmod();\n    }\n\n    @Override\n    public String getId() {\n        return \"chmod\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        @NonNull\n        private final String permissions;\n\n        private final boolean recursive;\n\n        @Override\n        public void executeImpl() throws Exception {\n            for (BrowserEntry entry : getEntries()) {\n                model.getFileSystem().chmod(entry.getRawFileEntry().getPath(), permissions, recursive);\n            }\n            model.refreshBrowserEntriesSync(getEntries());\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/impl/ChownActionProvider.java",
    "content": "package io.xpipe.app.browser.action.impl;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\n\nimport lombok.NonNull;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\n\npublic class ChownActionProvider implements BrowserActionProvider {\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return model.getFileSystem().supportsChown();\n    }\n\n    @Override\n    public String getId() {\n        return \"chown\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        @NonNull\n        private final String owner;\n\n        private final boolean recursive;\n\n        @Override\n        public void executeImpl() throws Exception {\n            for (BrowserEntry entry : getEntries()) {\n                model.getFileSystem().chown(entry.getRawFileEntry().getPath(), owner, recursive);\n            }\n            model.refreshBrowserEntriesSync(getEntries());\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/impl/ComputeDirectorySizesActionProvider.java",
    "content": "package io.xpipe.app.browser.action.impl;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.ext.FileKind;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\n\npublic class ComputeDirectorySizesActionProvider implements BrowserActionProvider {\n\n    @Override\n    public String getId() {\n        return \"computeDirectorySizes\";\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return model.getFileSystem().supportsDirectorySizes();\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        @Override\n        public void executeImpl() throws Exception {\n            var entries = getEntries();\n            if (entries.size() == 1 && entries.getFirst().getRawFileEntry().equals(model.getCurrentDirectory())) {\n                entries = model.getFileList().getAll().getValue();\n            }\n\n            for (BrowserEntry be : entries) {\n                if (be.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY) {\n                    continue;\n                }\n\n                var size = model.getFileSystem()\n                        .getDirectorySize(be.getRawFileEntry().resolved().getPath());\n                var fileEntry = be.getRawFileEntry();\n                fileEntry.resolved().setSize(\"\" + size);\n                model.getFileList().updateEntry(be.getRawFileEntry().getPath(), fileEntry);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/impl/DeleteActionProvider.java",
    "content": "package io.xpipe.app.browser.action.impl;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.file.*;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class DeleteActionProvider implements BrowserActionProvider {\n\n    @Override\n    public String getId() {\n        return \"deleteFile\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        @Override\n        public void executeImpl() {\n            var toDelete =\n                    getEntries().stream().map(entry -> entry.getRawFileEntry()).toList();\n            BrowserFileSystemHelper.delete(toDelete);\n            model.refreshSync();\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/impl/MoveFileActionProvider.java",
    "content": "package io.xpipe.app.browser.action.impl;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.core.FilePath;\n\nimport lombok.NonNull;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class MoveFileActionProvider implements BrowserActionProvider {\n\n    @Override\n    public String getId() {\n        return \"moveFile\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        @NonNull\n        FilePath target;\n\n        @Override\n        public void executeImpl() throws Exception {\n            model.getFileSystem().move(getEntries().getFirst().getRawFileEntry().getPath(), target);\n            model.refreshSync();\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/impl/NewDirectoryActionProvider.java",
    "content": "package io.xpipe.app.browser.action.impl;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.ext.FileKind;\n\nimport lombok.NonNull;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class NewDirectoryActionProvider implements BrowserActionProvider {\n\n    @Override\n    public String getId() {\n        return \"newDirectory\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        @NonNull\n        String name;\n\n        @Override\n        public void executeImpl() throws Exception {\n            for (BrowserEntry entry : getEntries()) {\n                if (entry.getRawFileEntry().getKind() != FileKind.DIRECTORY) {\n                    continue;\n                }\n\n                var file = entry.getRawFileEntry().getPath().join(name);\n                model.getFileSystem().mkdirs(file);\n            }\n            model.refreshSync();\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/impl/NewFileActionProvider.java",
    "content": "package io.xpipe.app.browser.action.impl;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.ext.FileKind;\n\nimport lombok.NonNull;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class NewFileActionProvider implements BrowserActionProvider {\n\n    @Override\n    public String getId() {\n        return \"newFile\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        @NonNull\n        String name;\n\n        @Override\n        public void executeImpl() throws Exception {\n            for (BrowserEntry entry : getEntries()) {\n                if (entry.getRawFileEntry().getKind() != FileKind.DIRECTORY) {\n                    continue;\n                }\n\n                var file = entry.getRawFileEntry().getPath().join(name);\n                model.getFileSystem().touch(file);\n            }\n            model.refreshSync();\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/impl/NewLinkActionProvider.java",
    "content": "package io.xpipe.app.browser.action.impl;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.core.FilePath;\n\nimport lombok.NonNull;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class NewLinkActionProvider implements BrowserActionProvider {\n\n    @Override\n    public String getId() {\n        return \"newLink\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        @NonNull\n        String name;\n\n        @NonNull\n        FilePath target;\n\n        @Override\n        public void executeImpl() throws Exception {\n            for (BrowserEntry entry : getEntries()) {\n                if (entry.getRawFileEntry().getKind() != FileKind.DIRECTORY) {\n                    continue;\n                }\n\n                var file = entry.getRawFileEntry().getPath().join(name);\n                model.getFileSystem().symbolicLink(file, target);\n            }\n            model.refreshSync();\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/impl/OpenDirectoryActionProvider.java",
    "content": "package io.xpipe.app.browser.action.impl;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.ext.FileKind;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\n\npublic class OpenDirectoryActionProvider implements BrowserActionProvider {\n\n    @Override\n    public String getId() {\n        return \"openDirectory\";\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return entries.size() == 1\n                && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY);\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        @Override\n        public void executeImpl() {\n            var first = getEntries().getFirst();\n            model.cdSync(first.getRawFileEntry().getPath().toString());\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileDefaultActionProvider.java",
    "content": "package io.xpipe.app.browser.action.impl;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileOpener;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.ext.FileKind;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\n\npublic class OpenFileDefaultActionProvider implements BrowserActionProvider {\n\n    @Override\n    public String getId() {\n        return \"openFileDefault\";\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return model.getFileList().getEditing().getValue() == null\n                && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE);\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        @Override\n        public void executeImpl() {\n            for (var entry : getEntries()) {\n                BrowserFileOpener.openInDefaultApplication(model, entry.getRawFileEntry());\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileNativeDetailsActionProvider.java",
    "content": "package io.xpipe.app.browser.action.impl;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.core.OsType;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\n\npublic class OpenFileNativeDetailsActionProvider implements BrowserActionProvider {\n\n    @Override\n    public String getId() {\n        return \"openFileNativeDetails\";\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        if (model.getFileSystem().getShell().isEmpty()) {\n            return false;\n        }\n\n        var sc = model.getFileSystem().getShell().orElseThrow();\n        return sc.getLocalSystemAccess().supportsFileSystemAccess();\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        @Override\n        public void executeImpl() throws Exception {\n            ShellControl sc = model.getFileSystem().getShell().get();\n            for (BrowserEntry entry : getEntries()) {\n                var e = entry.getRawFileEntry().getPath();\n                var localFile = sc.getLocalSystemAccess().translateToLocalSystemPath(e);\n                switch (OsType.ofLocal()) {\n                    case OsType.Windows ignored -> {\n                        var shell = LocalShell.getLocalPowershell();\n                        if (shell.isEmpty()) {\n                            return;\n                        }\n\n                        var parent = localFile.getParent();\n                        // If we execute this on a drive root there will be no parent, so we have to check for that!\n                        var content = parent != null\n                                ? String.format(\n                                        \"$shell = New-Object -ComObject Shell.Application; $shell.NameSpace('%s').ParseName('%s').InvokeVerb('Properties')\",\n                                        parent, localFile.getFileName())\n                                : String.format(\n                                        \"$shell = New-Object -ComObject Shell.Application; $shell.NameSpace('%s').Self.InvokeVerb('Properties')\",\n                                        localFile);\n\n                        // The Windows shell invoke verb functionality behaves kinda weirdly and only shows the window\n                        // as\n                        // long as the parent process is running.\n                        // So let's keep one process running\n                        // Ignore exit value as this can fail somehow (maybe if the system blocks shell com objects?)\n                        shell.get().command(content).notComplex().executeAndCheck();\n                    }\n                    case OsType.Linux ignored -> {\n                        var dbus = String.format(\"\"\"\n                                                 dbus-send --session --print-reply --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItemProperties array:string:\"file://%s\" string:\"\"\n                                                 \"\"\", localFile);\n                        var success = sc.executeSimpleBooleanCommand(dbus);\n                        if (success) {\n                            return;\n                        }\n\n                        sc.command(CommandBuilder.of()\n                                        .add(\"xdg-open\")\n                                        .addFile(\n                                                entry.getRawFileEntry().getKind() == FileKind.DIRECTORY\n                                                        ? e\n                                                        : e.getParent()))\n                                .execute();\n                    }\n                    case OsType.MacOs ignored -> {\n                        sc.osascriptCommand(String.format(\"\"\"\n                                                          set fileEntry to (POSIX file \"%s\") as text\n                                                          tell application \"Finder\"\n                                                              activate\n                                                              open information window of alias fileEntry\n                                                          end tell\n                                                          \"\"\", localFile)).execute();\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileNativeManagerActionProvider.java",
    "content": "package io.xpipe.app.browser.action.impl;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.util.DesktopHelper;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\n\npublic class OpenFileNativeManagerActionProvider implements BrowserActionProvider {\n\n    @Override\n    public String getId() {\n        return \"openFileNativeManager\";\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        if (model.getFileSystem().getShell().isEmpty()) {\n            return false;\n        }\n\n        var sc = model.getFileSystem().getShell().orElseThrow();\n        return sc.getLocalSystemAccess().supportsFileSystemAccess();\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        @Override\n        public void executeImpl() throws Exception {\n            ShellControl sc = model.getFileSystem().getShell().get();\n            for (BrowserEntry entry : getEntries()) {\n                var e = entry.getRawFileEntry().getPath();\n                var localFile = sc.getLocalSystemAccess().translateToLocalSystemPath(e);\n                DesktopHelper.browseFileInDirectory(localFile.asLocalPath());\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileWithActionProvider.java",
    "content": "package io.xpipe.app.browser.action.impl;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileOpener;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.core.OsType;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\n\npublic class OpenFileWithActionProvider implements BrowserActionProvider {\n\n    @Override\n    public String getId() {\n        return \"openFileWith\";\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return OsType.ofLocal() == OsType.WINDOWS\n                && entries.size() == 1\n                && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE);\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        @Override\n        public void executeImpl() {\n            for (var entry : getEntries()) {\n                BrowserFileOpener.openWithAnyApplication(model, entry.getRawFileEntry());\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBackgroundActionProvider.java",
    "content": "package io.xpipe.app.browser.action.impl;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.ProcessOutputException;\n\nimport lombok.NonNull;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicReference;\n\npublic class RunCommandInBackgroundActionProvider implements BrowserActionProvider {\n\n    @Override\n    public String getId() {\n        return \"runFileInBackground\";\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return model.getFileSystem().getShell().isPresent();\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        @NonNull\n        String command;\n\n        @Override\n        public void executeImpl() throws Exception {\n            AtomicReference<String> out = new AtomicReference<>();\n            AtomicReference<String> err = new AtomicReference<>();\n            long exitCode;\n            try (var cc = model.getFileSystem()\n                    .getShell()\n                    .orElseThrow()\n                    .command(command)\n                    .withWorkingDirectory(files.getFirst())\n                    .start()) {\n                var r = cc.readStdoutAndStderr();\n                out.set(r[0]);\n                err.set(r[1]);\n                exitCode = cc.getExitCode();\n            }\n\n            model.refreshSync();\n\n            // Only throw actual error output\n            if (exitCode != 0) {\n                throw ErrorEventFactory.expected(ProcessOutputException.of(command, exitCode, out.get(), err.get()));\n            }\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n\n        @Override\n        public Map<String, String> toDisplayMap() {\n            var map = new LinkedHashMap<>(super.toDisplayMap());\n            map.remove(\"Title\");\n            map.remove(\"Files\");\n            map.put(\"Working Directory\", files.getFirst().toString());\n            return map;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBrowserActionProvider.java",
    "content": "package io.xpipe.app.browser.action.impl;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.util.CommandDialog;\n\nimport lombok.NonNull;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic class RunCommandInBrowserActionProvider implements BrowserActionProvider {\n\n    @Override\n    public String getId() {\n        return \"runCommandInBrowser\";\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return model.getFileSystem().getShell().isPresent();\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        @NonNull\n        String command;\n\n        @Override\n        public void executeImpl() {\n            var cmd = model.getFileSystem()\n                    .getShell()\n                    .orElseThrow()\n                    .command(command)\n                    .withWorkingDirectory(files.getFirst());\n            CommandDialog.runAndShow(cmd);\n            model.refreshSync();\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n\n        @Override\n        public Map<String, String> toDisplayMap() {\n            var map = new LinkedHashMap<>(super.toDisplayMap());\n            map.remove(\"Files\");\n            map.put(\"Working Directory\", files.getFirst().toString());\n            return map;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInTerminalActionProvider.java",
    "content": "package io.xpipe.app.browser.action.impl;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\n\nimport lombok.NonNull;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic class RunCommandInTerminalActionProvider implements BrowserActionProvider {\n\n    @Override\n    public String getId() {\n        return \"runCommandInTerminal\";\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return model.getFileSystem().getShell().isPresent();\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        String title;\n\n        @NonNull\n        String command;\n\n        @Override\n        public void executeImpl() throws Exception {\n            var wd = files.getFirst();\n            model.openTerminalSync(\n                    title,\n                    wd,\n                    model.getFileSystem()\n                            .getShell()\n                            .orElseThrow()\n                            .command(command)\n                            .withWorkingDirectory(wd),\n                    true);\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n\n        @Override\n        public Map<String, String> toDisplayMap() {\n            var map = new LinkedHashMap<>(super.toDisplayMap());\n            map.remove(\"Title\");\n            map.remove(\"Files\");\n            map.put(\"Working Directory\", files.getFirst().toString());\n            return map;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/action/impl/TransferFilesActionProvider.java",
    "content": "package io.xpipe.app.browser.action.impl;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.action.StoreContextAction;\nimport io.xpipe.app.browser.file.BrowserFileTransferOperation;\nimport io.xpipe.app.ext.FileSystemStore;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport lombok.NonNull;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\npublic class TransferFilesActionProvider implements ActionProvider {\n\n    @Override\n    public String getId() {\n        return \"transferFiles\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends AbstractAction implements StoreContextAction {\n\n        @NonNull\n        DataStoreEntryRef<FileSystemStore> target;\n\n        @NonNull\n        BrowserFileTransferOperation operation;\n\n        boolean download;\n\n        @Override\n        public void executeImpl() throws Exception {\n            operation.execute();\n        }\n\n        @Override\n        public boolean isMutation() {\n            return !download;\n        }\n\n        @Override\n        public boolean forceConfirmation() {\n            return operation.isMove();\n        }\n\n        @Override\n        public Map<String, String> toDisplayMap() {\n            var name = operation.isMove() ? \"Move files\" : getDisplayName();\n            var map = new LinkedHashMap<String, String>();\n            map.put(\"Action\", name);\n            map.put(\n                    \"Sources\",\n                    operation.getFiles().stream()\n                            .map(fileEntry -> fileEntry.getName())\n                            .collect(Collectors.joining(\"\\n\")));\n            map.put(\"Target system\", DataStorage.get().getStoreEntryDisplayName(target.get()));\n            map.put(\"Target directory\", operation.getTarget().getPath().toString());\n            return map;\n        }\n\n        @Override\n        public List<DataStoreEntry> getStoreEntryContext() {\n            return List.of(target.get());\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserBreadcrumbBar.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.util.BooleanScope;\nimport io.xpipe.app.util.GlobalTimer;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.FilePath;\n\nimport javafx.application.Platform;\nimport javafx.css.PseudoClass;\nimport javafx.scene.Node;\nimport javafx.scene.control.Button;\nimport javafx.scene.control.ButtonBase;\nimport javafx.scene.control.Label;\nimport javafx.scene.input.DragEvent;\nimport javafx.scene.input.TransferMode;\nimport javafx.scene.layout.Region;\nimport javafx.util.Callback;\n\nimport atlantafx.base.controls.Breadcrumbs;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\n\npublic class BrowserBreadcrumbBar extends SimpleRegionBuilder {\n\n    private final BrowserFileSystemTabModel model;\n    private Instant lastHoverUpdate;\n\n    public BrowserBreadcrumbBar(BrowserFileSystemTabModel model) {\n        this.model = model;\n    }\n\n    @Override\n    protected Region createSimple() {\n        Callback<Breadcrumbs.BreadCrumbItem<FilePath>, ButtonBase> crumbFactory = crumb -> {\n            var name = crumb.getValue().toString().equals(\"/\")\n                    ? \"/\"\n                    : crumb.getValue().getFileName();\n            var btn = new Button(name, null);\n            btn.setMnemonicParsing(false);\n            btn.setFocusTraversable(false);\n            btn.setOnDragEntered(event -> onDragEntered(btn, crumb.getValue()));\n            btn.setOnDragOver(event -> onDragOver(event));\n            btn.setOnDragExited(event -> onDragExited(btn));\n            return btn;\n        };\n        return createBreadcrumbs(crumbFactory, null);\n    }\n\n    private void onDragEntered(Button button, FilePath path) {\n        button.pseudoClassStateChanged(PseudoClass.getPseudoClass(\"hover\"), true);\n\n        var timestamp = Instant.now();\n        lastHoverUpdate = timestamp;\n        // Reduce printed window updates\n        GlobalTimer.delay(\n                () -> {\n                    if (!timestamp.equals(lastHoverUpdate)) {\n                        return;\n                    }\n\n                    model.cdAsync(path);\n                },\n                Duration.ofMillis(500));\n    }\n\n    private void onDragOver(DragEvent event) {\n        event.acceptTransferModes(TransferMode.COPY_OR_MOVE);\n        event.consume();\n    }\n\n    private void onDragExited(Button button) {\n        button.pseudoClassStateChanged(PseudoClass.getPseudoClass(\"hover\"), false);\n\n        lastHoverUpdate = null;\n    }\n\n    private Region createBreadcrumbs(\n            Callback<Breadcrumbs.BreadCrumbItem<FilePath>, ButtonBase> crumbFactory,\n            Callback<Breadcrumbs.BreadCrumbItem<FilePath>, ? extends Node> dividerFactory) {\n\n        var breadcrumbs = new Breadcrumbs<FilePath>();\n        breadcrumbs.setMinWidth(0);\n        model.getCurrentPath().subscribe(val -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                if (val == null) {\n                    breadcrumbs.setSelectedCrumb(null);\n                    return;\n                }\n\n                breadcrumbs.setDividerFactory(item -> {\n                    if (item == null) {\n                        return null;\n                    }\n\n                    if (item.isFirst() && item.getValue().toString().equals(\"/\")) {\n                        return new Label(\"\");\n                    }\n\n                    return new Label(model.getFileSystem().getFileSeparator());\n                });\n\n                var elements = createBreadcrumbHierarchy(val);\n                Breadcrumbs.BreadCrumbItem<FilePath> items =\n                        Breadcrumbs.buildTreeModel(elements.toArray(FilePath[]::new));\n                breadcrumbs.setSelectedCrumb(items);\n            });\n        });\n\n        if (crumbFactory != null) {\n            breadcrumbs.setCrumbFactory(crumbFactory);\n        }\n        if (dividerFactory != null) {\n            breadcrumbs.setDividerFactory(dividerFactory);\n        }\n\n        breadcrumbs.selectedCrumbProperty().addListener((obs, old, val) -> {\n            ThreadHelper.runAsync(() -> {\n                BooleanScope.executeExclusive(model.getBusy(), () -> {\n                    model.cdSync(val != null ? val.getValue().toString() : null);\n                    var now = model.getCurrentPath().getValue();\n                    // If we initiated a cd from the navbar, but it was rejected, reflect the changes\n                    if (!Objects.equals(now, val != null ? val.getValue() : null)) {\n                        Platform.runLater(() -> {\n                            breadcrumbs.setSelectedCrumb(old);\n                        });\n                    }\n                });\n            });\n        });\n\n        return breadcrumbs;\n    }\n\n    private List<FilePath> createBreadcrumbHierarchy(FilePath filePath) {\n        var f = filePath.toDirectory().toString();\n        var list = new ArrayList<FilePath>();\n        int lastElementStart = 0;\n        for (int i = 0; i < f.length(); i++) {\n            if (f.charAt(i) == '\\\\' || f.charAt(i) == '/') {\n                if (i - lastElementStart > 0) {\n                    list.add(FilePath.of(f.substring(0, i)).toDirectory());\n                }\n\n                lastElementStart = i + 1;\n            }\n        }\n\n        if (filePath.toString().startsWith(\"/\")) {\n            list.addFirst(FilePath.of(\"/\"));\n        }\n\n        return list;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserClipboard.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.ext.FileEntry;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.GlobalClipboard;\nimport io.xpipe.app.platform.GlobalObjectProperty;\n\nimport javafx.beans.property.Property;\nimport javafx.scene.input.ClipboardContent;\nimport javafx.scene.input.DataFormat;\nimport javafx.scene.input.Dragboard;\n\nimport lombok.SneakyThrows;\nimport lombok.Value;\n\nimport java.awt.datatransfer.Clipboard;\nimport java.awt.datatransfer.DataFlavor;\nimport java.io.File;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.function.Consumer;\nimport java.util.stream.Collectors;\n\npublic class BrowserClipboard {\n\n    public static final Property<Instance> currentCopyClipboard = new GlobalObjectProperty<>();\n    private static final DataFormat DATA_FORMAT = new DataFormat(\"application/xpipe-file-list\");\n    public static Instance currentDragClipboard;\n\n    static {\n        GlobalClipboard.addListener(new Consumer<>() {\n            @Override\n            @SuppressWarnings(\"unchecked\")\n            public void accept(Clipboard clipboard) {\n                try {\n                    if (!clipboard.isDataFlavorAvailable(DataFlavor.javaFileListFlavor)) {\n                        return;\n                    }\n\n                    List<File> data = (List<File>) clipboard.getData(DataFlavor.javaFileListFlavor);\n                    // Sometimes file data can contain invalid chars. Why?\n                    var files = data.stream()\n                            .filter(file -> file.toString().chars().noneMatch(value -> Character.isISOControl(value)))\n                            .filter(file -> !file.toString().isBlank())\n                            .filter(file -> file.exists())\n                            .map(f -> f.toPath())\n                            .toList();\n                    if (files.size() == 0) {\n                        return;\n                    }\n\n                    var entries = new ArrayList<BrowserEntry>();\n                    for (Path file : files) {\n                        entries.add(BrowserLocalFileSystem.getLocalBrowserEntry(file));\n                    }\n\n                    currentCopyClipboard.setValue(\n                            new Instance(UUID.randomUUID(), null, entries, BrowserFileTransferMode.COPY));\n                } catch (Exception e) {\n                    ErrorEventFactory.fromThrowable(e).expected().omit().handle();\n                }\n            }\n        });\n    }\n\n    @SneakyThrows\n    public static ClipboardContent startDrag(\n            FileEntry base, List<BrowserEntry> selected, BrowserFileTransferMode mode) {\n        if (selected.isEmpty()) {\n            return null;\n        }\n\n        var content = new ClipboardContent();\n        var id = UUID.randomUUID();\n        currentDragClipboard = new Instance(id, base, new ArrayList<>(selected), mode);\n        content.put(DATA_FORMAT, currentDragClipboard.toClipboardString());\n        return content;\n    }\n\n    @SneakyThrows\n    public static void startCopy(FileEntry base, List<BrowserEntry> selected) {\n        if (selected.isEmpty()) {\n            currentCopyClipboard.setValue(null);\n            return;\n        }\n\n        var id = UUID.randomUUID();\n        currentCopyClipboard.setValue(new Instance(id, base, new ArrayList<>(selected), BrowserFileTransferMode.COPY));\n    }\n\n    public static Instance retrieveCopy() {\n        return currentCopyClipboard.getValue();\n    }\n\n    public static Instance retrieveDrag(Dragboard dragboard) {\n        if (currentDragClipboard == null) {\n            return null;\n        }\n\n        try {\n            var s = dragboard.getContent(DATA_FORMAT);\n            if (s != null && s.equals(currentDragClipboard.toClipboardString())) {\n                var current = currentDragClipboard;\n                currentDragClipboard = null;\n                return current;\n            }\n        } catch (Exception ex) {\n            return null;\n        }\n\n        return null;\n    }\n\n    @Value\n    public static class Instance {\n        UUID uuid;\n        FileEntry baseDirectory;\n        List<BrowserEntry> entries;\n        BrowserFileTransferMode mode;\n\n        public String toClipboardString() {\n            return entries.stream()\n                    .map(fileEntry -> \"\\\"\" + fileEntry.getRawFileEntry().getPath() + \"\\\"\")\n                    .collect(Collectors.joining(ProcessControlProvider.get()\n                            .getEffectiveLocalDialect()\n                            .getNewLine()\n                            .getNewLineString()));\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListComp.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.comp.*;\nimport io.xpipe.app.hub.comp.*;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.storage.DataStoreEntry;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.*;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.FXCollections;\nimport javafx.css.PseudoClass;\nimport javafx.scene.control.Button;\nimport javafx.scene.layout.Region;\n\nimport java.util.HashSet;\nimport java.util.Set;\nimport java.util.function.BiConsumer;\nimport java.util.function.Predicate;\n\npublic final class BrowserConnectionListComp extends SimpleRegionBuilder {\n\n    private static final PseudoClass SELECTED = PseudoClass.getPseudoClass(\"selected\");\n    private final ObservableValue<DataStoreEntry> selected;\n    private final Predicate<StoreEntryWrapper> applicable;\n    private final BiConsumer<StoreEntryWrapper, BooleanProperty> action;\n    private final Property<StoreCategoryWrapper> category;\n    private final Property<String> filter;\n\n    public BrowserConnectionListComp(\n            ObservableValue<DataStoreEntry> selected,\n            Predicate<StoreEntryWrapper> applicable,\n            BiConsumer<StoreEntryWrapper, BooleanProperty> action,\n            Property<StoreCategoryWrapper> category,\n            Property<String> filter) {\n        this.selected = selected;\n        this.applicable = applicable;\n        this.action = action;\n        this.category = category;\n        this.filter = filter;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var busyEntries = FXCollections.<StoreSection>observableSet(new HashSet<>());\n        BiConsumer<StoreSection, RegionBuilder<Button>> augment = (s, comp) -> {\n            comp.disable(Bindings.createBooleanBinding(\n                    () -> {\n                        return busyEntries.contains(s) || !applicable.test(s.getWrapper());\n                    },\n                    busyEntries));\n            comp.apply(struc -> {\n                selected.addListener((observable, oldValue, newValue) -> {\n                    PlatformThread.runLaterIfNeeded(() -> {\n                        struc.pseudoClassStateChanged(\n                                SELECTED,\n                                newValue != null\n                                        && newValue.equals(s.getWrapper().getEntry()));\n                    });\n                });\n            });\n        };\n\n        var section = new StoreSectionMiniComp(\n                StoreSection.createTopLevel(\n                        StoreViewState.get().getAllEntries(),\n                        Set.of(),\n                        this::filter,\n                        filter,\n                        category,\n                        StoreViewState.get().getEntriesListVisibilityObservable(),\n                        StoreViewState.get().getEntriesListUpdateObservable(),\n                        new ReadOnlyBooleanWrapper(true)),\n                augment,\n                selectedAction -> {\n                    BooleanProperty busy = new SimpleBooleanProperty(false);\n                    action.accept(selectedAction.getWrapper(), busy);\n                    busy.addListener((observable, oldValue, newValue) -> {\n                        if (newValue) {\n                            busyEntries.add(selectedAction);\n                        } else {\n                            busyEntries.remove(selectedAction);\n                        }\n                    });\n                },\n                false);\n\n        var r = section.vgrow().build();\n        r.getStyleClass().add(\"bookmark-list\");\n        return r;\n    }\n\n    private boolean filter(StoreEntryWrapper w) {\n        return applicable.test(w);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListFilterComp.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.FilterComp;\nimport io.xpipe.app.comp.base.HorizontalComp;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.hub.comp.DataStoreCategoryChoiceComp;\nimport io.xpipe.app.hub.comp.StoreCategoryWrapper;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.util.ObservableSubscriber;\n\nimport javafx.beans.property.Property;\nimport javafx.scene.layout.Region;\n\nimport atlantafx.base.theme.Styles;\nimport lombok.AllArgsConstructor;\nimport lombok.Getter;\n\nimport java.util.List;\n\n@Getter\n@AllArgsConstructor\npublic final class BrowserConnectionListFilterComp extends SimpleRegionBuilder {\n\n    private final ObservableSubscriber filterTrigger;\n    private final Property<StoreCategoryWrapper> category;\n    private final Property<String> filter;\n\n    @Override\n    protected Region createSimple() {\n        var category = new DataStoreCategoryChoiceComp(\n                        StoreViewState.get().getAllConnectionsCategory(),\n                        StoreViewState.get().getActiveCategory(),\n                        this.category,\n                        true,\n                ignored -> true)\n                .style(Styles.LEFT_PILL)\n                .apply(struc -> {\n                    AppFontSizes.base(struc);\n                });\n        var filter = new FilterComp(this.filter)\n                .style(Styles.RIGHT_PILL)\n                .minWidth(0)\n                .hgrow()\n                .apply(struc -> {\n                    AppFontSizes.base(struc);\n                    filterTrigger.subscribe(() -> {\n                        struc.requestFocus();\n                    });\n                });\n\n        var top = new HorizontalComp(List.of(category, filter))\n                .apply(struc -> struc.setFillHeight(true))\n                .apply(struc -> {\n                    var first = ((Region) struc.getChildren().get(0));\n                    var second = ((Region) struc.getChildren().get(1));\n                    first.prefHeightProperty().bind(second.heightProperty());\n                    first.minHeightProperty().bind(second.heightProperty());\n                    first.maxHeightProperty().bind(second.heightProperty());\n                    AppFontSizes.xl(struc);\n                })\n                .style(\"bookmarks-header\")\n                .build();\n        return top;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserContextMenu.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuItemProvider;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.platform.InputHelper;\nimport io.xpipe.app.prefs.AppPrefs;\n\nimport javafx.scene.control.ContextMenu;\nimport javafx.scene.control.SeparatorMenuItem;\nimport javafx.scene.input.KeyEvent;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic final class BrowserContextMenu extends ContextMenu {\n\n    private final BrowserFileSystemTabModel model;\n    private final BrowserEntry source;\n    private final boolean quickAccess;\n\n    public BrowserContextMenu(BrowserFileSystemTabModel model, BrowserEntry source, boolean quickAccess) {\n        this.model = model;\n        this.source = source;\n        this.quickAccess = quickAccess;\n        createMenu();\n    }\n\n    private void createMenu() {\n        AppFontSizes.lg(getStyleableNode());\n\n        setAutoHide(!AppPrefs.get().limitedTouchscreenMode().get());\n\n        InputHelper.onLeft(this, false, e -> {\n            hide();\n            e.consume();\n        });\n\n        var empty = source == null;\n        var selected = new ArrayList<>(\n                empty\n                        ? List.of(new BrowserEntry(model.getCurrentDirectory(), model.getFileList()))\n                        : quickAccess ? List.of() : model.getFileList().getSelection());\n        if (source != null && !selected.contains(source)) {\n            selected.add(source);\n        }\n\n        for (var cat : BrowserMenuCategory.values()) {\n            var all = ActionProvider.ALL.stream()\n                    .map(actionProvider -> actionProvider instanceof BrowserMenuItemProvider ba ? ba : null)\n                    .filter(browserActionProvider -> browserActionProvider != null)\n                    .filter(browserAction -> browserAction.getCategory() == cat)\n                    .filter(browserAction -> {\n                        var used = browserAction.resolveFilesIfNeeded(selected);\n                        if (!browserAction.isApplicable(model, used)) {\n                            return false;\n                        }\n\n                        if (!browserAction.acceptsEmptySelection() && empty) {\n                            return false;\n                        }\n\n                        return true;\n                    })\n                    .toList();\n            if (all.size() == 0) {\n                continue;\n            }\n\n            if (getItems().size() > 0) {\n                getItems().add(new SeparatorMenuItem());\n            }\n\n            for (var a : all) {\n                var used = a.resolveFilesIfNeeded(selected);\n                var item = a.toMenuItem(model, used);\n                if (item != null) {\n                    getItems().add(item);\n\n                    if (a.getShortcut() != null) {\n                        addEventHandler(KeyEvent.KEY_PRESSED, event -> {\n                            if (!a.getShortcut().match(event)) {\n                                return;\n                            }\n\n                            hide();\n                        });\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserDialogs.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.core.FilePath;\n\nimport javafx.beans.property.SimpleObjectProperty;\n\npublic class BrowserDialogs {\n\n    public static FileConflictChoice showFileConflictDialog(FilePath file, boolean multiple) {\n        var choice = new SimpleObjectProperty<FileConflictChoice>();\n        var key = multiple ? \"fileConflictAlertContentMultiple\" : \"fileConflictAlertContent\";\n        var w = multiple ? 900 : 400;\n        var modal = ModalOverlay.of(\n                \"fileConflictAlertTitle\",\n                AppDialog.dialogText(AppI18n.observable(key, file)).prefWidth(w));\n        modal.addButton(new ModalButton(\"cancel\", () -> choice.set(FileConflictChoice.CANCEL), true, false));\n        if (multiple) {\n            modal.addButton(new ModalButton(\"skip\", () -> choice.set(FileConflictChoice.SKIP), true, false));\n            modal.addButton(new ModalButton(\"skipAll\", () -> choice.set(FileConflictChoice.SKIP_ALL), true, false));\n        }\n        modal.addButton(new ModalButton(\"replace\", () -> choice.set(FileConflictChoice.REPLACE), true, false));\n        if (multiple) {\n            modal.addButton(\n                    new ModalButton(\"replaceAll\", () -> choice.set(FileConflictChoice.REPLACE_ALL), true, false));\n        }\n        modal.addButton(new ModalButton(\"rename\", () -> choice.set(FileConflictChoice.RENAME), true, false));\n        if (multiple) {\n            modal.addButton(new ModalButton(\"renameAll\", () -> choice.set(FileConflictChoice.RENAME_ALL), true, false));\n        }\n        modal.showAndWait();\n        return choice.get() != null ? choice.get() : FileConflictChoice.CANCEL;\n    }\n\n    public enum FileConflictChoice {\n        CANCEL,\n        SKIP,\n        SKIP_ALL,\n        REPLACE,\n        REPLACE_ALL,\n        RENAME,\n        RENAME_ALL\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserEntry.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.browser.icon.BrowserIconDirectoryType;\nimport io.xpipe.app.browser.icon.BrowserIconFileType;\nimport io.xpipe.app.ext.FileEntry;\nimport io.xpipe.app.ext.FileKind;\n\nimport lombok.Getter;\n\n@Getter\npublic class BrowserEntry {\n\n    private final BrowserFileListModel model;\n    private final FileEntry rawFileEntry;\n    private final BrowserIconFileType fileType;\n    private final BrowserIconDirectoryType directoryType;\n\n    public BrowserEntry(FileEntry rawFileEntry, BrowserFileListModel model) {\n        this.rawFileEntry = rawFileEntry;\n        this.model = model;\n        this.fileType = fileType(rawFileEntry);\n        this.directoryType = directoryType(rawFileEntry);\n    }\n\n    private static BrowserIconFileType fileType(FileEntry rawFileEntry) {\n        if (rawFileEntry == null) {\n            return null;\n        }\n        rawFileEntry = rawFileEntry.resolved();\n\n        if (rawFileEntry.getKind() != FileKind.FILE) {\n            return null;\n        }\n\n        for (var f : BrowserIconFileType.getAll()) {\n            if (f.matches(rawFileEntry)) {\n                return f;\n            }\n        }\n\n        return null;\n    }\n\n    private static BrowserIconDirectoryType directoryType(FileEntry rawFileEntry) {\n        if (rawFileEntry == null) {\n            return null;\n        }\n        rawFileEntry = rawFileEntry.resolved();\n\n        if (rawFileEntry.getKind() != FileKind.DIRECTORY) {\n            return null;\n        }\n\n        for (var f : BrowserIconDirectoryType.getAll()) {\n            if (f.matches(rawFileEntry)) {\n                return f;\n            }\n        }\n\n        return null;\n    }\n\n    public String getIcon() {\n        if (fileType != null) {\n            return fileType.getIcon();\n        } else if (directoryType != null) {\n            return directoryType.getIcon();\n        } else {\n            return rawFileEntry != null && rawFileEntry.resolved().getKind() == FileKind.DIRECTORY\n                    ? \"browser/default_folder.svg\"\n                    : \"browser/default_file.svg\";\n        }\n    }\n\n    public String getFileName() {\n        return getRawFileEntry().getPath().getFileName();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserFileDuplicates.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.ext.FileSystem;\nimport io.xpipe.core.FilePath;\n\nimport java.util.regex.Pattern;\n\npublic class BrowserFileDuplicates {\n\n    public static FilePath renameFileDuplicate(FileSystem fileSystem, FilePath target, boolean dir) throws Exception {\n        // Who has more than 10 copies?\n        for (int i = 0; i < 10; i++) {\n            target = renameFile(target, dir);\n            if ((dir && !fileSystem.directoryExists(target)) || (!dir && !fileSystem.fileExists(target))) {\n                return target;\n            }\n        }\n        return target;\n    }\n\n    private static FilePath renameFile(FilePath target, boolean dir) {\n        var name = dir ? target.getFileName() : target.getBaseName().getFileName();\n        var pattern = Pattern.compile(\"(.+)_(\\\\d+)\");\n        var matcher = pattern.matcher(name);\n        if (matcher.matches()) {\n            try {\n                var number = Integer.parseInt(matcher.group(2));\n                var suffix = dir ? \"\" : target.getExtension().map(s -> \".\" + s).orElse(\"\");\n                var newFile = target.getParent().join(matcher.group(1) + \"_\" + (number + 1) + suffix);\n                return newFile;\n            } catch (NumberFormatException ignored) {\n            }\n        }\n\n        var ext = target.getExtension();\n        return FilePath.of(\n                target.removeTrailingSlash().getBaseName() + \"_\" + 1 + (ext.isPresent() ? \".\" + ext.get() : \"\"));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserFileInput.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.ext.ConnectionFileSystem;\nimport io.xpipe.app.ext.FileEntry;\nimport io.xpipe.app.ext.FileInfo;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.ElevationFunction;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport java.io.InputStream;\n\npublic interface BrowserFileInput {\n\n    static BrowserFileInput openFileInput(BrowserFileSystemTabModel model, FileEntry file) throws Exception {\n        var defOutput = createFileInputImpl(model, file, false);\n        if (model.getFileSystem().getShell().isEmpty()) {\n            return defOutput;\n        }\n\n        var sc = model.getFileSystem().getShell().orElseThrow();\n        var requiresSudo =\n                sc.getOsType() != OsType.WINDOWS && requiresSudo(model, (FileInfo.Unix) file.getInfo(), file.getPath());\n\n        if (!requiresSudo) {\n            return defOutput;\n        }\n\n        var elevate = AppDialog.confirm(\"fileReadSudo\");\n        if (!elevate) {\n            return defOutput;\n        }\n\n        var rootOutput = createFileInputImpl(model, file, true);\n        return rootOutput;\n    }\n\n    private static boolean requiresSudo(BrowserFileSystemTabModel model, FileInfo.Unix info, FilePath filePath)\n            throws Exception {\n        if (model.getFileSystem().getShell().isEmpty()) {\n            return false;\n        }\n\n        var sc = model.getFileSystem().getShell().get();\n        if (sc.view().isRoot()) {\n            return false;\n        }\n\n        if (info != null) {\n            var otherWrite = info.getPermissions().charAt(6) == 'r';\n            if (otherWrite) {\n                return false;\n            }\n\n            var userOwned = info.getUid() != null\n                            && sc.view().getPasswdFile().getUidForUser(sc.view().user()) == info.getUid()\n                    || info.getUser() != null && sc.view().user().equals(info.getUser());\n            var userWrite = info.getPermissions().charAt(0) == 'r';\n            if (userOwned && userWrite) {\n                return false;\n            }\n        }\n\n        var test = model.getFileSystem()\n                .getShell()\n                .orElseThrow()\n                .command(CommandBuilder.of().add(\"test\", \"-r\").addFile(filePath))\n                .executeAndCheck();\n        return !test;\n    }\n\n    private static BrowserFileInput createFileInputImpl(\n            BrowserFileSystemTabModel model, FileEntry file, boolean elevate) throws Exception {\n        var shell = model.getFileSystem().getShell();\n        var sc = shell.isEmpty()\n                ? null\n                : elevate\n                        ? shell.orElseThrow()\n                                .identicalDialectSubShell()\n                                .elevated(ElevationFunction.elevated(null))\n                                .start()\n                        : model.getFileSystem().getShell().orElseThrow().start();\n        var fs = elevate ? new ConnectionFileSystem(sc) : model.getFileSystem();\n        var output = new BrowserFileInput() {\n\n            @Override\n            public InputStream open() throws Exception {\n                try {\n                    return fs.openInput(file.getPath());\n                } catch (Exception ex) {\n                    if (elevate) {\n                        fs.close();\n                    }\n                    throw ex;\n                }\n            }\n\n            @Override\n            public void onFinish() throws Exception {\n                if (elevate) {\n                    fs.close();\n                }\n            }\n        };\n        return output;\n    }\n\n    static BrowserFileInput none() {\n        return new BrowserFileInput() {\n\n            @Override\n            public InputStream open() {\n                return null;\n            }\n\n            @Override\n            public void onFinish() {}\n        };\n    }\n\n    static BrowserFileInput of(InputStream in) {\n        return new BrowserFileInput() {\n            @Override\n            public InputStream open() {\n                return in;\n            }\n\n            @Override\n            public void onFinish() {}\n        };\n    }\n\n    InputStream open() throws Exception;\n\n    void onFinish() throws Exception;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.browser.menu.BrowserMenuProviders;\nimport io.xpipe.app.comp.RegionDescriptor;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.FileEntry;\nimport io.xpipe.app.ext.FileInfo;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.util.*;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.*;\nimport javafx.collections.FXCollections;\nimport javafx.collections.ListChangeListener;\nimport javafx.css.PseudoClass;\nimport javafx.geometry.Bounds;\nimport javafx.scene.control.*;\nimport javafx.scene.control.skin.TableViewSkin;\nimport javafx.scene.control.skin.VirtualFlow;\nimport javafx.scene.input.*;\nimport javafx.scene.layout.Region;\n\nimport atlantafx.base.theme.Styles;\nimport lombok.SneakyThrows;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport java.util.*;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.function.BiConsumer;\n\nimport static io.xpipe.app.util.HumanReadableFormat.byteCount;\nimport static javafx.scene.control.TableColumn.SortType.ASCENDING;\n\npublic final class BrowserFileListComp extends SimpleRegionBuilder {\n\n    private static final PseudoClass EMPTY = PseudoClass.getPseudoClass(\"empty\");\n    private static final PseudoClass FILE = PseudoClass.getPseudoClass(\"file\");\n    private static final PseudoClass FOLDER = PseudoClass.getPseudoClass(\"folder\");\n    private static final PseudoClass DRAG = PseudoClass.getPseudoClass(\"drag\");\n    private static final PseudoClass DRAG_OVER = PseudoClass.getPseudoClass(\"drag-over\");\n    private static final PseudoClass DRAG_INTO_CURRENT = PseudoClass.getPseudoClass(\"drag-into-current\");\n\n    private final BrowserFileListModel fileList;\n    private final StringProperty typedSelection = new SimpleStringProperty(\"\");\n\n    public BrowserFileListComp(BrowserFileListModel fileList) {\n        this.fileList = fileList;\n    }\n\n    private static void prepareTableScrollFix(TableView<BrowserEntry> table) {\n        table.lookupAll(\".scroll-bar\").stream()\n                .filter(node -> node.getPseudoClassStates().contains(PseudoClass.getPseudoClass(\"horizontal\")))\n                .findFirst()\n                .ifPresent(node -> {\n                    Region region = (Region) node;\n                    region.setMinHeight(0);\n                    region.setPrefHeight(0);\n                    region.setMaxHeight(0);\n                });\n    }\n\n    @Override\n    protected Region createSimple() {\n        return createTable();\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private TableView<BrowserEntry> createTable() {\n        var filenameCol = new TableColumn<BrowserEntry, String>();\n        filenameCol.textProperty().bind(AppI18n.observable(\"name\"));\n        filenameCol.setCellValueFactory(param -> new SimpleStringProperty(\n                param.getValue() != null\n                        ? param.getValue().getRawFileEntry().getPath().getFileName()\n                        : null));\n        filenameCol.setComparator(Comparator.comparing(String::toLowerCase));\n        filenameCol.setSortType(ASCENDING);\n        filenameCol.setCellFactory(col ->\n                new BrowserFileListNameCell(fileList, typedSelection, fileList.getEditing(), col.getTableView()));\n        filenameCol.setReorderable(false);\n        filenameCol.setResizable(false);\n\n        var sizeCol = new TableColumn<BrowserEntry, String>();\n        sizeCol.textProperty().bind(AppI18n.observable(\"size\"));\n        sizeCol.setCellValueFactory(param -> new ReadOnlyStringWrapper(\n                param.getValue().getRawFileEntry().resolved().getSize()));\n        sizeCol.setComparator((size1, size2) -> {\n            if (size1 == null && size2 == null) {\n                return 0;\n            }\n            if (size1 == null) {\n                return -1;\n            }\n            if (size2 == null) {\n                return 1;\n            }\n\n            try {\n                long long1 = Long.parseLong(size1);\n                long long2 = Long.parseLong(size2);\n                return Long.compare(long1, long2);\n            } catch (NumberFormatException e) {\n                return size1.compareTo(size2);\n            }\n        });\n        sizeCol.setCellFactory(col -> new FileSizeCell());\n        sizeCol.setResizable(false);\n        sizeCol.setReorderable(false);\n\n        var mtimeCol = new TableColumn<BrowserEntry, Instant>();\n        mtimeCol.textProperty().bind(AppI18n.observable(\"modified\"));\n        mtimeCol.setCellValueFactory(param -> new SimpleObjectProperty<>(\n                param.getValue().getRawFileEntry().resolved().getDate()));\n        mtimeCol.setCellFactory(col -> new FileTimeCell());\n        mtimeCol.setResizable(false);\n        mtimeCol.setPrefWidth(150);\n        mtimeCol.setReorderable(false);\n\n        var modeCol = new TableColumn<BrowserEntry, String>();\n        modeCol.textProperty().bind(AppI18n.observable(\"attributes\"));\n        modeCol.setCellValueFactory(param -> new SimpleObjectProperty<>(\n                param.getValue().getRawFileEntry().resolved().getInfo() instanceof FileInfo.Unix u\n                        ? u.getPermissions()\n                        : null));\n        modeCol.setCellFactory(col -> new FileModeCell());\n        modeCol.setResizable(false);\n        modeCol.setPrefWidth(120);\n        modeCol.setSortable(false);\n        modeCol.setReorderable(false);\n\n        var ownerCol = new TableColumn<BrowserEntry, String>();\n        ownerCol.textProperty().bind(AppI18n.observable(\"owner\"));\n        ownerCol.setCellValueFactory(param -> {\n            return new SimpleObjectProperty<>(formatOwner(param.getValue()));\n        });\n        ownerCol.setCellFactory(col -> new FileOwnerCell());\n        ownerCol.setSortable(false);\n        ownerCol.setReorderable(false);\n        ownerCol.setResizable(false);\n\n        var table = new TableView<BrowserEntry>();\n        table.setSkin(new TableViewSkin<>(table));\n        RegionDescriptor.builder()\n                .nameKey(\"directoryContents\")\n                .showTooltips(false)\n                .build()\n                .apply(table);\n\n        var placeholder = new Label();\n        var placeholderText = Bindings.createStringBinding(\n                () -> {\n                    if (fileList.getFileSystemModel().getCurrentPath().get() == null) {\n                        return null;\n                    }\n\n                    if (fileList.getFileSystemModel().getBusy().get()) {\n                        return null;\n                    }\n\n                    return AppI18n.get(\"emptyDirectory\");\n                },\n                AppI18n.activeLanguage(),\n                fileList.getFileSystemModel().getBusy(),\n                fileList.getFileSystemModel().getCurrentPath());\n        placeholder.textProperty().bind(PlatformThread.sync(placeholderText));\n        table.setPlaceholder(placeholder);\n        AppFontSizes.base(placeholder);\n\n        table.getStyleClass().add(Styles.STRIPED);\n        table.getColumns().setAll(filenameCol, mtimeCol, modeCol, ownerCol, sizeCol);\n        table.getSortOrder().add(filenameCol);\n        table.setFocusTraversable(true);\n        table.setSortPolicy(param -> {\n            fileList.setComparator(table.getComparator());\n            return true;\n        });\n        table.setFixedCellSize(30.0);\n\n        prepareColumnVisibility(table, filenameCol, mtimeCol, modeCol, ownerCol, sizeCol);\n        prepareTableScrollFix(table);\n        prepareTableSelectionModel(table);\n        prepareTableShortcuts(table);\n        prepareTableEntries(table);\n        prepareTableChanges(table, filenameCol, mtimeCol, modeCol, ownerCol);\n        prepareTypedSelectionModel(table);\n        table.setMinWidth(0);\n        return table;\n    }\n\n    private void prepareColumnVisibility(\n            TableView<BrowserEntry> table,\n            TableColumn<BrowserEntry, String> filenameCol,\n            TableColumn<BrowserEntry, Instant> mtimeCol,\n            TableColumn<BrowserEntry, String> modeCol,\n            TableColumn<BrowserEntry, String> ownerCol,\n            TableColumn<BrowserEntry, String> sizeCol) {\n        table.widthProperty().subscribe((newValue) -> {\n            if (fileList.getFileSystemModel().getFileSystem().supportsOwnerColumn()) {\n                ownerCol.setVisible(newValue.doubleValue() > 1000);\n            }\n\n            if (fileList.getFileSystemModel().getFileSystem().supportsModeColumn()) {\n                modeCol.setVisible(newValue.doubleValue() > 600);\n            }\n\n            mtimeCol.setPrefWidth(newValue.doubleValue() == 0.0 || newValue.doubleValue() > 600 ? 150 : 110);\n            sizeCol.setPrefWidth(newValue.doubleValue() == 0.0 || newValue.doubleValue() > 600 ? 120 : 90);\n\n            var width = getFilenameWidth(table);\n            filenameCol.setPrefWidth(width);\n        });\n    }\n\n    private double getFilenameWidth(TableView<?> tableView) {\n        var sum = tableView.getColumns().stream()\n                        .filter(tableColumn -> tableColumn.isVisible()\n                                && tableView.getColumns().indexOf(tableColumn) != 0)\n                        .mapToDouble(value -> value.getPrefWidth())\n                        .sum()\n                + 7;\n        return Math.max(200, tableView.getWidth() - sum);\n    }\n\n    @SneakyThrows\n    private String formatOwner(BrowserEntry param) {\n        FileInfo.Unix unix = param.getRawFileEntry().resolved().getInfo() instanceof FileInfo.Unix u ? u : null;\n        if (unix == null) {\n            return null;\n        }\n\n        if (unix.getUid() == null && unix.getGid() == null && unix.getUser() == null && unix.getGroup() == null) {\n            return null;\n        }\n\n        var m = fileList.getFileSystemModel();\n        var v = m.getFileSystem().getShell().isPresent()\n                ? m.getFileSystem().getShell().get().view()\n                : null;\n\n        var user = unix.getUser() != null\n                ? unix.getUser()\n                : v != null ? v.getPasswdFile().getUsers().getOrDefault(unix.getUid(), \"?\") : null;\n        var group = unix.getGroup() != null\n                ? unix.getGroup()\n                : v != null ? v.getGroupFile().getGroups().getOrDefault(unix.getGid(), \"?\") : null;\n        var uid = unix.getUid() != null\n                ? String.valueOf(unix.getUid())\n                : v != null ? v.getPasswdFile().getUidForUser(user) : null;\n        var gid = unix.getGid() != null\n                ? String.valueOf(unix.getGid())\n                : v != null ? v.getGroupFile().getGidForGroup(group) : null;\n\n        var userFormat = user + (uid != null ? \" [\" + uid + \"]\" : \"\");\n        var groupFormat = group + (gid != null ? \" [\" + gid + \"]\" : \"\");\n\n        if (uid != null && uid.equals(gid) && user != null && user.equals(group)) {\n            return userFormat;\n        }\n\n        if (uid == null && gid == null && user != null && user.equals(group)) {\n            return userFormat;\n        }\n\n        return userFormat + \"  / \" + groupFormat;\n    }\n\n    private void prepareTypedSelectionModel(TableView<BrowserEntry> table) {\n        AtomicReference<Instant> lastFail = new AtomicReference<>();\n        table.addEventHandler(KeyEvent.KEY_PRESSED, event -> {\n            updateTypedSelection(table, lastFail, event, false);\n        });\n\n        table.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {\n            typedSelection.set(\"\");\n            lastFail.set(null);\n        });\n\n        fileList.getFileSystemModel().getCurrentPath().addListener((observable, oldValue, newValue) -> {\n            typedSelection.set(\"\");\n            lastFail.set(null);\n        });\n\n        table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {\n            if (event.getCode() == KeyCode.ESCAPE) {\n                typedSelection.set(\"\");\n                lastFail.set(null);\n            }\n        });\n    }\n\n    private void updateTypedSelection(\n            TableView<BrowserEntry> table, AtomicReference<Instant> lastType, KeyEvent event, boolean recursive) {\n        var typed = event.getText();\n        if (typed.isEmpty()) {\n            return;\n        }\n\n        if (event.isControlDown() || event.isShiftDown() || event.isAltDown() || event.isMetaDown()) {\n            return;\n        }\n\n        if (typedSelection.get().isEmpty() && typed.equals(\" \")) {\n            return;\n        }\n\n        var updated = typedSelection.get() + typed;\n        var found = fileList.getShown().getValue().stream()\n                .filter(browserEntry -> browserEntry.getFileName().toLowerCase().startsWith(updated.toLowerCase()))\n                .findFirst();\n        if (found.isEmpty()) {\n            if (typedSelection.get().isEmpty()) {\n                return;\n            }\n\n            var inCooldown = lastType.get() != null\n                    && Duration.between(lastType.get(), Instant.now()).toMillis() < 1000;\n            if (inCooldown) {\n                lastType.set(Instant.now());\n                event.consume();\n            } else {\n                lastType.set(null);\n                typedSelection.set(\"\");\n                table.getSelectionModel().clearSelection();\n                if (!recursive) {\n                    updateTypedSelection(table, lastType, event, true);\n                }\n            }\n            return;\n        }\n\n        lastType.set(Instant.now());\n        typedSelection.set(updated);\n        table.scrollTo(found.get());\n        table.getSelectionModel().clearAndSelect(fileList.getShown().getValue().indexOf(found.get()));\n        event.consume();\n    }\n\n    private void prepareTableSelectionModel(TableView<BrowserEntry> table) {\n        table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);\n        table.getSelectionModel().setCellSelectionEnabled(false);\n\n        var updateFromModel = new BooleanScope(new SimpleBooleanProperty());\n        table.getSelectionModel().getSelectedItems().addListener((ListChangeListener<? super BrowserEntry>) c -> {\n            if (updateFromModel.get()) {\n                return;\n            }\n\n            try (var ignored = updateFromModel) {\n                // Attempt to preserve ordering. Works at least when selecting single entries\n                var existing = new HashSet<>(fileList.getSelection());\n                c.getList().forEach(browserEntry -> {\n                    if (!existing.contains(browserEntry)) {\n                        fileList.getSelection().add(browserEntry);\n                    }\n                });\n                fileList.getSelection().removeIf(browserEntry -> !c.getList().contains(browserEntry));\n            }\n        });\n\n        fileList.getSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {\n            var existing = new HashSet<>(table.getSelectionModel().getSelectedItems());\n            var toApply = new HashSet<>(c.getList());\n            if (existing.equals(toApply)) {\n                return;\n            }\n\n            Platform.runLater(() -> {\n                var tableIndices = table.getSelectionModel().getSelectedItems().stream()\n                        .mapToInt(entry -> table.getItems().indexOf(entry))\n                        .toArray();\n                var indices = c.getList().stream()\n                        .mapToInt(entry -> table.getItems().indexOf(entry))\n                        .toArray();\n                if (Arrays.equals(indices, tableIndices)) {\n                    return;\n                }\n\n                if (indices.length == 0) {\n                    table.getSelectionModel().clearSelection();\n                    return;\n                }\n\n                if (indices.length == 1) {\n                    table.getSelectionModel().clearAndSelect(indices[0]);\n                } else {\n                    table.getSelectionModel().clearSelection();\n                    table.getSelectionModel().selectIndices(indices[0], indices);\n                }\n            });\n        });\n    }\n\n    private void prepareTableShortcuts(TableView<BrowserEntry> table) {\n        table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {\n            // Don't apply actions while renaming\n            if (fileList.getEditing().getValue() != null) {\n                return;\n            }\n\n            var selected = fileList.getSelection();\n            var action = BrowserMenuProviders.getFlattened(fileList.getFileSystemModel(), selected).stream()\n                    .filter(browserAction -> browserAction.isApplicable(fileList.getFileSystemModel(), selected)\n                            && browserAction.isActive(fileList.getFileSystemModel()))\n                    .filter(browserAction -> browserAction.getShortcut() != null)\n                    .filter(browserAction -> browserAction.getShortcut().match(event))\n                    .findAny();\n            action.ifPresent(browserAction -> {\n                // Prevent concurrent modification by creating copy on platform thread\n                var selectionCopy = new ArrayList<>(selected);\n                try {\n                    browserAction.execute(fileList.getFileSystemModel(), selectionCopy);\n                } catch (Exception e) {\n                    throw new RuntimeException(e);\n                }\n                event.consume();\n            });\n            if (action.isPresent()) {\n                return;\n            }\n\n            if (event.getCode() == KeyCode.ESCAPE) {\n                table.getSelectionModel().clearSelection();\n                event.consume();\n            }\n        });\n    }\n\n    private void prepareTableEntries(TableView<BrowserEntry> table) {\n        var emptyEntry = new BrowserFileListCompEntry(table, table, null, fileList);\n        table.setOnMouseClicked(event -> {\n            emptyEntry.onMouseClick(event);\n        });\n        table.setOnMouseDragEntered(event -> {\n            emptyEntry.onMouseDragEntered(event);\n        });\n        table.setOnDragOver(event -> {\n            emptyEntry.onDragOver(event);\n        });\n        table.setOnDragEntered(event -> {\n            emptyEntry.onDragEntered(event);\n        });\n        table.setOnDragDetected(event -> {\n            emptyEntry.startDrag(event);\n        });\n        table.setOnDragExited(event -> {\n            emptyEntry.onDragExited(event);\n        });\n        table.setOnDragDropped(event -> {\n            emptyEntry.onDragDrop(event);\n        });\n        table.setOnDragDone(event -> {\n            emptyEntry.onDragDone(event);\n        });\n\n        // Don't let the list view see this event\n        // otherwise it unselects everything as it doesn't understand shift clicks\n        table.addEventFilter(MouseEvent.MOUSE_CLICKED, t -> {\n            if (t.getButton() == MouseButton.PRIMARY && t.isShiftDown() && t.getClickCount() == 1) {\n                t.consume();\n            }\n        });\n\n        table.setRowFactory(param -> {\n            TableRow<BrowserEntry> row = new TableRow<>();\n            row.accessibleTextProperty()\n                    .bind(Bindings.createStringBinding(\n                            () -> {\n                                return row.getItem() != null ? row.getItem().getFileName() : null;\n                            },\n                            row.itemProperty()));\n            row.focusTraversableProperty()\n                    .bind(Bindings.createBooleanBinding(\n                            () -> {\n                                return row.getItem() != null;\n                            },\n                            row.itemProperty()));\n            var listEntry = Bindings.createObjectBinding(\n                    () -> new BrowserFileListCompEntry(table, row, row.getItem(), fileList), row.itemProperty());\n\n            // Don't let the list view see this event\n            // otherwise it unselects everything as it doesn't understand shift clicks\n            row.addEventFilter(MouseEvent.MOUSE_PRESSED, t -> {\n                if (t.getButton() == MouseButton.PRIMARY && t.isShiftDown()) {\n                    listEntry.get().onMouseShiftClick(t);\n                }\n            });\n\n            row.itemProperty().addListener((observable, oldValue, newValue) -> {\n                row.pseudoClassStateChanged(DRAG, false);\n                row.pseudoClassStateChanged(DRAG_OVER, false);\n            });\n\n            row.itemProperty().addListener((observable, oldValue, newValue) -> {\n                row.pseudoClassStateChanged(EMPTY, newValue == null);\n                row.pseudoClassStateChanged(\n                        FILE, newValue != null && newValue.getRawFileEntry().getKind() != FileKind.DIRECTORY);\n                row.pseudoClassStateChanged(\n                        FOLDER, newValue != null && newValue.getRawFileEntry().getKind() == FileKind.DIRECTORY);\n            });\n\n            fileList.getDraggedOverDirectory().addListener((observable, oldValue, newValue) -> {\n                row.pseudoClassStateChanged(DRAG_OVER, newValue != null && newValue == row.getItem());\n            });\n\n            fileList.getDraggedOverEmpty().addListener((observable, oldValue, newValue) -> {\n                table.pseudoClassStateChanged(DRAG_INTO_CURRENT, newValue);\n            });\n\n            row.setOnMouseClicked(event -> {\n                listEntry.get().onMouseClick(event);\n            });\n            row.setOnMouseDragEntered(event -> {\n                listEntry.get().onMouseDragEntered(event);\n            });\n            row.setOnDragEntered(event -> {\n                listEntry.get().onDragEntered(event);\n            });\n            row.setOnDragOver(event -> {\n                borderScroll(table, event);\n                listEntry.get().onDragOver(event);\n            });\n            row.setOnDragDetected(event -> {\n                listEntry.get().startDrag(event);\n            });\n            row.setOnDragExited(event -> {\n                listEntry.get().onDragExited(event);\n            });\n            row.setOnDragDropped(event -> {\n                listEntry.get().onDragDrop(event);\n            });\n            row.setOnDragDone(event -> {\n                listEntry.get().onDragDone(event);\n            });\n\n            return row;\n        });\n    }\n\n    private void prepareTableChanges(\n            TableView<BrowserEntry> table,\n            TableColumn<BrowserEntry, String> filenameCol,\n            TableColumn<BrowserEntry, Instant> mtimeCol,\n            TableColumn<BrowserEntry, String> modeCol,\n            TableColumn<BrowserEntry, String> ownerCol) {\n        var lastDir = new SimpleObjectProperty<FileEntry>();\n        BiConsumer<List<BrowserEntry>, List<BrowserEntry>> updateHandler = (o, n) -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                // Optimization for single entry updates\n                if (o != null && n != null && o.size() == n.size()) {\n                    var left = new HashSet<>(n);\n                    o.forEach(left::remove);\n                    if (left.size() == 1) {\n                        var updatedEntry = left.iterator().next();\n                        var found = o.stream()\n                                .filter(browserEntry -> browserEntry\n                                        .getRawFileEntry()\n                                        .getPath()\n                                        .equals(updatedEntry.getRawFileEntry().getPath()))\n                                .findFirst();\n                        if (found.isPresent()) {\n                            table.refresh();\n                            table.getItems().set(table.getItems().indexOf(found.get()), updatedEntry);\n                            return;\n                        }\n                    }\n                }\n\n                table.setDisable(true);\n                var newItems = new ArrayList<>(fileList.getShown().getValue());\n                table.getItems().clear();\n\n                var hasModifiedDate = newItems.size() == 0\n                        || newItems.stream()\n                                .anyMatch(entry -> entry.getRawFileEntry().getDate() != null);\n                if (!hasModifiedDate) {\n                    mtimeCol.setVisible(false);\n                } else {\n                    mtimeCol.setVisible(true);\n                }\n\n                var hasOwner = fileList.getAll().getValue().stream()\n                        .map(browserEntry -> formatOwner(browserEntry))\n                        .anyMatch(s -> s != null);\n                if (hasOwner) {\n                    ownerCol.setPrefWidth(fileList.getAll().getValue().stream()\n                            .map(browserEntry -> formatOwner(browserEntry))\n                            .map(s -> s != null ? s.length() * 9 : 0)\n                            .max(Comparator.naturalOrder())\n                            .orElse(150));\n                } else {\n                    ownerCol.setPrefWidth(0);\n                }\n\n                if (!fileList.getFileSystemModel().getFileSystem().supportsModeColumn()) {\n                    modeCol.setVisible(false);\n                } else {\n                    modeCol.setVisible(table.getWidth() > 600);\n                }\n\n                if (!fileList.getFileSystemModel().getFileSystem().supportsOwnerColumn()) {\n                    ownerCol.setVisible(false);\n                } else {\n                    if (table.getWidth() > 1000) {\n                        ownerCol.setVisible(hasOwner);\n                    } else if (!hasOwner) {\n                        ownerCol.setVisible(false);\n                    }\n                }\n\n                // Sort the list ourselves as sorting the table would incur a lot of cell updates\n                var obs = FXCollections.observableList(newItems);\n                table.getItems().setAll(obs);\n\n                var width = getFilenameWidth(table);\n                filenameCol.setPrefWidth(width);\n\n                TableViewSkin<?> skin = (TableViewSkin<?>) table.getSkin();\n                var currentDirectory = fileList.getFileSystemModel().getCurrentDirectory();\n                if (skin != null && !Objects.equals(lastDir.get(), currentDirectory)) {\n                    VirtualFlow<?> flow = (VirtualFlow<?>) skin.getChildren().get(1);\n                    ScrollBar vbar = (ScrollBar) flow.getChildrenUnmodifiable().get(2);\n                    if (vbar.getValue() != 0.0) {\n                        table.scrollTo(0);\n                    }\n                }\n                lastDir.setValue(currentDirectory);\n                table.setDisable(false);\n            });\n        };\n\n        updateHandler.accept(null, null);\n        fileList.getShown().addListener((observable, oldValue, newValue) -> {\n            // Delay to prevent internal tableview exceptions when sorting\n            var isSortChange = oldValue.size() == newValue.size() && new HashSet<>(oldValue).containsAll(newValue);\n            if (isSortChange) {\n                Platform.runLater(() -> {\n                    updateHandler.accept(oldValue, newValue);\n                });\n            } else {\n                updateHandler.accept(oldValue, newValue);\n            }\n        });\n        fileList.getFileSystemModel().getCurrentPath().addListener((observable, oldValue, newValue) -> {\n            if (oldValue == null) {\n                updateHandler.accept(null, null);\n            }\n        });\n    }\n\n    private void borderScroll(TableView<?> tableView, DragEvent event) {\n        TableViewSkin<?> skin = (TableViewSkin<?>) tableView.getSkin();\n        if (skin == null) {\n            return;\n        }\n\n        VirtualFlow<?> flow = (VirtualFlow<?>) skin.getChildren().get(1);\n        ScrollBar vbar = (ScrollBar) flow.getChildrenUnmodifiable().get(2);\n\n        if (!vbar.isVisible()) {\n            return;\n        }\n\n        double proximity = 100;\n        Bounds tableBounds = tableView.localToScene(tableView.getBoundsInLocal());\n        double dragY = event.getSceneY();\n        // Include table header as well in calculations\n        double topYProximity = tableBounds.getMinY() + proximity + 20;\n        double bottomYProximity = tableBounds.getMaxY() - proximity;\n\n        // clamp new values between 0 and 1 to prevent scrollbar flicking around at the edges\n        if (dragY < topYProximity) {\n            var scrollValue = Math.min(topYProximity - dragY, 100) / 10000.0;\n            vbar.setValue(Math.max(vbar.getValue() - scrollValue, 0));\n        } else if (dragY > bottomYProximity) {\n            var scrollValue = Math.min(dragY - bottomYProximity, 100) / 10000.0;\n            vbar.setValue(Math.min(vbar.getValue() + scrollValue, 1.0));\n        }\n    }\n\n    private static class FileSizeCell extends TableCell<BrowserEntry, String> {\n\n        @Override\n        protected void updateItem(String fileSize, boolean empty) {\n            super.updateItem(fileSize, empty);\n            if (empty || getTableRow() == null || getTableRow().getItem() == null) {\n                setText(null);\n            } else {\n                if (fileSize != null) {\n                    try {\n                        var l = Long.parseLong(fileSize);\n                        setText(byteCount(l));\n                    } catch (NumberFormatException e) {\n                        setText(fileSize);\n                    }\n                } else {\n                    setText(null);\n                }\n            }\n        }\n    }\n\n    private static class FileModeCell extends TableCell<BrowserEntry, String> {\n\n        @Override\n        protected void updateItem(String mode, boolean empty) {\n            super.updateItem(mode, empty);\n            if (empty || getTableRow() == null || getTableRow().getItem() == null) {\n                setText(null);\n            } else {\n                setText(mode);\n            }\n        }\n    }\n\n    private static class FileOwnerCell extends TableCell<BrowserEntry, String> {\n\n        public FileOwnerCell() {\n            setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);\n        }\n\n        @Override\n        protected void updateItem(String owner, boolean empty) {\n            super.updateItem(owner, empty);\n            if (empty || getTableRow() == null || getTableRow().getItem() == null) {\n                setText(null);\n            } else {\n                setText(owner);\n            }\n        }\n    }\n\n    private static class FileTimeCell extends TableCell<BrowserEntry, Instant> {\n\n        @Override\n        protected void updateItem(Instant fileTime, boolean empty) {\n            super.updateItem(fileTime, empty);\n            if (empty) {\n                setText(null);\n            } else {\n                setText(\n                        fileTime != null\n                                ? HumanReadableFormat.date(\n                                        fileTime.atZone(ZoneId.systemDefault()).toLocalDateTime())\n                                : \"\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserFileListCompEntry.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.util.BooleanScope;\nimport io.xpipe.app.util.GlobalTimer;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.OsType;\n\nimport javafx.scene.Node;\nimport javafx.scene.control.ContextMenu;\nimport javafx.scene.control.TableView;\nimport javafx.scene.image.Image;\nimport javafx.scene.input.*;\n\nimport lombok.Getter;\n\nimport java.io.IOException;\nimport java.nio.file.InvalidPathException;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.Objects;\n\n@Getter\npublic class BrowserFileListCompEntry {\n\n    private final TableView<BrowserEntry> tv;\n    private final Node row;\n    private final BrowserEntry item;\n    private final BrowserFileListModel model;\n\n    private Instant lastHoverUpdate;\n    private ContextMenu lastContextMenu;\n\n    public BrowserFileListCompEntry(\n            TableView<BrowserEntry> tv, Node row, BrowserEntry item, BrowserFileListModel model) {\n        this.tv = tv;\n        this.row = row;\n        this.item = item;\n        this.model = model;\n    }\n\n    public void onMouseClick(MouseEvent t) {\n        if (lastContextMenu != null) {\n            lastContextMenu.hide();\n            lastContextMenu = null;\n        }\n\n        if (showContextMenu(t)) {\n            var cm = new BrowserContextMenu(model.getFileSystemModel(), item, false);\n            cm.show(row, t.getScreenX(), t.getScreenY());\n            lastContextMenu = cm;\n            t.consume();\n            return;\n        }\n\n        if (t.getButton() == MouseButton.BACK) {\n            ThreadHelper.runFailableAsync(() -> {\n                BooleanScope.executeExclusive(model.getFileSystemModel().getBusy(), () -> {\n                    model.getFileSystemModel().backSync(1);\n                });\n            });\n            t.consume();\n            return;\n        }\n\n        if (t.getButton() == MouseButton.FORWARD) {\n            ThreadHelper.runFailableAsync(() -> {\n                BooleanScope.executeExclusive(model.getFileSystemModel().getBusy(), () -> {\n                    model.getFileSystemModel().forthSync(1);\n                });\n            });\n            t.consume();\n            return;\n        }\n\n        if (item == null) {\n            // Only clear for normal clicks\n            if (t.isStillSincePress()) {\n                model.getSelection().clear();\n                if (tv != null) {\n                    tv.requestFocus();\n                }\n            }\n            t.consume();\n            return;\n        }\n\n        row.requestFocus();\n        if (t.getClickCount() == 2 && t.getButton() == MouseButton.PRIMARY) {\n            model.onDoubleClick(item);\n            t.consume();\n        }\n\n        t.consume();\n    }\n\n    private boolean showContextMenu(MouseEvent event) {\n        if (item == null) {\n            return event.getButton() == MouseButton.SECONDARY;\n        }\n\n        if (item.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {\n            return event.getButton() == MouseButton.SECONDARY;\n        }\n\n        if (item.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY) {\n            return event.getButton() == MouseButton.SECONDARY\n                    || !AppPrefs.get().editFilesWithDoubleClick().get()\n                            && event.getButton() == MouseButton.PRIMARY\n                            && event.getClickCount() == 2;\n        }\n\n        return false;\n    }\n\n    public void onMouseShiftClick(MouseEvent t) {\n        if (t.getButton() != MouseButton.PRIMARY) {\n            return;\n        }\n\n        var all = tv.getItems();\n        var index = item != null ? all.indexOf(item) : all.size() - 1;\n        var min = Math.min(\n                index,\n                tv.getSelectionModel().getSelectedIndices().stream()\n                        .mapToInt(value -> value)\n                        .min()\n                        .orElse(1));\n        var max = Math.max(\n                index,\n                tv.getSelectionModel().getSelectedIndices().stream()\n                        .mapToInt(value -> value)\n                        .max()\n                        .orElse(all.indexOf(item)));\n\n        var toSelect = new ArrayList<BrowserEntry>();\n        for (int i = min; i <= max; i++) {\n            if (!model.getSelection().contains(model.getShown().getValue().get(i))) {\n                toSelect.add(model.getShown().getValue().get(i));\n            }\n        }\n        model.getSelection().addAll(toSelect);\n        t.consume();\n    }\n\n    private boolean acceptsDrop(DragEvent event) {\n        // Accept drops from outside the app window\n        if (event.getGestureSource() == null) {\n            // Don't accept 7zip temp files\n            if (OsType.ofLocal() == OsType.WINDOWS\n                    && event.getDragboard().getFiles().stream().anyMatch(file -> {\n                        try {\n                            return file.toPath()\n                                            .toRealPath()\n                                            .startsWith(\n                                                    AppSystemInfo.ofWindows().getTemp())\n                                    && file.toPath().getFileName().toString().matches(\"7z[A-Z0-9]+\");\n                        } catch (IOException ignored) {\n                            return false;\n                        }\n                    })) {\n                return false;\n            }\n\n            return true;\n        }\n\n        BrowserClipboard.Instance cb = BrowserClipboard.currentDragClipboard;\n        if (cb == null) {\n            return false;\n        }\n\n        if (model.getFileSystemModel().getCurrentDirectory() == null) {\n            return false;\n        }\n\n        if (!Objects.equals(\n                model.getFileSystemModel().getFileSystem(),\n                cb.getEntries().getFirst().getRawFileEntry().getFileSystem())) {\n            return true;\n        }\n\n        // Prevent drag and drops of files into the current directory\n        if (cb.getBaseDirectory() != null\n                && cb.getBaseDirectory()\n                        .getPath()\n                        .equals(model.getFileSystemModel().getCurrentDirectory().getPath())\n                && (item == null || item.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY)) {\n            return false;\n        }\n\n        // Prevent dropping items onto themselves\n        if (item != null && cb.getEntries().contains(item)) {\n            return false;\n        }\n\n        return true;\n    }\n\n    public void onDragDrop(DragEvent event) {\n        model.getDraggedOverEmpty().setValue(false);\n        model.getDraggedOverDirectory().setValue(null);\n\n        // Accept drops from outside the app window\n        if (event.getGestureSource() == null && event.getDragboard().hasFiles()) {\n            Dragboard db = event.getDragboard();\n            var list = db.getFiles().stream()\n                    .map(file -> {\n                        try {\n                            return file.toPath();\n                        } catch (InvalidPathException ignored) {\n                            return null;\n                        }\n                    })\n                    .filter(path -> path != null)\n                    .toList();\n            var target = item != null && item.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY\n                    ? item.getRawFileEntry().resolved()\n                    : model.getFileSystemModel().getCurrentDirectory();\n            model.getFileSystemModel().dropLocalFilesIntoAsync(target, list);\n            event.setDropCompleted(true);\n            event.consume();\n        }\n\n        // Accept drops from inside the app window\n        if (event.getGestureSource() != null) {\n            var db = BrowserClipboard.retrieveDrag(event.getDragboard());\n            if (db == null) {\n                return;\n            }\n\n            var files = db.getEntries();\n            var target = item != null && item.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY\n                    ? item.getRawFileEntry().resolved()\n                    : model.getFileSystemModel().getCurrentDirectory();\n            // We could already have changed the current dir\n            if (target == null) {\n                return;\n            }\n            model.getFileSystemModel()\n                    .dropFilesIntoAsync(\n                            target,\n                            files.stream()\n                                    .map(browserEntry ->\n                                            browserEntry.getRawFileEntry().resolved())\n                                    .toList(),\n                            db.getMode());\n            event.setDropCompleted(true);\n            event.consume();\n        }\n    }\n\n    public void onDragExited(DragEvent event) {\n        if (item != null && item.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {\n            model.getDraggedOverDirectory().setValue(null);\n        } else {\n            model.getDraggedOverEmpty().setValue(false);\n        }\n        lastHoverUpdate = null;\n        event.consume();\n    }\n\n    public void startDrag(MouseEvent event) {\n        if (item == null) {\n            return;\n        }\n\n        if (event.getButton() != MouseButton.PRIMARY) {\n            return;\n        }\n\n        if (model.getFileSystemModel().getBrowserModel() instanceof BrowserFullSessionModel sessionModel) {\n            sessionModel.getDraggingFiles().setValue(true);\n        }\n        var selected = model.getSelection();\n        Dragboard db = row.startDragAndDrop(TransferMode.COPY);\n        db.setContent(BrowserClipboard.startDrag(\n                model.getFileSystemModel().getCurrentDirectory(),\n                selected,\n                event.isAltDown() ? BrowserFileTransferMode.MOVE : BrowserFileTransferMode.NORMAL));\n\n        Image image = BrowserFileSelectionListComp.snapshot(selected);\n        db.setDragView(image, -20, 15);\n\n        event.setDragDetect(true);\n        event.consume();\n    }\n\n    public void onDragDone(DragEvent event) {\n        if (model.getFileSystemModel().getBrowserModel() instanceof BrowserFullSessionModel sessionModel) {\n            sessionModel.getDraggingFiles().setValue(false);\n            event.consume();\n        }\n    }\n\n    public void onDragEntered(DragEvent event) {\n        event.consume();\n        if (!acceptsDrop(event)) {\n            return;\n        }\n\n        var isDir = item != null && item.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY;\n        model.getDraggedOverEmpty().setValue(!isDir);\n        model.getDraggedOverDirectory().setValue(item);\n        if (!isDir) {\n            return;\n        }\n\n        var timestamp = Instant.now();\n        lastHoverUpdate = timestamp;\n        // Reduce printed window updates\n        GlobalTimer.delay(\n                () -> {\n                    if (!timestamp.equals(lastHoverUpdate)) {\n                        return;\n                    }\n\n                    if (item != model.getDraggedOverDirectory().getValue()) {\n                        return;\n                    }\n\n                    model.getFileSystemModel()\n                            .cdAsync(item.getRawFileEntry().resolved().getPath());\n                },\n                Duration.ofMillis(500));\n    }\n\n    public void onDragOver(DragEvent event) {\n        event.consume();\n        if (!acceptsDrop(event)) {\n            return;\n        }\n\n        event.acceptTransferModes(TransferMode.COPY_OR_MOVE);\n        event.consume();\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public void onMouseDragEntered(MouseDragEvent event) {\n        event.consume();\n\n        if (model.getFileSystemModel().getCurrentDirectory() == null) {\n            return;\n        }\n\n        if (item == null) {\n            return;\n        }\n\n        var tv = ((TableView<BrowserEntry>)\n                row.getParent().getParent().getParent().getParent());\n        tv.getSelectionModel().select(item);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserFileListFilterComp.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.comp.*;\nimport io.xpipe.app.comp.base.TextFieldComp;\nimport io.xpipe.app.platform.InputHelper;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.Button;\nimport javafx.scene.control.TextField;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\nimport javafx.scene.layout.HBox;\n\nimport atlantafx.base.theme.Styles;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\npublic class BrowserFileListFilterComp extends RegionStructureBuilder<HBox, BrowserFileListFilterComp.Structure> {\n\n    private final BrowserFileSystemTabModel model;\n    private final Property<String> filterString;\n\n    public BrowserFileListFilterComp(BrowserFileSystemTabModel model, Property<String> filterString) {\n        this.model = model;\n        this.filterString = filterString;\n    }\n\n    @Override\n    public Structure createBase() {\n        var expanded = new SimpleBooleanProperty();\n        var text = new TextFieldComp(filterString, false).build();\n        var button = new Button();\n        RegionDescriptor.builder()\n                .nameKey(\"search\")\n                .shortcut(new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN))\n                .build()\n                .apply(button);\n        button.minWidthProperty().bind(button.heightProperty());\n        InputHelper.onExactKeyCode(text, KeyCode.ESCAPE, true, keyEvent -> {\n            if (!expanded.get()) {\n                return;\n            }\n\n            text.clear();\n            button.fire();\n            keyEvent.consume();\n        });\n        text.focusedProperty().addListener((observable, oldValue, newValue) -> {\n            if (!newValue && filterString.getValue() == null) {\n                if (button.isFocused()) {\n                    return;\n                }\n\n                expanded.set(false);\n            }\n        });\n        filterString.addListener((observable, oldValue, newValue) -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                if (newValue == null && !text.isFocused()) {\n                    expanded.set(false);\n                }\n            });\n        });\n        text.setMinWidth(0);\n        Styles.toggleStyleClass(text, Styles.LEFT_PILL);\n\n        filterString.subscribe(val -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                if (val == null) {\n                    text.getStyleClass().remove(Styles.SUCCESS);\n                } else {\n                    text.getStyleClass().add(Styles.SUCCESS);\n                }\n            });\n        });\n\n        var fi = new FontIcon(\"mdi2m-magnify\");\n        button.setGraphic(fi);\n        button.setOnAction(event -> {\n            if (model.getCurrentDirectory() == null) {\n                return;\n            }\n\n            if (expanded.get()) {\n                if (filterString.getValue() == null) {\n                    expanded.set(false);\n                }\n                event.consume();\n            } else {\n                expanded.set(true);\n                text.requestFocus();\n                event.consume();\n            }\n        });\n\n        var box = new HBox(text, button);\n        box.getStyleClass().add(\"browser-filter\");\n        box.setAlignment(Pos.CENTER);\n\n        text.setPrefWidth(0);\n        text.setFocusTraversable(false);\n        button.getStyleClass().add(Styles.FLAT);\n        button.disableProperty().bind(model.getInOverview());\n        expanded.addListener((observable, oldValue, val) -> {\n            if (val) {\n                text.setPrefWidth(250);\n                text.setFocusTraversable(true);\n            } else {\n                text.setPrefWidth(0);\n                text.setFocusTraversable(false);\n            }\n        });\n        button.minHeightProperty().bind(text.heightProperty());\n        button.minWidthProperty().bind(text.heightProperty());\n        button.maxHeightProperty().bind(text.heightProperty());\n        button.maxWidthProperty().bind(text.heightProperty());\n        return new Structure(box, text, button);\n    }\n\n    public record Structure(HBox box, TextField textField, Button toggleButton) implements RegionStructure<HBox> {\n\n        @Override\n        public HBox get() {\n            return box;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.browser.action.impl.MoveFileActionProvider;\nimport io.xpipe.app.ext.FileEntry;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.core.FilePath;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.collections.FXCollections;\nimport javafx.collections.ObservableList;\n\nimport lombok.Getter;\n\nimport java.util.*;\nimport java.util.stream.Stream;\n\n@Getter\npublic final class BrowserFileListModel {\n\n    static final Comparator<BrowserEntry> FILE_TYPE_COMPARATOR =\n            Comparator.comparing(path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);\n\n    private final BrowserFileSystemTabModel fileSystemModel;\n    private final Property<Comparator<BrowserEntry>> comparatorProperty =\n            new SimpleObjectProperty<>(FILE_TYPE_COMPARATOR);\n    private final Property<List<BrowserEntry>> all = new SimpleObjectProperty<>(new ArrayList<>());\n    private final Property<List<BrowserEntry>> shown = new SimpleObjectProperty<>(new ArrayList<>());\n    private final ObservableList<BrowserEntry> selection = FXCollections.observableArrayList();\n\n    private final Property<BrowserEntry> draggedOverDirectory = new SimpleObjectProperty<>();\n    private final Property<Boolean> draggedOverEmpty = new SimpleBooleanProperty();\n    private final Property<BrowserEntry> editing = new SimpleObjectProperty<>();\n\n    public BrowserFileListModel(BrowserFileSystemTabModel fileSystemModel) {\n        this.fileSystemModel = fileSystemModel;\n\n        fileSystemModel.getFilter().addListener((observable, oldValue, newValue) -> {\n            refreshShown();\n        });\n    }\n\n    public void setAll(Stream<FileEntry> newFiles) {\n        try (var s = newFiles) {\n            var l = s.filter(entry -> entry != null)\n                    .map(entry -> new BrowserEntry(entry, this))\n                    .toList();\n            all.setValue(l);\n            refreshShown();\n        }\n    }\n\n    public void updateEntry(FilePath p, FileEntry n) {\n        var found = all.getValue().stream()\n                .filter(browserEntry -> browserEntry.getRawFileEntry().getPath().equals(p))\n                .findFirst();\n        if (found.isEmpty()) {\n            return;\n        }\n\n        var index = all.getValue().indexOf(found.get());\n        var l = new ArrayList<>(all.getValue());\n        if (n != null) {\n            l.set(index, new BrowserEntry(n, this));\n        } else {\n            l.remove(index);\n        }\n        all.setValue(l);\n        refreshShown();\n    }\n\n    public void setComparator(Comparator<BrowserEntry> comparator) {\n        comparatorProperty.setValue(comparator);\n        refreshShown();\n    }\n\n    void refreshShown() {\n        List<BrowserEntry> filtered = fileSystemModel.getFilter().getValue() != null\n                ? all.getValue().stream()\n                        .filter(entry -> {\n                            var name = entry.getRawFileEntry()\n                                    .getPath()\n                                    .getFileName()\n                                    .toLowerCase(Locale.ROOT);\n                            var filterString =\n                                    fileSystemModel.getFilter().getValue().toLowerCase(Locale.ROOT);\n                            return name.contains(filterString);\n                        })\n                        .toList()\n                : all.getValue();\n\n        var listCopy = new ArrayList<>(filtered);\n        listCopy.sort(order());\n        shown.setValue(listCopy);\n    }\n\n    public Comparator<BrowserEntry> order() {\n        var dirsFirst = Comparator.<BrowserEntry, Boolean>comparing(\n                path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);\n        var comp = comparatorProperty.getValue();\n\n        Comparator<BrowserEntry> us = comp != null ? dirsFirst.thenComparing(comp) : dirsFirst;\n        return us;\n    }\n\n    public BrowserEntry rename(BrowserEntry old, String newName) {\n        if (old == null\n                || newName == null\n                || fileSystemModel == null\n                || fileSystemModel.getCurrentPath().get() == null) {\n            return old;\n        }\n\n        if (newName.isEmpty() || !newName.strip().equals(newName)) {\n            return old;\n        }\n\n        var newFullPath = fileSystemModel.getCurrentPath().get().join(newName);\n\n        // This check will fail on case-insensitive file systems when changing the case of the file\n        // So skip it in this case\n        var skipExistCheck = old.getFileName().equalsIgnoreCase(newName);\n        if (!skipExistCheck) {\n            boolean exists;\n            try {\n                exists = fileSystemModel.getFileSystem().fileExists(newFullPath)\n                        || fileSystemModel.getFileSystem().directoryExists(newFullPath);\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).handle();\n                return old;\n            }\n\n            if (exists) {\n                ErrorEventFactory.fromMessage(\"Target \" + newFullPath + \" does already exist\")\n                        .expected()\n                        .handle();\n                fileSystemModel.refreshSync();\n                return old;\n            }\n        }\n\n        try {\n            var builder = MoveFileActionProvider.Action.builder();\n            builder.initEntries(fileSystemModel, List.of(old));\n            builder.target(newFullPath);\n            builder.build().executeSync();\n\n            var b = all.getValue().stream()\n                    .filter(browserEntry ->\n                            browserEntry.getRawFileEntry().getPath().equals(newFullPath))\n                    .findFirst()\n                    .orElse(old);\n            return b;\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n            return old;\n        }\n    }\n\n    public void onDoubleClick(BrowserEntry entry) {\n        var r = entry.getRawFileEntry().resolved();\n        if (r.getKind() == FileKind.DIRECTORY) {\n            fileSystemModel.cdAsync(r.getPath().toString());\n        }\n\n        if (AppPrefs.get().editFilesWithDoubleClick().get() && r.getKind() == FileKind.FILE) {\n            var selection = new LinkedHashSet<>(getSelection());\n            selection.add(entry);\n            for (BrowserEntry e : selection) {\n                BrowserFileOpener.openInTextEditor(getFileSystemModel(), e.getRawFileEntry());\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserFileListNameCell.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.browser.icon.BrowserIconManager;\nimport io.xpipe.app.comp.base.LazyTextFieldComp;\nimport io.xpipe.app.comp.base.PrettyImageHelper;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.platform.InputHelper;\nimport io.xpipe.app.platform.MenuHelper;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.util.BooleanScope;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.FilePath;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.*;\nimport javafx.beans.value.ChangeListener;\nimport javafx.beans.value.ObservableStringValue;\nimport javafx.css.PseudoClass;\nimport javafx.geometry.Pos;\nimport javafx.geometry.Side;\nimport javafx.scene.AccessibleRole;\nimport javafx.scene.Node;\nimport javafx.scene.control.ButtonBase;\nimport javafx.scene.control.TableCell;\nimport javafx.scene.control.TableView;\nimport javafx.scene.control.TextField;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Priority;\nimport javafx.scene.layout.Region;\n\nimport atlantafx.base.controls.Spacer;\n\nclass BrowserFileListNameCell extends TableCell<BrowserEntry, String> {\n\n    private final BrowserFileListModel fileList;\n    private final ObservableStringValue typedSelection;\n    private final StringProperty img = new SimpleStringProperty();\n    private final StringProperty text = new SimpleStringProperty();\n\n    private final BooleanProperty updating = new SimpleBooleanProperty();\n    private final Property<BrowserEntry> editing;\n\n    public BrowserFileListNameCell(\n            BrowserFileListModel fileList,\n            ObservableStringValue typedSelection,\n            Property<BrowserEntry> editing,\n            TableView<BrowserEntry> tableView) {\n        this.editing = editing;\n        this.fileList = fileList;\n        this.typedSelection = typedSelection;\n\n        accessibleTextProperty()\n                .bind(Bindings.createStringBinding(\n                        () -> {\n                            return getItem() != null ? getItem() : null;\n                        },\n                        itemProperty()));\n        setAccessibleRole(AccessibleRole.TEXT);\n\n        var textField = new LazyTextFieldComp(text)\n                .minWidth(USE_PREF_SIZE)\n                .buildStructure()\n                .getTextField();\n        var quickAccess = createQuickAccessButton();\n        setupShortcuts(tableView, (ButtonBase) quickAccess);\n        setupRename(fileList, textField);\n\n        Node imageView = PrettyImageHelper.ofFixedSize(img, 24, 24).build();\n        HBox graphic = new HBox(imageView, new Spacer(5), quickAccess, new Spacer(1), textField);\n        quickAccess.prefHeightProperty().bind(graphic.heightProperty());\n        graphic.setAlignment(Pos.CENTER_LEFT);\n        graphic.setPrefHeight(34);\n        HBox.setHgrow(textField, Priority.ALWAYS);\n        graphic.setAlignment(Pos.CENTER_LEFT);\n        setGraphic(graphic);\n    }\n\n    private Region createQuickAccessButton() {\n        var quickAccess = new BrowserQuickAccessButtonComp(() -> getTableRow().getItem(), fileList.getFileSystemModel())\n                .hide(Bindings.createBooleanBinding(\n                        () -> {\n                            if (getTableRow() == null) {\n                                return true;\n                            }\n\n                            var item = getTableRow().getItem();\n                            if (item == null) {\n                                return false;\n                            }\n\n                            var notDir = item.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY;\n                            var isParentLink = item.getRawFileEntry()\n                                    .equals(fileList.getFileSystemModel().getCurrentParentDirectory());\n                            return notDir || isParentLink;\n                        },\n                        itemProperty()))\n                .build();\n        return quickAccess;\n    }\n\n    private void setupShortcuts(TableView<BrowserEntry> tableView, ButtonBase quickAccess) {\n        InputHelper.onExactKeyCode(tableView, KeyCode.RIGHT, false, event -> {\n            var selected = fileList.getSelection();\n            if (selected.size() == 1 && selected.getFirst() == getTableRow().getItem()) {\n                quickAccess.fire();\n                event.consume();\n            }\n        });\n        InputHelper.onExactKeyCode(tableView, KeyCode.SPACE, true, event -> {\n            // Don't show when renaming files\n            if (fileList.getEditing().getValue() != null) {\n                return;\n            }\n\n            if (!typedSelection.get().isEmpty()) {\n                var selection = typedSelection.get() + \" \";\n                var found = fileList.getShown().getValue().stream()\n                        .filter(browserEntry ->\n                                browserEntry.getFileName().toLowerCase().startsWith(selection))\n                        .findFirst();\n                // Ugly fix to prevent space from showing the menu when there is a file matching\n                // Due to the table view input map, these events always get sent and consumed, not allowing us to\n                // differentiate between these cases\n                if (found.isPresent()) {\n                    return;\n                }\n            }\n\n            var selected = fileList.getSelection();\n            // Only show one menu across all selected entries\n            if (selected.size() > 0 && selected.getLast() == getTableRow().getItem()) {\n                var cm = new BrowserContextMenu(\n                        fileList.getFileSystemModel(), getTableRow().getItem(), false);\n                MenuHelper.toggleMenuShow(cm, this, Side.RIGHT);\n                event.consume();\n            }\n        });\n    }\n\n    private void setupRename(BrowserFileListModel fileList, TextField textField) {\n        ChangeListener<String> listener = (observable, oldValue, newValue) -> {\n            if (updating.get()) {\n                return;\n            }\n\n            getTableRow().requestFocus();\n            var it = getTableRow().getItem();\n            editing.setValue(null);\n            ThreadHelper.runFailableAsync(() -> {\n                if (it == null) {\n                    return;\n                }\n\n                var r = fileList.rename(it, newValue);\n                Platform.runLater(() -> {\n                    updateItem(getItem(), isEmpty());\n                    fileList.getSelection().setAll(r);\n                });\n            });\n        };\n        text.addListener(listener);\n\n        editing.addListener((observable, oldValue, newValue) -> {\n            var item = getTableRow().getItem();\n            if (item != null && item.equals(newValue)) {\n                PlatformThread.runLaterIfNeeded(() -> {\n                    textField.setDisable(false);\n                    textField.requestFocus();\n\n                    var content = textField.getText();\n                    if (content != null && !content.isEmpty() && !content.startsWith(\".\")) {\n                        var name = FilePath.of(content);\n                        var baseNameEnd = item.getRawFileEntry().getKind() == FileKind.DIRECTORY\n                                ? content.length()\n                                : name.getBaseName().toString().length();\n                        textField.selectRange(0, baseNameEnd);\n                    }\n                });\n            } else {\n                PlatformThread.runLaterIfNeeded(() -> {\n                    textField.setDisable(true);\n                });\n            }\n        });\n\n        textField.disabledProperty().addListener((observable, oldValue, newValue) -> {\n            if (!oldValue && newValue) {\n                Platform.runLater(() -> {\n                    editing.setValue(null);\n                });\n            }\n        });\n    }\n\n    @Override\n    protected void updateItem(String newName, boolean empty) {\n        // Cancel rename on any change\n        editing.setValue(null);\n\n        if (updating.get()) {\n            super.updateItem(newName, empty);\n            return;\n        }\n\n        try (var ignored = new BooleanScope(updating).start()) {\n            super.updateItem(newName, empty);\n            if (empty || newName == null || getTableRow().getItem() == null) {\n                // Don't set image as that would trigger image comp update\n                // and cells are emptied on each change, leading to unnecessary changes\n                // img.set(null);\n\n                // Visibility seems to be bugged, so use opacity\n                setOpacity(0.0);\n            } else {\n                var icon = getTableRow().getItem().getIcon();\n                BrowserIconManager.loadIfNecessary(icon);\n                img.set(icon);\n\n                var isDirectory = getTableRow().getItem().getRawFileEntry().getKind() == FileKind.DIRECTORY;\n                pseudoClassStateChanged(PseudoClass.getPseudoClass(\"folder\"), isDirectory);\n\n                var normalName = getTableRow().getItem().getRawFileEntry().getKind() == FileKind.LINK\n                        ? getTableRow().getItem().getFileName() + \" -> \"\n                                + getTableRow()\n                                        .getItem()\n                                        .getRawFileEntry()\n                                        .resolved()\n                                        .getPath()\n                        : getTableRow().getItem().getFileName();\n                var fileName = normalName;\n                var info = getTableRow().getItem().getRawFileEntry().getInfo();\n                var hidden = (info != null && info.explicitlyHidden()) || fileName.startsWith(\".\");\n                getTableRow().pseudoClassStateChanged(PseudoClass.getPseudoClass(\"hidden\"), hidden);\n                text.set(fileName);\n\n                // Visibility seems to be bugged, so use opacity\n                setOpacity(1.0);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserFileOpener.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.ext.FileEntry;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.util.BooleanScope;\nimport io.xpipe.app.util.FileBridge;\nimport io.xpipe.app.util.FileOpener;\nimport io.xpipe.app.util.HumanReadableFormat;\n\nimport lombok.SneakyThrows;\n\nimport java.util.Objects;\n\npublic class BrowserFileOpener {\n\n    @SneakyThrows\n    private static int calculateKey(BrowserFileSystemTabModel model, FileEntry entry) {\n        // Use different key for empty / non-empty files to prevent any issues from blanked files when transfer fails\n        var empty = model.getFileSystem().getFileSize(entry.getPath()) == 0;\n        return Objects.hash(entry.getPath(), entry.getFileSystem(), entry.getKind(), entry.getInfo(), empty);\n    }\n\n    public static void openWithAnyApplication(BrowserFileSystemTabModel model, FileEntry entry) {\n        if (model.getFileSystem().getShell().isPresent()\n                && model.getFileSystem().getShell().get().isLocal()) {\n            FileOpener.openWithAnyApplication(entry.getPath().toString());\n            return;\n        }\n\n        var file = entry.getPath();\n        var key = calculateKey(model, entry);\n        FileBridge.get()\n                .openIO(\n                        file.getFileName(),\n                        key,\n                        new BooleanScope(model.getBusy()).exclusive(),\n                        () -> BrowserFileInput.openFileInput(model, entry),\n                        (size) -> BrowserFileOutput.openFileOutput(model, entry, size),\n                        s -> FileOpener.openWithAnyApplication(s));\n    }\n\n    public static void openInDefaultApplication(BrowserFileSystemTabModel model, FileEntry entry) {\n        if (model.getFileSystem().getShell().isPresent()\n                && model.getFileSystem().getShell().get().isLocal()) {\n            FileOpener.openInDefaultApplication(entry.getPath().toString());\n            return;\n        }\n\n        var file = entry.getPath();\n        var key = calculateKey(model, entry);\n        FileBridge.get()\n                .openIO(\n                        file.getFileName(),\n                        key,\n                        new BooleanScope(model.getBusy()).exclusive(),\n                        () -> BrowserFileInput.openFileInput(model, entry),\n                        (size) -> BrowserFileOutput.openFileOutput(model, entry, size),\n                        s -> FileOpener.openInDefaultApplication(s));\n    }\n\n    public static void openInTextEditor(BrowserFileSystemTabModel model, FileEntry entry) {\n        var editor = AppPrefs.get().externalEditor().getValue();\n        if (editor == null) {\n            return;\n        }\n\n        if (model.getFileSystem().getShell().isPresent()\n                && model.getFileSystem().getShell().get().isLocal()) {\n            FileOpener.openInTextEditor(entry.getPath().toString());\n            return;\n        }\n\n        var size = entry.getFileSizeLong().orElse(0L);\n        if (size > 1_000_000) {\n            var confirm = AppDialog.confirm(\n                    \"largeFileWarningTitle\",\n                    AppI18n.observable(\"largeFileWarningContent\", HumanReadableFormat.byteCount(size)));\n            if (!confirm) {\n                return;\n            }\n        }\n\n        var file = entry.getPath();\n        var key = calculateKey(model, entry);\n        FileBridge.get()\n                .openIO(\n                        file.getFileName(),\n                        key,\n                        new BooleanScope(model.getBusy()).exclusive(),\n                        () -> BrowserFileInput.openFileInput(model, entry),\n                        (os) -> {\n                            return BrowserFileOutput.openFileOutput(model, entry, os);\n                        },\n                        FileOpener::openInTextEditor);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserFileOutput.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.ext.ConnectionFileSystem;\nimport io.xpipe.app.ext.FileEntry;\nimport io.xpipe.app.ext.FileInfo;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.ElevationFunction;\nimport io.xpipe.app.process.ProcessOutputException;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport java.io.OutputStream;\nimport java.util.List;\nimport java.util.Optional;\n\npublic interface BrowserFileOutput {\n\n    static BrowserFileOutput openFileOutput(BrowserFileSystemTabModel model, FileEntry file, long totalBytes)\n            throws Exception {\n        var defOutput = createFileOutputImpl(model, file, totalBytes, false);\n        if (model.getFileSystem().getShell().isEmpty()) {\n            return defOutput;\n        }\n\n        var sc = model.getFileSystem().getShell().orElseThrow();\n        var requiresSudo =\n                sc.getOsType() != OsType.WINDOWS && requiresSudo(model, (FileInfo.Unix) file.getInfo(), file.getPath());\n\n        if (!requiresSudo) {\n            return defOutput;\n        }\n\n        var elevate = AppDialog.confirm(\"fileWriteSudo\");\n        if (!elevate) {\n            return defOutput;\n        }\n\n        var rootOutput = createFileOutputImpl(model, file, totalBytes, true);\n        return rootOutput;\n    }\n\n    private static boolean requiresSudo(BrowserFileSystemTabModel model, FileInfo.Unix info, FilePath filePath)\n            throws Exception {\n        if (model.getFileSystem().getShell().isEmpty()) {\n            return false;\n        }\n\n        var sc = model.getFileSystem().getShell().get();\n        if (sc.view().isRoot()) {\n            return false;\n        }\n\n        if (info != null) {\n            var otherWrite = info.getPermissions().charAt(7) == 'w';\n            if (otherWrite) {\n                return false;\n            }\n\n            var userOwned = info.getUid() != null\n                            && sc.view().getPasswdFile().getUidForUser(sc.view().user()) == info.getUid()\n                    || info.getUser() != null && sc.view().user().equals(info.getUser());\n            var userWrite = info.getPermissions().charAt(1) == 'w';\n            if (userOwned && userWrite) {\n                return false;\n            }\n        }\n\n        var test = model.getFileSystem()\n                .getShell()\n                .orElseThrow()\n                .command(CommandBuilder.of().add(\"test\", \"-w\").addFile(filePath))\n                .executeAndCheck();\n        return !test;\n    }\n\n    private static BrowserFileOutput createFileOutputImpl(\n            BrowserFileSystemTabModel model, FileEntry file, long totalBytes, boolean elevate) throws Exception {\n        var shell = model.getFileSystem().getShell();\n        var sc = shell.isEmpty()\n                ? null\n                : elevate\n                        ? shell.orElseThrow()\n                                .identicalDialectSubShell()\n                                .elevated(ElevationFunction.elevated(null))\n                                .start()\n                        : model.getFileSystem().getShell().orElseThrow().start();\n        var fs = elevate ? new ConnectionFileSystem(sc) : model.getFileSystem();\n        var checkSudoersFile = shell.isPresent() && file.getPath().startsWith(\"/etc/sudo\");\n        var output = new BrowserFileOutput() {\n\n            @Override\n            public Optional<DataStoreEntry> target() {\n                return Optional.of(model.getEntry().get());\n            }\n\n            @Override\n            public boolean hasOutput() {\n                return true;\n            }\n\n            @Override\n            public OutputStream open() throws Exception {\n                try {\n                    return fs.openOutput(file.getPath(), totalBytes);\n                } catch (Exception ex) {\n                    if (elevate) {\n                        fs.close();\n                    }\n                    throw ex;\n                }\n            }\n\n            @Override\n            public void beforeTransfer() throws Exception {\n                if (checkSudoersFile) {\n                    fs.copy(file.getPath(), sc.getSystemTemporaryDirectory().join(file.getName()));\n                }\n            }\n\n            @Override\n            public void onFinish() throws Exception {\n                if (checkSudoersFile) {\n                    if (sc.view().findProgram(\"visudo\").isPresent()) {\n                        try {\n                            sc.command(CommandBuilder.of()\n                                            .add(\"visudo\", \"-c\", \"-f\")\n                                            .addFile(file.getPath()))\n                                    .execute();\n                        } catch (ProcessOutputException ex) {\n                            ErrorEventFactory.fromThrowable(ex).expected().handle();\n                            fs.copy(sc.getSystemTemporaryDirectory().join(file.getName()), file.getPath());\n                        }\n                    }\n                }\n\n                if (elevate) {\n                    fs.close();\n                }\n\n                model.refreshFileEntriesSync(List.of(file));\n            }\n        };\n        return output;\n    }\n\n    static BrowserFileOutput none() {\n        return new BrowserFileOutput() {\n\n            @Override\n            public Optional<DataStoreEntry> target() {\n                return Optional.empty();\n            }\n\n            @Override\n            public boolean hasOutput() {\n                return false;\n            }\n\n            @Override\n            public OutputStream open() {\n                return null;\n            }\n\n            @Override\n            public void beforeTransfer() {}\n\n            @Override\n            public void onFinish() {}\n        };\n    }\n\n    Optional<DataStoreEntry> target();\n\n    boolean hasOutput();\n\n    OutputStream open() throws Exception;\n\n    void beforeTransfer() throws Exception;\n\n    void onFinish() throws Exception;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserFileOverviewComp.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.browser.icon.BrowserIcons;\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.HorizontalComp;\nimport io.xpipe.app.comp.base.ListBoxViewComp;\nimport io.xpipe.app.ext.FileEntry;\n\nimport javafx.collections.ObservableList;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.Button;\nimport javafx.scene.layout.Region;\n\nimport lombok.EqualsAndHashCode;\nimport lombok.Value;\n\nimport java.util.List;\nimport java.util.function.Function;\n\n@Value\n@EqualsAndHashCode(callSuper = true)\npublic class BrowserFileOverviewComp extends SimpleRegionBuilder {\n\n    BrowserFileSystemTabModel model;\n    ObservableList<FileEntry> list;\n    boolean grow;\n\n    @Override\n    protected Region createSimple() {\n        Function<FileEntry, BaseRegionBuilder<?, ?>> factory = entry -> {\n            return RegionBuilder.of(() -> {\n                var be = new BrowserEntry(entry, model.getFileList());\n                var icon = BrowserIcons.createIcon(be.getIcon());\n                var graphic = new HorizontalComp(List.of(\n                        icon,\n                        new BrowserQuickAccessButtonComp(() -> new BrowserEntry(entry, model.getFileList()), model)));\n                var l = new Button(entry.getPath().toString(), graphic.build());\n                l.setGraphicTextGap(1);\n                l.setOnAction(event -> {\n                    model.cdAsync(entry.getPath().toString());\n                    event.consume();\n                });\n                l.setAlignment(Pos.CENTER_LEFT);\n                l.setMaxWidth(10000);\n                return l;\n            });\n        };\n\n        var c = new ListBoxViewComp<>(list, list, factory, true).style(\"overview-file-list\");\n        if (!grow) {\n            c.apply(struc -> struc.setFitToHeight(true));\n        }\n        return c.build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserFileSelectionListComp.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.browser.icon.BrowserIconManager;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.ListBoxViewComp;\nimport io.xpipe.app.comp.base.PrettyImageHelper;\nimport io.xpipe.app.core.AppStyle;\nimport io.xpipe.app.core.window.AppWindowStyle;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.ObservableList;\nimport javafx.scene.Scene;\nimport javafx.scene.SnapshotParameters;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.OverrunStyle;\nimport javafx.scene.image.Image;\nimport javafx.scene.layout.Region;\nimport javafx.scene.paint.Color;\n\nimport lombok.AllArgsConstructor;\nimport lombok.EqualsAndHashCode;\nimport lombok.Value;\n\nimport java.util.function.Function;\n\n@Value\n@EqualsAndHashCode(callSuper = true)\n@AllArgsConstructor\npublic class BrowserFileSelectionListComp extends SimpleRegionBuilder {\n\n    ObservableList<BrowserEntry> list;\n    Function<BrowserEntry, ObservableValue<String>> nameTransformation;\n\n    public BrowserFileSelectionListComp(ObservableList<BrowserEntry> list) {\n        this(list, entry -> new SimpleStringProperty(entry.getFileName()));\n    }\n\n    public static Image snapshot(ObservableList<BrowserEntry> list) {\n        var r = new BrowserFileSelectionListComp(list).style(\"drag\").build();\n        var scene = new Scene(r);\n        AppWindowStyle.addStylesheets(scene);\n        AppStyle.addStylesheets(scene);\n        SnapshotParameters parameters = new SnapshotParameters();\n        parameters.setFill(Color.TRANSPARENT);\n        return r.snapshot(parameters, null);\n    }\n\n    @Override\n    protected Region createSimple() {\n        var c = new ListBoxViewComp<>(\n                        list,\n                        list,\n                        entry -> {\n                            return RegionBuilder.of(() -> {\n                                var icon = entry.getIcon();\n                                BrowserIconManager.loadIfNecessary(icon);\n                                var image = PrettyImageHelper.ofFixedSizeSquare(icon, 24)\n                                        .build();\n                                var t = nameTransformation.apply(entry);\n                                var l = new Label(t.getValue(), image);\n                                l.setGraphicTextGap(6);\n                                l.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);\n                                t.addListener((observable, oldValue, newValue) -> {\n                                    PlatformThread.runLaterIfNeeded(() -> {\n                                        l.setText(newValue);\n                                    });\n                                });\n                                BindingsHelper.preserve(l, t);\n                                return l;\n                            });\n                        },\n                        true)\n                .style(\"selected-file-list\")\n                .hide(Bindings.isEmpty(list));\n        return c.build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHelper.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.ext.FileEntry;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.ext.FileSystem;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport java.time.Instant;\nimport java.util.List;\n\npublic class BrowserFileSystemHelper {\n\n    public static String adjustPath(BrowserFileSystemTabModel model, String path) {\n        if (path == null) {\n            return null;\n        }\n\n        path = path.strip();\n        if (path.isBlank()) {\n            return null;\n        }\n\n        if (path.startsWith(\"\\\"\") && path.endsWith(\"\\\"\")) {\n            path = path.substring(1, path.length() - 1);\n        } else if (path.startsWith(\"'\") && path.endsWith(\"'\")) {\n            path = path.substring(1, path.length() - 1);\n        }\n\n        // Handle special case when file system creation has failed\n\n        var shell = model.getFileSystem().getShell();\n        if (shell.isEmpty()) {\n            return path;\n        }\n\n        if (shell.get().getOsType() == OsType.WINDOWS && path.length() == 2 && path.endsWith(\":\")) {\n            return path + \"\\\\\";\n        }\n\n        return path;\n    }\n\n    public static String evaluatePath(BrowserFileSystemTabModel model, String path) throws Exception {\n        if (path == null) {\n            return null;\n        }\n\n        var shell = model.getFileSystem().getShell();\n        if (shell.isEmpty() || !shell.get().isRunning(true)) {\n            return path;\n        }\n\n        try {\n            var r = shell.get()\n                    .getShellDialect()\n                    .evaluateExpression(shell.get(), path)\n                    .readStdoutOrThrow();\n            return !r.isBlank() ? r : null;\n        } catch (Exception ex) {\n            ErrorEventFactory.expected(ex);\n            throw ex;\n        }\n    }\n\n    public static FilePath resolveDirectoryPath(BrowserFileSystemTabModel model, FilePath path, boolean allowRewrite)\n            throws Exception {\n        if (path == null) {\n            return null;\n        }\n\n        var shell = model.getFileSystem().getShell();\n        if (shell.isEmpty()) {\n            return path;\n        }\n\n        var tildeResolved = path.resolveTildeHome(\n                model.getFileSystem().getShell().orElseThrow().view().userHome());\n        var resolved = FilePath.of(shell.get()\n                .getShellDialect()\n                .resolveDirectory(shell.get(), tildeResolved.toString())\n                .readStdoutOrThrow());\n\n        if (!resolved.isAbsolute()) {\n            throw ErrorEventFactory.expected(\n                    new IllegalArgumentException(String.format(\"Directory %s is not absolute\", resolved)));\n        }\n\n        if (allowRewrite && model.getFileSystem().fileExists(resolved)) {\n            return resolved.getParent().toDirectory();\n        }\n\n        return resolved.toDirectory();\n    }\n\n    public static void validateDirectoryPath(FileSystem fs, FilePath path, boolean verifyExists) throws Exception {\n        if (path == null) {\n            return;\n        }\n\n        if (verifyExists && !fs.directoryExists(path)) {\n            throw ErrorEventFactory.expected(new IllegalArgumentException(\n                    String.format(\"Directory %s does not exist or is not accessible\", path)));\n        }\n\n        try {\n            fs.directoryAccessible(path);\n        } catch (Exception ex) {\n            ErrorEventFactory.expected(ex);\n            throw ex;\n        }\n    }\n\n    public static FileEntry getRemoteWrapper(FileSystem fileSystem, FilePath file) throws Exception {\n        return new FileEntry(\n                fileSystem,\n                file,\n                Instant.now(),\n                \"\" + fileSystem.getFileSize(file),\n                null,\n                fileSystem.directoryExists(file) ? FileKind.DIRECTORY : FileKind.FILE);\n    }\n\n    public static void delete(List<FileEntry> files) {\n        if (files.isEmpty()) {\n            return;\n        }\n\n        for (var file : files) {\n            try {\n                file.getFileSystem().delete(file.getPath());\n            } catch (Throwable t) {\n                ErrorEventFactory.fromThrowable(t).handle();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHistory.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.core.FilePath;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.binding.BooleanBinding;\nimport javafx.beans.property.IntegerProperty;\nimport javafx.beans.property.SimpleIntegerProperty;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\n\npublic final class BrowserFileSystemHistory {\n\n    private final IntegerProperty cursor = new SimpleIntegerProperty(-1);\n    private final List<FilePath> history = new ArrayList<>();\n    private final BooleanBinding canGoBack =\n            Bindings.createBooleanBinding(() -> cursor.get() > 0 && history.size() > 1, cursor);\n    private final BooleanBinding canGoForth =\n            Bindings.createBooleanBinding(() -> cursor.get() < history.size() - 1, cursor);\n\n    public List<FilePath> getForwardHistory(int max) {\n        var l = new ArrayList<FilePath>();\n        for (var i = cursor.get() + 1; i < Math.min(history.size(), cursor.get() + max); i++) {\n            l.add(history.get(i));\n        }\n        return l;\n    }\n\n    public List<FilePath> getBackwardHistory(int max) {\n        var l = new ArrayList<FilePath>();\n        for (var i = cursor.get() - 1; i >= Math.max(0, cursor.get() - max); i--) {\n            l.add(history.get(i));\n        }\n        return l;\n    }\n\n    public FilePath getCurrent() {\n        return history.size() > 0 ? history.get(cursor.get()) : null;\n    }\n\n    public void updateCurrent(FilePath s) {\n        if (s == null) {\n            return;\n        }\n\n        var lastString = getCurrent();\n        if (cursor.get() != -1 && Objects.equals(lastString, s)) {\n            return;\n        }\n\n        if (canGoForth.get()) {\n            history.subList(cursor.get() + 1, history.size()).clear();\n        }\n\n        history.add(s);\n        cursor.set(history.size() - 1);\n    }\n\n    public FilePath back(int i) {\n        if (!canGoBack.get()) {\n            return null;\n        }\n        cursor.set(Math.max(0, cursor.get() - i));\n        return history.get(cursor.get());\n    }\n\n    public FilePath forth(int i) {\n        if (!canGoForth.get()) {\n            return history.getLast();\n        }\n        cursor.set(Math.min(history.size() - 1, cursor.get() + i));\n        return history.get(cursor.get());\n    }\n\n    public BooleanBinding canGoBackProperty() {\n        return canGoBack;\n    }\n\n    public BooleanBinding canGoForthProperty() {\n        return canGoForth;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemSavedState.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.util.GlobalTimer;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.JacksonMapper;\n\nimport javafx.application.Platform;\nimport javafx.collections.FXCollections;\nimport javafx.collections.ObservableList;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.JavaType;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.fasterxml.jackson.databind.deser.std.StdDeserializer;\nimport com.fasterxml.jackson.databind.node.JsonNodeFactory;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport lombok.*;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.io.IOException;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.*;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\n\n@AllArgsConstructor\n@Getter\n@JsonSerialize(using = BrowserFileSystemSavedState.Serializer.class)\n@JsonDeserialize(using = BrowserFileSystemSavedState.Deserializer.class)\npublic class BrowserFileSystemSavedState {\n\n    private static final int STORED = 15;\n\n    @Setter\n    private BrowserFileSystemTabModel model;\n\n    private FilePath lastDirectory;\n\n    @NonNull\n    private ObservableList<RecentEntry> recentDirectories;\n\n    public BrowserFileSystemSavedState(FilePath lastDirectory, @NonNull ObservableList<RecentEntry> recentDirectories) {\n        this.lastDirectory = lastDirectory;\n        this.recentDirectories = recentDirectories;\n    }\n\n    public BrowserFileSystemSavedState() {\n        lastDirectory = null;\n        recentDirectories = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());\n    }\n\n    static BrowserFileSystemSavedState loadForStore(BrowserFileSystemTabModel model) {\n        var state = AppCache.getNonNull(\n                \"fs-state-\" + model.getEntry().get().getUuid(), BrowserFileSystemSavedState.class, () -> {\n                    return new BrowserFileSystemSavedState();\n                });\n        state.setModel(model);\n        return state;\n    }\n\n    public synchronized void save() {\n        if (model == null) {\n            return;\n        }\n\n        AppCache.update(\"fs-state-\" + model.getEntry().get().getUuid(), this);\n    }\n\n    public void cd(FilePath dir) {\n        if (dir == null) {\n            lastDirectory = null;\n            return;\n        }\n\n        lastDirectory = dir;\n\n        // After 10 seconds\n        GlobalTimer.delayAsync(\n                new Runnable() {\n                    @Override\n                    public void run() {\n                        // Synchronize with platform thread\n                        Platform.runLater(() -> {\n                            if (Objects.equals(lastDirectory, dir)) {\n                                updateRecent(dir);\n                                save();\n                            }\n                        });\n                    }\n                },\n                Duration.ofMillis(10000));\n    }\n\n    private synchronized void updateRecent(FilePath dir) {\n        var without = dir.removeTrailingSlash();\n        var with = dir.toDirectory();\n        var copy = new ArrayList<>(recentDirectories);\n        for (RecentEntry recentEntry : copy) {\n            if (Objects.equals(recentEntry.directory, without) || Objects.equals(recentEntry.directory, with)) {\n                recentDirectories.remove(recentEntry);\n            }\n        }\n\n        var o = new RecentEntry(with, Instant.now());\n        if (recentDirectories.size() < STORED) {\n            recentDirectories.addFirst(o);\n        } else {\n            recentDirectories.removeLast();\n            recentDirectories.addFirst(o);\n        }\n    }\n\n    public static class Serializer extends StdSerializer<BrowserFileSystemSavedState> {\n\n        protected Serializer() {\n            super(BrowserFileSystemSavedState.class);\n        }\n\n        @Override\n        public void serialize(BrowserFileSystemSavedState value, JsonGenerator gen, SerializerProvider provider)\n                throws IOException {\n            var node = JsonNodeFactory.instance.objectNode();\n            node.set(\"recentDirectories\", JacksonMapper.getDefault().valueToTree(value.getRecentDirectories()));\n            gen.writeTree(node);\n        }\n    }\n\n    public static class Deserializer extends StdDeserializer<BrowserFileSystemSavedState> {\n\n        protected Deserializer() {\n            super(BrowserFileSystemSavedState.class);\n        }\n\n        private static <T> Predicate<T> distinctBy(Function<? super T, ?> f) {\n            Set<Object> objects = new HashSet<>();\n            return t -> objects.add(f.apply(t));\n        }\n\n        @Override\n        @SneakyThrows\n        public BrowserFileSystemSavedState deserialize(JsonParser p, DeserializationContext ctxt) {\n            var tree = (ObjectNode) JacksonMapper.getDefault().readTree(p);\n            JavaType javaType = JacksonMapper.getDefault()\n                    .getTypeFactory()\n                    .constructCollectionLikeType(List.class, RecentEntry.class);\n            List<RecentEntry> recentDirectories =\n                    JacksonMapper.getDefault().treeToValue(tree.remove(\"recentDirectories\"), javaType);\n            if (recentDirectories == null) {\n                recentDirectories = List.of();\n            }\n            var cleaned = recentDirectories.stream()\n                    .map(recentEntry -> new RecentEntry(recentEntry.directory.toDirectory(), recentEntry.time))\n                    .filter(distinctBy(recentEntry -> recentEntry.getDirectory()))\n                    .collect(Collectors.toCollection(CopyOnWriteArrayList::new));\n            return new BrowserFileSystemSavedState(null, FXCollections.observableList(cleaned));\n        }\n    }\n\n    @Value\n    @Jacksonized\n    @Builder\n    public static class RecentEntry {\n\n        FilePath directory;\n        Instant time;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabComp.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.browser.menu.BrowserMenuProviders;\nimport io.xpipe.app.comp.*;\nimport io.xpipe.app.comp.augment.ContextMenuAugment;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.platform.InputHelper;\nimport io.xpipe.app.platform.MenuHelper;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.util.GlobalTimer;\nimport io.xpipe.core.FilePath;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.ReadOnlyBooleanWrapper;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.Button;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\nimport javafx.scene.input.MouseButton;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Priority;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.VBox;\n\nimport atlantafx.base.controls.Spacer;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\npublic class BrowserFileSystemTabComp extends SimpleRegionBuilder {\n\n    private final BrowserFileSystemTabModel model;\n    private final boolean showStatusBar;\n\n    public BrowserFileSystemTabComp(BrowserFileSystemTabModel model, boolean showStatusBar) {\n        this.model = model;\n        this.showStatusBar = showStatusBar;\n    }\n\n    @Override\n    protected Region createSimple() {\n        return createContent();\n    }\n\n    private Region createContent() {\n        var root = new VBox();\n        root.setMinWidth(190);\n        var overview = new Button(null, new FontIcon(\"mdi2m-monitor\"));\n        overview.setOnAction(e -> model.cdAsync((FilePath) null));\n        RegionDescriptor.builder()\n                .nameKey(\"overview\")\n                .shortcut(new KeyCodeCombination(KeyCode.HOME, KeyCombination.ALT_DOWN))\n                .build()\n                .apply(overview);\n        overview.disableProperty().bind(model.getInOverview());\n        InputHelper.onKeyCombination(\n                root, new KeyCodeCombination(KeyCode.HOME, KeyCombination.ALT_DOWN), true, keyEvent -> {\n                    overview.fire();\n                    keyEvent.consume();\n                });\n\n        var backBtn = BrowserMenuProviders.byId(\"back\", model, List.of()).toButton(root, model, List.of());\n        var forthBtn = BrowserMenuProviders.byId(\"forward\", model, List.of()).toButton(root, model, List.of());\n        var refreshBtn = BrowserMenuProviders.byId(\"refresh\", model, List.of()).toButton(root, model, List.of());\n        // Don't handle key events for this button, we also have that available as a menu item\n        var terminalBtn =\n                BrowserMenuProviders.byId(\"openInTerminal\", model, List.of()).toButton(new Region(), model, List.of());\n\n        var menuButton = MenuHelper.createMenuButton();\n        menuButton.setGraphic(new FontIcon(\"mdral-folder_open\"));\n        new ContextMenuAugment<>(\n                        event -> event.getButton() == MouseButton.PRIMARY,\n                        null,\n                        () -> new BrowserContextMenu(model, null, false))\n                .accept(menuButton);\n        menuButton.disableProperty().bind(model.getInOverview());\n        RegionDescriptor.builder().nameKey(\"directoryOptions\").build().apply(menuButton);\n\n        var smallWidth = Bindings.createBooleanBinding(\n                () -> {\n                    return root.getWidth() < 450;\n                },\n                root.widthProperty());\n\n        refreshBtn.managedProperty().bind(smallWidth.not());\n        refreshBtn.visibleProperty().bind(refreshBtn.managedProperty());\n\n        var terminalSupported =\n                BrowserMenuProviders.byId(\"openInTerminal\", model, List.of()).isApplicable(model, List.of());\n        terminalBtn.managedProperty().bind(smallWidth.not().and(new ReadOnlyBooleanWrapper(terminalSupported)));\n        terminalBtn\n                .visibleProperty()\n                .bind(terminalBtn.managedProperty().and(new ReadOnlyBooleanWrapper(terminalSupported)));\n\n        var filter = new BrowserFileListFilterComp(model, model.getFilter())\n                .hide(smallWidth)\n                .buildStructure();\n\n        var topBar = new HBox();\n        topBar.setAlignment(Pos.CENTER);\n        topBar.getStyleClass().add(\"top-bar\");\n        AppFontSizes.xl(topBar);\n        var navBar = new BrowserNavBarComp(model).buildStructure();\n        filter.textField().prefHeightProperty().bind(navBar.get().heightProperty());\n        AppFontSizes.base(navBar.get());\n\n        var leftBox = new HBox(overview, backBtn, forthBtn);\n        leftBox.setFillHeight(true);\n        leftBox.getStyleClass().add(\"button-bar\");\n        var rightBox = new HBox(filter.get(), refreshBtn, terminalBtn, menuButton);\n        rightBox.setFillHeight(true);\n        rightBox.getStyleClass().add(\"button-bar\");\n\n        topBar.getChildren().setAll(leftBox, new Spacer(6), navBar.get(), new Spacer(6), rightBox);\n        topBar.setMinWidth(0);\n\n        if (model.getBrowserModel() instanceof BrowserFullSessionModel fullSessionModel) {\n            var pinButton = new Button();\n            RegionDescriptor.builder().nameKey(\"pinTab\").build().apply(pinButton);\n            pinButton\n                    .graphicProperty()\n                    .bind(PlatformThread.sync(Bindings.createObjectBinding(\n                            () -> {\n                                if (fullSessionModel.getGlobalPinnedTab().getValue() != model) {\n                                    return new FontIcon(\"mdi2p-pin\");\n                                }\n\n                                return new FontIcon(\"mdi2p-pin-off\");\n                            },\n                            fullSessionModel.getGlobalPinnedTab())));\n            pinButton.setOnAction(e -> {\n                if (fullSessionModel.getGlobalPinnedTab().getValue() != model) {\n                    fullSessionModel.pinTab(model);\n                } else {\n                    fullSessionModel.unpinTab();\n                }\n                e.consume();\n            });\n            rightBox.getChildren().add(1, pinButton);\n            squaredSize(navBar.get(), pinButton, true);\n        }\n\n        squaredSize(navBar.get(), overview, true);\n        squaredSize(navBar.get(), backBtn, true);\n        squaredSize(navBar.get(), forthBtn, true);\n        squaredSize(navBar.get(), refreshBtn, true);\n        squaredSize(navBar.get(), terminalBtn, true);\n        squaredSize(navBar.get(), menuButton, false);\n\n        var content = createFileListContent();\n        root.getChildren().addAll(topBar, content);\n        VBox.setVgrow(content, Priority.ALWAYS);\n        root.focusedProperty().addListener((observable, oldValue, newValue) -> {\n            if (newValue) {\n                content.requestFocus();\n            }\n        });\n\n        InputHelper.onKeyCombination(\n                root, new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN), true, keyEvent -> {\n                    filter.toggleButton().fire();\n                    filter.textField().requestFocus();\n                    keyEvent.consume();\n                });\n        InputHelper.onKeyCombination(\n                root, new KeyCodeCombination(KeyCode.L, KeyCombination.SHORTCUT_DOWN), true, keyEvent -> {\n                    navBar.textField().requestFocus();\n                    keyEvent.consume();\n                });\n        InputHelper.onKeyCombination(\n                root, new KeyCodeCombination(KeyCode.H, KeyCombination.ALT_DOWN), true, keyEvent -> {\n                    navBar.historyButton().fire();\n                    keyEvent.consume();\n                });\n        InputHelper.onKeyCombination(\n                root, new KeyCodeCombination(KeyCode.UP, KeyCombination.ALT_DOWN), true, keyEvent -> {\n                    var p = model.getCurrentParentDirectory();\n                    if (p != null) {\n                        model.cdAsync(p.getPath().toString());\n                    }\n                    keyEvent.consume();\n                });\n        InputHelper.onKeyCombination(root, new KeyCodeCombination(KeyCode.BACK_SPACE), false, keyEvent -> {\n            var p = model.getCurrentParentDirectory();\n            if (p != null) {\n                model.cdAsync(p.getPath().toString());\n            }\n            keyEvent.consume();\n        });\n        return root;\n    }\n\n    private void squaredSize(Region ref, Region toResize, boolean width) {\n        if (width) {\n            toResize.minWidthProperty().bind(ref.heightProperty());\n        }\n        toResize.minHeightProperty().bind(ref.heightProperty().add(-2));\n        if (width) {\n            toResize.maxWidthProperty().bind(ref.heightProperty());\n        }\n        toResize.maxHeightProperty().bind(ref.heightProperty().add(-2));\n    }\n\n    private Region createFileListContent() {\n        var directoryView =\n                new BrowserFileListComp(model.getFileList()).apply(struc -> VBox.setVgrow(struc, Priority.ALWAYS));\n        var fileListElements = new ArrayList<BaseRegionBuilder<?, ?>>();\n        fileListElements.add(directoryView);\n        if (showStatusBar) {\n            var statusBar = new BrowserStatusBarComp(model);\n            fileListElements.add(statusBar);\n        }\n        var fileList = new VerticalComp(fileListElements)\n                .style(\"browser-content\")\n                .style(\"color-box\")\n                .style(\"gray\")\n                .apply(struc -> {\n                    struc.focusedProperty().addListener((observable, oldValue, newValue) -> {\n                        if (newValue) {\n                            struc.getChildren().getFirst().requestFocus();\n                        }\n                    });\n                });\n\n        // Delay show to hide file list changes happening\n        // Not perfect, but covers most of the cases of small directories\n        var showOverview = new SimpleBooleanProperty(true);\n        model.getCurrentPath().subscribe(path -> {\n            GlobalTimer.delay(\n                    () -> {\n                        showOverview.setValue(path == null);\n                    },\n                    Duration.ofMillis(250));\n        });\n\n        var home = new BrowserOverviewComp(model).style(\"browser-overview\");\n        var stack = new MultiContentComp(false, Map.of(home, showOverview, fileList, showOverview.not()), false);\n        var r = stack.style(\"browser-content-container\").build();\n        r.focusedProperty().addListener((observable, oldValue, newValue) -> {\n            if (newValue) {\n                if (r.getChildrenUnmodifiable().get(0).isVisible()) {\n                    r.getChildrenUnmodifiable().getFirst().requestFocus();\n                } else {\n                    r.getChildrenUnmodifiable().get(1).requestFocus();\n                }\n            }\n        });\n        return r;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.browser.BrowserAbstractSessionModel;\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.browser.BrowserStoreSessionTab;\nimport io.xpipe.app.browser.action.impl.TransferFilesActionProvider;\nimport io.xpipe.app.browser.menu.BrowserMenuItemProvider;\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.core.window.AppMainWindow;\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.*;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.terminal.*;\nimport io.xpipe.app.util.BooleanScope;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.FailableFunction;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.*;\nimport javafx.beans.value.ObservableBooleanValue;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.FXCollections;\nimport javafx.collections.ObservableList;\n\nimport lombok.Getter;\nimport lombok.NonNull;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n@Getter\npublic final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<FileSystemStore> {\n\n    private static boolean wasTerminalDocked;\n\n    private final Property<String> filter = new SimpleStringProperty();\n    private final BrowserFileListModel fileList;\n    private final ReadOnlyObjectWrapper<FilePath> currentPath = new ReadOnlyObjectWrapper<>();\n    private final BrowserFileSystemHistory history = new BrowserFileSystemHistory();\n    private final ObservableBooleanValue inOverview = Bindings.createBooleanBinding(\n            () -> {\n                return currentPath.get() == null;\n            },\n            currentPath);\n    private final ObservableList<UUID> terminalRequests = FXCollections.observableArrayList();\n    private final BooleanProperty transferCancelled = new SimpleBooleanProperty();\n    private final Property<BrowserTransferProgress> progress = new SimpleObjectProperty<>();\n    private final ObservableList<BrowserTransferProgress> progressesIntervalHistory =\n            FXCollections.observableArrayList();\n    private final LongProperty progressTransferSpeed = new SimpleLongProperty();\n    private final Property<Duration> progressRemaining = new SimpleObjectProperty<>();\n    private final FailableFunction<DataStoreEntryRef<FileSystemStore>, FileSystem, Exception> fileSystemFactory;\n    private final StringProperty fileSystemNameSuffix = new SimpleStringProperty();\n    private WrapperFileSystem fileSystem;\n    private BrowserFileSystemSavedState savedState;\n\n    public BrowserFileSystemTabModel(\n            BrowserAbstractSessionModel<?> model,\n            DataStoreEntryRef<? extends FileSystemStore> entry,\n            FailableFunction<DataStoreEntryRef<FileSystemStore>, FileSystem, Exception> fileSystemFactory) {\n        super(model, entry);\n        this.fileList = new BrowserFileListModel(this);\n        this.fileSystemFactory = fileSystemFactory;\n    }\n\n    @Override\n    public ObservableValue<String> getName() {\n        var name = super.getName();\n        return Bindings.createStringBinding(() -> {\n            var suffix = fileSystemNameSuffix.get();\n            return name.getValue() + (suffix != null ? \" [\" + suffix + \"]\" : \"\");\n        });\n    }\n\n    public void updateProgress(BrowserTransferProgress n) {\n        if (n == null) {\n            progress.setValue(null);\n            progressesIntervalHistory.clear();\n            progressTransferSpeed.setValue(0);\n            return;\n        }\n\n        if (n.getTransferred() == 0) {\n            progress.setValue(n);\n            return;\n        }\n\n        var changedHistory = false;\n        if (progress.getValue() != null) {\n            var last = progressesIntervalHistory.isEmpty()\n                    ? Instant.EPOCH\n                    : progressesIntervalHistory.getLast().getTimestamp();\n            var elapsed = Duration.between(last, n.getTimestamp());\n            if (elapsed.toMillis() >= 1000) {\n                progressesIntervalHistory.add(progress.getValue());\n                changedHistory = true;\n            }\n        }\n\n        progress.setValue(n);\n        if (progressesIntervalHistory.isEmpty()) {\n            return;\n        }\n\n        if (changedHistory && progressesIntervalHistory.size() >= 2) {\n            var speed = BrowserTransferProgress.estimateTransferSpeed(progressesIntervalHistory, n);\n            progressTransferSpeed.setValue(speed);\n            var remaining = n.getTotal() - n.getTransferred();\n            var estimate = remaining / (double) speed;\n\n            var newDuration = Duration.ofMillis((long) (estimate * 1000.0));\n            var smooth = progressRemaining.getValue() != null\n                    && progressRemaining.getValue().toSeconds() + 1 == newDuration.toSeconds();\n            if (!smooth) {\n                progressRemaining.setValue(newDuration);\n            }\n        }\n    }\n\n    public ObservableValue<BrowserTransferProgress> getProgress() {\n        return progress;\n    }\n\n    public Optional<FileEntry> findFile(FilePath path) {\n        return getFileList().getAll().getValue().stream()\n                .filter(browserEntry -> browserEntry.getFileName().equals(path.toString())\n                        || browserEntry.getRawFileEntry().getPath().equals(path))\n                .findFirst()\n                .map(browserEntry -> browserEntry.getRawFileEntry());\n    }\n\n    @Override\n    public BaseRegionBuilder<?, ?> comp() {\n        return new BrowserFileSystemTabComp(this, true);\n    }\n\n    @Override\n    public boolean canImmediatelyClose() {\n        if (fileSystem.getShell().isEmpty()\n                || !fileSystem.getShell().get().getLock().isLocked()) {\n            return true;\n        }\n\n        return progress.getValue() == null || progress.getValue().done();\n    }\n\n    @Override\n    public void init() throws Exception {\n        BooleanScope.executeExclusive(busy, () -> {\n            var fs = new WrapperFileSystem(fileSystemFactory.apply(getEntry().asNeeded()));\n            Platform.runLater(() -> {\n                getFileSystemNameSuffix().set(fs.getSuffix());\n            });\n            if (fs.getShell().isPresent()) {\n                ProcessControlProvider.get().withDefaultScripts(fs.getShell().get());\n            }\n            fs.open();\n\n            // Listen to kill after init as the shell might get killed during init for certain reasons\n            if (fs.getRawShellControl().isPresent()) {\n                fs.getRawShellControl().get().onKill(() -> {\n                    browserModel.closeAsync(this);\n                });\n            }\n            this.fileSystem = fs;\n\n            // Cache for later usage\n            if (fs.getShell().isPresent()) {\n                fs.getShell().get().view().getPasswdFile();\n                fs.getShell().get().view().getGroupFile();\n            }\n\n            for (var a : ActionProvider.ALL) {\n                if (a instanceof BrowserMenuItemProvider ba) {\n                    ba.init(this);\n                }\n            }\n        });\n        this.savedState = BrowserFileSystemSavedState.loadForStore(this);\n    }\n\n    @Override\n    public void close() {\n        BooleanScope.executeExclusive(busy, () -> {\n            var current = currentPath.getValue();\n            if (savedState != null && current != null) {\n                savedState.cd(current);\n                BrowserHistorySavedStateImpl.get()\n                        .add(new BrowserHistorySavedState.Entry(getEntry().get().getUuid(), current));\n                BrowserHistorySavedStateImpl.get().save();\n            }\n            try {\n                fileSystem.close();\n            } catch (IOException e) {\n                ErrorEventFactory.fromThrowable(e).handle();\n            }\n        });\n    }\n\n    public void startIfNeeded() throws Exception {\n        fileSystem.reinitIfNeeded();\n    }\n\n    public void killTransfer() {\n        transferCancelled.set(true);\n    }\n\n    public void refreshSync() {\n        cdSyncWithoutCheck(currentPath.get());\n    }\n\n    public void refreshBrowserEntriesSync(List<BrowserEntry> entries) {\n        refreshFileEntriesSync(\n                entries.stream().map(BrowserEntry::getRawFileEntry).collect(Collectors.toList()));\n    }\n\n    public void refreshFileEntriesSync(List<FileEntry> entries) {\n        if (fileList.getAll().getValue().size() < 10) {\n            refreshSync();\n            return;\n        }\n\n        if (entries.size() > 10 && fileList.getAll().getValue().size() < 100) {\n            refreshSync();\n            return;\n        }\n\n        var all = new ArrayList<FileEntry>();\n        all.addAll(entries);\n        for (BrowserEntry browserEntry : fileList.getAll().getValue()) {\n            var fe = browserEntry.getRawFileEntry();\n            if (fe.getKind() == FileKind.LINK\n                    && entries.stream()\n                            .anyMatch(o -> o.getPath().equals(fe.resolved().getPath()))) {\n                all.add(fe);\n            }\n        }\n\n        for (FileEntry fileEntry : entries) {\n            if (fileEntry.getKind() == FileKind.LINK) {\n                all.add(fileEntry.resolved());\n            }\n        }\n\n        try {\n            for (var e : all) {\n                var refresh = fileSystem.getFileInfo(e.getPath());\n                fileList.updateEntry(e.getPath(), refresh.orElse(null));\n            }\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n        }\n    }\n\n    public FileEntry getCurrentParentDirectory() {\n        if (currentPath.get() == null) {\n            return null;\n        }\n\n        var parent = currentPath.get().getParent();\n        if (parent == null) {\n            return null;\n        }\n\n        return new FileEntry(fileSystem, parent, null, null, null, FileKind.DIRECTORY);\n    }\n\n    public FileEntry getCurrentDirectory() {\n        if (currentPath.get() == null) {\n            return null;\n        }\n\n        return new FileEntry(fileSystem, currentPath.get(), null, null, null, FileKind.DIRECTORY);\n    }\n\n    public void cdAsync(FilePath path) {\n        cdAsync(path != null ? path.toString() : null);\n    }\n\n    public void cdAsync(String path) {\n        ThreadHelper.runFailableAsync(() -> {\n            BooleanScope.executeExclusive(busy, () -> {\n                cdSync(path);\n            });\n        });\n    }\n\n    public void cdSync(String path) {\n        cdSyncOrRetry(path, false).ifPresent(s -> cdSyncOrRetry(s, false));\n    }\n\n    private boolean shouldLaunchSplitTerminal() {\n        if (!AppPrefs.get().enableFileBrowserTerminalDocking().get()) {\n            return false;\n        }\n\n        if (OsType.ofLocal() != OsType.WINDOWS) {\n            return false;\n        }\n\n        if (AppMainWindow.get().getStage().getWidth() <= 1380) {\n            return false;\n        }\n\n        var term = AppPrefs.get().terminalType().getValue();\n        if (term == null || term.getOpenFormat() == TerminalOpenFormat.TABBED) {\n            return false;\n        }\n\n        if (!(browserModel instanceof BrowserFullSessionModel f)) {\n            return false;\n        }\n\n        // Check if the right side is already occupied\n        var existingSplit = f.getEffectiveRightTab().getValue();\n        if (existingSplit == this) {\n            return false;\n        }\n        if (existingSplit != null && !(existingSplit instanceof BrowserTerminalDockTabModel)) {\n            return false;\n        }\n\n        return true;\n    }\n\n    public Optional<String> cdSyncOrRetry(String path, boolean customInput) {\n        if (!fileSystem.isRunning()) {\n            return Optional.empty();\n        }\n\n        var cps = currentPath.get() != null ? currentPath.get().toString() : null;\n        if (Objects.equals(path, cps)) {\n            return Optional.empty();\n        }\n\n        if (path == null) {\n            savedState.cd(null);\n            currentPath.set(null);\n            fileList.setAll(Stream.of());\n            return Optional.empty();\n        }\n\n        try {\n            // Start shell in case we exited\n            startIfNeeded();\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).handle();\n            return Optional.ofNullable(cps);\n        }\n\n        // Fix common issues with paths\n        var adjustedPath = BrowserFileSystemHelper.adjustPath(this, path);\n        if (!Objects.equals(path, adjustedPath)) {\n            return Optional.of(adjustedPath);\n        }\n\n        // Open UNC paths in another tab if needed\n        if (handleUncPath(path)) {\n            return Optional.ofNullable(cps);\n        }\n\n        // Evaluate optional expressions\n        String evaluatedPath;\n        if (customInput) {\n            try {\n                evaluatedPath = BrowserFileSystemHelper.evaluatePath(this, adjustedPath);\n            } catch (Exception ex) {\n                ErrorEventFactory.fromThrowable(ex).handle();\n                return Optional.ofNullable(cps);\n            }\n        } else {\n            evaluatedPath = adjustedPath;\n        }\n\n        if (evaluatedPath == null) {\n            return Optional.empty();\n        }\n\n        // Handle commands typed into navigation bar\n        if (customInput\n                && !evaluatedPath.isBlank()\n                && !FilePath.of(evaluatedPath).isAbsolute()\n                && fileSystem.getShell().isPresent()) {\n            var directory = currentPath.get();\n            var name = adjustedPath;\n            ThreadHelper.runFailableAsync(() -> {\n                if (ShellDialects.getStartableDialects().stream().anyMatch(dialect -> adjustedPath\n                        .toLowerCase()\n                        .startsWith(dialect.getExecutableName().toLowerCase()))) {\n                    var sub = fileSystem.getShell().get().subShell();\n                    var open = new ShellOpenFunction() {\n\n                        @Override\n                        public CommandBuilder prepareWithoutInitCommand() {\n                            return CommandBuilder.ofString(adjustedPath);\n                        }\n\n                        @Override\n                        public CommandBuilder prepareWithInitCommand(@NonNull String command) {\n                            return CommandBuilder.ofString(command);\n                        }\n                    };\n                    sub.setDumbOpen(open);\n                    sub.setTerminalOpen(open);\n                    ThreadHelper.runFailableAsync(() -> {\n                        BooleanScope.executeExclusive(busy, () -> {\n                            try (var ignored = sub.start()) {\n                                openTerminalSync(name, directory, sub, true);\n                            }\n                        });\n                    });\n                } else {\n                    ThreadHelper.runFailableAsync(() -> {\n                        BooleanScope.executeExclusive(busy, () -> {\n                            openTerminalSync(name, directory, fileSystem.getShell().get().command(adjustedPath), true);\n                        });\n                    });\n                }\n            });\n            return Optional.ofNullable(cps);\n        }\n\n        // Evaluate optional links\n        FilePath resolvedPath;\n        try {\n            resolvedPath = BrowserFileSystemHelper.resolveDirectoryPath(this, FilePath.of(evaluatedPath), customInput);\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).handle();\n            return Optional.ofNullable(cps);\n        }\n\n        if (!Objects.equals(path, resolvedPath.toString())) {\n            return Optional.of(resolvedPath.toString());\n        }\n\n        try {\n            BrowserFileSystemHelper.validateDirectoryPath(fileSystem, resolvedPath, true);\n            cdSyncWithoutCheck(resolvedPath);\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).handle();\n            return Optional.ofNullable(cps);\n        }\n\n        return Optional.empty();\n    }\n\n    private boolean handleUncPath(String path) {\n        if (path.startsWith(\"\\\\\\\\\")\n                && getBrowserModel() instanceof BrowserFullSessionModel bm\n                && getFileSystem()\n                                .getShell()\n                                .map(shellControl -> shellControl.getShellDialect())\n                                .orElse(null)\n                        == ShellDialects.CMD) {\n            var env =\n                    ProcessControlProvider.get().subShellEnvironment(getEntry().asNeeded(), ShellDialects.POWERSHELL);\n            var entry = DataStoreEntry.createNew(getName().getValue() + \" (PowerShell)\", env);\n            entry.setColor(DataStorage.get().getEffectiveColor(getEntry().get()));\n            entry.setCategoryUuid(getEntry().get().getCategoryUuid());\n            bm.openFileSystemAsync(entry.ref(), null, m -> FilePath.of(path), null);\n            return true;\n        } else {\n            return false;\n        }\n    }\n\n    private void cdSyncWithoutCheck(FilePath path) {\n\n        // Assume that the path is normalized to improve performance!\n        // path = FileSystemHelper.normalizeDirectoryPath(this, path);\n\n        loadFilesSync(path);\n        filter.setValue(null);\n        savedState.cd(path);\n        history.updateCurrent(path);\n        currentPath.set(path);\n    }\n\n    private boolean loadFilesSync(FilePath dir) {\n        try {\n            startIfNeeded();\n            var fs = getFileSystem();\n            if (dir != null) {\n                var stream = fs.listFiles(fs, dir);\n                fileList.setAll(stream);\n            } else {\n                fileList.setAll(Stream.of());\n            }\n            return true;\n        } catch (Exception e) {\n            fileList.setAll(Stream.of());\n            ErrorEventFactory.fromThrowable(e).handle();\n            return false;\n        }\n    }\n\n    public void dropLocalFilesIntoAsync(FileEntry entry, List<Path> files) {\n        ThreadHelper.runFailableAsync(() -> {\n            BooleanScope.executeExclusive(busy, () -> {\n                startIfNeeded();\n                var op = BrowserFileTransferOperation.ofLocal(\n                        entry, files, BrowserFileTransferMode.COPY, true, p -> updateProgress(p), transferCancelled);\n                var action = TransferFilesActionProvider.Action.builder()\n                        .operation(op)\n                        .target(this.entry.asNeeded())\n                        .build();\n                // Might have been killed\n                if (action.executeSync() && fileSystem != null && fileSystem.isRunning()) {\n                    refreshSync();\n                }\n            });\n        });\n    }\n\n    public void dropFilesIntoAsync(FileEntry target, List<FileEntry> files, BrowserFileTransferMode mode) {\n        // We don't have to do anything in this case\n        if (files.isEmpty()) {\n            return;\n        }\n\n        ThreadHelper.runFailableAsync(() -> {\n            BooleanScope.executeExclusive(busy, () -> {\n                startIfNeeded();\n                var op = new BrowserFileTransferOperation(\n                        target, files, mode, true, this::updateProgress, transferCancelled);\n                var action = TransferFilesActionProvider.Action.builder()\n                        .operation(op)\n                        .target(entry.asNeeded())\n                        .build();\n                action.executeSync();\n                refreshSync();\n            });\n        });\n    }\n\n    public void duplicateFile(FileEntry entry) {\n        // Technically we would have to create an action to allow confirmations for this\n        // But in practice, this is almost a non mutable action, so we will save the effort\n        ThreadHelper.runFailableAsync(() -> {\n            BooleanScope.executeExclusive(busy, () -> {\n                startIfNeeded();\n                var adjusted = BrowserFileDuplicates.renameFileDuplicate(\n                        fileSystem, entry.getPath(), entry.getKind() == FileKind.DIRECTORY);\n                fileSystem.copy(entry.getPath(), adjusted);\n                refreshSync();\n            });\n        });\n    }\n\n    public void initWithGivenDirectory(FilePath dir) {\n        cdSync(dir != null ? dir.toString() : null);\n    }\n\n    public void initWithDefaultDirectory() {\n        savedState.cd(null);\n        history.updateCurrent(null);\n    }\n\n    public void openTerminalAsync(\n            String name, FilePath directory, ProcessControl processControl, boolean dockIfPossible) {\n        ThreadHelper.runFailableAsync(() -> {\n            BooleanScope.executeExclusive(busy, () -> {\n                openTerminalSync(name, directory, processControl, dockIfPossible);\n            });\n        });\n    }\n\n    public void openTerminalSync(String name, FilePath directory, ProcessControl processControl, boolean dockIfPossible)\n            throws Exception {\n        var dock = shouldLaunchSplitTerminal() && dockIfPossible;\n        var uuid = UUID.randomUUID();\n        if (dock\n                && browserModel instanceof BrowserFullSessionModel fullSessionModel\n                && !(fullSessionModel.getSplits().get(this) instanceof BrowserTerminalDockTabModel)) {\n            terminalRequests.add(uuid);\n            fullSessionModel.splitTab(this, new BrowserTerminalDockTabModel(browserModel, this, terminalRequests));\n        }\n\n        // If we docked once, we don't want to break it by opening new tabs in maybe still docked tabs\n        var preferTabs = !wasTerminalDocked && !dock;\n        wasTerminalDocked = wasTerminalDocked || dock;\n\n        TerminalLaunch.builder()\n                .entry(entry.get())\n                .title(name)\n                .directory(directory)\n                .command(processControl)\n                .request(uuid)\n                .preferTabs(preferTabs)\n                .launch();\n\n        // Restart connection as we will have to start it anyway, so we speed it up by doing it preemptively\n        startIfNeeded();\n    }\n\n    public void backSync(int i) {\n        var b = history.back(i);\n        if (b != null) {\n            cdSync(b.toString());\n        }\n    }\n\n    public void forthSync(int i) {\n        var f = history.forth(i);\n        if (f != null) {\n            cdSync(f.toString());\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferMode.java",
    "content": "package io.xpipe.app.browser.file;\n\npublic enum BrowserFileTransferMode {\n    NORMAL,\n    COPY,\n    MOVE\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.ext.FileEntry;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.ext.FileSystem;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.FilePath;\n\nimport javafx.beans.property.BooleanProperty;\nimport javafx.beans.value.ChangeListener;\n\nimport lombok.Getter;\n\nimport java.io.*;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Duration;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.function.Consumer;\n\npublic class BrowserFileTransferOperation {\n\n    private static final int DEFAULT_BUFFER_SIZE = 1024;\n\n    @Getter\n    private final FileEntry target;\n\n    @Getter\n    private final List<FileEntry> files;\n\n    private final BrowserFileTransferMode transferMode;\n    private final boolean checkConflicts;\n    private final Consumer<BrowserTransferProgress> progress;\n    private final BooleanProperty cancelled;\n    BrowserDialogs.FileConflictChoice lastConflictChoice;\n\n    public BrowserFileTransferOperation(\n            FileEntry target,\n            List<FileEntry> files,\n            BrowserFileTransferMode transferMode,\n            boolean checkConflicts,\n            Consumer<BrowserTransferProgress> progress,\n            BooleanProperty cancelled) {\n        this.target = target;\n        this.files = files;\n        this.transferMode = transferMode;\n        this.checkConflicts = checkConflicts;\n        this.progress = progress;\n        this.cancelled = cancelled;\n    }\n\n    public static BrowserFileTransferOperation ofLocal(\n            FileEntry target,\n            List<Path> files,\n            BrowserFileTransferMode transferMode,\n            boolean checkConflicts,\n            Consumer<BrowserTransferProgress> progress,\n            BooleanProperty cancelled) {\n        var entries = files.stream()\n                .map(path -> {\n                    if (!Files.exists(path)) {\n                        return null;\n                    }\n\n                    try {\n                        return BrowserLocalFileSystem.getLocalFileEntry(path);\n                    } catch (Exception e) {\n                        throw new RuntimeException(e);\n                    }\n                })\n                .filter(entry -> entry != null)\n                .toList();\n        return new BrowserFileTransferOperation(target, entries, transferMode, checkConflicts, progress, cancelled);\n    }\n\n    private void reinitFileSystemsIfNeeded() throws Exception {\n        getFiles().getFirst().getFileSystem().reinitIfNeeded();\n        getTarget().getFileSystem().reinitIfNeeded();\n    }\n\n    private void updateProgress(BrowserTransferProgress progress) {\n        this.progress.accept(progress);\n    }\n\n    private BrowserDialogs.FileConflictChoice handleChoice(FileSystem fileSystem, FilePath target, boolean multiple)\n            throws Exception {\n        if (lastConflictChoice == BrowserDialogs.FileConflictChoice.CANCEL) {\n            return BrowserDialogs.FileConflictChoice.CANCEL;\n        }\n\n        if (lastConflictChoice == BrowserDialogs.FileConflictChoice.REPLACE_ALL) {\n            return BrowserDialogs.FileConflictChoice.REPLACE;\n        }\n\n        if (lastConflictChoice == BrowserDialogs.FileConflictChoice.RENAME_ALL) {\n            return BrowserDialogs.FileConflictChoice.RENAME;\n        }\n\n        if (fileSystem.fileExists(target)) {\n            if (lastConflictChoice == BrowserDialogs.FileConflictChoice.SKIP_ALL) {\n                return BrowserDialogs.FileConflictChoice.SKIP;\n            }\n\n            var choice = BrowserDialogs.showFileConflictDialog(target, multiple);\n            if (choice == BrowserDialogs.FileConflictChoice.CANCEL) {\n                lastConflictChoice = BrowserDialogs.FileConflictChoice.CANCEL;\n                return BrowserDialogs.FileConflictChoice.CANCEL;\n            }\n\n            if (choice == BrowserDialogs.FileConflictChoice.SKIP) {\n                return BrowserDialogs.FileConflictChoice.SKIP;\n            }\n\n            if (choice == BrowserDialogs.FileConflictChoice.SKIP_ALL) {\n                lastConflictChoice = BrowserDialogs.FileConflictChoice.SKIP_ALL;\n                return BrowserDialogs.FileConflictChoice.SKIP;\n            }\n\n            if (choice == BrowserDialogs.FileConflictChoice.REPLACE_ALL) {\n                lastConflictChoice = BrowserDialogs.FileConflictChoice.REPLACE_ALL;\n                return BrowserDialogs.FileConflictChoice.REPLACE;\n            }\n\n            if (choice == BrowserDialogs.FileConflictChoice.RENAME_ALL) {\n                lastConflictChoice = BrowserDialogs.FileConflictChoice.RENAME_ALL;\n                return BrowserDialogs.FileConflictChoice.RENAME;\n            }\n\n            return choice;\n        }\n        return BrowserDialogs.FileConflictChoice.REPLACE;\n    }\n\n    private boolean cancelled() {\n        return cancelled.get() || AppOperationMode.isInShutdown();\n    }\n\n    public boolean isMove() {\n        if (files.isEmpty()) {\n            return false;\n        }\n\n        var same = files.getFirst().getFileSystem().equals(target.getFileSystem());\n        var doesMove = transferMode == BrowserFileTransferMode.MOVE\n                || (same && transferMode == BrowserFileTransferMode.NORMAL);\n        return doesMove;\n    }\n\n    public void execute() throws Exception {\n        if (files.isEmpty()) {\n            updateProgress(null);\n            return;\n        }\n\n        reinitFileSystemsIfNeeded();\n\n        if (target.getKind() != FileKind.DIRECTORY) {\n            throw new IllegalStateException(\"Target \" + target.getPath() + \" is not a directory\");\n        }\n\n        BrowserFileSystemHelper.validateDirectoryPath(target.getFileSystem(), target.getPath(), true);\n\n        cancelled.set(false);\n\n        var same = files.getFirst().getFileSystem().equals(target.getFileSystem());\n        var doesMove = transferMode == BrowserFileTransferMode.MOVE\n                || (same && transferMode == BrowserFileTransferMode.NORMAL);\n        try {\n            for (var file : files) {\n                if (cancelled()) {\n                    break;\n                }\n\n                if (same) {\n                    handleSingleOnSameFileSystem(file);\n                } else {\n                    // Transfers might change the working directory\n                    var currentDir = file.getFileSystem().pwd();\n                    handleSingleAcrossFileSystems(file);\n\n                    // Expect a kill\n                    if (currentDir.isPresent() && !file.getFileSystem().requiresReinit()) {\n                        file.getFileSystem().cd(currentDir.get());\n                    }\n                }\n            }\n\n            if (!same && doesMove) {\n                for (var file : files) {\n                    if (cancelled()) {\n                        break;\n                    }\n\n                    deleteSingle(file);\n                }\n            }\n        } finally {\n            updateProgress(null);\n        }\n    }\n\n    private void handleSingleOnSameFileSystem(FileEntry source) throws Exception {\n        // Prevent dropping files into itself\n        if ((source.getKind() == FileKind.DIRECTORY && source.getPath().equals(target.getPath()))\n                || (source.getKind() != FileKind.DIRECTORY\n                        && source.getPath().getParent().equals(target.getPath()))) {\n            return;\n        }\n\n        var sourceFile = source.getPath();\n        var targetFile = target.getPath().join(sourceFile.getFileName());\n\n        if (sourceFile.equals(targetFile)) {\n            // Duplicate file by renaming it\n            targetFile = BrowserFileDuplicates.renameFileDuplicate(\n                    target.getFileSystem(), targetFile, source.getKind() == FileKind.DIRECTORY);\n        }\n\n        if (source.getKind() == FileKind.DIRECTORY && target.getFileSystem().directoryExists(targetFile)) {\n            throw ErrorEventFactory.expected(\n                    new IllegalArgumentException(\"Target directory \" + targetFile + \" does already exist\"));\n        }\n\n        if (checkConflicts) {\n            var fileConflictChoice = handleChoice(target.getFileSystem(), targetFile, files.size() > 1);\n            if (fileConflictChoice == BrowserDialogs.FileConflictChoice.SKIP\n                    || fileConflictChoice == BrowserDialogs.FileConflictChoice.CANCEL) {\n                return;\n            }\n\n            if (fileConflictChoice == BrowserDialogs.FileConflictChoice.RENAME) {\n                targetFile = BrowserFileDuplicates.renameFileDuplicate(\n                        target.getFileSystem(), targetFile, source.getKind() == FileKind.DIRECTORY);\n            }\n        }\n\n        var doesMove = transferMode == BrowserFileTransferMode.MOVE || transferMode == BrowserFileTransferMode.NORMAL;\n        if (doesMove) {\n            target.getFileSystem().move(sourceFile, targetFile);\n        } else {\n            target.getFileSystem().copy(sourceFile, targetFile);\n        }\n    }\n\n    private void handleSingleAcrossFileSystems(FileEntry source) throws Exception {\n        var flatFiles = new LinkedHashMap<FileEntry, FilePath>();\n\n        // Prevent dropping directory into itself\n        if (source.getFileSystem().equals(target.getFileSystem())\n                && source.getPath().startsWith(target.getPath())) {\n            return;\n        }\n\n        AtomicLong totalSize = new AtomicLong();\n        if (source.getKind() == FileKind.DIRECTORY) {\n            // Source might have been deleted meanwhile\n            var exists = source.getFileSystem().directoryExists(source.getPath());\n            if (!exists) {\n                progress.accept(BrowserTransferProgress.finished(source.getName(), 0));\n                return;\n            }\n\n            var directoryName = source.getPath().getFileName();\n            if (!source.getPath().isRoot()) {\n                flatFiles.put(source, FilePath.of(directoryName));\n            }\n\n            var baseRelative = source.getPath().getParent().toDirectory();\n            source.getFileSystem().traverseFilesRecursively(source.getFileSystem(), source.getPath(), fileEntry -> {\n                if (cancelled()) {\n                    progress.accept(BrowserTransferProgress.finished(source.getName() + \" ...\", totalSize.get()));\n                    return false;\n                }\n\n                var rel = fileEntry.getPath().relativize(baseRelative).toUnix();\n                flatFiles.put(fileEntry, rel);\n                if (fileEntry.getKind() == FileKind.FILE) {\n                    // This one is up-to-date and does not need to be recalculated\n                    // If we don't have a size, it doesn't matter that much as the total size is only for display\n                    totalSize.addAndGet(fileEntry.getFileSizeLong().orElse(0));\n                    progress.accept(new BrowserTransferProgress(source.getName() + \" ...\", 0, totalSize.get()));\n                }\n                return true;\n            });\n        } else if (source.getKind() == FileKind.FILE) {\n            // Source might have been deleted meanwhile\n            var exists = source.getFileSystem().fileExists(source.getPath());\n            if (!exists) {\n                progress.accept(BrowserTransferProgress.finished(source.getName(), 0));\n                return;\n            }\n\n            flatFiles.put(source, FilePath.of(source.getPath().getFileName()));\n            // If we don't have a size, it doesn't matter that much as the total size is only for display\n            totalSize.addAndGet(source.getFileSizeLong().orElse(0));\n        } else {\n            // Unsupported type, e.g. a socket\n            progress.accept(BrowserTransferProgress.finished(source.getName(), 0));\n            return;\n        }\n\n        var originalSourceFs = flatFiles.keySet().iterator().next().getFileSystem();\n        if (!flatFiles.keySet().stream()\n                .allMatch(fileEntry -> fileEntry.getFileSystem().equals(originalSourceFs))) {\n            throw new IllegalArgumentException(\"Mixed source file systems\");\n        }\n\n        var optimizedSourceFs = originalSourceFs.createTransferOptimizedFileSystem();\n        var targetFs = target.getFileSystem().createTransferOptimizedFileSystem();\n\n        try {\n            AtomicLong transferred = new AtomicLong();\n            for (var e : flatFiles.entrySet()) {\n                if (cancelled()) {\n                    return;\n                }\n\n                var sourceFile = e.getKey();\n                var fixedRelPath = targetFs.makeFileSystemCompatible(e.getValue());\n                var targetFile = target.getPath().join(fixedRelPath.toString());\n                if (sourceFile.getFileSystem().equals(targetFs)) {\n                    throw new IllegalStateException();\n                }\n\n                if (sourceFile.getKind() == FileKind.DIRECTORY) {\n                    targetFs.mkdirs(targetFile);\n                } else if (sourceFile.getKind() == FileKind.FILE) {\n                    if (checkConflicts) {\n                        var fileConflictChoice =\n                                handleChoice(targetFs, targetFile, files.size() > 1 || flatFiles.size() > 1);\n                        if (fileConflictChoice == BrowserDialogs.FileConflictChoice.SKIP\n                                || fileConflictChoice == BrowserDialogs.FileConflictChoice.CANCEL) {\n                            continue;\n                        }\n\n                        if (fileConflictChoice == BrowserDialogs.FileConflictChoice.RENAME) {\n                            targetFile = BrowserFileDuplicates.renameFileDuplicate(targetFs, targetFile, false);\n                        }\n                    }\n\n                    transfer(sourceFile.getPath(), optimizedSourceFs, targetFile, targetFs, transferred, totalSize);\n                }\n            }\n        } finally {\n            updateProgress(BrowserTransferProgress.finished(source.getName(), totalSize.get()));\n\n            if (optimizedSourceFs != originalSourceFs) {\n                optimizedSourceFs.close();\n            }\n            if (target.getFileSystem() != targetFs) {\n                targetFs.close();\n            }\n        }\n    }\n\n    private boolean transferInline(FilePath sourceFile, FileSystem sourceFs, FilePath targetFile, FileSystem targetFs)\n            throws Exception {\n        var wasRun = new AtomicBoolean(false);\n        var active = new AtomicBoolean(true);\n        var ex = new AtomicReference<Exception>();\n        ThreadHelper.runAsync(() -> {\n            try {\n                if (targetFs.writeInstantIfPossible(sourceFs, sourceFile, targetFile)\n                        || sourceFs.readInstantIfPossible(sourceFile, targetFs, targetFile)) {\n                    wasRun.set(true);\n                }\n                active.set(false);\n            } catch (Exception e) {\n                wasRun.set(true);\n                active.set(false);\n                ex.set(e);\n            }\n        });\n\n        while (active.get()) {\n            if (cancelled()) {\n                sourceFs.kill();\n                targetFs.kill();\n                break;\n            }\n\n            ThreadHelper.sleep(10);\n        }\n\n        if (ex.get() != null) {\n            throw ex.get();\n        }\n\n        return wasRun.get();\n    }\n\n    private void transfer(\n            FilePath sourceFile,\n            FileSystem sourceFs,\n            FilePath targetFile,\n            FileSystem targetFs,\n            AtomicLong transferred,\n            AtomicLong totalSize)\n            throws Exception {\n        if (cancelled()) {\n            return;\n        }\n\n        updateProgress(new BrowserTransferProgress(sourceFile.getFileName(), 0, 0));\n\n        var fileSize = sourceFs.getFileSize(sourceFile);\n\n        if (transferInline(sourceFile, sourceFs, targetFile, targetFs) || cancelled()) {\n            if (!cancelled()) {\n                updateProgress(BrowserTransferProgress.finished(sourceFile.getFileName(), fileSize));\n            }\n            return;\n        }\n\n        InputStream inputStream = null;\n        OutputStream outputStream = null;\n        try {\n\n            // Read the first few bytes to figure out possible command failure early\n            // before creating the output stream\n            inputStream = new BufferedInputStream(sourceFs.openInput(sourceFile), 1024);\n            inputStream.mark(1024);\n            var streamStart = new byte[1024];\n            var streamStartLength = inputStream.read(streamStart, 0, 1024);\n            if (streamStartLength < 1024) {\n                inputStream.close();\n                inputStream = new ByteArrayInputStream(streamStart, 0, streamStartLength);\n            } else {\n                inputStream.reset();\n            }\n\n            outputStream = targetFs.openOutput(targetFile, fileSize);\n            transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, fileSize);\n        } catch (Exception ex) {\n            // Mark progress as finished to reset any progress display\n            updateProgress(BrowserTransferProgress.finished(sourceFile.getFileName(), transferred.get()));\n\n            if (inputStream != null) {\n                try {\n                    inputStream.close();\n                } catch (Exception om) {\n                    // This is expected as the process control has to be killed\n                    // When calling close, it will throw an exception when it has to kill\n                    ErrorEventFactory.fromThrowable(om).expected().omit().handle();\n                }\n            }\n            if (outputStream != null) {\n                try {\n                    outputStream.close();\n                } catch (Exception om) {\n                    // This is expected as the process control has to be killed\n                    // When calling close, it will throw an exception when it has to kill\n                    ErrorEventFactory.fromThrowable(om).expected().omit().handle();\n                }\n            }\n            throw ex;\n        }\n\n        // If we receive a cancel while we are closing, there's a good chance that the close is stuck\n        // Then, we just straight up kill the shells\n        ChangeListener<Boolean> closeCancelListener = (observableValue, oldValue, newValue) -> {\n            if (!newValue) {\n                return;\n            }\n\n            sourceFs.kill();\n            targetFs.kill();\n        };\n        cancelled.addListener(closeCancelListener);\n\n        Exception exception = null;\n        try {\n            inputStream.close();\n        } catch (Exception om) {\n            exception = om;\n        }\n        try {\n            outputStream.close();\n        } catch (Exception om) {\n            if (exception != null) {\n                exception.addSuppressed(om);\n            } else {\n                exception = om;\n            }\n        }\n\n        cancelled.removeListener(closeCancelListener);\n\n        if (exception != null) {\n            ErrorEventFactory.preconfigure(ErrorEventFactory.fromThrowable(exception)\n                    .reportable(!cancelled())\n                    .omitted(cancelled()));\n            throw exception;\n        }\n    }\n\n    private void deleteSingle(FileEntry source) throws Exception {\n        source.getFileSystem().delete(source.getPath());\n    }\n\n    private void transferFile(\n            FilePath sourceFile,\n            InputStream inputStream,\n            OutputStream outputStream,\n            AtomicLong transferred,\n            AtomicLong total,\n            long expectedFileSize)\n            throws Exception {\n        // Initialize progress immediately prior to reading anything\n        updateProgress(new BrowserTransferProgress(sourceFile.getFileName(), transferred.get(), total.get()));\n\n        var killStreams = new AtomicBoolean(false);\n        var exception = new AtomicReference<Exception>();\n        var readCount = new AtomicLong();\n        var thread = ThreadHelper.createPlatformThread(\"transfer\", true, () -> {\n            try {\n                var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, expectedFileSize);\n                byte[] buffer = new byte[bs];\n                int read;\n                while ((read = inputStream.read(buffer, 0, bs)) > 0) {\n                    if (cancelled()) {\n                        killStreams.set(true);\n                        break;\n                    }\n\n                    if (!checkTransferValidity()) {\n                        killStreams.set(true);\n                        break;\n                    }\n\n                    outputStream.write(buffer, 0, read);\n                    transferred.addAndGet(read);\n                    readCount.addAndGet(read);\n                    updateProgress(\n                            new BrowserTransferProgress(sourceFile.getFileName(), transferred.get(), total.get()));\n                }\n\n                outputStream.flush();\n                inputStream.transferTo(OutputStream.nullOutputStream());\n\n                var incomplete = !killStreams.get() && readCount.get() < expectedFileSize;\n                if (incomplete) {\n                    throw new IOException(\"Source file \" + sourceFile + \" input size mismatch: Expected \"\n                            + expectedFileSize + \" but got \" + readCount.get() + \". Did the source file get updated?\");\n                }\n            } catch (Exception ex) {\n                exception.set(ex);\n                killStreams.set(true);\n            }\n        });\n\n        thread.start();\n        while (true) {\n            var alive = thread.isAlive();\n            var cancelled = cancelled();\n\n            if (cancelled) {\n                killStreams(thread, readCount, false);\n                break;\n            }\n\n            if (alive) {\n                Thread.sleep(100);\n                continue;\n            }\n\n            if (killStreams.get()) {\n                killStreams(thread, readCount, true);\n            }\n\n            var ex = exception.get();\n            if (ex != null) {\n                throw ex;\n            } else {\n                break;\n            }\n        }\n    }\n\n    private boolean checkTransferValidity() {\n        var sourceFs = files.getFirst().getFileSystem();\n        var targetFs = target.getFileSystem();\n        var same = files.getFirst().getFileSystem().equals(target.getFileSystem());\n        if (!same) {\n            return !sourceFs.requiresReinit() && !targetFs.requiresReinit();\n        } else {\n            return true;\n        }\n    }\n\n    private void killStreams(Thread thread, AtomicLong transferred, boolean instant) throws Exception {\n        var sourceFs = files.getFirst().getFileSystem();\n        var targetFs = target.getFileSystem();\n        var same = files.getFirst().getFileSystem().equals(target.getFileSystem());\n\n        if (!instant && !same && checkTransferValidity()) {\n            var initialTransferred = transferred.get();\n            if (!thread.join(Duration.ofMillis(2000))) {\n                var nowTransferred = transferred.get();\n                var stuck = initialTransferred == nowTransferred;\n                if (stuck) {\n                    sourceFs.kill();\n                    targetFs.kill();\n                    return;\n                }\n            }\n        }\n\n        if (!same) {\n            if (sourceFs.getShell().isPresent()) {\n                try {\n                    sourceFs.getShell().get().closeStdout();\n                } catch (Exception ignored) {\n                }\n            }\n\n            if (targetFs.getShell().isPresent()) {\n                try {\n                    targetFs.getShell().get().closeStdin();\n                } catch (Exception ignored) {\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserGreetingComp.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.scene.control.Label;\nimport javafx.scene.layout.Region;\n\nimport atlantafx.base.theme.Styles;\n\nimport java.time.LocalDateTime;\n\npublic class BrowserGreetingComp extends SimpleRegionBuilder {\n\n    @Override\n    protected Region createSimple() {\n        var r = new Label(getText());\n        AppLayoutModel.get().getSelected().addListener((observableValue, entry, t1) -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                r.setText(getText());\n            });\n        });\n        AppFontSizes.title(r);\n        r.getStyleClass().add(Styles.TEXT_BOLD);\n        return r;\n    }\n\n    private String getText() {\n        var ldt = LocalDateTime.now();\n        var hour = ldt.getHour();\n        String text;\n        if (hour > 18 || hour < 5) {\n            text = AppI18n.get(\"goodEvening\");\n        } else if (hour < 12) {\n            text = AppI18n.get(\"goodMorning\");\n        } else {\n            text = AppI18n.get(\"goodAfternoon\");\n        }\n        return text;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserHistorySavedState.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.core.FilePath;\n\nimport javafx.collections.ObservableList;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.UUID;\n\npublic interface BrowserHistorySavedState {\n\n    void add(Entry entry);\n\n    void save();\n\n    ObservableList<Entry> getEntries();\n\n    @Value\n    @Jacksonized\n    @Builder\n    @AllArgsConstructor\n    class Entry {\n\n        UUID uuid;\n        FilePath path;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserHistorySavedStateImpl.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.core.JacksonMapper;\n\nimport javafx.collections.FXCollections;\nimport javafx.collections.ObservableList;\n\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.JavaType;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.fasterxml.jackson.databind.deser.std.StdDeserializer;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport lombok.SneakyThrows;\nimport lombok.Value;\n\nimport java.util.List;\n\n@Value\n@JsonDeserialize(using = BrowserHistorySavedStateImpl.Deserializer.class)\npublic class BrowserHistorySavedStateImpl implements BrowserHistorySavedState {\n\n    private static BrowserHistorySavedStateImpl INSTANCE;\n\n    @JsonSerialize(as = List.class)\n    ObservableList<Entry> lastSystems;\n\n    public BrowserHistorySavedStateImpl(List<Entry> lastSystems) {\n        this.lastSystems = FXCollections.synchronizedObservableList(FXCollections.observableArrayList(lastSystems));\n    }\n\n    public static BrowserHistorySavedState get() {\n        if (INSTANCE == null) {\n            INSTANCE = load();\n        }\n        return INSTANCE;\n    }\n\n    private static BrowserHistorySavedStateImpl load() {\n        return AppCache.getNonNull(\"browser-state\", BrowserHistorySavedStateImpl.class, () -> {\n            return new BrowserHistorySavedStateImpl(\n                    FXCollections.synchronizedObservableList(FXCollections.observableArrayList()));\n        });\n    }\n\n    @Override\n    public synchronized void add(BrowserHistorySavedState.Entry entry) {\n        synchronized (lastSystems) {\n            lastSystems.removeIf(e -> e == null || e.getUuid().equals(entry.getUuid()));\n            lastSystems.addFirst(entry);\n            if (lastSystems.size() > 15) {\n                lastSystems.removeLast();\n            }\n        }\n    }\n\n    @Override\n    public synchronized void save() {\n        AppCache.update(\"browser-state\", this);\n    }\n\n    @Override\n    public ObservableList<Entry> getEntries() {\n        return lastSystems;\n    }\n\n    public static class Deserializer extends StdDeserializer<BrowserHistorySavedStateImpl> {\n\n        protected Deserializer() {\n            super(BrowserHistorySavedStateImpl.class);\n        }\n\n        @Override\n        @SneakyThrows\n        public BrowserHistorySavedStateImpl deserialize(JsonParser p, DeserializationContext ctxt) {\n            var tree = (ObjectNode) JacksonMapper.getDefault().readTree(p);\n            JavaType javaType =\n                    JacksonMapper.getDefault().getTypeFactory().constructCollectionLikeType(List.class, Entry.class);\n            List<Entry> ls = JacksonMapper.getDefault().treeToValue(tree.remove(\"lastSystems\"), javaType);\n            if (ls == null) {\n                ls = List.of();\n            }\n            var valid = ls.stream()\n                    .filter(entry -> entry.getUuid() != null && entry.getPath() != null)\n                    .toList();\n            return new BrowserHistorySavedStateImpl(valid);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserHistoryTabComp.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.DerivedObservableList;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.BooleanProperty;\nimport javafx.beans.property.ReadOnlyStringWrapper;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.ObservableList;\nimport javafx.geometry.Insets;\nimport javafx.geometry.Pos;\nimport javafx.scene.layout.*;\n\nimport atlantafx.base.theme.Styles;\n\nimport java.util.LinkedHashMap;\nimport java.util.List;\n\npublic class BrowserHistoryTabComp extends SimpleRegionBuilder {\n\n    private final BrowserFullSessionModel model;\n\n    public BrowserHistoryTabComp(BrowserFullSessionModel model) {\n        this.model = model;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var state = BrowserHistorySavedStateImpl.get();\n        var list = DerivedObservableList.wrap(state.getEntries(), true)\n                .filtered(e -> {\n                    if (DataStorage.get() == null) {\n                        return false;\n                    }\n\n                    var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());\n                    if (entry.isEmpty()) {\n                        return false;\n                    }\n\n                    if (!entry.get().getValidity().isUsable()) {\n                        return false;\n                    }\n\n                    return true;\n                })\n                .getList();\n        var empty = Bindings.createBooleanBinding(() -> list.isEmpty(), list);\n        var contentDisplay = createListDisplay(list);\n        var emptyDisplay = createEmptyDisplay();\n        var map = new LinkedHashMap<BaseRegionBuilder<?, ?>, ObservableValue<Boolean>>();\n        map.put(emptyDisplay, empty);\n        map.put(contentDisplay, empty.not());\n        var stack = new MultiContentComp(false, map, false);\n        return stack.build();\n    }\n\n    private BaseRegionBuilder<?, ?> createListDisplay(ObservableList<BrowserHistorySavedState.Entry> list) {\n        var state = BrowserHistorySavedStateImpl.get();\n\n        var welcome = new BrowserGreetingComp();\n        var header = new LabelComp(AppI18n.observable(\"browserWelcomeSystems\"));\n        var vbox = new VerticalComp(List.of(welcome, RegionBuilder.vspacer(4), header));\n        vbox.apply(struc -> struc.setAlignment(Pos.CENTER_LEFT));\n\n        var listBox = new ListBoxViewComp<>(\n                        list,\n                        list,\n                        e -> {\n                            var disable = new SimpleBooleanProperty();\n                            var entryButton = entryButton(e, disable);\n                            var dirButton = dirButton(e, disable);\n                            return new HorizontalComp(List.of(entryButton, dirButton)).apply(struc -> {\n                                ((Region) struc.getChildren().get(0))\n                                        .prefHeightProperty()\n                                        .bind(struc.heightProperty());\n                                ((Region) struc.getChildren().get(1))\n                                        .prefHeightProperty()\n                                        .bind(struc.heightProperty());\n                            });\n                        },\n                        true)\n                .apply(struc -> {\n                    VBox vBox = (VBox) struc.getContent();\n                    vBox.setSpacing(10);\n                });\n\n        var tile = new TileButtonComp(\"restore\", \"restoreAllSessions\", \"mdmz-restore\", actionEvent -> {\n                    model.restoreState(state);\n                    actionEvent.consume();\n                })\n                .maxWidth(2000)\n                .describe(d -> d.nameKey(\"restoreAllSessions\"));\n\n        var layout =\n                new VerticalComp(List.of(vbox, RegionBuilder.vspacer(5), listBox, RegionBuilder.hseparator(), tile));\n        layout.style(\"welcome\");\n        layout.spacing(14);\n        layout.maxWidth(1000);\n        layout.padding(new Insets(45, 40, 40, 50));\n        layout.apply(struc -> {\n            struc.setMaxWidth(1000);\n        });\n        return layout;\n    }\n\n    private BaseRegionBuilder<?, ?> createEmptyDisplay() {\n        var docs = new IntroComp(\"browserWelcomeDocs\", new LabelGraphic.IconGraphic(\"mdi2b-book-open-variant\"));\n        docs.setButtonAction(() -> {\n            DocumentationLink.INTRO.open();\n        });\n        docs.setButtonGraphic(new LabelGraphic.IconGraphic(\"mdi2w-web\"));\n        docs.setButtonDefault(true);\n\n        var open = new IntroComp(\n                \"browserWelcomeEmpty\",\n                new LabelGraphic.CompGraphic(PrettyImageHelper.ofSpecificFixedSize(\"welcome/hips.svg\", 100, 122)));\n        open.setButtonGraphic(new LabelGraphic.IconGraphic(\"mdi2f-folder-open-outline\"));\n        open.setButtonAction(() -> {\n            BrowserFullSessionModel.DEFAULT.openFileSystemAsync(\n                    DataStorage.get().local().ref(), null, null, null);\n        });\n\n        var list = new IntroListComp(List.of(docs, open));\n        return list;\n    }\n\n    private BaseRegionBuilder<?, ?> entryButton(BrowserHistorySavedState.Entry e, BooleanProperty disable) {\n        var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());\n        var graphic = entry.get().getEffectiveIconFile();\n        var view = PrettyImageHelper.ofFixedSize(graphic, 22, 16);\n        var name = Bindings.createStringBinding(\n                () -> {\n                    var n = DataStorage.get().getStoreEntryDisplayName(entry.get());\n                    return AppPrefs.get().censorMode().get() ? \"*\".repeat(n.length()) : n;\n                },\n                AppPrefs.get().censorMode());\n        return new ButtonComp(name, view.build(), () -> {\n                    ThreadHelper.runAsync(() -> {\n                        var storageEntry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());\n                        if (storageEntry.isPresent()) {\n                            model.openFileSystemAsync(storageEntry.get().ref(), null, null, disable);\n                        }\n                    });\n                })\n                .minWidth(300)\n                .describe(\n                        d -> d.name(new ReadOnlyStringWrapper(DataStorage.get().getStoreEntryDisplayName(entry.get()))))\n                .disable(disable)\n                .style(\"entry-button\")\n                .style(Styles.LEFT_PILL)\n                .apply(struc -> struc.setAlignment(Pos.CENTER_LEFT));\n    }\n\n    private BaseRegionBuilder<?, ?> dirButton(BrowserHistorySavedState.Entry e, BooleanProperty disable) {\n        var name = Bindings.createStringBinding(\n                () -> {\n                    var n = e.getPath();\n                    return AppPrefs.get().censorMode().get()\n                            ? \"*\".repeat(n.toString().length())\n                            : n.toString();\n                },\n                AppPrefs.get().censorMode());\n        return new ButtonComp(name, () -> {\n                    ThreadHelper.runAsync(() -> {\n                        model.restoreStateAsync(e, disable);\n                    });\n                })\n                .describe(d -> d.name(new ReadOnlyStringWrapper(e.getPath().toString())))\n                .disable(disable)\n                .style(\"directory-button\")\n                .apply(struc -> struc.setMaxWidth(20000))\n                .style(Styles.RIGHT_PILL)\n                .hgrow()\n                .apply(struc -> struc.setAlignment(Pos.CENTER_LEFT));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserHistoryTabModel.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.browser.BrowserAbstractSessionModel;\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.browser.BrowserSessionTab;\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.storage.DataStoreColor;\n\nimport javafx.beans.value.ObservableValue;\n\npublic final class BrowserHistoryTabModel extends BrowserSessionTab {\n\n    public BrowserHistoryTabModel(BrowserAbstractSessionModel<?> browserModel) {\n        super(browserModel);\n    }\n\n    @Override\n    public BaseRegionBuilder<?, ?> comp() {\n        return new BrowserHistoryTabComp((BrowserFullSessionModel) browserModel);\n    }\n\n    @Override\n    public boolean canImmediatelyClose() {\n        return true;\n    }\n\n    @Override\n    public void init() {}\n\n    @Override\n    public void close() {}\n\n    @Override\n    public ObservableValue<String> getName() {\n        return AppI18n.observable(\"history\").map(s -> \" \" + s + \" \");\n    }\n\n    @Override\n    public String getIcon() {\n        return null;\n    }\n\n    @Override\n    public DataStoreColor getColor() {\n        return null;\n    }\n\n    @Override\n    public boolean isCloseable() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserLocalFileSystem.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.ext.FileEntry;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.ext.FileSystem;\nimport io.xpipe.app.ext.LocalStore;\nimport io.xpipe.core.FilePath;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic class BrowserLocalFileSystem {\n\n    private static FileSystem localFileSystem;\n\n    public static void init() throws Exception {\n        if (localFileSystem == null) {\n            localFileSystem = new LocalStore().createFileSystem();\n            localFileSystem.open();\n        } else {\n            localFileSystem.reinitIfNeeded();\n        }\n    }\n\n    public static void reset() throws Exception {\n        if (localFileSystem != null) {\n            localFileSystem.close();\n            localFileSystem = null;\n        }\n    }\n\n    public static FileEntry getLocalFileEntry(Path file) throws Exception {\n        init();\n        return new FileEntry(\n                localFileSystem.open(),\n                FilePath.of(file),\n                Files.getLastModifiedTime(file).toInstant(),\n                \"\" + Files.size(file),\n                null,\n                Files.isDirectory(file) ? FileKind.DIRECTORY : FileKind.FILE);\n    }\n\n    public static BrowserEntry getLocalBrowserEntry(Path file) throws Exception {\n        var e = getLocalFileEntry(file);\n        return new BrowserEntry(e, null);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.browser.icon.BrowserIconManager;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.RegionStructure;\nimport io.xpipe.app.comp.RegionStructureBuilder;\nimport io.xpipe.app.comp.augment.ContextMenuAugment;\nimport io.xpipe.app.comp.base.ButtonComp;\nimport io.xpipe.app.comp.base.PrettyImageHelper;\nimport io.xpipe.app.comp.base.TextFieldComp;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.MenuHelper;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.util.BooleanScope;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.css.PseudoClass;\nimport javafx.geometry.Insets;\nimport javafx.geometry.Pos;\nimport javafx.scene.Node;\nimport javafx.scene.control.*;\nimport javafx.scene.input.*;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Priority;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\nimport javafx.scene.shape.Rectangle;\n\nimport atlantafx.base.theme.Styles;\n\npublic class BrowserNavBarComp extends RegionStructureBuilder<HBox, BrowserNavBarComp.Structure> {\n\n    private static final PseudoClass INVISIBLE = PseudoClass.getPseudoClass(\"invisible\");\n    private final BrowserFileSystemTabModel model;\n\n    public BrowserNavBarComp(BrowserFileSystemTabModel model) {\n        this.model = model;\n    }\n\n    @Override\n    public Structure createBase() {\n        var pathBar = createPathBar();\n\n        var graphic = Bindings.createStringBinding(\n                () -> {\n                    if (model.getCurrentDirectory() == null) {\n                        return null;\n                    }\n\n                    var icon = new BrowserEntry(model.getCurrentDirectory(), model.getFileList()).getIcon();\n                    BrowserIconManager.loadIfNecessary(icon);\n                    return icon;\n                },\n                PlatformThread.sync(model.getCurrentPath()));\n        var breadcrumbsGraphic = PrettyImageHelper.ofFixedSize(graphic, 24, 24)\n                .style(\"path-graphic\")\n                .build();\n\n        var homeButton = new ButtonComp(null, breadcrumbsGraphic, null)\n                .describe(d -> d.nameKey(\"directoryOptions\"))\n                .apply(new ContextMenuAugment<>(event -> event.getButton() == MouseButton.PRIMARY, null, () -> {\n                    return model.getInOverview().get() ? null : new BrowserContextMenu(model, null, false);\n                }))\n                .build();\n        homeButton.getStyleClass().add(Styles.LEFT_PILL);\n        homeButton.getStyleClass().add(\"path-graphic-button\");\n        AppFontSizes.sm(homeButton);\n\n        var historyButton = new ButtonComp(null, new LabelGraphic.IconGraphic(\"mdi2h-history\"), null)\n                .describe(\n                        d -> d.nameKey(\"history\").shortcut(new KeyCodeCombination(KeyCode.H, KeyCombination.ALT_DOWN)))\n                .style(Styles.RIGHT_PILL)\n                .apply(new ContextMenuAugment<>(\n                        event -> event.getButton() == MouseButton.PRIMARY, null, this::createContextMenu))\n                .build();\n        AppFontSizes.xs(historyButton);\n\n        var breadcrumbs = new BrowserBreadcrumbBar(model);\n\n        var pathRegion = pathBar.build();\n        var breadcrumbsRegion = breadcrumbs.build();\n        breadcrumbsRegion.setOnMouseClicked(event -> {\n            pathRegion.requestFocus();\n            event.consume();\n        });\n        breadcrumbsRegion.setFocusTraversable(false);\n        breadcrumbsRegion\n                .visibleProperty()\n                .bind(Bindings.createBooleanBinding(\n                        () -> {\n                            return !pathRegion.isFocused()\n                                    && !model.getInOverview().get();\n                        },\n                        pathRegion.focusedProperty(),\n                        PlatformThread.sync(model.getInOverview())));\n        var stack = new StackPane(pathRegion, breadcrumbsRegion);\n        stack.setAlignment(Pos.CENTER_LEFT);\n        pathRegion.prefHeightProperty().bind(stack.heightProperty());\n\n        stack.widthProperty().addListener((observable, oldValue, newValue) -> {\n            setMargin(stack, breadcrumbsRegion);\n        });\n\n        model.getCurrentPath().addListener((observable, oldValue, newValue) -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                setMargin(stack, breadcrumbsRegion);\n            });\n        });\n\n        // Prevent overflow\n        var clip = new Rectangle();\n        clip.widthProperty().bind(stack.widthProperty());\n        clip.heightProperty().bind(stack.heightProperty());\n        stack.setClip(clip);\n\n        HBox.setHgrow(stack, Priority.ALWAYS);\n\n        var topBox = new HBox(homeButton, stack, historyButton);\n        topBox.setFillHeight(true);\n        topBox.setAlignment(Pos.CENTER);\n        homeButton.minWidthProperty().bind(pathRegion.heightProperty());\n        homeButton.maxWidthProperty().bind(pathRegion.heightProperty());\n        homeButton.minHeightProperty().bind(pathRegion.heightProperty());\n        homeButton.maxHeightProperty().bind(pathRegion.heightProperty());\n        historyButton.minHeightProperty().bind(pathRegion.heightProperty());\n        historyButton.maxHeightProperty().bind(pathRegion.heightProperty());\n        topBox.setPickOnBounds(false);\n        HBox.setHgrow(topBox, Priority.ALWAYS);\n\n        return new Structure(topBox, pathRegion, historyButton);\n    }\n\n    private void setMargin(StackPane stackPane, Region region) {\n        var off = region.getWidth() - stackPane.getWidth();\n        if (off <= 0) {\n            StackPane.setMargin(region, new Insets(0, 0, 0, 0));\n        } else {\n            StackPane.setMargin(region, new Insets(0, 20, 0, -off - 20));\n        }\n    }\n\n    private RegionBuilder<TextField> createPathBar() {\n        var path = new SimpleStringProperty();\n        model.getCurrentPath().subscribe((newValue) -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                path.set(newValue != null ? newValue.toString() : null);\n            });\n        });\n        path.addListener((observable, oldValue, newValue) -> {\n            ThreadHelper.runFailableAsync(() -> {\n                BooleanScope.executeExclusive(model.getBusy(), () -> {\n                    var changed = model.cdSyncOrRetry(newValue != null && !newValue.isBlank() ? newValue : null, true);\n                    changed.ifPresent(s -> {\n                        Platform.runLater(() -> path.set(!s.isBlank() ? s : null));\n                    });\n                });\n            });\n        });\n        var pathBar = new TextFieldComp(path, true).style(Styles.CENTER_PILL).style(\"path-text\");\n        pathBar.describe(d -> d.nameKey(\"currentPath\"));\n        pathBar.apply(struc -> {\n            struc.focusedProperty().subscribe(val -> {\n                struc.pseudoClassStateChanged(\n                        INVISIBLE, !val && !model.getInOverview().get());\n\n                if (val) {\n                    Platform.runLater(() -> {\n                        struc.end();\n                    });\n                }\n            });\n\n            struc.addEventHandler(KeyEvent.KEY_PRESSED, ke -> {\n                if (ke.getCode().equals(KeyCode.ENTER)) {\n                    ke.consume();\n                }\n            });\n\n            model.getInOverview().subscribe(val -> {\n                // Pseudo classes do not apply if set instantly before shown\n                // If we start a new tab with a directory set, we have to set the pseudo class one pulse later\n                Platform.runLater(() -> {\n                    struc.pseudoClassStateChanged(INVISIBLE, !val && !struc.isFocused());\n                });\n            });\n        });\n        return pathBar;\n    }\n\n    private ContextMenu createContextMenu() {\n        var cm = MenuHelper.createContextMenu();\n\n        var f = model.getHistory().getForwardHistory(8).stream().toList();\n        for (int i = f.size() - 1; i >= 0; i--) {\n            if (f.get(i) == null) {\n                continue;\n            }\n\n            var mi = new MenuItem(f.get(i).toString());\n            int target = i + 1;\n            mi.setOnAction(event -> {\n                ThreadHelper.runFailableAsync(() -> {\n                    BooleanScope.executeExclusive(model.getBusy(), () -> {\n                        model.forthSync(target);\n                    });\n                });\n                event.consume();\n            });\n            cm.getItems().add(mi);\n        }\n        if (!f.isEmpty()) {\n            cm.getItems().add(new SeparatorMenuItem());\n        }\n\n        if (model.getHistory().getCurrent() != null) {\n            var current = new MenuItem(model.getHistory().getCurrent().toString());\n            current.setDisable(true);\n            cm.getItems().add(current);\n        }\n\n        var b = model.getHistory().getBackwardHistory(Integer.MAX_VALUE).stream()\n                .toList();\n        if (!b.isEmpty()) {\n            cm.getItems().add(new SeparatorMenuItem());\n        }\n        for (int i = 0; i < b.size(); i++) {\n            if (b.get(i) == null) {\n                continue;\n            }\n\n            var mi = new MenuItem(b.get(i).toString());\n            int target = i + 1;\n            mi.setOnAction(event -> {\n                ThreadHelper.runFailableAsync(() -> {\n                    BooleanScope.executeExclusive(model.getBusy(), () -> {\n                        model.backSync(target);\n                    });\n                });\n                event.consume();\n            });\n            cm.getItems().add(mi);\n        }\n\n        cm.addEventHandler(Menu.ON_SHOWING, e -> {\n            Node content = cm.getSkin().getNode();\n            if (content instanceof Region r) {\n                r.setMaxHeight(600);\n            }\n        });\n        return cm;\n    }\n\n    public record Structure(HBox box, TextField textField, Button historyButton) implements RegionStructure<HBox> {\n\n        @Override\n        public HBox get() {\n            return box;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserOverviewComp.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.SimpleTitledPaneComp;\nimport io.xpipe.app.comp.base.VerticalComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.FileEntry;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.DerivedObservableList;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.FilePath;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.collections.FXCollections;\nimport javafx.scene.layout.Priority;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.VBox;\n\nimport lombok.SneakyThrows;\n\nimport java.util.ArrayList;\n\npublic class BrowserOverviewComp extends SimpleRegionBuilder {\n\n    private final BrowserFileSystemTabModel model;\n\n    public BrowserOverviewComp(BrowserFileSystemTabModel model) {\n        this.model = model;\n    }\n\n    @Override\n    @SneakyThrows\n    protected Region createSimple() {\n        // The open file system might have already been closed\n\n        var list = new ArrayList<BaseRegionBuilder<?, ?>>();\n\n        var recent = DerivedObservableList.wrap(model.getSavedState().getRecentDirectories(), true)\n                .mapped(s -> FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory()))\n                .getList();\n        var recentOverview = new BrowserFileOverviewComp(model, recent, true);\n        var recentPane = new SimpleTitledPaneComp(AppI18n.observable(\"recent\"), recentOverview, false);\n        recentPane.hide(Bindings.isEmpty(recent));\n        list.add(recentPane);\n\n        var commonPlatform = FXCollections.<FileEntry>synchronizedObservableList(FXCollections.observableArrayList());\n        ThreadHelper.runFailableAsync(() -> {\n            try {\n                var all = new ArrayList<FileEntry>();\n                for (FilePath cd : model.getFileSystem().listCommonDirectories()) {\n                    var entry = FileEntry.ofDirectory(model.getFileSystem(), cd);\n                    var fs = model.getFileSystem();\n                    if (fs.directoryExists(entry.getPath())) {\n                        all.add(entry);\n                    }\n                }\n                Platform.runLater(() -> {\n                    commonPlatform.setAll(all);\n                });\n            } catch (Exception e) {\n                // The file system can die\n                ErrorEventFactory.fromThrowable(e).expected().omit().handle();\n            }\n        });\n        var commonOverview = new BrowserFileOverviewComp(model, commonPlatform, false);\n        var commonPane = new SimpleTitledPaneComp(AppI18n.observable(\"common\"), commonOverview, false)\n                .apply(struc -> VBox.setVgrow(struc, Priority.NEVER));\n        commonPane.hide(Bindings.isEmpty(commonPlatform));\n        list.add(commonPane);\n\n        var rootPlatform = FXCollections.<FileEntry>synchronizedObservableList(FXCollections.observableArrayList());\n        ThreadHelper.runFailableAsync(() -> {\n            var roots = model.getFileSystem().listRoots().stream()\n                    .map(s -> FileEntry.ofDirectory(model.getFileSystem(), s))\n                    .toList();\n            Platform.runLater(() -> {\n                rootPlatform.setAll(roots);\n            });\n        });\n        var rootsOverview = new BrowserFileOverviewComp(model, rootPlatform, false);\n        var rootsPane = new SimpleTitledPaneComp(AppI18n.observable(\"roots\"), rootsOverview, false);\n        rootsPane.hide(Bindings.isEmpty(rootPlatform));\n        list.add(rootsPane);\n\n        var vbox = new VerticalComp(list).style(\"overview\");\n        var r = vbox.build();\n        return r;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessButtonComp.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.comp.RegionDescriptor;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.IconButtonComp;\nimport io.xpipe.app.platform.InputHelper;\n\nimport javafx.scene.layout.Region;\n\nimport java.util.function.Supplier;\n\npublic class BrowserQuickAccessButtonComp extends SimpleRegionBuilder {\n\n    private final Supplier<BrowserEntry> base;\n    private final BrowserFileSystemTabModel model;\n\n    public BrowserQuickAccessButtonComp(Supplier<BrowserEntry> base, BrowserFileSystemTabModel model) {\n        this.base = base;\n        this.model = model;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var cm = new BrowserQuickAccessContextMenu(base, model);\n        var button = new IconButtonComp(\"mdi2c-chevron-double-right\");\n        button.describe(d -> d.nameKey(\"quickAccess\").focusTraversal(RegionDescriptor.FocusTraversal.DISABLED));\n        button.apply(struc -> {\n            struc.setOnAction(event -> {\n                if (!cm.isShowing()) {\n                    cm.showMenu(struc);\n                } else {\n                    cm.hide();\n                }\n                event.consume();\n            });\n            InputHelper.onRight(struc, false, keyEvent -> {\n                cm.showMenu(struc);\n                keyEvent.consume();\n            });\n        });\n        button.style(\"quick-access-button\");\n        return button.build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.browser.icon.BrowserIcons;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.ext.FileEntry;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.platform.BooleanAnimationTimer;\nimport io.xpipe.app.platform.InputHelper;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.util.BooleanScope;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.application.Platform;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.collections.ListChangeListener;\nimport javafx.geometry.Side;\nimport javafx.scene.Node;\nimport javafx.scene.control.ContextMenu;\nimport javafx.scene.control.Menu;\nimport javafx.scene.control.MenuItem;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyEvent;\nimport javafx.scene.input.MouseEvent;\nimport javafx.scene.layout.Region;\n\nimport lombok.Getter;\n\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\n\npublic class BrowserQuickAccessContextMenu extends ContextMenu {\n\n    private final Supplier<BrowserEntry> base;\n    private final BrowserFileSystemTabModel model;\n    private ContextMenu shownBrowserActionsMenu;\n    private boolean expandBrowserActionMenuKey;\n    private boolean keyBasedNavigation;\n    private boolean closeBrowserActionMenuKey;\n\n    public BrowserQuickAccessContextMenu(Supplier<BrowserEntry> base, BrowserFileSystemTabModel model) {\n        this.base = base;\n        this.model = model;\n\n        addEventFilter(Menu.ON_SHOWING, e -> {\n            Node content = getSkin().getNode();\n            if (content instanceof Region r) {\n                r.setMaxWidth(500);\n            }\n        });\n        addEventFilter(Menu.ON_SHOWN, e -> {\n            Platform.runLater(() -> {\n                var items = getItems();\n                if (items.size() > 0) {\n                    items.getFirst().getStyleableNode().requestFocus();\n                }\n            });\n        });\n        InputHelper.onLeft(this, false, e -> {\n            hide();\n            e.consume();\n        });\n        setAutoHide(!AppPrefs.get().limitedTouchscreenMode().get());\n        getStyleClass().add(\"condensed\");\n\n        var modalListener = new ListChangeListener<ModalOverlay>() {\n            @Override\n            public void onChanged(Change<? extends ModalOverlay> c) {\n                if (!c.getList().isEmpty()) {\n                    hide();\n                }\n            }\n        };\n        showingProperty().addListener((observable, oldValue, newValue) -> {\n            if (newValue) {\n                AppDialog.getModalOverlays().addListener(modalListener);\n            } else {\n                AppDialog.getModalOverlays().removeListener(modalListener);\n            }\n        });\n    }\n\n    public void showMenu(Node anchor) {\n        getItems().clear();\n        ThreadHelper.runFailableAsync(() -> {\n            var entry = base.get();\n            if (entry == null) {\n                return;\n            }\n\n            if (entry.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY) {\n                return;\n            }\n\n            var r = new Menu();\n            updateMenuItems(r, entry, true);\n            Platform.runLater(() -> {\n                getItems().addAll(r.getItems());\n\n                // Prevent NPE in show()\n                if (getScene() == null || anchor == null || anchor.getScene() == null) {\n                    return;\n                }\n                show(anchor, Side.RIGHT, 0, 0);\n            });\n        });\n    }\n\n    private MenuItem createItem(BrowserEntry browserEntry) {\n        return new QuickAccessMenu(browserEntry).getMenu();\n    }\n\n    private List<MenuItem> updateMenuItems(Menu m, BrowserEntry entry, boolean updateInstantly) throws Exception {\n        List<FileEntry> list = new ArrayList<>();\n        BooleanScope.executeExclusive(model.getBusy(), () -> {\n            var dir = entry.getRawFileEntry().resolved().getPath();\n            try (var stream = model.getFileSystem().listFiles(model.getFileSystem(), dir)) {\n                var l = stream.toList();\n                // Wait until all files are listed, i.e. do not skip the stream elements\n                list.addAll(l.subList(0, Math.min(l.size(), 150)));\n            }\n        });\n\n        var newItems = new ArrayList<MenuItem>();\n        if (list.isEmpty()) {\n            var empty = new Menu(\"<empty>\");\n            empty.getStyleClass().add(\"leaf\");\n            newItems.add(empty);\n        } else {\n            var browserEntries = list.stream()\n                    .map(fileEntry -> new BrowserEntry(fileEntry, model.getFileList()))\n                    .toList();\n            var menus = browserEntries.stream()\n                    .sorted(model.getFileList().order())\n                    .collect(Collectors.toMap(e -> e, e -> createItem(e), (v1, v2) -> v2, LinkedHashMap::new));\n            var dirs = browserEntries.stream()\n                    .filter(e -> e.getRawFileEntry().getKind() == FileKind.DIRECTORY)\n                    .toList();\n            // Expand subdir if only one\n            // Note that if we have a link to the directory itself, we shouldn't do it, otherwise we are stuck in a loop\n            if (dirs.size() == 1\n                    && !dirs.getFirst()\n                            .getRawFileEntry()\n                            .getPath()\n                            .equals(entry.getRawFileEntry().getPath())) {\n                updateMenuItems((Menu) menus.get(dirs.getFirst()), dirs.getFirst(), true);\n            }\n            newItems.addAll(menus.values());\n        }\n        if (updateInstantly) {\n            m.getItems().setAll(newItems);\n        }\n        return newItems;\n    }\n\n    @Getter\n    class QuickAccessMenu {\n        private final BrowserEntry browserEntry;\n        private final Menu menu;\n        private final MenuItem empty;\n        private ContextMenu browserActionMenu;\n\n        public QuickAccessMenu(BrowserEntry browserEntry) {\n            empty = new Menu(\"...\");\n            empty.getStyleClass().add(\"leaf\");\n\n            this.browserEntry = browserEntry;\n            this.menu = new Menu(\n                    // Use original name, not the link target\n                    browserEntry.getRawFileEntry().getName(),\n                    BrowserIcons.createIcon(browserEntry.getIcon()).build());\n            createMenu();\n            addInputListeners();\n        }\n\n        private void createMenu() {\n            var fileEntry = browserEntry.getRawFileEntry();\n            if (fileEntry.resolved().getKind() != FileKind.DIRECTORY) {\n                createFileMenu();\n            } else {\n                createDirectoryMenu();\n            }\n        }\n\n        private void createFileMenu() {\n            menu.setMnemonicParsing(false);\n            menu.addEventFilter(Menu.ON_SHOWN, event -> {\n                menu.hide();\n                if (keyBasedNavigation && expandBrowserActionMenuKey) {\n                    if (!hideBrowserActionsMenu()) {\n                        showBrowserActionsMenu();\n                    }\n                }\n            });\n            menu.setOnAction(event -> {\n                if (event.getTarget() != menu) {\n                    return;\n                }\n\n                if (!hideBrowserActionsMenu()) {\n                    showBrowserActionsMenu();\n                }\n            });\n            menu.getStyleClass().add(\"leaf\");\n            menu.getItems().add(empty);\n        }\n\n        private void createDirectoryMenu() {\n            menu.setMnemonicParsing(false);\n            menu.getItems().add(empty);\n            addHoverHandling();\n\n            menu.setOnAction(event -> {\n                if (event.getTarget() != menu) {\n                    return;\n                }\n\n                if (hideBrowserActionsMenu()) {\n                    menu.show();\n                    event.consume();\n                    return;\n                }\n\n                showBrowserActionsMenu();\n                event.consume();\n            });\n\n            menu.addEventFilter(Menu.ON_SHOWING, event -> {\n                hideBrowserActionsMenu();\n            });\n\n            menu.addEventFilter(Menu.ON_SHOWN, event -> {\n                if (keyBasedNavigation && expandBrowserActionMenuKey) {\n                    if (hideBrowserActionsMenu()) {\n                        menu.show();\n                    } else {\n                        showBrowserActionsMenu();\n                    }\n                } else if (keyBasedNavigation) {\n                    expandDirectoryMenu(empty);\n                }\n            });\n\n            menu.addEventFilter(Menu.ON_HIDING, event -> {\n                if (closeBrowserActionMenuKey) {\n                    menu.show();\n                }\n            });\n        }\n\n        private void addHoverHandling() {\n            var hover = new SimpleBooleanProperty();\n            menu.addEventFilter(Menu.ON_SHOWING, event -> {\n                if (!keyBasedNavigation) {\n                    hover.set(true);\n                }\n            });\n            menu.addEventFilter(Menu.ON_HIDING, event -> {\n                if (!keyBasedNavigation) {\n                    hover.set(false);\n                }\n            });\n            new BooleanAnimationTimer(hover, 100, () -> {\n                        expandDirectoryMenu(empty);\n                    })\n                    .start();\n        }\n\n        private void addInputListeners() {\n            menu.parentPopupProperty().subscribe(contextMenu -> {\n                if (contextMenu != null) {\n                    contextMenu.addEventFilter(KeyEvent.KEY_PRESSED, event -> {\n                        keyBasedNavigation = true;\n                        if (event.getCode().equals(KeyCode.SPACE)\n                                || event.getCode().equals(KeyCode.ENTER)) {\n                            expandBrowserActionMenuKey = true;\n                        } else {\n                            expandBrowserActionMenuKey = false;\n                        }\n                        if (event.getCode().equals(KeyCode.LEFT)\n                                && browserActionMenu != null\n                                && browserActionMenu.isShowing()) {\n                            closeBrowserActionMenuKey = true;\n                        } else {\n                            closeBrowserActionMenuKey = false;\n                        }\n                    });\n                    contextMenu.addEventFilter(MouseEvent.ANY, event -> {\n                        keyBasedNavigation = false;\n                    });\n                }\n            });\n        }\n\n        private void expandDirectoryMenu(MenuItem empty) {\n            if (menu.isShowing() && !menu.getItems().getFirst().equals(empty)) {\n                return;\n            }\n\n            ThreadHelper.runFailableAsync(() -> {\n                var newItems = updateMenuItems(menu, browserEntry, false);\n                Platform.runLater(() -> {\n                    var reshow = (browserActionMenu == null || !browserActionMenu.isShowing()) && menu.isShowing();\n                    if (reshow) {\n                        menu.hide();\n                    }\n                    menu.getItems().setAll(newItems);\n                    if (reshow) {\n                        menu.show();\n                    }\n                });\n            });\n        }\n\n        private boolean hideBrowserActionsMenu() {\n            if (shownBrowserActionsMenu != null && shownBrowserActionsMenu.isShowing()) {\n                shownBrowserActionsMenu.hide();\n                shownBrowserActionsMenu = null;\n                return true;\n            }\n            return false;\n        }\n\n        private void showBrowserActionsMenu() {\n            if (browserActionMenu == null) {\n                this.browserActionMenu = new BrowserContextMenu(model, browserEntry, true);\n                this.browserActionMenu.setOnAction(e -> {\n                    hide();\n                });\n                InputHelper.onLeft(this.browserActionMenu, true, keyEvent -> {\n                    this.browserActionMenu.hide();\n                    keyEvent.consume();\n                });\n            }\n\n            menu.hide();\n            browserActionMenu.show(menu.getStyleableNode(), Side.RIGHT, 0, 0);\n            shownBrowserActionsMenu = browserActionMenu;\n            Platform.runLater(() -> {\n                var items = browserActionMenu.getItems();\n                if (items.size() > 0) {\n                    items.getFirst().getStyleableNode().requestFocus();\n                }\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.augment.ContextMenuAugment;\nimport io.xpipe.app.comp.base.HorizontalComp;\nimport io.xpipe.app.comp.base.IconButtonComp;\nimport io.xpipe.app.comp.base.LabelComp;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.util.HumanReadableFormat;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.geometry.Pos;\nimport javafx.scene.input.MouseButton;\nimport javafx.scene.layout.Region;\n\nimport lombok.EqualsAndHashCode;\nimport lombok.Value;\n\nimport java.util.List;\n\n@Value\n@EqualsAndHashCode(callSuper = true)\npublic class BrowserStatusBarComp extends SimpleRegionBuilder {\n\n    BrowserFileSystemTabModel model;\n\n    @Override\n    protected Region createSimple() {\n        var bar = new HorizontalComp(List.of(\n                createProgressNameStatus(),\n                createProgressStatus(),\n                createProgressEstimateStatus(),\n                RegionBuilder.hspacer(),\n                createClipboardStatus(),\n                createSelectionStatus(),\n                createKillButton()));\n        bar.spacing(15);\n        bar.style(\"status-bar\");\n\n        bar.apply(struc -> {\n            struc.widthProperty().subscribe(value -> {\n                var veryConstrained = value.doubleValue() < 600;\n                var somewhatConstrained = value.doubleValue() < 710;\n                struc.getChildren().get(2).setVisible(!somewhatConstrained);\n                struc.getChildren().get(2).setManaged(!somewhatConstrained);\n                struc.getChildren().get(4).setVisible(!veryConstrained);\n                struc.getChildren().get(4).setManaged(!veryConstrained);\n                struc.getChildren().get(5).setVisible(!veryConstrained);\n                struc.getChildren().get(5).setManaged(!veryConstrained);\n            });\n        });\n\n        var r = bar.build();\n        r.setOnDragDetected(event -> {\n            event.consume();\n            r.startFullDrag();\n        });\n        AppFontSizes.xs(r);\n        simulateEmptyCell(r);\n        return r;\n    }\n\n    private BaseRegionBuilder<?, ?> createKillButton() {\n        var button = new IconButtonComp(\"mdi2s-stop\", () -> {\n            ThreadHelper.runAsync(() -> {\n                model.killTransfer();\n            });\n        });\n        button.describe(d -> d.nameKey(\"killTransfer\"));\n        var cancel = PlatformThread.sync(model.getTransferCancelled());\n        var hide = Bindings.createBooleanBinding(\n                () -> {\n                    if (model.getProgress().getValue() == null) {\n                        return true;\n                    }\n\n                    if (cancel.getValue()) {\n                        return true;\n                    }\n\n                    return false;\n                },\n                cancel,\n                model.getProgress());\n        button.hide(hide);\n        return button;\n    }\n\n    private BaseRegionBuilder<?, ?> createProgressEstimateStatus() {\n        var text = Bindings.createStringBinding(\n                () -> {\n                    var p = model.getProgress().getValue();\n                    var expected = model.getProgressRemaining().getValue();\n                    if (p == null || expected == null) {\n                        return null;\n                    }\n\n                    // Handle unknown transfers\n                    if (p.getTotal() == 0) {\n                        return HumanReadableFormat.byteCount(p.getTransferred());\n                    }\n\n                    var elapsed = (p.getTotal() - p.getTransferred() / (double) p.getTotal()) * expected.toMillis();\n                    var show = elapsed > 3000;\n                    if (!show) {\n                        return \"...\";\n                    }\n\n                    var time = HumanReadableFormat.duration(expected) + \" @ \";\n                    var progress = HumanReadableFormat.transferSpeed(\n                            model.getProgressTransferSpeed().getValue());\n                    return time + progress;\n                },\n                model.getProgressRemaining(),\n                model.getProgressTransferSpeed(),\n                model.getProgress());\n\n        var progressComp = new LabelComp(text)\n                .style(\"progress\")\n                .apply(struc -> struc.setAlignment(Pos.CENTER_LEFT))\n                .minWidth(Region.USE_PREF_SIZE);\n        return progressComp;\n    }\n\n    private BaseRegionBuilder<?, ?> createProgressStatus() {\n        var text = BindingsHelper.map(model.getProgress(), p -> {\n            if (p == null) {\n                return null;\n            } else {\n                var transferred = HumanReadableFormat.progressByteCount(p.getTransferred());\n\n                // Handle unknown transfers\n                if (p.getTotal() == 0) {\n                    if (p.getTransferred() == 0) {\n                        return \"...\";\n                    } else {\n                        return transferred;\n                    }\n                }\n\n                var all = HumanReadableFormat.byteCount(p.getTotal());\n                return transferred + \" / \" + all;\n            }\n        });\n        var progressComp = new LabelComp(text)\n                .style(\"progress\")\n                .apply(struc -> struc.setAlignment(Pos.CENTER_LEFT))\n                .minWidth(Region.USE_PREF_SIZE);\n        return progressComp;\n    }\n\n    private BaseRegionBuilder<?, ?> createProgressNameStatus() {\n        var text = BindingsHelper.map(model.getProgress(), p -> {\n            if (p == null) {\n                return null;\n            } else {\n                return p.getName();\n            }\n        });\n        var progressComp = new LabelComp(text)\n                .style(\"progress\")\n                .apply(struc -> struc.setAlignment(Pos.CENTER_LEFT))\n                .hgrow();\n        return progressComp;\n    }\n\n    private BaseRegionBuilder<?, ?> createClipboardStatus() {\n        var cc = BrowserClipboard.currentCopyClipboard;\n        var ccCount = Bindings.createStringBinding(\n                () -> {\n                    if (cc.getValue() != null && cc.getValue().getEntries().size() > 0) {\n                        return cc.getValue().getEntries().size() + \" file\"\n                                + (cc.getValue().getEntries().size() > 1 ? \"s\" : \"\") + \" in clipboard\";\n                    } else {\n                        return null;\n                    }\n                },\n                cc);\n        return new LabelComp(ccCount).minWidth(Region.USE_PREF_SIZE);\n    }\n\n    private BaseRegionBuilder<?, ?> createSelectionStatus() {\n        var selectedCount = Bindings.createIntegerBinding(\n                () -> {\n                    return model.getFileList().getSelection().size();\n                },\n                model.getFileList().getSelection());\n\n        var allCount = Bindings.createIntegerBinding(\n                () -> {\n                    return model.getFileList().getAll().getValue().size();\n                },\n                model.getFileList().getAll());\n        var selectedComp = new LabelComp(Bindings.createStringBinding(\n                () -> {\n                    if (selectedCount.getValue() == 0) {\n                        return null;\n                    } else {\n                        return selectedCount.getValue() + \" / \" + allCount.getValue() + \" selected\";\n                    }\n                },\n                selectedCount,\n                allCount));\n        return selectedComp.minWidth(Region.USE_PREF_SIZE);\n    }\n\n    private void simulateEmptyCell(Region r) {\n        var emptyEntry = new BrowserFileListCompEntry(null, r, null, model.getFileList());\n        r.setOnMouseClicked(e -> {\n            emptyEntry.onMouseClick(e);\n        });\n        r.setOnMouseDragEntered(event -> {\n            emptyEntry.onMouseDragEntered(event);\n        });\n        r.setOnDragOver(event -> {\n            emptyEntry.onDragOver(event);\n        });\n        r.setOnDragEntered(event -> {\n            emptyEntry.onDragEntered(event);\n        });\n        r.setOnDragDetected(event -> {\n            emptyEntry.startDrag(event);\n        });\n        r.setOnDragExited(event -> {\n            emptyEntry.onDragExited(event);\n        });\n        r.setOnDragDropped(event -> {\n            emptyEntry.onDragDrop(event);\n        });\n        r.setOnDragDone(event -> {\n            emptyEntry.onDragDone(event);\n        });\n\n        // Use status bar as an extension of file list\n        new ContextMenuAugment<>(\n                        mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY,\n                        null,\n                        () -> new BrowserContextMenu(model, null, false))\n                .accept(r);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserTerminalDockTabModel.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.browser.BrowserAbstractSessionModel;\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.browser.BrowserSessionTab;\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.storage.DataStoreColor;\nimport io.xpipe.app.terminal.TerminalDockBrowserComp;\nimport io.xpipe.app.terminal.TerminalDockView;\nimport io.xpipe.app.terminal.TerminalView;\nimport io.xpipe.app.terminal.WindowsTerminalType;\nimport io.xpipe.app.util.GlobalTimer;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.BooleanProperty;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.value.ObservableBooleanValue;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.ListChangeListener;\nimport javafx.collections.ObservableList;\n\nimport java.time.Duration;\nimport java.util.UUID;\nimport java.util.function.UnaryOperator;\n\npublic final class BrowserTerminalDockTabModel extends BrowserSessionTab {\n\n    private final BrowserSessionTab origin;\n    private final ObservableList<UUID> terminalRequests;\n    private final TerminalDockView dockModel = new TerminalDockView(UnaryOperator.identity());\n    private final BooleanProperty opened = new SimpleBooleanProperty();\n    private TerminalView.Listener listener;\n    private ObservableBooleanValue viewActive;\n    private boolean closed;\n\n    public BrowserTerminalDockTabModel(\n            BrowserAbstractSessionModel<?> browserModel,\n            BrowserSessionTab origin,\n            ObservableList<UUID> terminalRequests) {\n        super(browserModel);\n        this.origin = origin;\n        this.terminalRequests = terminalRequests;\n    }\n\n    @Override\n    public BaseRegionBuilder<?, ?> comp() {\n        return new TerminalDockBrowserComp(dockModel, opened);\n    }\n\n    @Override\n    public boolean canImmediatelyClose() {\n        return true;\n    }\n\n    @Override\n    public void init() throws Exception {\n        listener = new TerminalView.Listener() {\n            @Override\n            public void onSessionOpened(TerminalView.ShellSession session) {\n                if (!terminalRequests.contains(session.getRequest())) {\n                    return;\n                }\n\n                opened.set(true);\n\n                var closed = dockModel.closeOtherTerminals(session.getRequest());\n                // Closing and opening windows at the same time might be problematic for some bad implementations\n                if (closed) {\n                    ThreadHelper.sleep(250);\n                }\n\n                var controllable = session.getTerminal().controllable();\n                if (controllable.isEmpty()) {\n                    return;\n                }\n                dockModel.trackTerminal(controllable.get(), true);\n            }\n\n            @Override\n            public void onSessionClosed(TerminalView.ShellSession session) {\n                if (!terminalRequests.contains(session.getRequest())) {\n                    return;\n                }\n\n                // Ugly fix for Windows Terminal instances not closing properly if multiple windows exist\n                if (AppPrefs.get().terminalType().getValue() instanceof WindowsTerminalType) {\n                    var sessions = TerminalView.get().getSessions();\n                    var others = sessions.stream()\n                            .filter(shellSession -> shellSession.getTerminal().equals(session.getTerminal()))\n                            .count();\n                    if (others == 0) {\n                        session.getTerminal().controllable().ifPresent(controllableTerminalSession -> {\n                            controllableTerminalSession.close();\n                        });\n                    }\n                }\n            }\n\n            @Override\n            public void onTerminalClosed(TerminalView.TerminalSession instance) {\n                refreshShowingState();\n            }\n        };\n        TerminalView.get().addListener(listener);\n\n        // If the terminal launch fails\n        ThreadHelper.runAsync(() -> {\n            ThreadHelper.sleep(5000);\n            if (!opened.get()) {\n                refreshShowingState();\n            }\n        });\n\n        viewActive = Bindings.createBooleanBinding(\n                () -> {\n                    return this.browserModel.getSelectedEntry().getValue() == origin\n                            && AppLayoutModel.get()\n                                            .getEntries()\n                                            .indexOf(AppLayoutModel.get()\n                                                    .getSelected()\n                                                    .getValue())\n                                    == 1;\n                },\n                this.browserModel.getSelectedEntry(),\n                AppLayoutModel.get().getSelected());\n        viewActive.subscribe(aBoolean -> {\n            Platform.runLater(() -> {\n                if (aBoolean) {\n                    dockModel.activateView();\n                } else {\n                    dockModel.deactivateView();\n                }\n            });\n        });\n        AppDialog.getModalOverlays().addListener((ListChangeListener<? super ModalOverlay>) c -> {\n            if (c.getList().size() > 0) {\n                dockModel.deactivateView();\n            } else {\n                if (viewActive.get()) {\n                    dockModel.activateView();\n                } else {\n                    dockModel.deactivateView();\n                }\n            }\n        });\n\n        GlobalTimer.scheduleUntil(Duration.ofMillis(300), false, () -> {\n            if (viewActive.get()) {\n                dockModel.clearDeadTerminals();\n                dockModel.updateCustomBounds();\n            }\n            return closed;\n        });\n    }\n\n    @Override\n    public void close() {\n        if (listener != null) {\n            TerminalView.get().removeListener(listener);\n        }\n        dockModel.onClose();\n        closed = true;\n    }\n\n    @Override\n    public ObservableValue<String> getName() {\n        return AppI18n.observable(\"terminal\");\n    }\n\n    @Override\n    public String getIcon() {\n        return null;\n    }\n\n    @Override\n    public DataStoreColor getColor() {\n        return null;\n    }\n\n    private void refreshShowingState() {\n        var sessions = TerminalView.get().getSessions();\n        var remaining = sessions.stream()\n                .filter(s -> terminalRequests.contains(s.getRequest())\n                        && s.getTerminal().isRunning())\n                .toList();\n        if (remaining.isEmpty()) {\n            ((BrowserFullSessionModel) browserModel).unsplitTab(BrowserTerminalDockTabModel.this);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserTransferComp.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.DerivedObservableList;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.collections.FXCollections;\nimport javafx.css.PseudoClass;\nimport javafx.geometry.Insets;\nimport javafx.scene.control.ContentDisplay;\nimport javafx.scene.image.Image;\nimport javafx.scene.input.*;\nimport javafx.scene.layout.Region;\nimport javafx.scene.text.TextAlignment;\n\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.util.List;\nimport java.util.Optional;\n\npublic class BrowserTransferComp extends SimpleRegionBuilder {\n\n    private final BrowserTransferModel model;\n\n    public BrowserTransferComp(BrowserTransferModel model) {\n        this.model = model;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var background = new LabelComp(AppI18n.observable(\"transferDescription\"))\n                .apply(struc -> struc.setGraphic(new FontIcon(\"mdi2d-download-outline\")))\n                .apply(struc -> struc.setWrapText(true))\n                .apply(struc -> struc.setTextAlignment(TextAlignment.CENTER))\n                .apply(struc -> struc.setContentDisplay(ContentDisplay.TOP))\n                .visible(model.getEmpty());\n        var backgroundStack = new StackComp(List.of(background))\n                .style(\"color-box\")\n                .style(\"gray\")\n                .style(\"download-background\");\n\n        var binding = DerivedObservableList.wrap(model.getItems(), true)\n                .mapped(item -> item.getBrowserEntry())\n                .getList();\n        var list = new BrowserFileSelectionListComp(binding, entry -> {\n            var sourceItem = model.getCurrentItems().stream()\n                    .filter(item -> item.getBrowserEntry() == entry)\n                    .findAny();\n            if (sourceItem.isEmpty()) {\n                return new SimpleStringProperty(\"?\");\n            }\n            synchronized (sourceItem.get().getProgress()) {\n                return Bindings.createStringBinding(\n                        () -> {\n                            var p = sourceItem.get().getProgress().getValue();\n                            if (p == null || p.getTotal() == 0) {\n                                return entry.getFileName();\n                            }\n\n                            var hideProgress =\n                                    sourceItem.get().getDownloadFinished().get();\n                            var share = p.getTransferred() * 100 / p.getTotal();\n                            var progressSuffix = hideProgress ? \"\" : \" \" + share + \"%\";\n                            return entry.getFileName() + progressSuffix;\n                        },\n                        sourceItem.get().getProgress());\n            }\n        }).vgrow();\n        var dragNotice = new LabelComp(AppI18n.observable(\"dragLocalFiles\"))\n                .apply(struc -> struc.setGraphic(new FontIcon(\"mdi2h-hand-back-left-outline\")))\n                .apply(struc -> struc.setWrapText(true))\n                .hide(Bindings.or(model.getEmpty(), model.getTransferring()));\n\n        var clearButton = new IconButtonComp(\"mdi2c-close\", () -> {\n                    ThreadHelper.runAsync(() -> {\n                        model.clear(true);\n                    });\n                })\n                .hide(Bindings.or(model.getEmpty(), model.getTransferring()))\n                .describe(d -> d.nameKey(\"clearTransferDescription\"));\n\n        var downloadButton = new IconButtonComp(\"mdi2f-folder-move-outline\", null)\n                .apply(struc -> {\n                    struc.setOnMouseClicked(e -> {\n                        if (e.getButton() == MouseButton.PRIMARY) {\n                            var open = !e.isShiftDown();\n                            ThreadHelper.runFailableAsync(() -> {\n                                model.transferToDownloads(open);\n                            });\n                            e.consume();\n                        }\n                    });\n                    struc.setOnAction(e -> {\n                        ThreadHelper.runFailableAsync(() -> {\n                            model.transferToDownloads(true);\n                        });\n                        e.consume();\n                    });\n                })\n                .hide(Bindings.or(model.getEmpty(), model.getTransferring()))\n                .describe(d -> d.nameKey(\"downloadStageDescription\"));\n\n        var bottom = new HorizontalComp(List.of(\n                RegionBuilder.hspacer(),\n                dragNotice,\n                RegionBuilder.hspacer(),\n                downloadButton,\n                RegionBuilder.hspacer(4),\n                clearButton));\n        var listBox = new VerticalComp(List.of(list, bottom))\n                .spacing(5)\n                .padding(new Insets(10, 10, 5, 10))\n                .apply(struc -> struc.setMinHeight(200))\n                .apply(struc -> struc.setMaxHeight(200));\n        var stack = new StackComp(List.of(backgroundStack, listBox)).apply(struc -> {\n            struc.addEventFilter(DragEvent.DRAG_ENTERED, event -> {\n                struc.pseudoClassStateChanged(PseudoClass.getPseudoClass(\"drag-over\"), true);\n            });\n            struc.addEventFilter(\n                    DragEvent.DRAG_EXITED,\n                    event -> struc.pseudoClassStateChanged(PseudoClass.getPseudoClass(\"drag-over\"), false));\n            struc.setOnDragOver(event -> {\n                // Accept drops from inside the app window\n                if (event.getGestureSource() != null && event.getGestureSource() != struc) {\n                    event.acceptTransferModes(TransferMode.ANY);\n                    event.consume();\n                }\n            });\n            struc.setOnDragDropped(event -> {\n                // Accept drops from inside the app window\n                if (event.getGestureSource() != null) {\n                    var drag = BrowserClipboard.retrieveDrag(event.getDragboard());\n                    if (drag == null) {\n                        return;\n                    }\n\n                    if (!(model.getBrowserSessionModel().getSelectedEntry().getValue()\n                            instanceof BrowserFileSystemTabModel fileSystemModel)) {\n                        return;\n                    }\n\n                    var files = drag.getEntries();\n                    model.drop(fileSystemModel, files);\n                    event.setDropCompleted(true);\n                    event.consume();\n                }\n            });\n            struc.setOnDragDetected(event -> {\n                var items = model.getCurrentItems();\n                var selected =\n                        items.stream().map(item -> item.getBrowserEntry()).toList();\n                var files = items.stream()\n                        .filter(item -> item.getDownloadFinished().get())\n                        .map(item -> {\n                            try {\n                                var file = item.getLocalFile();\n                                if (!Files.exists(file)) {\n                                    return Optional.<File>empty();\n                                }\n\n                                return Optional.of(file.toRealPath().toFile());\n                            } catch (IOException e) {\n                                throw new RuntimeException(e);\n                            }\n                        })\n                        .flatMap(Optional::stream)\n                        .toList();\n                if (files.isEmpty()) {\n                    return;\n                }\n\n                var cc = new ClipboardContent();\n                cc.putFiles(files);\n                Dragboard db = struc.startDragAndDrop(TransferMode.COPY);\n                db.setContent(cc);\n\n                Image image = BrowserFileSelectionListComp.snapshot(FXCollections.observableList(selected));\n                db.setDragView(image, -20, 15);\n\n                event.setDragDetect(true);\n                event.consume();\n            });\n            struc.setOnDragDone(event -> {\n                if (!event.isAccepted()) {\n                    return;\n                }\n\n                // The files might not have been transferred yet\n                // We can't listen to this, so just don't delete them\n                model.clear(false);\n                event.consume();\n            });\n        });\n\n        stack.apply(struc -> {\n            model.getBrowserSessionModel().getDraggingFiles().addListener((observable, oldValue, newValue) -> {\n                struc.pseudoClassStateChanged(PseudoClass.getPseudoClass(\"highlighted\"), newValue);\n            });\n        });\n\n        var r = stack.style(\"transfer\").build();\n        return r;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserTransferModel.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.browser.action.impl.TransferFilesActionProvider;\nimport io.xpipe.app.core.AppLocalTemp;\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.OsFileSystem;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.util.BooleanScope;\nimport io.xpipe.app.util.DesktopHelper;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.BooleanProperty;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.value.ObservableBooleanValue;\nimport javafx.collections.FXCollections;\nimport javafx.collections.ObservableList;\n\nimport lombok.Value;\nimport org.apache.commons.io.FileUtils;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.InvalidPathException;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\n\n@Value\npublic class BrowserTransferModel {\n\n    private static final Path TEMP = AppLocalTemp.getLocalTempDataDirectory(\"download\");\n\n    BrowserFullSessionModel browserSessionModel;\n    ObservableList<Item> items = FXCollections.observableArrayList();\n    ObservableBooleanValue empty = Bindings.createBooleanBinding(() -> items.isEmpty(), items);\n    BooleanProperty transferring = new SimpleBooleanProperty();\n\n    public BrowserTransferModel(BrowserFullSessionModel browserSessionModel) {\n        this.browserSessionModel = browserSessionModel;\n        var thread = ThreadHelper.createPlatformThread(\"file downloader\", true, () -> {\n            while (true) {\n                Optional<Item> toDownload;\n                synchronized (items) {\n                    toDownload = items.stream()\n                            .filter(item -> !item.getDownloadFinished().get())\n                            .findFirst();\n                }\n                if (toDownload.isPresent()) {\n                    downloadSingle(toDownload.get());\n                } else {\n                    ThreadHelper.sleep(20);\n                }\n            }\n        });\n        thread.start();\n    }\n\n    public List<Item> getCurrentItems() {\n        synchronized (items) {\n            return new ArrayList<>(items);\n        }\n    }\n\n    private void cleanItem(Item item) {\n        if (!Files.isDirectory(TEMP)) {\n            return;\n        }\n\n        if (!Files.exists(item.getLocalFile())) {\n            return;\n        }\n\n        try {\n            FileUtils.forceDelete(item.getLocalFile().toFile());\n        } catch (IOException e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n        }\n    }\n\n    public void clear(boolean delete) {\n        List<Item> toClear;\n        synchronized (items) {\n            toClear = items.stream()\n                    .filter(item -> item.getDownloadFinished().get())\n                    .toList();\n            if (toClear.isEmpty()) {\n                return;\n            }\n            items.removeAll(toClear);\n        }\n        if (delete) {\n            for (Item item : toClear) {\n                cleanItem(item);\n            }\n        }\n    }\n\n    public void drop(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        synchronized (items) {\n            entries.forEach(entry -> {\n                var resolved = entry.getRawFileEntry().resolved();\n                var name = resolved.getName();\n                if (items.stream().anyMatch(item -> item.getName().equals(name))) {\n                    return;\n                }\n\n                var fixedFile = OsFileSystem.ofLocal().makeFileSystemCompatible(resolved.getPath());\n                Path file = TEMP.resolve(fixedFile.getFileName());\n                var item = new Item(model, name, entry, file);\n                items.add(item);\n            });\n        }\n    }\n\n    public void downloadSingle(Item item) {\n        try {\n            FileUtils.forceMkdir(TEMP.toFile());\n        } catch (IOException e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n            return;\n        }\n\n        if (item.getDownloadFinished().get()) {\n            return;\n        }\n\n        var itemModel = item.getOpenFileSystemModel();\n        if (itemModel == null) {\n            return;\n        }\n\n        if (AppOperationMode.isInShutdown()) {\n            return;\n        }\n\n        try (var ignored = new BooleanScope(itemModel.getBusy()).exclusive().start()) {\n            transferring.setValue(true);\n            var op = new BrowserFileTransferOperation(\n                    BrowserLocalFileSystem.getLocalFileEntry(TEMP),\n                    List.of(item.getBrowserEntry().getRawFileEntry().resolved()),\n                    BrowserFileTransferMode.COPY,\n                    false,\n                    progress -> {\n                        // Don't update item progress to keep it as finished\n                        if (progress == null) {\n                            itemModel.updateProgress(null);\n                            return;\n                        }\n\n                        synchronized (item.getProgress()) {\n                            item.getProgress().setValue(progress);\n                        }\n                        itemModel.updateProgress(progress);\n                    },\n                    itemModel.getTransferCancelled());\n            var action = TransferFilesActionProvider.Action.builder()\n                    .operation(op)\n                    .target(DataStorage.get().local().ref())\n                    .download(true)\n                    .build();\n            if (!action.executeSync()) {\n                synchronized (items) {\n                    items.remove(item);\n                }\n            }\n        } catch (Throwable t) {\n            ErrorEventFactory.fromThrowable(t).handle();\n            synchronized (items) {\n                items.remove(item);\n            }\n        } finally {\n            transferring.setValue(false);\n        }\n    }\n\n    public void transferToDownloads(boolean open) throws Exception {\n        List<Item> toMove;\n        synchronized (items) {\n            toMove = items.stream()\n                    .filter(item -> item.getDownloadFinished().get())\n                    .toList();\n            if (toMove.isEmpty()) {\n                return;\n            }\n            items.removeAll(toMove);\n        }\n\n        var files = toMove.stream().map(item -> item.getLocalFile()).toList();\n        var downloads = getDownloadsTargetDirectory();\n        Files.createDirectories(downloads);\n        Path firstToOpen = null;\n        for (Path file : files) {\n            if (!Files.exists(file)) {\n                continue;\n            }\n\n            var target = downloads.resolve(file.getFileName());\n            // Prevent DirectoryNotEmptyException\n            if (Files.exists(target) && Files.isDirectory(target)) {\n                FileUtils.deleteDirectory(target.toFile());\n            }\n            if (Files.isDirectory(file)) {\n                FileUtils.moveDirectory(file.toFile(), target.toFile());\n            } else {\n                Files.move(file, target, StandardCopyOption.REPLACE_EXISTING);\n            }\n\n            if (firstToOpen == null) {\n                firstToOpen = target;\n            }\n        }\n        if (open && firstToOpen != null) {\n            DesktopHelper.browseFileInDirectory(firstToOpen);\n        }\n    }\n\n    private Path getDownloadsTargetDirectory() {\n        var def = AppSystemInfo.ofCurrent().getDownloads();\n        var custom = AppPrefs.get().downloadsDirectory().getValue();\n        if (custom == null) {\n            return def;\n        }\n\n        try {\n            var path = custom.asLocalPath();\n            if (Files.isDirectory(path)) {\n                return path;\n            }\n        } catch (InvalidPathException ignored) {\n        }\n        return def;\n    }\n\n    @Value\n    public static class Item {\n        BrowserFileSystemTabModel openFileSystemModel;\n        String name;\n        BrowserEntry browserEntry;\n        Path localFile;\n        Property<BrowserTransferProgress> progress;\n        ObservableBooleanValue downloadFinished;\n\n        public Item(\n                BrowserFileSystemTabModel openFileSystemModel, String name, BrowserEntry browserEntry, Path localFile) {\n            this.openFileSystemModel = openFileSystemModel;\n            this.name = name;\n            this.browserEntry = browserEntry;\n            this.localFile = localFile;\n            this.progress = new SimpleObjectProperty<>();\n            this.downloadFinished = Bindings.createBooleanBinding(\n                    () -> {\n                        return progress.getValue() != null\n                                && progress.getValue().done();\n                    },\n                    progress);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/file/BrowserTransferProgress.java",
    "content": "package io.xpipe.app.browser.file;\n\nimport lombok.Value;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.List;\n\n@Value\npublic class BrowserTransferProgress {\n\n    String name;\n    long transferred;\n    long total;\n    Instant timestamp = Instant.now();\n\n    public static BrowserTransferProgress finished(String name, long size) {\n        return new BrowserTransferProgress(name, size, size);\n    }\n\n    public static long estimateTransferSpeed(BrowserTransferProgress start, BrowserTransferProgress end) {\n        var diff = end.transferred - start.transferred;\n        var duration = Duration.between(start.timestamp, end.timestamp);\n        return (long) (diff / (duration.toMillis() / 1000.0));\n    }\n\n    public static long estimateTransferSpeed(List<BrowserTransferProgress> list, BrowserTransferProgress now) {\n        if (list.isEmpty()) {\n            return 0;\n        }\n\n        var rSize = list.size() > 1 ? list.size() - 1 : list.size();\n        var r = new double[rSize];\n        for (int i = 0; i < rSize; i++) {\n            r[i] = estimateTransferSpeed(list.get(i), now);\n        }\n\n        double sum = 0;\n        var lookBack = Math.min(r.length, 5);\n        for (int i = 0; i < lookBack; i++) {\n            sum += r[r.length - i - 1];\n        }\n        return (long) (sum / lookBack);\n    }\n\n    public boolean done() {\n        return transferred >= total;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/icon/BrowserIconDirectoryType.java",
    "content": "package io.xpipe.app.browser.icon;\n\nimport io.xpipe.app.core.AppResources;\nimport io.xpipe.app.ext.FileEntry;\nimport io.xpipe.app.ext.FileKind;\n\nimport java.io.BufferedReader;\nimport java.io.InputStreamReader;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\npublic abstract class BrowserIconDirectoryType {\n\n    private static final List<BrowserIconDirectoryType> ALL = new ArrayList<>();\n\n    public static synchronized void loadDefinitions() {\n        ALL.add(new BrowserIconDirectoryType() {\n\n            @Override\n            public boolean matches(FileEntry entry) {\n                return entry.getPath().toString().equals(\"/\")\n                        || entry.getPath().toString().matches(\"\\\\w:\\\\\\\\\");\n            }\n\n            @Override\n            public String getIcon() {\n                return \"browser/default_root_folder.svg\";\n            }\n        });\n\n        AppResources.with(AppResources.MAIN_MODULE, \"folder_list.txt\", path -> {\n            try (var reader =\n                    new BufferedReader(new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8))) {\n                String line;\n                while ((line = reader.readLine()) != null) {\n                    var split = line.split(\"\\\\|\");\n                    var filter = Arrays.stream(split[1].split(\",\"))\n                            .map(s -> {\n                                return s.strip();\n                            })\n                            .collect(Collectors.toSet());\n\n                    var closedIcon = \"browser/\" + split[2].strip();\n                    var lightClosedIcon = split.length > 4 ? \"browser/\" + split[4].strip() : closedIcon;\n\n                    ALL.add(new Simple(new BrowserIconVariant(lightClosedIcon, closedIcon), filter));\n                }\n            }\n        });\n    }\n\n    public static synchronized List<BrowserIconDirectoryType> getAll() {\n        return ALL;\n    }\n\n    public abstract boolean matches(FileEntry entry);\n\n    public abstract String getIcon();\n\n    public static class Simple extends BrowserIconDirectoryType {\n\n        private final BrowserIconVariant closed;\n        private final Set<String> names;\n\n        public Simple(BrowserIconVariant closed, Set<String> names) {\n            this.closed = closed;\n            this.names = names;\n        }\n\n        @Override\n        public boolean matches(FileEntry entry) {\n            if (entry.getKind() != FileKind.DIRECTORY) {\n                return false;\n            }\n\n            var name = entry.getPath().getFileName();\n            return names.contains(name);\n        }\n\n        @Override\n        public String getIcon() {\n            return this.closed.getIcon();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java",
    "content": "package io.xpipe.app.browser.icon;\n\nimport io.xpipe.app.core.AppResources;\nimport io.xpipe.app.ext.FileEntry;\nimport io.xpipe.app.ext.FileKind;\n\nimport lombok.Getter;\n\nimport java.io.BufferedReader;\nimport java.io.InputStreamReader;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic abstract class BrowserIconFileType {\n\n    private static final List<BrowserIconFileType> ALL = new ArrayList<>();\n\n    public static synchronized BrowserIconFileType byId(String id) {\n        return ALL.stream()\n                .filter(fileType -> fileType.getId().equals(id))\n                .findAny()\n                .orElseThrow();\n    }\n\n    public static synchronized void loadDefinitions() {\n        AppResources.with(AppResources.MAIN_MODULE, \"file_list.txt\", path -> {\n            try (var reader =\n                    new BufferedReader(new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8))) {\n                String line;\n                while ((line = reader.readLine()) != null) {\n                    var split = line.split(\"\\\\|\");\n                    var id = split[0].strip();\n                    var filter = Arrays.stream(split[1].split(\",\"))\n                            .map(s -> {\n                                var r = s.strip();\n                                if (r.startsWith(\".\")) {\n                                    return r;\n                                }\n\n                                if (r.contains(\".\")) {\n                                    return r;\n                                }\n\n                                return \".\" + r;\n                            })\n                            .collect(Collectors.toSet());\n                    var darkIcon = \"browser/\" + split[2].strip();\n                    var lightIcon = (split.length > 3 ? \"browser/\" + split[3].strip() : darkIcon);\n                    ALL.add(new BrowserIconFileType.Simple(id, lightIcon, darkIcon, filter));\n                }\n            }\n        });\n    }\n\n    public static synchronized List<BrowserIconFileType> getAll() {\n        return ALL;\n    }\n\n    public abstract String getId();\n\n    public abstract boolean matches(FileEntry entry);\n\n    public abstract String getIcon();\n\n    @Getter\n    public static class Simple extends BrowserIconFileType {\n\n        private final String id;\n        private final BrowserIconVariant icon;\n        private final Set<String> endings;\n\n        public Simple(String id, String lightIcon, String darkIcon, Set<String> endings) {\n            this.icon = new BrowserIconVariant(lightIcon, darkIcon);\n            this.id = id;\n            this.endings = endings;\n        }\n\n        @Override\n        public boolean matches(FileEntry entry) {\n            if (entry.getKind() == FileKind.DIRECTORY) {\n                return false;\n            }\n\n            var name = entry.getPath().getFileName();\n            var ext = entry.getPath().getExtension();\n            return (ext.isPresent() && endings.contains(\".\" + ext.get().toLowerCase(Locale.ROOT)))\n                    || endings.contains(name);\n        }\n\n        @Override\n        public String getIcon() {\n            return icon.getIcon();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/icon/BrowserIconManager.java",
    "content": "package io.xpipe.app.browser.icon;\n\nimport io.xpipe.app.core.AppDisplayScale;\nimport io.xpipe.app.core.AppImages;\nimport io.xpipe.app.core.AppResources;\n\nimport org.apache.commons.io.FilenameUtils;\n\npublic class BrowserIconManager {\n\n    private static boolean loaded;\n\n    public static synchronized void init() {\n        if (!loaded) {\n            BrowserIconFileType.loadDefinitions();\n            BrowserIconDirectoryType.loadDefinitions();\n            loaded = true;\n        }\n    }\n\n    public static void loadIfNecessary(String s) {\n        var res = AppDisplayScale.hasOnlyDefaultDisplayScale() ? \"24\" : \"40\";\n        var key = \"browser/\" + FilenameUtils.getBaseName(s) + \"-\" + res + \".png\";\n        if (AppImages.hasImage(key)) {\n            return;\n        }\n\n        AppResources.with(AppResources.MAIN_MODULE, key, file -> {\n            AppImages.loadImage(file, key);\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/icon/BrowserIconVariant.java",
    "content": "package io.xpipe.app.browser.icon;\n\nimport io.xpipe.app.prefs.AppPrefs;\n\npublic class BrowserIconVariant {\n\n    private final String lightIcon;\n    private final String darkIcon;\n\n    public BrowserIconVariant(String lightIcon, String darkIcon) {\n        this.lightIcon = lightIcon;\n        this.darkIcon = darkIcon;\n    }\n\n    protected final String getIcon() {\n        var t = AppPrefs.get() != null ? AppPrefs.get().theme().getValue() : null;\n        if (t == null) {\n            return lightIcon;\n        }\n\n        return t.isDark() ? darkIcon : lightIcon;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/icon/BrowserIcons.java",
    "content": "package io.xpipe.app.browser.icon;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.base.PrettyImageHelper;\n\npublic class BrowserIcons {\n\n    public static BaseRegionBuilder<?, ?> createDefaultFileIcon() {\n        var s = \"browser/default_file.svg\";\n        BrowserIconManager.loadIfNecessary(s);\n        return PrettyImageHelper.ofFixedSizeSquare(s, 24);\n    }\n\n    public static BaseRegionBuilder<?, ?> createDefaultDirectoryIcon() {\n        var s = \"browser/default_folder.svg\";\n        BrowserIconManager.loadIfNecessary(s);\n        return PrettyImageHelper.ofFixedSizeSquare(s, 24);\n    }\n\n    public static BaseRegionBuilder<?, ?> createContextMenuIcon(BrowserIconFileType type) {\n        BrowserIconManager.loadIfNecessary(type.getIcon());\n        return PrettyImageHelper.ofFixedSizeSquare(type.getIcon(), 16);\n    }\n\n    public static BaseRegionBuilder<?, ?> createIcon(String s) {\n        BrowserIconManager.loadIfNecessary(s);\n        return PrettyImageHelper.ofFixedSizeSquare(s, 24);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/BrowserApplicationPathMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu;\n\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\n\nimport lombok.SneakyThrows;\n\nimport java.util.List;\n\npublic interface BrowserApplicationPathMenuProvider extends BrowserMenuItemProvider {\n\n    String getExecutable();\n\n    @Override\n    default void init(BrowserFileSystemTabModel model) throws Exception {\n        if (model.getFileSystem().getShell().isEmpty()) {\n            return;\n        }\n\n        // Cache result for later calls\n        model.getFileSystem().getShell().get().view().isInPath(getExecutable(), true);\n    }\n\n    @Override\n    default boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return model.getFileSystem().getShell().isPresent();\n    }\n\n    @Override\n    @SneakyThrows\n    default boolean isActive(BrowserFileSystemTabModel model) {\n        // This will always return without an exception as it is cached\n        return model.getFileSystem().getShell().orElseThrow().view().isInPath(getExecutable(), true);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuBranchProvider.java",
    "content": "package io.xpipe.app.browser.menu;\n\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\n\nimport javafx.scene.control.Menu;\nimport javafx.scene.control.MenuItem;\n\nimport java.util.List;\n\npublic interface BrowserMenuBranchProvider extends BrowserMenuItemProvider {\n\n    default MenuItem toMenuItem(BrowserFileSystemTabModel model, List<BrowserEntry> selected) {\n        var m = new Menu(getName(model, selected).getValue() + \" ...\");\n        for (var sub : getBranchingActions(model, selected)) {\n            var subselected = resolveFilesIfNeeded(selected);\n            if (!sub.isApplicable(model, subselected)) {\n                continue;\n            }\n            var item = sub.toMenuItem(model, subselected);\n            if (item != null) {\n                m.getItems().add(item);\n            }\n        }\n\n        if (m.getItems().isEmpty()) {\n            return null;\n        }\n\n        var graphic = getIcon();\n        if (graphic != null) {\n            m.setGraphic(graphic.createGraphicNode());\n        }\n        m.setDisable(!isActive(model));\n\n        return m;\n    }\n\n    List<? extends BrowserMenuItemProvider> getBranchingActions(\n            BrowserFileSystemTabModel model, List<BrowserEntry> entries);\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuCategory.java",
    "content": "package io.xpipe.app.browser.menu;\n\npublic enum BrowserMenuCategory {\n    CUSTOM,\n    OPEN,\n    COPY_PASTE,\n    ACTION,\n    MUTATION\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuItemProvider.java",
    "content": "package io.xpipe.app.browser.menu;\n\nimport io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.control.MenuItem;\nimport javafx.scene.input.KeyCombination;\n\nimport java.util.List;\n\npublic interface BrowserMenuItemProvider extends ActionProvider {\n\n    MenuItem toMenuItem(BrowserFileSystemTabModel model, List<BrowserEntry> selected);\n\n    default void init(BrowserFileSystemTabModel model) throws Exception {}\n\n    default boolean automaticallyResolveLinks() {\n        return true;\n    }\n\n    default List<BrowserEntry> resolveFilesIfNeeded(List<BrowserEntry> selected) {\n        return automaticallyResolveLinks()\n                ? selected.stream()\n                        .map(browserEntry ->\n                                new BrowserEntry(browserEntry.getRawFileEntry().resolved(), browserEntry.getModel()))\n                        .toList()\n                : selected;\n    }\n\n    default LabelGraphic getIcon() {\n        return null;\n    }\n\n    default BrowserMenuCategory getCategory() {\n        return null;\n    }\n\n    default KeyCombination getShortcut() {\n        return null;\n    }\n\n    ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries);\n\n    default boolean acceptsEmptySelection() {\n        return false;\n    }\n\n    default boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return true;\n    }\n\n    default boolean isActive(BrowserFileSystemTabModel model) {\n        return true;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuLeafProvider.java",
    "content": "package io.xpipe.app.browser.menu;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.action.BrowserActionProviders;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.comp.RegionDescriptor;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.scene.control.Button;\nimport javafx.scene.control.MenuItem;\nimport javafx.scene.input.KeyEvent;\nimport javafx.scene.layout.Region;\n\nimport lombok.SneakyThrows;\n\nimport java.util.List;\n\npublic interface BrowserMenuLeafProvider extends BrowserMenuItemProvider {\n\n    default void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        createAction(model, entries).executeAsync();\n    }\n\n    default Class<? extends BrowserActionProvider> getDelegateActionProvider() {\n        return null;\n    }\n\n    @SneakyThrows\n    default AbstractAction createAction(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var c = getDelegateActionProvider() != null\n                ? BrowserActionProviders.forClass(getDelegateActionProvider())\n                        .getActionClass()\n                        .orElseThrow()\n                : getActionClass().orElseThrow();\n        var bm = c.getDeclaredMethod(\"builder\");\n        bm.setAccessible(true);\n        var b = bm.invoke(null);\n\n        if (StoreAction.class.isAssignableFrom(c)) {\n            var refMethod = b.getClass().getMethod(\"ref\", DataStoreEntryRef.class);\n            refMethod.setAccessible(true);\n            refMethod.invoke(b, model.getEntry());\n        }\n\n        if (BrowserAction.class.isAssignableFrom(c)) {\n            var modelMethod = b.getClass().getMethod(\"model\", BrowserFileSystemTabModel.class);\n            modelMethod.setAccessible(true);\n            modelMethod.invoke(b, model);\n\n            var entriesMethod = b.getClass().getMethod(\"initEntries\", BrowserFileSystemTabModel.class, List.class);\n            entriesMethod.setAccessible(true);\n            entriesMethod.invoke(b, model, entries);\n        }\n\n        var m = b.getClass().getDeclaredMethod(\"build\");\n        m.setAccessible(true);\n        var defValue = c.cast(m.invoke(b));\n        return defValue;\n    }\n\n    default Button toButton(Region root, BrowserFileSystemTabModel model, List<BrowserEntry> selected) {\n        var name = getName(model, selected);\n        var b = new Button();\n        b.setOnAction(event -> {\n            try {\n                execute(model, selected);\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n            event.consume();\n        });\n        RegionDescriptor.builder().name(name).shortcut(getShortcut()).build().apply(b);\n        var graphic = getIcon();\n        if (graphic != null) {\n            b.setGraphic(graphic.createGraphicNode());\n        }\n        b.setMnemonicParsing(false);\n        root.addEventFilter(KeyEvent.KEY_PRESSED, event -> {\n            if (getShortcut() != null && getShortcut().match(event)) {\n                b.fire();\n                event.consume();\n            }\n        });\n\n        b.setDisable(!isActive(model));\n        model.getCurrentPath().addListener((observable, oldValue, newValue) -> {\n            b.setDisable(!isActive(model));\n        });\n\n        return b;\n    }\n\n    default MenuItem toMenuItem(BrowserFileSystemTabModel model, List<BrowserEntry> selected) {\n        var name = getName(model, selected);\n        var mi = new MenuItem();\n        mi.textProperty().bind(name);\n        mi.setOnAction(event -> {\n            try {\n                execute(model, selected);\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n            event.consume();\n        });\n        if (getShortcut() != null) {\n            mi.setAccelerator(getShortcut());\n        }\n        var graphic = getIcon();\n        if (graphic != null) {\n            mi.setGraphic(graphic.createGraphicNode());\n        }\n        mi.setMnemonicParsing(false);\n        mi.setDisable(!isActive(model));\n\n        return mi;\n    }\n\n    @Override\n    default boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        if (getDelegateActionProvider() != null) {\n            var provider = BrowserActionProviders.forClass(getDelegateActionProvider());\n            return provider.isApplicable(model, entries);\n        } else {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuProviders.java",
    "content": "package io.xpipe.app.browser.menu;\n\nimport io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\n\nimport java.util.List;\n\npublic class BrowserMenuProviders {\n\n    public static List<BrowserMenuLeafProvider> getFlattened(\n            BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return ActionProvider.ALL.stream()\n                .map(browserAction -> browserAction instanceof BrowserMenuItemProvider ba\n                        ? getFlattened(ba, model, entries)\n                        : List.<BrowserMenuLeafProvider>of())\n                .flatMap(List::stream)\n                .toList();\n    }\n\n    public static List<BrowserMenuLeafProvider> getFlattened(\n            BrowserMenuItemProvider browserAction, BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return browserAction instanceof BrowserMenuLeafProvider\n                ? List.of((BrowserMenuLeafProvider) browserAction)\n                : browserAction.isApplicable(model, entries)\n                        ? ((BrowserMenuBranchProvider) browserAction)\n                                .getBranchingActions(model, entries).stream()\n                                        .map(action -> getFlattened(action, model, entries))\n                                        .flatMap(List::stream)\n                                        .toList()\n                        : List.of();\n    }\n\n    public static BrowserMenuLeafProvider byId(String id, BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return getFlattened(model, entries).stream()\n                .filter(browserAction -> id.equals(browserAction.getId()))\n                .findAny()\n                .orElseThrow();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/FileTypeMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu;\n\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.icon.BrowserIconFileType;\nimport io.xpipe.app.browser.icon.BrowserIcons;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport java.util.List;\n\npublic interface FileTypeMenuProvider extends BrowserMenuItemProvider {\n\n    @Override\n    default LabelGraphic getIcon() {\n        return new LabelGraphic.CompGraphic(BrowserIcons.createContextMenuIcon(getType()));\n    }\n\n    @Override\n    default boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var t = getType();\n        return entries.stream().allMatch(entry -> t.matches(entry.getRawFileEntry()));\n    }\n\n    BrowserIconFileType getType();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/MultiExecuteMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu;\n\nimport io.xpipe.app.browser.action.impl.RunCommandInBackgroundActionProvider;\nimport io.xpipe.app.browser.action.impl.RunCommandInBrowserActionProvider;\nimport io.xpipe.app.browser.action.impl.RunCommandInTerminalActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.CommandBuilder;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.SneakyThrows;\n\nimport java.util.List;\n\npublic abstract class MultiExecuteMenuProvider implements BrowserMenuBranchProvider {\n\n    protected abstract List<CommandBuilder> createCommand(BrowserFileSystemTabModel model, List<BrowserEntry> entries);\n\n    @Override\n    public List<BrowserMenuLeafProvider> getBranchingActions(\n            BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return List.of(\n                new BrowserMenuLeafProvider() {\n\n                    @Override\n                    @SneakyThrows\n                    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        if (model.getCurrentPath().getValue() == null) {\n                            return;\n                        }\n\n                        var commands = createCommand(model, entries);\n                        for (CommandBuilder command : commands) {\n                            var builder = RunCommandInTerminalActionProvider.Action.builder();\n                            builder.initFiles(\n                                    model, List.of(model.getCurrentPath().getValue()));\n                            builder.command(command.buildFull(\n                                    model.getFileSystem().getShell().orElseThrow()));\n                            builder.build().executeAsync();\n                        }\n                    }\n\n                    @Override\n                    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        return AppPrefs.get().terminalType().getValue() != null;\n                    }\n\n                    @Override\n                    public ObservableValue<String> getName(\n                            BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        var t = AppPrefs.get().terminalType().getValue();\n                        return AppI18n.observable(\n                                \"executeInTerminal\",\n                                t != null ? t.toTranslatedString().getValue() : \"?\");\n                    }\n                },\n                new BrowserMenuLeafProvider() {\n\n                    @Override\n                    @SneakyThrows\n                    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        var commands = createCommand(model, entries);\n                        for (CommandBuilder command : commands) {\n                            var builder = RunCommandInBrowserActionProvider.Action.builder();\n                            builder.initFiles(\n                                    model, List.of(model.getCurrentPath().getValue()));\n                            builder.command(command.buildFull(\n                                    model.getFileSystem().getShell().orElseThrow()));\n                            builder.build().executeAsync();\n                        }\n                    }\n\n                    @Override\n                    public ObservableValue<String> getName(\n                            BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        return AppI18n.observable(\"runInFileBrowser\");\n                    }\n                },\n                new BrowserMenuLeafProvider() {\n\n                    @Override\n                    @SneakyThrows\n                    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        var commands = createCommand(model, entries);\n                        for (CommandBuilder command : commands) {\n                            var builder = RunCommandInBackgroundActionProvider.Action.builder();\n                            builder.initFiles(\n                                    model, List.of(model.getCurrentPath().getValue()));\n                            builder.command(command.buildFull(\n                                    model.getFileSystem().getShell().orElseThrow()));\n                            builder.build().executeAsync();\n                        }\n                    }\n\n                    @Override\n                    public ObservableValue<String> getName(\n                            BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        return AppI18n.observable(\"runSilent\");\n                    }\n                });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/BackMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.util.BooleanScope;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\n\nimport java.util.List;\n\npublic class BackMenuProvider implements BrowserMenuLeafProvider {\n\n    @Override\n    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        ThreadHelper.runAsync(() -> {\n            BooleanScope.executeExclusive(model.getBusy(), () -> {\n                model.backSync(1);\n            });\n        });\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return false;\n    }\n\n    public String getId() {\n        return \"back\";\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2a-arrow-left\");\n    }\n\n    @Override\n    public KeyCombination getShortcut() {\n        return new KeyCodeCombination(KeyCode.LEFT, KeyCombination.ALT_DOWN);\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"back\");\n    }\n\n    @Override\n    public boolean isActive(BrowserFileSystemTabModel model) {\n        return model.getHistory().canGoBackProperty().get();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/BrowseInNativeManagerMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.action.impl.BrowseInNativeManagerActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\n\npublic class BrowseInNativeManagerMenuProvider implements BrowserMenuLeafProvider {\n\n    @Override\n    public Class<? extends BrowserActionProvider> getDelegateActionProvider() {\n        return BrowseInNativeManagerActionProvider.class;\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.OPEN;\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return switch (OsType.ofLocal()) {\n            case OsType.Windows ignored -> AppI18n.observable(\"browseInWindowsExplorer\");\n            case OsType.Linux ignored -> AppI18n.observable(\"browseInDefaultFileManager\");\n            case OsType.MacOs ignored -> AppI18n.observable(\"browseInFinder\");\n        };\n    }\n\n    @Override\n    public boolean acceptsEmptySelection() {\n        return true;\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2f-folder-eye-outline\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/ChgrpMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.action.impl.ChgrpActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuBranchProvider;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuItemProvider;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.control.TextField;\n\nimport lombok.SneakyThrows;\n\nimport java.util.List;\nimport java.util.stream.Stream;\n\npublic class ChgrpMenuProvider implements BrowserMenuBranchProvider {\n\n    @SneakyThrows\n    private static List<BrowserMenuItemProvider> getLeafActions(BrowserFileSystemTabModel model, boolean recursive) {\n        if (model.getFileSystem().getShell().isEmpty()) {\n            return List.of(new CustomProvider(recursive));\n        }\n\n        List<BrowserMenuItemProvider> actions = Stream.<BrowserMenuItemProvider>concat(\n                        model.getFileSystem().getShell().get().view().getGroupFile().getGroups().entrySet().stream()\n                                .filter(e -> !e.getValue().equals(\"nohome\")\n                                        && !e.getValue().equals(\"nogroup\")\n                                        && !e.getValue().equals(\"nobody\")\n                                        && (e.getKey().equals(0) || e.getKey() >= 900))\n                                .map(e -> e.getValue())\n                                .map(s -> (BrowserMenuLeafProvider) new FixedProvider(s, recursive)),\n                        Stream.of(new CustomProvider(recursive)))\n                .toList();\n        return actions;\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2a-account-group-outline\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.MUTATION;\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"chgrp\");\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return model.getFileSystem().supportsChgrp();\n    }\n\n    @Override\n    public List<BrowserMenuItemProvider> getBranchingActions(\n            BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        if (entries.stream()\n                .anyMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY)) {\n            return List.of(new FlatProvider(), new RecursiveProvider());\n        } else {\n            return getLeafActions(model, false);\n        }\n    }\n\n    private static class FlatProvider implements BrowserMenuBranchProvider {\n\n        @Override\n        public LabelGraphic getIcon() {\n            return new LabelGraphic.IconGraphic(\"mdi2f-file-outline\");\n        }\n\n        @Override\n        public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return AppI18n.observable(\"flat\");\n        }\n\n        @Override\n        public List<BrowserMenuItemProvider> getBranchingActions(\n                BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return getLeafActions(model, false);\n        }\n    }\n\n    private static class RecursiveProvider implements BrowserMenuBranchProvider {\n\n        @Override\n        public LabelGraphic getIcon() {\n            return new LabelGraphic.IconGraphic(\"mdi2f-file-tree\");\n        }\n\n        @Override\n        public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return AppI18n.observable(\"recursive\");\n        }\n\n        @Override\n        public List<BrowserMenuItemProvider> getBranchingActions(\n                BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return getLeafActions(model, true);\n        }\n    }\n\n    private static class FixedProvider implements BrowserMenuLeafProvider {\n\n        private final String group;\n        private final boolean recursive;\n\n        private FixedProvider(String group, boolean recursive) {\n            this.group = group;\n            this.recursive = recursive;\n        }\n\n        @Override\n        public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return new SimpleStringProperty(group);\n        }\n\n        @Override\n        public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            var builder = ChgrpActionProvider.Action.builder();\n            builder.initEntries(model, entries);\n            builder.group(group);\n            builder.recursive(recursive);\n            var action = builder.build();\n            action.executeAsync();\n        }\n    }\n\n    private static class CustomProvider implements BrowserMenuLeafProvider {\n\n        private final boolean recursive;\n\n        private CustomProvider(boolean recursive) {\n            this.recursive = recursive;\n        }\n\n        @Override\n        public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return AppI18n.observable(\"custom\");\n        }\n\n        @Override\n        public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            var group = new SimpleStringProperty();\n            var modal = ModalOverlay.of(\n                    \"groupName\",\n                    RegionBuilder.of(() -> {\n                                var creationName = new TextField();\n                                creationName.textProperty().bindBidirectional(group);\n                                return creationName;\n                            })\n                            .prefWidth(350));\n            modal.withDefaultButtons(() -> {\n                if (group.getValue() == null) {\n                    return;\n                }\n\n                var builder = ChgrpActionProvider.Action.builder();\n                builder.initEntries(model, entries);\n                builder.group(group.getValue());\n                builder.recursive(recursive);\n                var action = builder.build();\n                action.executeAsync();\n            });\n            modal.show();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/ChmodMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.action.impl.ChmodActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuBranchProvider;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuItemProvider;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.control.TextField;\n\nimport java.util.List;\n\npublic class ChmodMenuProvider implements BrowserMenuBranchProvider {\n\n    private static List<BrowserMenuItemProvider> getLeafActions(BrowserFileSystemTabModel ignored, boolean recursive) {\n        var custom = new CustomProvider(recursive);\n        return List.of(\n                new FixedProvider(\"400\", recursive),\n                new FixedProvider(\"600\", recursive),\n                new FixedProvider(\"644\", recursive),\n                new FixedProvider(\"700\", recursive),\n                new FixedProvider(\"755\", recursive),\n                new FixedProvider(\"777\", recursive),\n                new FixedProvider(\"u+x\", recursive),\n                new FixedProvider(\"a+x\", recursive),\n                custom);\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2w-wrench-outline\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.MUTATION;\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"chmod\");\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return model.getFileSystem().supportsChmod();\n    }\n\n    @Override\n    public List<BrowserMenuItemProvider> getBranchingActions(\n            BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        if (entries.stream()\n                .anyMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY)) {\n            return List.of(new FlatProvider(), new RecursiveProvider());\n        } else {\n            return getLeafActions(model, false);\n        }\n    }\n\n    private static class FlatProvider implements BrowserMenuBranchProvider {\n\n        @Override\n        public LabelGraphic getIcon() {\n            return new LabelGraphic.IconGraphic(\"mdi2f-file-outline\");\n        }\n\n        @Override\n        public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return AppI18n.observable(\"flat\");\n        }\n\n        @Override\n        public List<BrowserMenuItemProvider> getBranchingActions(\n                BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return getLeafActions(model, false);\n        }\n    }\n\n    private static class RecursiveProvider implements BrowserMenuBranchProvider {\n\n        @Override\n        public LabelGraphic getIcon() {\n            return new LabelGraphic.IconGraphic(\"mdi2f-file-tree\");\n        }\n\n        @Override\n        public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return AppI18n.observable(\"recursive\");\n        }\n\n        @Override\n        public List<BrowserMenuItemProvider> getBranchingActions(\n                BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return getLeafActions(model, true);\n        }\n    }\n\n    private static class FixedProvider implements BrowserMenuLeafProvider {\n\n        private final String permissions;\n        private final boolean recursive;\n\n        private FixedProvider(String permissions, boolean recursive) {\n            this.permissions = permissions;\n            this.recursive = recursive;\n        }\n\n        @Override\n        public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return new SimpleStringProperty(permissions);\n        }\n\n        @Override\n        public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            var builder = ChmodActionProvider.Action.builder();\n            builder.initEntries(model, entries);\n            builder.permissions(permissions);\n            builder.recursive(recursive);\n            var action = builder.build();\n            action.executeAsync();\n        }\n    }\n\n    private static class CustomProvider implements BrowserMenuLeafProvider {\n\n        private final boolean recursive;\n\n        private CustomProvider(boolean recursive) {\n            this.recursive = recursive;\n        }\n\n        @Override\n        public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return AppI18n.observable(\"custom\");\n        }\n\n        @Override\n        public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            var permissions = new SimpleStringProperty();\n            var modal = ModalOverlay.of(\n                    \"chmodPermissions\",\n                    RegionBuilder.of(() -> {\n                                var creationName = new TextField();\n                                creationName.textProperty().bindBidirectional(permissions);\n                                return creationName;\n                            })\n                            .prefWidth(350));\n            modal.withDefaultButtons(() -> {\n                if (permissions.getValue() == null) {\n                    return;\n                }\n\n                var builder = ChmodActionProvider.Action.builder();\n                builder.initEntries(model, entries);\n                builder.permissions(permissions.getValue());\n                builder.recursive(recursive);\n                var action = builder.build();\n                action.executeAsync();\n            });\n            modal.show();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/ChownMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.action.impl.ChownActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuBranchProvider;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuItemProvider;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.control.TextField;\n\nimport lombok.SneakyThrows;\n\nimport java.util.List;\nimport java.util.stream.Stream;\n\npublic class ChownMenuProvider implements BrowserMenuBranchProvider {\n\n    @SneakyThrows\n    private static List<BrowserMenuItemProvider> getLeafActions(BrowserFileSystemTabModel model, boolean recursive) {\n        if (model.getFileSystem().getShell().isEmpty()) {\n            return List.of(new CustomProvider(recursive));\n        }\n\n        var actions = Stream.<BrowserMenuItemProvider>concat(\n                        model.getFileSystem().getShell().get().view().getPasswdFile().getUsers().entrySet().stream()\n                                .filter(e -> !e.getValue().equals(\"nohome\")\n                                        && !e.getValue().equals(\"nobody\")\n                                        && (e.getKey().equals(0) || e.getKey() >= 900))\n                                .map(e -> e.getValue())\n                                .map(s -> (BrowserMenuLeafProvider) new FixedProvider(s, recursive)),\n                        Stream.of(new CustomProvider(recursive)))\n                .toList();\n        return actions;\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2a-account-edit\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.MUTATION;\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"chown\");\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return model.getFileSystem().supportsChown();\n    }\n\n    @Override\n    public List<BrowserMenuItemProvider> getBranchingActions(\n            BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        if (entries.stream()\n                .anyMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY)) {\n            return List.of(new FlatProvider(), new RecursiveProvider());\n        } else {\n            return getLeafActions(model, false);\n        }\n    }\n\n    private static class FlatProvider implements BrowserMenuBranchProvider {\n\n        @Override\n        public LabelGraphic getIcon() {\n            return new LabelGraphic.IconGraphic(\"mdi2f-file-outline\");\n        }\n\n        @Override\n        public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return AppI18n.observable(\"flat\");\n        }\n\n        @Override\n        public List<BrowserMenuItemProvider> getBranchingActions(\n                BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return getLeafActions(model, false);\n        }\n    }\n\n    private static class RecursiveProvider implements BrowserMenuBranchProvider {\n\n        @Override\n        public LabelGraphic getIcon() {\n            return new LabelGraphic.IconGraphic(\"mdi2f-file-tree\");\n        }\n\n        @Override\n        public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return AppI18n.observable(\"recursive\");\n        }\n\n        @Override\n        public List<BrowserMenuItemProvider> getBranchingActions(\n                BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return getLeafActions(model, true);\n        }\n    }\n\n    private static class FixedProvider implements BrowserMenuLeafProvider {\n\n        private final String owner;\n        private final boolean recursive;\n\n        private FixedProvider(String owner, boolean recursive) {\n            this.owner = owner;\n            this.recursive = recursive;\n        }\n\n        @Override\n        public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return new SimpleStringProperty(owner);\n        }\n\n        @Override\n        public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            var builder = ChownActionProvider.Action.builder();\n            builder.initEntries(model, entries);\n            builder.owner(owner);\n            builder.recursive(recursive);\n            var action = builder.build();\n            action.executeAsync();\n        }\n    }\n\n    private static class CustomProvider implements BrowserMenuLeafProvider {\n\n        private final boolean recursive;\n\n        private CustomProvider(boolean recursive) {\n            this.recursive = recursive;\n        }\n\n        @Override\n        public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return AppI18n.observable(\"custom\");\n        }\n\n        @Override\n        public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            var user = new SimpleStringProperty();\n            var modal = ModalOverlay.of(\n                    \"userName\",\n                    RegionBuilder.of(() -> {\n                                var creationName = new TextField();\n                                creationName.textProperty().bindBidirectional(user);\n                                return creationName;\n                            })\n                            .prefWidth(350));\n            modal.withDefaultButtons(() -> {\n                if (user.getValue() == null) {\n                    return;\n                }\n\n                var builder = ChownActionProvider.Action.builder();\n                builder.initEntries(model, entries);\n                builder.owner(user.getValue());\n                builder.recursive(recursive);\n                var action = builder.build();\n                action.executeAsync();\n            });\n            modal.show();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/ComputeDirectorySizesMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.browser.action.impl.ComputeDirectorySizesActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\n\npublic class ComputeDirectorySizesMenuProvider implements BrowserMenuLeafProvider {\n\n    public String getId() {\n        return \"computeDirectorySizes\";\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2f-format-list-text\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.ACTION;\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var topLevel =\n                entries.size() == 1 && entries.getFirst().getRawFileEntry().equals(model.getCurrentDirectory());\n        return AppI18n.observable(topLevel ? \"computeDirectorySizes\" : \"computeSize\");\n    }\n\n    @Override\n    public boolean acceptsEmptySelection() {\n        return true;\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return model.getFileSystem().supportsDirectorySizes()\n                && entries.stream()\n                        .allMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY);\n    }\n\n    @Override\n    public AbstractAction createAction(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var builder = ComputeDirectorySizesActionProvider.Action.builder();\n        builder.initEntries(model, entries);\n        return builder.build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/CopyMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.file.BrowserClipboard;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\n\nimport java.util.List;\n\npublic class CopyMenuProvider implements BrowserMenuLeafProvider {\n\n    @Override\n    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        BrowserClipboard.startCopy(model.getCurrentDirectory(), entries);\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdoal-file_copy\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.COPY_PASTE;\n    }\n\n    @Override\n    public KeyCombination getShortcut() {\n        return new KeyCodeCombination(KeyCode.C, KeyCombination.SHORTCUT_DOWN);\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"copy\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/CopyPathMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuBranchProvider;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.platform.ClipboardHelper;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\npublic class CopyPathMenuProvider implements BrowserMenuBranchProvider {\n\n    private static String centerEllipsis(String input, int length) {\n        if (input == null) {\n            return \"\";\n        }\n\n        if (input.length() <= length) {\n            return input;\n        }\n\n        var half = (length / 2) - 5;\n        return input.substring(0, half) + \" ... \" + input.substring(input.length() - half);\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2c-content-copy\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.COPY_PASTE;\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"copyLocation\");\n    }\n\n    @Override\n    public boolean acceptsEmptySelection() {\n        return true;\n    }\n\n    @Override\n    public List<BrowserMenuLeafProvider> getBranchingActions(\n            BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return List.of(\n                new BrowserMenuLeafProvider() {\n                    @Override\n                    public KeyCombination getShortcut() {\n                        return new KeyCodeCombination(KeyCode.C, KeyCombination.ALT_DOWN, KeyCombination.SHORTCUT_DOWN);\n                    }\n\n                    @Override\n                    public ObservableValue<String> getName(\n                            BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        if (entries.size() == 1) {\n                            return new SimpleObjectProperty<>(centerEllipsis(\n                                    entries.getFirst()\n                                            .getRawFileEntry()\n                                            .getPath()\n                                            .toString(),\n                                    50));\n                        }\n\n                        return AppI18n.observable(\"absolutePaths\");\n                    }\n\n                    @Override\n                    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        var s = entries.stream()\n                                .map(entry -> entry.getRawFileEntry().getPath().toString())\n                                .collect(Collectors.joining(\"\\n\"));\n                        ClipboardHelper.copyText(s);\n                    }\n                },\n                new BrowserMenuLeafProvider() {\n                    @Override\n                    public boolean automaticallyResolveLinks() {\n                        return false;\n                    }\n\n                    @Override\n                    public ObservableValue<String> getName(\n                            BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        if (entries.size() == 1) {\n                            return new SimpleObjectProperty<>(centerEllipsis(\n                                    entries.getFirst()\n                                            .getRawFileEntry()\n                                            .getPath()\n                                            .toString(),\n                                    50));\n                        }\n\n                        return AppI18n.observable(\"absoluteLinkPaths\");\n                    }\n\n                    @Override\n                    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        var s = entries.stream()\n                                .map(entry -> entry.getRawFileEntry().getPath().toString())\n                                .collect(Collectors.joining(\"\\n\"));\n                        ClipboardHelper.copyText(s);\n                    }\n\n                    @Override\n                    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        return entries.stream()\n                                .allMatch(browserEntry ->\n                                        browserEntry.getRawFileEntry().getKind() == FileKind.LINK);\n                    }\n                },\n                new BrowserMenuLeafProvider() {\n                    @Override\n                    public ObservableValue<String> getName(\n                            BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        if (entries.size() == 1) {\n                            return new SimpleObjectProperty<>(\"\\\"\"\n                                    + centerEllipsis(\n                                            entries.getFirst()\n                                                    .getRawFileEntry()\n                                                    .getPath()\n                                                    .toString(),\n                                            50) + \"\\\"\");\n                        }\n\n                        return AppI18n.observable(\"absolutePathsQuoted\");\n                    }\n\n                    @Override\n                    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        var s = entries.stream()\n                                .map(entry -> \"\\\"\" + entry.getRawFileEntry().getPath() + \"\\\"\")\n                                .collect(Collectors.joining(\"\\n\"));\n                        ClipboardHelper.copyText(s);\n                    }\n\n                    @Override\n                    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        return entries.stream().anyMatch(entry -> entry.getRawFileEntry()\n                                .getPath()\n                                .toString()\n                                .contains(\" \"));\n                    }\n                },\n                new BrowserMenuLeafProvider() {\n                    @Override\n                    public KeyCombination getShortcut() {\n                        return new KeyCodeCombination(\n                                KeyCode.C, KeyCombination.SHIFT_DOWN, KeyCombination.SHORTCUT_DOWN);\n                    }\n\n                    @Override\n                    public ObservableValue<String> getName(\n                            BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        if (entries.size() == 1) {\n                            return new SimpleObjectProperty<>(centerEllipsis(\n                                    entries.getFirst()\n                                            .getRawFileEntry()\n                                            .getPath()\n                                            .getFileName(),\n                                    50));\n                        }\n\n                        return AppI18n.observable(\"fileNames\");\n                    }\n\n                    @Override\n                    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        var s = entries.stream()\n                                .map(entry -> entry.getRawFileEntry().getPath().getFileName())\n                                .collect(Collectors.joining(\"\\n\"));\n                        ClipboardHelper.copyText(s);\n                    }\n                },\n                new BrowserMenuLeafProvider() {\n                    @Override\n                    public boolean automaticallyResolveLinks() {\n                        return false;\n                    }\n\n                    @Override\n                    public ObservableValue<String> getName(\n                            BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        if (entries.size() == 1) {\n                            return new SimpleObjectProperty<>(centerEllipsis(\n                                    entries.getFirst()\n                                            .getRawFileEntry()\n                                            .getPath()\n                                            .getFileName(),\n                                    50));\n                        }\n\n                        return AppI18n.observable(\"linkFileNames\");\n                    }\n\n                    @Override\n                    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        var s = entries.stream()\n                                .map(entry -> entry.getRawFileEntry().getPath().getFileName())\n                                .collect(Collectors.joining(\"\\n\"));\n                        ClipboardHelper.copyText(s);\n                    }\n\n                    @Override\n                    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        return entries.stream()\n                                        .allMatch(browserEntry ->\n                                                browserEntry.getRawFileEntry().getKind() == FileKind.LINK)\n                                && entries.stream().anyMatch(browserEntry -> !browserEntry\n                                        .getFileName()\n                                        .equals(browserEntry\n                                                .getRawFileEntry()\n                                                .resolved()\n                                                .getPath()\n                                                .getFileName()));\n                    }\n                },\n                new BrowserMenuLeafProvider() {\n                    @Override\n                    public ObservableValue<String> getName(\n                            BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        if (entries.size() == 1) {\n                            return new SimpleObjectProperty<>(\"\\\"\"\n                                    + centerEllipsis(\n                                            entries.getFirst()\n                                                    .getRawFileEntry()\n                                                    .getPath()\n                                                    .getFileName(),\n                                            50) + \"\\\"\");\n                        }\n\n                        return AppI18n.observable(\"fileNamesQuoted\");\n                    }\n\n                    @Override\n                    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        var s = entries.stream()\n                                .map(entry ->\n                                        \"\\\"\" + entry.getRawFileEntry().getPath().getFileName() + \"\\\"\")\n                                .collect(Collectors.joining(\"\\n\"));\n                        ClipboardHelper.copyText(s);\n                    }\n\n                    @Override\n                    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        return entries.stream().anyMatch(entry -> entry.getRawFileEntry()\n                                .getPath()\n                                .getFileName()\n                                .contains(\" \"));\n                    }\n                });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/DeleteMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.action.impl.DeleteActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\n\nimport java.util.List;\n\npublic class DeleteMenuProvider implements BrowserMenuLeafProvider {\n\n    @Override\n    public Class<? extends BrowserActionProvider> getDelegateActionProvider() {\n        return DeleteActionProvider.class;\n    }\n\n    @Override\n    public boolean automaticallyResolveLinks() {\n        return false;\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2d-delete\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.MUTATION;\n    }\n\n    @Override\n    public KeyCombination getShortcut() {\n        return new KeyCodeCombination(KeyCode.DELETE);\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\n                \"deleteFile\",\n                entries.stream()\n                                .anyMatch(browserEntry ->\n                                        browserEntry.getRawFileEntry().getKind() == FileKind.LINK)\n                        ? \"link\"\n                        : \"\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/DownloadMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\n\nimport java.util.List;\n\npublic class DownloadMenuProvider implements BrowserMenuLeafProvider {\n\n    @Override\n    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var transfer = model.getBrowserModel();\n        if (!(transfer instanceof BrowserFullSessionModel fullSessionModel)) {\n            return;\n        }\n\n        fullSessionModel.getLocalTransfersStage().drop(model, entries);\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var transfer = model.getBrowserModel();\n        if (!(transfer instanceof BrowserFullSessionModel)) {\n            return false;\n        }\n        return true;\n    }\n\n    public String getId() {\n        return \"download\";\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2d-download\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.ACTION;\n    }\n\n    @Override\n    public KeyCombination getShortcut() {\n        return new KeyCodeCombination(KeyCode.D, KeyCombination.SHORTCUT_DOWN);\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"download\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/EditFileMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileOpener;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\n\nimport java.util.List;\n\npublic class EditFileMenuProvider implements BrowserMenuLeafProvider {\n\n    @Override\n    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        ThreadHelper.runAsync(() -> {\n            for (BrowserEntry entry : entries) {\n                BrowserFileOpener.openInTextEditor(model, entry.getRawFileEntry());\n            }\n        });\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE);\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2p-pencil\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.OPEN;\n    }\n\n    @Override\n    public KeyCombination getShortcut() {\n        return new KeyCodeCombination(KeyCode.E, KeyCombination.SHORTCUT_DOWN);\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var e = AppPrefs.get().externalEditor().getValue();\n        return AppI18n.observable(\n                \"editWithEditor\", e != null ? e.toTranslatedString().getValue() : \"?\");\n    }\n\n    @Override\n    public boolean isActive(BrowserFileSystemTabModel model) {\n        var e = AppPrefs.get().externalEditor().getValue();\n        return e != null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/FollowLinkMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\n\npublic class FollowLinkMenuProvider implements BrowserMenuLeafProvider {\n\n    @Override\n    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var target = entries.getFirst().getRawFileEntry().resolved().getPath().getParent();\n        model.cdAsync(target);\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return entries.size() == 1\n                && entries.stream()\n                        .allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.LINK\n                                && entry.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);\n    }\n\n    @Override\n    public boolean automaticallyResolveLinks() {\n        return false;\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2a-arrow-top-right-thick\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.OPEN;\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"followLink\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/ForwardMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.util.BooleanScope;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\n\nimport java.util.List;\n\npublic class ForwardMenuProvider implements BrowserMenuLeafProvider {\n\n    @Override\n    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        ThreadHelper.runAsync(() -> {\n            BooleanScope.executeExclusive(model.getBusy(), () -> {\n                model.forthSync(1);\n            });\n        });\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return false;\n    }\n\n    public String getId() {\n        return \"forward\";\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2a-arrow-right\");\n    }\n\n    @Override\n    public KeyCombination getShortcut() {\n        return new KeyCodeCombination(KeyCode.RIGHT, KeyCombination.ALT_DOWN);\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"goForward\");\n    }\n\n    @Override\n    public boolean isActive(BrowserFileSystemTabModel model) {\n        return model.getHistory().canGoForthProperty().get();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/GradleRunMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.core.OsType;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.control.TextField;\n\nimport java.util.List;\n\npublic class GradleRunMenuProvider implements BrowserMenuLeafProvider {\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        if (model.getFileSystem().getShell().isEmpty()) {\n            return false;\n        }\n\n        if (entries.size() != 1) {\n            return false;\n        }\n\n        if (entries.getFirst().getRawFileEntry().getKind() != FileKind.FILE) {\n            return false;\n        }\n\n        OsType.Any osType = model.getFileSystem().getShell().orElseThrow().getOsType();\n        var ext = switch (osType) {\n            case OsType.Windows ignored -> \"gradlew.bat\";\n            default -> \"gradlew\";\n        };\n\n        if (!entries.getFirst().getFileName().equalsIgnoreCase(ext)) {\n            return false;\n        }\n\n        return true;\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"runTask\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.CUSTOM;\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2e-elephant\");\n    }\n\n    @Override\n    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var tasks = new SimpleStringProperty();\n        var modal = ModalOverlay.of(\n                \"gradleTasks\",\n                RegionBuilder.of(() -> {\n                            var creationName = new TextField();\n                            creationName.textProperty().bindBidirectional(tasks);\n                            return creationName;\n                        })\n                        .prefWidth(350));\n        modal.withDefaultButtons(() -> {\n            var fixedTasks = tasks.getValue();\n            if (fixedTasks == null) {\n                return;\n            }\n\n            var parent = entries.getFirst().getRawFileEntry().getPath().getParent();\n            var command = model.getFileSystem().getShell().orElseThrow().command(CommandBuilder.of()\n                    .add(\"sh\")\n                    .addFile(entries.getFirst().getRawFileEntry().getPath())\n                    .add(fixedTasks)\n            );\n\n            model.openTerminalAsync(fixedTasks, parent, command, true);\n        });\n        modal.show();\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/JarMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.icon.BrowserIconFileType;\nimport io.xpipe.app.browser.menu.*;\nimport io.xpipe.app.process.CommandBuilder;\n\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\n\npublic class JarMenuProvider extends MultiExecuteMenuProvider\n        implements BrowserApplicationPathMenuProvider, FileTypeMenuProvider {\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.CUSTOM;\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var arg = entries.size() == 1 ? entries.getFirst().getFileName() : \"(\" + entries.size() + \")\";\n        return new SimpleStringProperty(\"java -jar \" + arg);\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        if (!BrowserApplicationPathMenuProvider.super.isApplicable(model, entries)) {\n            return false;\n        }\n\n        return FileTypeMenuProvider.super.isApplicable(model, entries);\n    }\n\n    @Override\n    public BrowserIconFileType getType() {\n        return BrowserIconFileType.byId(\"jar\");\n    }\n\n    @Override\n    public String getExecutable() {\n        return \"java\";\n    }\n\n    @Override\n    protected List<CommandBuilder> createCommand(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return entries.stream()\n                .map(browserEntry -> {\n                    return CommandBuilder.of()\n                            .add(\"java\", \"-jar\")\n                            .addFile(browserEntry.getRawFileEntry().getPath());\n                })\n                .toList();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/JavapMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.icon.BrowserIconFileType;\nimport io.xpipe.app.browser.menu.BrowserApplicationPathMenuProvider;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.browser.menu.FileTypeMenuProvider;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.util.FileOpener;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\n\npublic class JavapMenuProvider\n        implements FileTypeMenuProvider, BrowserApplicationPathMenuProvider, BrowserMenuLeafProvider {\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.CUSTOM;\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var arg = entries.size() == 1 ? entries.getFirst().getFileName() : \"(\" + entries.size() + \")\";\n        return new SimpleStringProperty(\"javap -c -p \" + arg);\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        if (!BrowserApplicationPathMenuProvider.super.isApplicable(model, entries)\n                || !BrowserMenuLeafProvider.super.isApplicable(model, entries)) {\n            return false;\n        }\n\n        return FileTypeMenuProvider.super.isApplicable(model, entries);\n    }\n\n    @Override\n    public BrowserIconFileType getType() {\n        return BrowserIconFileType.byId(\"class\");\n    }\n\n    @Override\n    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        ThreadHelper.runFailableAsync(() -> {\n            ShellControl sc = model.getFileSystem().getShell().orElseThrow();\n            for (BrowserEntry entry : entries) {\n                var command = CommandBuilder.of()\n                        .add(\"javap\", \"-c\", \"-p\")\n                        .addFile(entry.getRawFileEntry().getPath());\n                var out = sc.command(command)\n                        .withWorkingDirectory(model.getCurrentDirectory().getPath())\n                        .readStdoutOrThrow();\n                FileOpener.openReadOnlyString(out);\n            }\n        });\n    }\n\n    @Override\n    public String getExecutable() {\n        return \"java\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/NewItemMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.action.impl.NewDirectoryActionProvider;\nimport io.xpipe.app.browser.action.impl.NewFileActionProvider;\nimport io.xpipe.app.browser.action.impl.NewLinkActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.icon.BrowserIcons;\nimport io.xpipe.app.browser.menu.BrowserMenuBranchProvider;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.core.FilePath;\n\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.control.TextField;\n\nimport java.util.List;\n\npublic class NewItemMenuProvider implements BrowserMenuBranchProvider {\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2p-plus-box-outline\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.ACTION;\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"new\");\n    }\n\n    @Override\n    public boolean acceptsEmptySelection() {\n        return true;\n    }\n\n    @Override\n    public List<BrowserMenuLeafProvider> getBranchingActions(\n            BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return List.of(\n                new BrowserMenuLeafProvider() {\n                    @Override\n                    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        var name = new SimpleStringProperty();\n                        var modal = ModalOverlay.of(\n                                \"newFile\",\n                                RegionBuilder.of(() -> {\n                                            var creationName = new TextField();\n                                            creationName.textProperty().bindBidirectional(name);\n                                            return creationName;\n                                        })\n                                        .prefWidth(350));\n                        modal.withDefaultButtons(() -> {\n                            if (name.getValue() == null || name.getValue().isEmpty()) {\n                                return;\n                            }\n\n                            var fixedFiles = entries.stream()\n                                    .map(browserEntry ->\n                                            browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY\n                                                    ? browserEntry\n                                                            .getRawFileEntry()\n                                                            .getPath()\n                                                    : browserEntry\n                                                            .getRawFileEntry()\n                                                            .getPath()\n                                                            .getParent())\n                                    .toList();\n                            var builder = NewFileActionProvider.Action.builder();\n                            builder.initFiles(model, fixedFiles);\n                            builder.name(name.getValue().strip());\n                            builder.build().executeAsync();\n                        });\n                        modal.show();\n                    }\n\n                    @Override\n                    public LabelGraphic getIcon() {\n                        return new LabelGraphic.CompGraphic(BrowserIcons.createDefaultFileIcon());\n                    }\n\n                    @Override\n                    public ObservableValue<String> getName(\n                            BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        return AppI18n.observable(\"file\");\n                    }\n                },\n                new BrowserMenuLeafProvider() {\n                    @Override\n                    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        var name = new SimpleStringProperty();\n                        var modal = ModalOverlay.of(\n                                \"newDirectory\",\n                                RegionBuilder.of(() -> {\n                                            var creationName = new TextField();\n                                            creationName.textProperty().bindBidirectional(name);\n                                            return creationName;\n                                        })\n                                        .prefWidth(350));\n                        modal.withDefaultButtons(() -> {\n                            if (name.getValue() == null || name.getValue().isEmpty()) {\n                                return;\n                            }\n\n                            var fixedFiles = entries.stream()\n                                    .map(browserEntry ->\n                                            browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY\n                                                    ? browserEntry\n                                                            .getRawFileEntry()\n                                                            .getPath()\n                                                    : browserEntry\n                                                            .getRawFileEntry()\n                                                            .getPath()\n                                                            .getParent())\n                                    .toList();\n                            var builder = NewDirectoryActionProvider.Action.builder();\n                            builder.initFiles(model, fixedFiles);\n                            builder.name(name.getValue().strip());\n                            builder.build().executeAsync();\n                        });\n                        modal.show();\n                    }\n\n                    @Override\n                    public LabelGraphic getIcon() {\n                        return new LabelGraphic.CompGraphic(BrowserIcons.createDefaultDirectoryIcon());\n                    }\n\n                    @Override\n                    public ObservableValue<String> getName(\n                            BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        return AppI18n.observable(\"directory\");\n                    }\n                },\n                new BrowserMenuLeafProvider() {\n                    @Override\n                    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        var linkName = new SimpleStringProperty();\n                        var target = new SimpleStringProperty();\n                        var modal = ModalOverlay.of(\n                                \"newLink\",\n                                new OptionsBuilder()\n                                        .name(\"linkName\")\n                                        .addString(linkName)\n                                        .name(\"targetPath\")\n                                        .addString(target)\n                                        .buildComp()\n                                        .prefWidth(350));\n                        modal.withDefaultButtons(() -> {\n                            if (linkName.getValue() == null\n                                    || linkName.getValue().isEmpty()\n                                    || target.getValue() == null\n                                    || target.getValue().isEmpty()) {\n                                return;\n                            }\n\n                            var fixedFiles = entries.stream()\n                                    .map(browserEntry ->\n                                            browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY\n                                                    ? browserEntry\n                                                            .getRawFileEntry()\n                                                            .getPath()\n                                                    : browserEntry\n                                                            .getRawFileEntry()\n                                                            .getPath()\n                                                            .getParent())\n                                    .toList();\n                            var builder = NewLinkActionProvider.Action.builder();\n                            builder.initFiles(model, fixedFiles);\n                            builder.name(linkName.getValue().strip());\n                            builder.target(FilePath.of(target.getValue()));\n                            builder.build().executeAsync();\n                        });\n                        modal.show();\n                    }\n\n                    @Override\n                    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        return model.getFileSystem().supportsLinkCreation();\n                    }\n\n                    @Override\n                    public LabelGraphic getIcon() {\n                        return new LabelGraphic.CompGraphic(BrowserIcons.createDefaultFileIcon());\n                    }\n\n                    @Override\n                    public ObservableValue<String> getName(\n                            BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                        return AppI18n.observable(\"symbolicLink\");\n                    }\n                });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryInNewTabMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\n\nimport java.util.List;\n\npublic class OpenDirectoryInNewTabMenuProvider implements BrowserMenuLeafProvider {\n\n    @Override\n    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        if (model.getBrowserModel() instanceof BrowserFullSessionModel bm) {\n            bm.openFileSystemAsync(\n                    model.getEntry(),\n                    null,\n                    m -> entries.getFirst().getRawFileEntry().getPath(),\n                    null);\n        }\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return model.getBrowserModel() instanceof BrowserFullSessionModel\n                && entries.size() == 1\n                && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY);\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2f-folder-open-outline\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.OPEN;\n    }\n\n    @Override\n    public KeyCombination getShortcut() {\n        return new KeyCodeCombination(KeyCode.ENTER, KeyCombination.SHIFT_DOWN);\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"openInNewTab\");\n    }\n\n    @Override\n    public boolean acceptsEmptySelection() {\n        return true;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.action.impl.OpenDirectoryActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\n\nimport java.util.List;\n\npublic class OpenDirectoryMenuProvider implements BrowserMenuLeafProvider {\n\n    @Override\n    public Class<? extends BrowserActionProvider> getDelegateActionProvider() {\n        return OpenDirectoryActionProvider.class;\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2f-folder-open\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.OPEN;\n    }\n\n    @Override\n    public KeyCombination getShortcut() {\n        return new KeyCodeCombination(KeyCode.ENTER);\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"open\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileDefaultMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.action.impl.OpenFileDefaultActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\n\nimport java.util.List;\n\npublic class OpenFileDefaultMenuProvider implements BrowserMenuLeafProvider {\n\n    @Override\n    public Class<? extends BrowserActionProvider> getDelegateActionProvider() {\n        return OpenFileDefaultActionProvider.class;\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2b-book-open-variant\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.OPEN;\n    }\n\n    @Override\n    public KeyCombination getShortcut() {\n        return new KeyCodeCombination(KeyCode.ENTER);\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"openWithDefaultApplication\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileWithMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.action.impl.OpenFileWithActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\n\nimport java.util.List;\n\npublic class OpenFileWithMenuProvider implements BrowserMenuLeafProvider {\n\n    @Override\n    public Class<? extends BrowserActionProvider> getDelegateActionProvider() {\n        return OpenFileWithActionProvider.class;\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return OsType.ofLocal() == OsType.WINDOWS\n                && entries.size() == 1\n                && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE);\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2b-book-open-page-variant-outline\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.OPEN;\n    }\n\n    @Override\n    public KeyCombination getShortcut() {\n        return new KeyCodeCombination(KeyCode.ENTER, KeyCombination.SHIFT_DOWN);\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"openFileWith\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/OpenNativeFileDetailsMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.action.impl.OpenFileNativeDetailsActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\n\nimport java.util.List;\n\npublic class OpenNativeFileDetailsMenuProvider implements BrowserMenuLeafProvider {\n\n    @Override\n    public Class<? extends BrowserActionProvider> getDelegateActionProvider() {\n        return OpenFileNativeDetailsActionProvider.class;\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.OPEN;\n    }\n\n    @Override\n    public KeyCombination getShortcut() {\n        return new KeyCodeCombination(KeyCode.ENTER, KeyCombination.ALT_DOWN);\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"showDetails\");\n    }\n\n    @Override\n    public boolean acceptsEmptySelection() {\n        return true;\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2f-folder-information-outline\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/OpenTerminalInDirectoryMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.core.FilePath;\n\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\n\nimport java.util.Collections;\nimport java.util.List;\n\npublic class OpenTerminalInDirectoryMenuProvider implements BrowserMenuLeafProvider {\n\n    @Override\n    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var dirs = entries.size() > 0\n                ? entries.stream()\n                        .map(browserEntry -> browserEntry.getRawFileEntry().getPath())\n                        .toList()\n                : model.getCurrentDirectory() != null\n                        ? List.of(model.getCurrentDirectory().getPath())\n                        : Collections.singletonList((FilePath) null);\n        for (var dir : dirs) {\n            var name = model.getFileSystem().supportsTerminalWorkingDirectory() && dir != null ? dir.toString() : null;\n            model.openTerminalAsync(\n                    name,\n                    model.getFileSystem().supportsTerminalWorkingDirectory() ? dir : null,\n                    model.getFileSystem().getRawShellControl().orElseThrow(),\n                    dirs.size() == 1);\n        }\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return model.getFileSystem().supportsTerminalOpen()\n                && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY);\n    }\n\n    public String getId() {\n        return \"openInTerminal\";\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2c-console\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.OPEN;\n    }\n\n    @Override\n    public KeyCombination getShortcut() {\n        return new KeyCodeCombination(KeyCode.T, KeyCombination.SHORTCUT_DOWN);\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"openInTerminal\");\n    }\n\n    @Override\n    public boolean isActive(BrowserFileSystemTabModel model) {\n        var t = AppPrefs.get().terminalType().getValue();\n        return t != null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/PasteMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.file.BrowserClipboard;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.file.BrowserFileTransferMode;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\n\nimport java.util.List;\n\npublic class PasteMenuProvider implements BrowserMenuLeafProvider {\n\n    @Override\n    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var clipboard = BrowserClipboard.retrieveCopy();\n        if (clipboard == null) {\n            return;\n        }\n\n        var target = entries.size() == 1 && entries.getFirst().getRawFileEntry().getKind() == FileKind.DIRECTORY\n                ? entries.getFirst().getRawFileEntry()\n                : model.getCurrentDirectory();\n        var files = clipboard.getEntries();\n        if (files.size() == 0) {\n            return;\n        }\n\n        var isDuplication = files.size() == 1\n                && model.getFileSystem()\n                        .equals(files.getFirst().getRawFileEntry().getFileSystem())\n                && target.getPath()\n                        .equals(files.getFirst().getRawFileEntry().getPath().getParent());\n        if (isDuplication) {\n            model.duplicateFile(files.getFirst().getRawFileEntry());\n        } else {\n            model.dropFilesIntoAsync(\n                    target,\n                    files.stream()\n                            .map(browserEntry -> browserEntry.getRawFileEntry())\n                            .toList(),\n                    BrowserFileTransferMode.COPY);\n        }\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var clipboard = BrowserClipboard.retrieveCopy();\n        if (clipboard == null) {\n            return false;\n        }\n\n        return (entries.size() == 1\n                        && entries.stream()\n                                .allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY))\n                || entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE);\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2c-content-paste\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.COPY_PASTE;\n    }\n\n    @Override\n    public KeyCombination getShortcut() {\n        return new KeyCodeCombination(KeyCode.V, KeyCombination.SHORTCUT_DOWN);\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"paste\");\n    }\n\n    @Override\n    public boolean acceptsEmptySelection() {\n        return true;\n    }\n\n    @Override\n    public boolean isActive(BrowserFileSystemTabModel model) {\n        return BrowserClipboard.retrieveCopy() != null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/RefreshDirectoryMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.util.BooleanScope;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\n\nimport java.util.List;\n\npublic class RefreshDirectoryMenuProvider implements BrowserMenuLeafProvider {\n\n    @Override\n    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        ThreadHelper.runAsync(() -> {\n            BooleanScope.executeExclusive(model.getBusy(), () -> {\n                model.refreshSync();\n            });\n        });\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return false;\n    }\n\n    public String getId() {\n        return \"refresh\";\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdmz-refresh\");\n    }\n\n    @Override\n    public KeyCombination getShortcut() {\n        return new KeyCodeCombination(KeyCode.F5);\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"refresh\");\n    }\n\n    @Override\n    public boolean isActive(BrowserFileSystemTabModel model) {\n        return !model.getInOverview().get();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/RenameMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\n\nimport java.util.List;\n\npublic class RenameMenuProvider implements BrowserMenuLeafProvider {\n\n    @Override\n    public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        model.getFileList().getEditing().setValue(entries.getFirst());\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return entries.size() == 1 && entries.getFirst().getRawFileEntry().getKind() != FileKind.LINK;\n    }\n\n    @Override\n    public boolean automaticallyResolveLinks() {\n        return false;\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2r-rename-box\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.MUTATION;\n    }\n\n    @Override\n    public KeyCombination getShortcut() {\n        return new KeyCodeCombination(KeyCode.R, KeyCombination.SHORTCUT_DOWN);\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"rename\");\n    }\n\n    @Override\n    public String getId() {\n        return \"renameFile\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/RunFileMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl;\n\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.MultiExecuteMenuProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.FileEntry;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.ShellDialects;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\nimport java.util.stream.Stream;\n\npublic class RunFileMenuProvider extends MultiExecuteMenuProvider {\n\n    private boolean isExecutable(FileEntry e) {\n        if (e.getKind() != FileKind.FILE) {\n            return false;\n        }\n\n        var shell = e.getFileSystem().getShell();\n        if (shell.isEmpty()) {\n            return false;\n        }\n        var os = shell.get().getOsType();\n\n        if (e.getInfo() != null && e.getInfo().possiblyExecutable() && os != OsType.WINDOWS) {\n            return true;\n        }\n\n        if (os == OsType.WINDOWS\n                && Stream.of(\"exe\", \"bat\", \"ps1\", \"cmd\")\n                        .anyMatch(s -> e.getPath().toString().endsWith(s))) {\n            return true;\n        }\n\n        if (ShellDialects.isPowershell(shell.get())\n                && Stream.of(\"ps1\").anyMatch(s -> e.getPath().toString().endsWith(s))) {\n            return true;\n        }\n\n        if (Stream.of(\"sh\", \"command\").anyMatch(s -> e.getPath().toString().endsWith(s))) {\n            return true;\n        }\n\n        return false;\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2p-play\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.CUSTOM;\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"run\");\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return entries.stream().allMatch(entry -> isExecutable(entry.getRawFileEntry()));\n    }\n\n    @Override\n    protected List<CommandBuilder> createCommand(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var sc = model.getFileSystem().getShell().orElseThrow();\n        return entries.stream()\n                .map(browserEntry -> {\n                    return CommandBuilder.of()\n                            .add(sc.getShellDialect()\n                                    .runScriptCommand(\n                                            sc,\n                                            browserEntry\n                                                    .getRawFileEntry()\n                                                    .getPath()\n                                                    .toString()));\n                })\n                .toList();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUntarMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl.compress;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.icon.BrowserIconFileType;\nimport io.xpipe.app.browser.icon.BrowserIcons;\nimport io.xpipe.app.browser.menu.BrowserApplicationPathMenuProvider;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.process.OsFileSystem;\nimport io.xpipe.core.FilePath;\n\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\n\npublic class BaseUntarMenuProvider implements BrowserApplicationPathMenuProvider, BrowserMenuLeafProvider {\n\n    private final boolean gz;\n    private final boolean toDirectory;\n\n    public BaseUntarMenuProvider(boolean gz, boolean toDirectory) {\n        this.gz = gz;\n        this.toDirectory = toDirectory;\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.CompGraphic(BrowserIcons.createContextMenuIcon(BrowserIconFileType.byId(\"zip\")));\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.CUSTOM;\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var sep = OsFileSystem.of(model.getFileSystem().getShell().orElseThrow().getOsType())\n                .getFileSystemSeparator();\n        var dir = entries.size() > 1\n                ? \"[...]\"\n                : getTarget(entries.getFirst().getRawFileEntry().getPath()).getFileName() + sep;\n        return toDirectory ? AppI18n.observable(\"untarDirectory\", dir) : AppI18n.observable(\"untarHere\");\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        if (!BrowserApplicationPathMenuProvider.super.isApplicable(model, entries)\n                || !BrowserMenuLeafProvider.super.isApplicable(model, entries)) {\n            return false;\n        }\n\n        if (gz) {\n            return entries.stream()\n                    .allMatch(entry -> entry.getRawFileEntry()\n                                    .getPath()\n                                    .toString()\n                                    .endsWith(\".tar.gz\")\n                            || entry.getRawFileEntry().getPath().toString().endsWith(\".tgz\"));\n        }\n\n        return entries.stream()\n                .allMatch(entry -> entry.getRawFileEntry().getPath().toString().endsWith(\".tar\"));\n    }\n\n    @Override\n    public String getExecutable() {\n        return \"tar\";\n    }\n\n    @Override\n    public AbstractAction createAction(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var builder = UntarActionProvider.Action.builder();\n        builder.initEntries(model, entries);\n        builder.gz(gz);\n        builder.toDirectory(toDirectory);\n        return builder.build();\n    }\n\n    @Override\n    public boolean automaticallyResolveLinks() {\n        return false;\n    }\n\n    private FilePath getTarget(FilePath name) {\n        return FilePath.of(name.toString()\n                .replaceAll(\"\\\\.tar$\", \"\")\n                .replaceAll(\"\\\\.tar.gz$\", \"\")\n                .replaceAll(\"\\\\.tgz$\", \"\"));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipUnixMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl.compress;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.icon.BrowserIconFileType;\nimport io.xpipe.app.browser.icon.BrowserIcons;\nimport io.xpipe.app.browser.menu.BrowserApplicationPathMenuProvider;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.process.OsFileSystem;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\n\npublic abstract class BaseUnzipUnixMenuProvider implements BrowserMenuLeafProvider, BrowserApplicationPathMenuProvider {\n\n    private final boolean toDirectory;\n\n    public BaseUnzipUnixMenuProvider(boolean toDirectory) {\n        this.toDirectory = toDirectory;\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.CompGraphic(BrowserIcons.createContextMenuIcon(BrowserIconFileType.byId(\"zip\")));\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.CUSTOM;\n    }\n\n    @Override\n    public boolean automaticallyResolveLinks() {\n        return false;\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var sep = OsFileSystem.of(model.getFileSystem().getShell().orElseThrow().getOsType())\n                .getFileSystemSeparator();\n        var dir = entries.size() > 1\n                ? \"[...]\"\n                : UnzipActionProvider.getTarget(\n                                        entries.getFirst().getRawFileEntry().getPath())\n                                .getFileName()\n                        + sep;\n        return toDirectory ? AppI18n.observable(\"unzipDirectory\", dir) : AppI18n.observable(\"unzipHere\");\n    }\n\n    @Override\n    public String getExecutable() {\n        return \"unzip\";\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        if (!BrowserApplicationPathMenuProvider.super.isApplicable(model, entries)\n                || !BrowserMenuLeafProvider.super.isApplicable(model, entries)) {\n            return false;\n        }\n\n        return entries.stream()\n                        .allMatch(entry ->\n                                entry.getRawFileEntry().getPath().toString().endsWith(\".zip\"))\n                && model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS;\n    }\n\n    @Override\n    public AbstractAction createAction(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var builder = UnzipActionProvider.Action.builder();\n        builder.initEntries(model, entries);\n        builder.toDirectory(toDirectory);\n        return builder.build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipWindowsActionProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl.compress;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.icon.BrowserIconFileType;\nimport io.xpipe.app.browser.icon.BrowserIcons;\nimport io.xpipe.app.browser.menu.BrowserMenuCategory;\nimport io.xpipe.app.browser.menu.BrowserMenuLeafProvider;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.process.OsFileSystem;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\n\npublic abstract class BaseUnzipWindowsActionProvider implements BrowserMenuLeafProvider {\n\n    private final boolean toDirectory;\n\n    public BaseUnzipWindowsActionProvider(boolean toDirectory) {\n        this.toDirectory = toDirectory;\n    }\n\n    @Override\n    public boolean automaticallyResolveLinks() {\n        return false;\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.CompGraphic(BrowserIcons.createContextMenuIcon(BrowserIconFileType.byId(\"zip\")));\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.CUSTOM;\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var sep = OsFileSystem.of(model.getFileSystem().getShell().orElseThrow().getOsType())\n                .getFileSystemSeparator();\n        var dir = entries.size() > 1\n                ? \"[...]\"\n                : UnzipActionProvider.getTarget(\n                                        entries.getFirst().getRawFileEntry().getPath())\n                                .getFileName()\n                        + sep;\n        return toDirectory ? AppI18n.observable(\"unzipDirectory\", dir) : AppI18n.observable(\"unzipHere\");\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        if (model.getFileSystem().getShell().isEmpty()) {\n            return false;\n        }\n\n        return entries.stream()\n                        .allMatch(entry ->\n                                entry.getRawFileEntry().getPath().toString().endsWith(\".zip\"))\n                && model.getFileSystem().getShell().orElseThrow().getOsType() == OsType.WINDOWS;\n    }\n\n    @Override\n    public AbstractAction createAction(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var builder = UnzipActionProvider.Action.builder();\n        builder.initEntries(model, entries);\n        builder.toDirectory(toDirectory);\n        return builder.build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/compress/CompressMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl.compress;\n\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.*;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.control.TextField;\n\nimport lombok.SneakyThrows;\n\nimport java.util.List;\n\npublic class CompressMenuProvider implements BrowserMenuBranchProvider {\n\n    @Override\n    public void init(BrowserFileSystemTabModel model) throws Exception {\n        if (model.getFileSystem().getShell().isEmpty()) {\n            return;\n        }\n\n        var sc = model.getFileSystem().getShell().orElseThrow();\n\n        sc.view().isInPath(\"tar\", true);\n        sc.view().isInPath(\"zip\", true);\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2a-archive\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.ACTION;\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"compress\");\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        if (model.getFileSystem().getShell().isEmpty()) {\n            return false;\n        }\n\n        var ext = List.of(\"zip\", \"tar\", \"tar.gz\", \"tgz\", \"rar\", \"xar\");\n        if (entries.stream().anyMatch(browserEntry -> ext.stream().anyMatch(s -> browserEntry\n                .getRawFileEntry()\n                .getPath()\n                .toString()\n                .toLowerCase()\n                .endsWith(\".\" + s)))) {\n            return false;\n        }\n\n        return true;\n    }\n\n    @Override\n    public List<BrowserMenuItemProvider> getBranchingActions(\n            BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var contentsOptions =\n                entries.size() == 1 && entries.getFirst().getRawFileEntry().getKind() == FileKind.DIRECTORY;\n        if (contentsOptions) {\n            return List.of(new BranchProvider(false), new BranchProvider(true));\n        }\n\n        return List.of(\n                new ZipActionProvider(false),\n                new TarBasedActionProvider(false, true) {\n\n                    @Override\n                    protected String getExtension() {\n                        return \"tar.gz\";\n                    }\n                },\n                new TarBasedActionProvider(false, false) {\n                    @Override\n                    protected String getExtension() {\n                        return \"tar\";\n                    }\n                });\n    }\n\n    private abstract static class LeafProvider implements BrowserMenuLeafProvider {\n\n        protected final boolean directory;\n\n        private LeafProvider(boolean directory) {\n            this.directory = directory;\n        }\n\n        @Override\n        public boolean automaticallyResolveLinks() {\n            return false;\n        }\n\n        @Override\n        public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            var name = new SimpleStringProperty(directory ? entries.getFirst().getFileName() : null);\n            var modal = ModalOverlay.of(\n                    \"archiveName\",\n                    RegionBuilder.of(() -> {\n                                var creationName = new TextField();\n                                creationName.textProperty().bindBidirectional(name);\n                                return creationName;\n                            })\n                            .prefWidth(350));\n            modal.withDefaultButtons(() -> {\n                var fixedName = name.getValue();\n                if (fixedName == null) {\n                    return;\n                }\n\n                if (!fixedName.endsWith(getExtension())) {\n                    fixedName = fixedName + \".\" + getExtension();\n                }\n\n                create(fixedName, model, entries);\n            });\n            modal.show();\n        }\n\n        @Override\n        public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return new SimpleStringProperty(\".\" + getExtension());\n        }\n\n        protected abstract void create(String fileName, BrowserFileSystemTabModel model, List<BrowserEntry> entries);\n\n        protected abstract String getExtension();\n    }\n\n    private class BranchProvider implements BrowserMenuBranchProvider {\n\n        private final boolean directory;\n\n        private BranchProvider(boolean directory) {\n            this.directory = directory;\n        }\n\n        @Override\n        public LabelGraphic getIcon() {\n            return directory\n                    ? new LabelGraphic.IconGraphic(\"mdi2f-file-tree\")\n                    : new LabelGraphic.IconGraphic(\"mdi2f-file-outline\");\n        }\n\n        @Override\n        public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return AppI18n.observable(directory ? \"excludeRoot\" : \"includeRoot\");\n        }\n\n        @Override\n        public List<? extends BrowserMenuItemProvider> getBranchingActions(\n                BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return List.of(\n                    new ZipActionProvider(directory),\n                    new TarBasedActionProvider(directory, true) {\n\n                        @Override\n                        protected String getExtension() {\n                            return \"tar.gz\";\n                        }\n                    },\n                    new TarBasedActionProvider(directory, false) {\n                        @Override\n                        protected String getExtension() {\n                            return \"tar\";\n                        }\n                    });\n        }\n    }\n\n    private class ZipActionProvider extends LeafProvider {\n\n        private ZipActionProvider(boolean directory) {\n            super(directory);\n        }\n\n        @Override\n        protected void create(String fileName, BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            var builder = io.xpipe.app.browser.menu.impl.compress.ZipActionProvider.Action.builder();\n            builder.initEntries(model, entries);\n            builder.target(model.getCurrentDirectory().getPath().join(fileName));\n            builder.directoryContentOnly(directory);\n            builder.build().executeAsync();\n        }\n\n        @Override\n        protected String getExtension() {\n            return \"zip\";\n        }\n    }\n\n    private abstract class TarBasedActionProvider extends LeafProvider {\n\n        private final boolean gz;\n\n        private TarBasedActionProvider(boolean directory, boolean gz) {\n            super(directory);\n            this.gz = gz;\n        }\n\n        @Override\n        protected void create(String fileName, BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            var builder = TarActionProvider.Action.builder();\n            builder.initEntries(model, entries);\n            builder.target(model.getCurrentDirectory().getPath().join(fileName));\n            builder.directoryContentOnly(directory);\n            builder.gz(gz);\n            builder.build().executeAsync();\n        }\n\n        @Override\n        @SneakyThrows\n        public boolean isActive(BrowserFileSystemTabModel model) {\n            return model.getFileSystem().getShell().orElseThrow().view().isInPath(\"tar\", true);\n        }\n\n        @Override\n        public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n            return model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS || !directory;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/compress/TarActionProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl.compress;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.core.FilePath;\n\nimport lombok.NonNull;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class TarActionProvider implements BrowserActionProvider {\n\n    @Override\n    public String getId() {\n        return \"tar\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        @NonNull\n        private final FilePath target;\n\n        private final boolean directoryContentOnly;\n\n        private final boolean gz;\n\n        @Override\n        public void executeImpl() throws Exception {\n            var sc = model.getFileSystem().getShell().orElseThrow();\n            var args = \"c\" + (gz ? \"z\" : \"\") + \"f\";\n            var tar = CommandBuilder.of().add(\"tar\", args).addFile(target);\n            var base = model.getCurrentDirectory().getPath();\n\n            if (directoryContentOnly) {\n                var dir = getEntries().getFirst().getRawFileEntry().getPath();\n                // Fix for bsd find, remove /\n                var command = CommandBuilder.of()\n                        .add(\"find\")\n                        .addFile(dir.removeTrailingSlash().toUnix())\n                        .add(\"|\", \"sed\")\n                        .addLiteral(\"s,^\" + dir.toDirectory().toUnix() + \"*,,\")\n                        .add(\"|\");\n                command.add(tar).add(\"-C\").addFile(dir.toDirectory().toUnix()).add(\"-T\", \"-\");\n                sc.command(command).execute();\n            } else {\n                var command = CommandBuilder.of().add(tar);\n                for (BrowserEntry entry : getEntries()) {\n                    var rel = entry.getRawFileEntry().getPath().relativize(base);\n                    command.addFile(rel);\n                }\n                sc.command(command).execute();\n            }\n            model.refreshSync();\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarActionProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl.compress;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.core.FilePath;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class UntarActionProvider implements BrowserActionProvider {\n\n    @Override\n    public String getId() {\n        return \"untar\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        private final boolean gz;\n        private final boolean toDirectory;\n\n        @Override\n        public void executeImpl() throws Exception {\n            ShellControl sc = model.getFileSystem().getShell().orElseThrow();\n            for (BrowserEntry entry : getEntries()) {\n                var target = getTarget(entry.getRawFileEntry().getPath());\n                var c = CommandBuilder.of().add(\"tar\");\n                var args = \"x\" + (gz ? \"z\" : \"\") + \"f\";\n                c.add(args);\n                c.addFile(entry.getRawFileEntry().getPath());\n                if (toDirectory) {\n                    model.getFileSystem().mkdirs(target);\n                }\n                sc.command(c)\n                        .withWorkingDirectory(\n                                toDirectory\n                                        ? target\n                                        : model.getCurrentDirectory().getPath())\n                        .execute();\n            }\n            model.refreshSync();\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n\n        private FilePath getTarget(FilePath name) {\n            return FilePath.of(name.toString()\n                    .replaceAll(\"\\\\.tar$\", \"\")\n                    .replaceAll(\"\\\\.tar.gz$\", \"\")\n                    .replaceAll(\"\\\\.tgz$\", \"\"));\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarDirectoryMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl.compress;\n\npublic class UntarDirectoryMenuProvider extends BaseUntarMenuProvider {\n\n    public UntarDirectoryMenuProvider() {\n        super(false, true);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarGzDirectoryMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl.compress;\n\npublic class UntarGzDirectoryMenuProvider extends BaseUntarMenuProvider {\n\n    public UntarGzDirectoryMenuProvider() {\n        super(true, true);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarGzHereMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl.compress;\n\npublic class UntarGzHereMenuProvider extends BaseUntarMenuProvider {\n\n    public UntarGzHereMenuProvider() {\n        super(true, false);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarHereMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl.compress;\n\npublic class UntarHereMenuProvider extends BaseUntarMenuProvider {\n\n    public UntarHereMenuProvider() {\n        super(false, false);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipActionProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl.compress;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.ShellDialects;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class UnzipActionProvider implements BrowserActionProvider {\n\n    public static FilePath getTarget(FilePath name) {\n        return FilePath.of(name.toString().replaceAll(\"\\\\.zip$\", \"\"));\n    }\n\n    @Override\n    public String getId() {\n        return \"unzip\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        private final boolean toDirectory;\n\n        @Override\n        public void executeImpl() throws Exception {\n            var sc = model.getFileSystem().getShell().orElseThrow();\n            if (sc.getOsType() == OsType.WINDOWS) {\n                if (ShellDialects.isPowershell(sc)) {\n                    for (BrowserEntry entry : getEntries()) {\n                        runPowershellCommand(sc, model, entry);\n                    }\n                } else {\n                    try (var sub = sc.subShell(ShellDialects.POWERSHELL)) {\n                        for (BrowserEntry entry : getEntries()) {\n                            runPowershellCommand(sub, model, entry);\n                        }\n                    }\n                }\n            } else {\n                for (BrowserEntry entry : getEntries()) {\n                    var command = CommandBuilder.of()\n                            .add(\"unzip\", \"-o\")\n                            .addFile(entry.getRawFileEntry().getPath());\n                    if (toDirectory) {\n                        command.add(\"-d\")\n                                .addFile(getTarget(entry.getRawFileEntry().getPath()));\n                    }\n                    try (var cc = sc.command(command)\n                            .withWorkingDirectory(model.getCurrentDirectory().getPath())\n                            .start()) {\n                        cc.discardOrThrow();\n                    }\n                }\n            }\n            model.refreshSync();\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n\n        private void runPowershellCommand(ShellControl sc, BrowserFileSystemTabModel model, BrowserEntry entry)\n                throws Exception {\n            var command = CommandBuilder.of().add(\"Expand-Archive\", \"-Force\");\n            if (toDirectory) {\n                var target = getTarget(entry.getRawFileEntry().getPath());\n                command.add(\"-DestinationPath\").addFile(target);\n            }\n            command.add(\"-Path\").addFile(entry.getRawFileEntry().getPath());\n            sc.command(command)\n                    .withWorkingDirectory(model.getCurrentDirectory().getPath())\n                    .execute();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipDirectoryUnixMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl.compress;\n\npublic class UnzipDirectoryUnixMenuProvider extends BaseUnzipUnixMenuProvider {\n\n    public UnzipDirectoryUnixMenuProvider() {\n        super(true);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipDirectoryWindowsActionProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl.compress;\n\npublic class UnzipDirectoryWindowsActionProvider extends BaseUnzipWindowsActionProvider {\n\n    public UnzipDirectoryWindowsActionProvider() {\n        super(true);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipHereUnixMenuProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl.compress;\n\npublic class UnzipHereUnixMenuProvider extends BaseUnzipUnixMenuProvider {\n\n    public UnzipHereUnixMenuProvider() {\n        super(false);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipHereWindowsActionProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl.compress;\n\npublic class UnzipHereWindowsActionProvider extends BaseUnzipWindowsActionProvider {\n\n    public UnzipHereWindowsActionProvider() {\n        super(false);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/browser/menu/impl/compress/ZipActionProvider.java",
    "content": "package io.xpipe.app.browser.menu.impl.compress;\n\nimport io.xpipe.app.browser.action.BrowserAction;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.ShellDialects;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport lombok.NonNull;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class ZipActionProvider implements BrowserActionProvider {\n\n    @Override\n    public String getId() {\n        return \"zip\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends BrowserAction {\n\n        @NonNull\n        private final FilePath target;\n\n        private final boolean directoryContentOnly;\n\n        @Override\n        public void executeImpl() throws Exception {\n            try {\n                var sc = model.getFileSystem().getShell().orElseThrow();\n                if (sc.getOsType() == OsType.WINDOWS) {\n                    var base = model.getCurrentDirectory().getPath();\n                    var command = CommandBuilder.of()\n                            .add(\"Compress-Archive\", \"-Force\", \"-DestinationPath\")\n                            .addFile(target)\n                            .add(\"-Path\");\n                    for (int i = 0; i < getEntries().size(); i++) {\n                        var rel =\n                                getEntries().get(i).getRawFileEntry().getPath().relativize(base);\n                        if (getEntries().get(i).getRawFileEntry().getKind() == FileKind.DIRECTORY\n                                && directoryContentOnly) {\n                            command.addQuoted(rel.toDirectory().toWindows() + \"*\");\n                        } else {\n                            command.addFile(rel.toWindows());\n                        }\n                        if (i != getEntries().size() - 1) {\n                            command.add(\",\");\n                        }\n                    }\n\n                    if (ShellDialects.isPowershell(sc)) {\n                        sc.command(command).withWorkingDirectory(base).execute();\n                    } else {\n                        try (var sub = sc.subShell(ShellDialects.POWERSHELL)) {\n                            sub.command(command).withWorkingDirectory(base).execute();\n                        }\n                    }\n                } else {\n                    var command = CommandBuilder.of().add(\"zip\", \"-q\", \"-y\", \"-r\", \"-\");\n                    for (BrowserEntry entry : getEntries()) {\n                        var base = target.getParent();\n                        var rel = entry.getRawFileEntry()\n                                .getPath()\n                                .relativize(base)\n                                .toUnix();\n                        if (entry.getRawFileEntry().getKind() == FileKind.DIRECTORY && directoryContentOnly) {\n                            command.add(\".\");\n                        } else {\n                            command.addFile(rel);\n                        }\n                    }\n                    command.add(\">\").addFile(target);\n\n                    if (directoryContentOnly) {\n                        sc.command(command)\n                                .withWorkingDirectory(getEntries()\n                                        .getFirst()\n                                        .getRawFileEntry()\n                                        .getPath())\n                                .execute();\n                    } else {\n                        sc.command(command).execute();\n                    }\n                }\n            } finally {\n                model.refreshSync();\n            }\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/BaseRegionBuilder.java",
    "content": "package io.xpipe.app.comp;\n\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.beans.value.ObservableValue;\nimport javafx.geometry.Insets;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Priority;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.VBox;\n\nimport org.int4.fx.builders.common.AbstractRegionBuilder;\n\nimport java.util.function.Consumer;\n\npublic abstract class BaseRegionBuilder<T extends Region, B extends BaseRegionBuilder<T, B>>\n        extends AbstractRegionBuilder<T, B> {\n\n    public BaseRegionBuilder() {\n        apply(t -> {\n            BindingsHelper.preserve(t, BaseRegionBuilder.this);\n        });\n    }\n\n    public B hgrow() {\n        apply(t -> HBox.setHgrow(t, Priority.ALWAYS));\n        return self();\n    }\n\n    public B vgrow() {\n        apply(t -> VBox.setVgrow(t, Priority.ALWAYS));\n        return self();\n    }\n\n    public B describe(Consumer<RegionDescriptor.RegionDescriptorBuilder> c) {\n        apply(r -> {\n            var b = RegionDescriptor.builder();\n            c.accept(b);\n            b.build().apply(r);\n        });\n        return self();\n    }\n\n    public B visible(ObservableValue<Boolean> o) {\n        return apply(struc -> {\n            var region = struc;\n            BindingsHelper.preserve(region, o);\n            o.subscribe(n -> {\n                PlatformThread.runLaterIfNeeded(() -> {\n                    region.setVisible(n);\n                });\n            });\n        });\n    }\n\n    public B padding(Insets insets) {\n        return apply(struc -> struc.setPadding(insets));\n    }\n\n    public B disable(ObservableValue<Boolean> o) {\n        return apply(struc -> {\n            var region = struc;\n            BindingsHelper.preserve(region, o);\n            o.subscribe(n -> {\n                PlatformThread.runLaterIfNeeded(() -> {\n                    region.setDisable(n);\n                });\n            });\n        });\n    }\n\n    public B show(ObservableValue<Boolean> when) {\n        return this.hide(when.map((b) -> !b).orElse(true));\n    }\n\n    public B hide(ObservableValue<Boolean> o) {\n        return apply(struc -> {\n            var region = struc;\n            BindingsHelper.preserve(region, o);\n            o.subscribe(n -> {\n                PlatformThread.runLaterIfNeeded(() -> {\n                    if (!n) {\n                        region.setVisible(true);\n                        region.setManaged(true);\n                    } else {\n                        region.setVisible(false);\n                        region.setManaged(false);\n                    }\n                });\n            });\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/RegionBuilder.java",
    "content": "package io.xpipe.app.comp;\n\nimport javafx.geometry.Orientation;\nimport javafx.scene.control.Separator;\nimport javafx.scene.layout.Region;\n\nimport atlantafx.base.controls.Spacer;\n\nimport java.util.function.Supplier;\n\npublic abstract class RegionBuilder<T extends Region> extends BaseRegionBuilder<T, RegionBuilder<T>> {\n\n    public static RegionBuilder<Region> empty() {\n        return of(() -> {\n            var r = new Region();\n            r.getStyleClass().add(\"empty\");\n            return r;\n        });\n    }\n\n    public static RegionBuilder<Spacer> hspacer() {\n        return of(() -> new Spacer(Orientation.HORIZONTAL));\n    }\n\n    public static RegionBuilder<Spacer> hspacer(double size) {\n        return of(() -> new Spacer(size));\n    }\n\n    public static RegionBuilder<Spacer> vspacer() {\n        return of(() -> new Spacer(Orientation.VERTICAL));\n    }\n\n    public static RegionBuilder<Spacer> vspacer(double size) {\n        return of(() -> new Spacer(size, Orientation.VERTICAL));\n    }\n\n    public static RegionBuilder<Separator> hseparator() {\n        return of(() -> new Separator(Orientation.HORIZONTAL));\n    }\n\n    public static RegionBuilder<Separator> vseparator() {\n        return of(() -> new Separator(Orientation.VERTICAL));\n    }\n\n    public static <R extends Region> RegionBuilder<R> of(Supplier<R> r) {\n        return new RegionBuilder<>() {\n\n            @Override\n            protected R createSimple() {\n                return r.get();\n            }\n        };\n    }\n\n    @Override\n    public final T build() {\n        var r = createSimple();\n        initialize(r);\n        return r;\n    }\n\n    protected abstract T createSimple();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/RegionDescriptor.java",
    "content": "package io.xpipe.app.comp;\n\nimport io.xpipe.app.comp.base.TooltipHelper;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.ReadOnlyObjectWrapper;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.control.Tooltip;\nimport javafx.scene.input.KeyCombination;\nimport javafx.scene.layout.Region;\n\nimport lombok.Builder;\nimport lombok.Value;\n\n@Value\n@Builder\npublic class RegionDescriptor {\n\n    ObservableValue<String> name;\n    ObservableValue<String> description;\n    KeyCombination shortcut;\n    FocusTraversal focusTraversal;\n\n    @Builder.Default\n    boolean showTooltips = true;\n\n    public enum FocusTraversal {\n        DISABLED,\n        ENABLED_FOR_ACCESSIBILITY,\n        ENABLED\n    }\n\n    public void apply(Region r) {\n        var accessibleText = getName() != null\n                ? Bindings.createStringBinding(\n                        () -> {\n                            var s = getName().getValue() + \"\\n\\n\";\n                            if (getShortcut() != null) {\n                                s += AppI18n.get(\"shortcut\") + \": \"\n                                        + getShortcut().getDisplayText();\n                            }\n                            return s;\n                        },\n                        AppI18n.activeLanguage(),\n                        getName())\n                : null;\n\n        if (showTooltips) {\n            var tooltipText = Bindings.createStringBinding(\n                    () -> {\n                        var s = \"\";\n                        if (getName() != null) {\n                            s += getName().getValue() + \"\\n\\n\";\n                        }\n                        if (getDescription() != null) {\n                            var desc = getDescription().getValue();\n                            if (desc != null) {\n                                s += desc + \"\\n\\n\";\n                            }\n                        }\n                        if (getShortcut() != null) {\n                            s += AppI18n.get(\"shortcut\") + \": \" + getShortcut().getDisplayText();\n                        }\n                        return s.strip();\n                    },\n                    AppI18n.activeLanguage(),\n                    getName() != null ? getName() : new ReadOnlyObjectWrapper<>(),\n                    getDescription() != null ? getDescription() : new ReadOnlyObjectWrapper<>());\n\n            var tt = TooltipHelper.create(PlatformThread.sync(tooltipText));\n            Tooltip.install(r, tt);\n        }\n\n        if (accessibleText != null) {\n            r.accessibleTextProperty().bind(PlatformThread.sync(getName()));\n        }\n        if (getDescription() != null) {\n            r.accessibleHelpProperty().bind(PlatformThread.sync(getDescription()));\n        }\n        if (getFocusTraversal() != null) {\n            switch (getFocusTraversal()) {\n                case DISABLED -> {\n                    r.setFocusTraversable(false);\n                }\n                case ENABLED_FOR_ACCESSIBILITY -> {\n                    r.focusTraversableProperty().bind(Platform.accessibilityActiveProperty());\n                }\n                case ENABLED -> {\n                    r.setFocusTraversable(true);\n                }\n            }\n        }\n    }\n\n    public static class RegionDescriptorBuilder {\n\n        public RegionDescriptorBuilder nameKey(String key) {\n            return name(AppI18n.observable(key));\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/RegionStructure.java",
    "content": "package io.xpipe.app.comp;\n\nimport javafx.scene.layout.Region;\n\npublic interface RegionStructure<R extends Region> {\n\n    R get();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/RegionStructureBuilder.java",
    "content": "package io.xpipe.app.comp;\n\nimport javafx.scene.layout.Region;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Consumer;\n\npublic abstract class RegionStructureBuilder<R extends Region, S extends RegionStructure<R>>\n        extends BaseRegionBuilder<R, RegionStructureBuilder<R, S>> {\n\n    private final List<Consumer<? super S>> options = new ArrayList<>();\n\n    public final RegionStructureBuilder<R, S> applyStructure(Consumer<? super S> option) {\n        options.add(option);\n        return self();\n    }\n\n    protected final void initializeStructure(S obj) {\n        for (Consumer<? super S> option : options) {\n            option.accept(obj);\n        }\n    }\n\n    @Override\n    public final R build() {\n        S struc = buildStructure();\n        return struc.get();\n    }\n\n    public final S buildStructure() {\n        S struc = createBase();\n        initializeStructure(struc);\n        initialize(struc.get());\n        return struc;\n    }\n\n    protected abstract S createBase();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/SimpleRegionBuilder.java",
    "content": "package io.xpipe.app.comp;\n\nimport javafx.scene.layout.Region;\n\npublic abstract class SimpleRegionBuilder extends RegionBuilder<Region> {}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/augment/ContextMenuAugment.java",
    "content": "package io.xpipe.app.comp.augment;\n\nimport javafx.event.ActionEvent;\nimport javafx.geometry.Side;\nimport javafx.scene.control.ButtonBase;\nimport javafx.scene.control.ContextMenu;\nimport javafx.scene.input.KeyEvent;\nimport javafx.scene.input.MouseEvent;\nimport javafx.scene.layout.Region;\n\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.function.Consumer;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\n\npublic class ContextMenuAugment<S extends Region> implements Consumer<S> {\n\n    private final Predicate<MouseEvent> mouseEventCheck;\n    private final Predicate<KeyEvent> keyEventCheck;\n    private final Supplier<ContextMenu> contextMenu;\n\n    public ContextMenuAugment(\n            Predicate<MouseEvent> mouseEventCheck,\n            Predicate<KeyEvent> keyEventCheck,\n            Supplier<ContextMenu> contextMenu) {\n        this.mouseEventCheck = mouseEventCheck;\n        this.keyEventCheck = keyEventCheck;\n        this.contextMenu = contextMenu;\n    }\n\n    @Override\n    public void accept(S struc) {\n        var currentContextMenu = new AtomicReference<ContextMenu>();\n\n        Supplier<Boolean> hide = () -> {\n            if (currentContextMenu.get() != null && currentContextMenu.get().isShowing()) {\n                currentContextMenu.get().hide();\n                currentContextMenu.set(null);\n                return true;\n            }\n            return false;\n        };\n\n        var r = struc;\n        r.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {\n            if (mouseEventCheck != null && mouseEventCheck.test(event)) {\n                if (!hide.get()) {\n                    var cm = contextMenu.get();\n                    if (cm != null) {\n                        cm.show(r, event.getScreenX(), event.getScreenY());\n                        currentContextMenu.set(cm);\n                    }\n                }\n\n                event.consume();\n            }\n        });\n        r.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {\n            if (mouseEventCheck != null && mouseEventCheck.test(event)) {\n                event.consume();\n            }\n        });\n\n        r.addEventHandler(KeyEvent.KEY_RELEASED, event -> {\n            if (keyEventCheck != null && keyEventCheck.test(event)) {\n                event.consume();\n            }\n        });\n        r.addEventHandler(KeyEvent.KEY_PRESSED, event -> {\n            if (keyEventCheck != null && keyEventCheck.test(event)) {\n                if (!hide.get()) {\n                    var cm = contextMenu.get();\n                    if (cm != null) {\n                        cm.show(r, Side.BOTTOM, 0, 0);\n                        currentContextMenu.set(cm);\n                    }\n                }\n                event.consume();\n            }\n        });\n\n        if (r instanceof ButtonBase buttonBase && keyEventCheck == null) {\n            buttonBase.addEventHandler(ActionEvent.ACTION, event -> {\n                if (buttonBase.getOnAction() != null) {\n                    return;\n                }\n\n                if (!hide.get()) {\n                    var cm = contextMenu.get();\n                    if (cm != null) {\n                        cm.show(r, Side.TOP, 0, 0);\n                        currentContextMenu.set(cm);\n                    }\n                }\n                event.consume();\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/AnchorComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\n\nimport javafx.scene.layout.AnchorPane;\n\nimport java.util.List;\n\npublic class AnchorComp extends RegionBuilder<AnchorPane> {\n\n    private final List<BaseRegionBuilder<?, ?>> comps;\n\n    public AnchorComp(List<BaseRegionBuilder<?, ?>> comps) {\n        this.comps = List.copyOf(comps);\n    }\n\n    @Override\n    public AnchorPane createSimple() {\n        var pane = new AnchorPane();\n        for (var c : comps) {\n            pane.getChildren().add(c.build());\n        }\n        return pane;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/AppLayoutComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionStructure;\nimport io.xpipe.app.comp.RegionStructureBuilder;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.core.AppRestart;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.storage.DataStorage;\n\nimport io.xpipe.app.util.GlobalTimer;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.Node;\nimport javafx.scene.Parent;\nimport javafx.scene.control.ButtonBase;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyEvent;\nimport javafx.scene.layout.BorderPane;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\nimport org.bouncycastle.math.raw.Mod;\n\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\npublic class AppLayoutComp extends RegionStructureBuilder<BorderPane, AppLayoutComp.Structure> {\n\n    @Override\n    public Structure createBase() {\n        var model = AppLayoutModel.get();\n        Map<BaseRegionBuilder<?, ?>, ObservableValue<Boolean>> map = model.getEntries().stream()\n                .filter(entry -> entry.comp() != null)\n                .collect(Collectors.toMap(\n                        entry -> entry.comp(),\n                        entry -> Bindings.createBooleanBinding(\n                                () -> {\n                                    return model.getSelected().getValue().equals(entry);\n                                },\n                                model.getSelected()),\n                        (v1, v2) -> v2,\n                        LinkedHashMap::new));\n        var multi = new MultiContentComp(true, map, true);\n        multi.style(\"background\");\n\n        var pane = new BorderPane();\n        var sidebar = new SideMenuBarComp(model.getSelected(), model.getEntries(), model.getQueueEntries());\n        StackPane multiR = (StackPane) multi.build();\n        pane.setCenter(multiR);\n        var sidebarR = sidebar.build();\n        pane.setRight(sidebarR);\n        model.getSelected().addListener((c, o, n) -> {\n            if (o != null && o.equals(model.getEntries().get(2))) {\n                var prefs = AppPrefs.get();\n                if (prefs != null) {\n                    prefs.save();\n                }\n                var storage = DataStorage.get();\n                if (storage != null) {\n                    storage.saveAsync();\n                }\n\n                if (AppPrefs.get() != null && AppPrefs.get().getRequiresRestart().get()) {\n                    GlobalTimer.delay(() -> {\n                        var modal = ModalOverlay.of(\"prefsRestartTitle\", AppDialog.dialogTextKey(\"prefsRestartContent\"));\n                        modal.addButton(ModalButton.cancel());\n                        modal.addButton(new ModalButton(\"restart\", () -> AppRestart.restart(), true, true));\n                        modal.show();\n                    }, Duration.ofSeconds(1));\n                }\n            }\n\n            if (o != null && o.equals(model.getEntries().get(0))) {\n                var svs = StoreViewState.get();\n                if (svs != null) {\n                    svs.triggerStoreListUpdate();\n                }\n            }\n        });\n        pane.addEventHandler(KeyEvent.KEY_PRESSED, event -> {\n            sidebarR.getChildrenUnmodifiable().forEach(node -> {\n                var shortcut = (KeyCodeCombination) node.getProperties().get(\"shortcut\");\n                if (shortcut != null && shortcut.match(event)) {\n                    ((ButtonBase) ((Parent) node).getChildrenUnmodifiable().get(1)).fire();\n                    event.consume();\n                }\n            });\n        });\n        pane.getStyleClass().add(\"layout\");\n        return new Structure(pane, multiR, sidebarR, new ArrayList<>(multiR.getChildren()));\n    }\n\n    public record Structure(BorderPane pane, StackPane stack, Region sidebar, List<Node> children)\n            implements RegionStructure<BorderPane> {\n\n        public void prepareAddition() {\n            stack.getChildren().clear();\n            sidebar.setDisable(true);\n        }\n\n        public void show() {\n            stack.getChildren().add(children.getFirst());\n            for (int i = 1; i < children.size(); i++) {\n                children.get(i).setVisible(false);\n                children.get(i).setManaged(false);\n                stack.getChildren().add(children.get(i));\n            }\n            PlatformThread.runNestedLoopIteration();\n            sidebar.setDisable(false);\n            stack.requestFocus();\n        }\n\n        @Override\n        public BorderPane get() {\n            return pane;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/AppMainWindowContentComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.core.*;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.core.window.AppMainWindow;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.platform.ColorHelper;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.util.GlobalTimer;\n\nimport javafx.animation.*;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleIntegerProperty;\nimport javafx.collections.ListChangeListener;\nimport javafx.css.PseudoClass;\nimport javafx.geometry.Pos;\nimport javafx.scene.effect.DropShadow;\nimport javafx.scene.image.ImageView;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\nimport javafx.scene.layout.VBox;\nimport javafx.scene.paint.Color;\nimport javafx.stage.Stage;\nimport javafx.stage.Window;\n\nimport java.time.Duration;\n\npublic class AppMainWindowContentComp extends SimpleRegionBuilder {\n\n    private final Stage stage;\n\n    public AppMainWindowContentComp(Stage stage) {\n        this.stage = stage;\n    }\n\n    @Override\n    public Region createSimple() {\n        var overlay = AppDialog.getModalOverlays();\n        var loaded = AppMainWindow.getLoadedContent();\n        var sidebarPresent = new SimpleBooleanProperty();\n        var bg = RegionBuilder.of(() -> {\n            var loadingIcon = new ImageView();\n            loadingIcon.setFitWidth(80);\n            loadingIcon.setFitHeight(80);\n\n            var dark =\n                    AppPrefs.get() != null && AppPrefs.get().theme().getValue().isDark();\n            loadingIcon.setOpacity(dark ? 0.95 : 0.93);\n\n            var color = AppPrefs.get() != null\n                    ? ColorHelper.withOpacity(\n                            AppPrefs.get().theme().getValue().getEmphasisColor().get(), dark ? 0.7 : 0.85)\n                    : Color.TRANSPARENT;\n            DropShadow shadow = new DropShadow();\n            shadow.setRadius(10);\n            shadow.setColor(color);\n\n            var loadingAnimation = new AnimationTimer() {\n\n                long offset;\n\n                @Override\n                public void handle(long now) {\n                    // Increment offset as we are always having 60fps\n                    // Prevents animation jumps when the animation timer isn't called for a long time\n                    offset += 1000 / 60;\n\n                    // Move shadow in a circle\n                    var rad = -(offset % 1300.0) / 1300.0 * 2 * Math.PI;\n                    var x = Math.sin(rad);\n                    var y = Math.cos(rad);\n                    shadow.setOffsetX(x * 3);\n                    shadow.setOffsetY(y * 3);\n                }\n            };\n\n            loadingIcon.setEffect(shadow);\n            loadingAnimation.start();\n\n            // This allows for assigning logos even if AppImages has not been initialized yet\n            AppResources.with(AppResources.MAIN_MODULE, \"\", path -> {\n                var image = AppPrefs.get() != null\n                                && AppPrefs.get().theme().getValue().isDark()\n                        ? path.resolve(\"loading-160-dark.png\")\n                        : path.resolve(\"loading-160.png\");\n                loadingIcon.setImage(AppImages.loadImage(image));\n            });\n\n            var version = new LabelComp(\n                    (AppNames.ofCurrent().getName()) + \" \" + AppProperties.get().getVersion());\n            version.apply(struc -> {\n                AppFontSizes.apply(struc, appFontSizes -> \"15\");\n                struc.setOpacity(0.65);\n            });\n\n            var loadingTextCounter = new SimpleIntegerProperty();\n            GlobalTimer.scheduleUntil(Duration.ofMillis(500), false, () -> {\n                if (loaded.getValue() != null) {\n                    return true;\n                }\n\n                loadingTextCounter.set((loadingTextCounter.get() + 1) % 4);\n                return false;\n            });\n            var loadingTextAnimated = Bindings.createStringBinding(\n                    () -> {\n                        var base = AppMainWindow.getLoadingText().getValue();\n                        if (base == null) {\n                            return null;\n                        }\n                        return base + \" \" + (\".\".repeat(loadingTextCounter.get()))\n                                + (\" \".repeat(3 - loadingTextCounter.get()));\n                    },\n                    AppMainWindow.getLoadingText(),\n                    loadingTextCounter);\n            var text = new LabelComp(loadingTextAnimated);\n            text.style(\"loading-text\");\n            text.apply(struc -> {\n                struc.setOpacity(0.8);\n            });\n\n            var vbox = new VBox(\n                    RegionBuilder.vspacer().build(),\n                    loadingIcon,\n                    RegionBuilder.vspacer(19).build(),\n                    version.build(),\n                    RegionBuilder.vspacer().build(),\n                    text.build(),\n                    RegionBuilder.vspacer(20).build());\n            vbox.setAlignment(Pos.CENTER);\n\n            var pane = new StackPane(vbox);\n            pane.setAlignment(Pos.TOP_LEFT);\n            pane.getStyleClass().add(\"background\");\n\n            loaded.subscribe(struc -> {\n                if (struc != null) {\n                    TrackEvent.info(\"Window content node set\");\n                    PlatformThread.runNestedLoopIteration();\n                    struc.prepareAddition();\n                    pane.getStyleClass().remove(\"background\");\n                    loadingAnimation.stop();\n                    pane.getChildren().remove(vbox);\n                    pane.getChildren().add(struc.get());\n                    sidebarPresent.set(true);\n                    PlatformThread.runNestedLoopIteration();\n                    struc.show();\n                    TrackEvent.info(\"Window content node shown\");\n                } else if (!pane.getChildren().contains(vbox)) {\n                    loadingTextCounter.set(3);\n                    TrackEvent.info(\"Window content node removed\");\n                    PlatformThread.runNestedLoopIteration();\n                    pane.getChildren().clear();\n                    pane.getStyleClass().add(\"background\");\n                    pane.getChildren().add(vbox);\n                    sidebarPresent.set(false);\n                    loadingAnimation.start();\n                    PlatformThread.runNestedLoopIteration();\n                }\n            });\n\n            overlay.addListener((ListChangeListener<? super ModalOverlay>) c -> {\n                if (c.next() && c.wasAdded()) {\n                    AppMainWindow.get().focus();\n\n                    // Close blocking modal windows\n                    var childWindows = Window.getWindows().stream()\n                            .filter(window -> window instanceof Stage s && stage.equals(s.getOwner()))\n                            .toList();\n                    childWindows.forEach(window -> {\n                        ((Stage) window).close();\n                    });\n                }\n            });\n\n            return pane;\n        });\n\n        var modal = new ModalOverlayStackComp(bg, overlay);\n        var r = modal.build();\n        var p = r.lookupAll(\".modal-overlay-stack-element\");\n        sidebarPresent.subscribe(v -> {\n            if (v) {\n                p.forEach(node -> {\n                    node.pseudoClassStateChanged(PseudoClass.getPseudoClass(\"loaded\"), true);\n                });\n            }\n        });\n\n        return r;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.beans.property.ReadOnlyObjectWrapper;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.css.Size;\nimport javafx.css.SizeUnits;\nimport javafx.scene.Node;\nimport javafx.scene.control.Button;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Getter;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\n@Getter\n@AllArgsConstructor\npublic class ButtonComp extends RegionBuilder<Button> {\n\n    private final ObservableValue<String> name;\n    private final ObservableValue<LabelGraphic> graphic;\n    private final Runnable listener;\n\n    public ButtonComp(ObservableValue<String> name, Runnable listener) {\n        this.name = name;\n        this.graphic = new SimpleObjectProperty<>(null);\n        this.listener = listener;\n    }\n\n    public ButtonComp(ObservableValue<String> name, Node graphic, Runnable listener) {\n        this.name = name;\n        this.graphic = new SimpleObjectProperty<>(new LabelGraphic.NodeGraphic(() -> graphic));\n        this.listener = listener;\n    }\n\n    public ButtonComp(ObservableValue<String> name, LabelGraphic graphic, Runnable listener) {\n        this.name = name;\n        this.graphic = new ReadOnlyObjectWrapper<>(graphic);\n        this.listener = listener;\n    }\n\n    @Override\n    public Button createSimple() {\n        var button = new Button(null);\n        button.setMnemonicParsing(false);\n        if (name != null) {\n            name.subscribe(t -> {\n                PlatformThread.runLaterIfNeeded(() -> button.setText(t));\n            });\n        }\n        if (graphic != null) {\n            graphic.subscribe(t -> {\n                PlatformThread.runLaterIfNeeded(() -> {\n                    if (t == null) {\n                        return;\n                    }\n\n                    var n = t.createGraphicNode();\n                    button.setGraphic(n);\n                    if (n instanceof FontIcon f && button.getFont() != null) {\n                        f.setIconSize((int) new Size(button.getFont().getSize(), SizeUnits.PT).pixels());\n                    }\n                });\n            });\n\n            button.fontProperty().subscribe(c -> {\n                if (button.getGraphic() instanceof FontIcon f) {\n                    f.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels());\n                }\n            });\n        }\n        if (listener != null) {\n            button.setOnAction(e -> getListener().run());\n        }\n        button.getStyleClass().add(\"button-comp\");\n        return button;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/ChoiceComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.MenuHelper;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.util.Translatable;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.FXCollections;\nimport javafx.scene.control.ComboBox;\nimport javafx.util.StringConverter;\n\nimport lombok.AccessLevel;\nimport lombok.AllArgsConstructor;\nimport lombok.experimental.FieldDefaults;\n\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\n@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)\n@AllArgsConstructor\npublic class ChoiceComp<T> extends RegionBuilder<ComboBox<T>> {\n\n    Property<T> value;\n    ObservableValue<Map<T, ObservableValue<String>>> range;\n    boolean includeNone;\n\n    public ChoiceComp(Property<T> value, Map<T, ObservableValue<String>> range, boolean includeNone) {\n        this.value = value;\n        this.range = new SimpleObjectProperty<>(range);\n        this.includeNone = includeNone;\n    }\n\n    public static <T extends Translatable> ChoiceComp<T> ofTranslatable(\n            Property<T> value, List<T> range, boolean includeNone) {\n        var map = range.stream()\n                .collect(\n                        Collectors.toMap(o -> o, Translatable::toTranslatedString, (v1, v2) -> v2, LinkedHashMap::new));\n        return new ChoiceComp<>(value, map, includeNone);\n    }\n\n    @Override\n    public ComboBox<T> createSimple() {\n        var cb = MenuHelper.<T>createComboBox();\n        cb.setConverter(new StringConverter<>() {\n            @Override\n            public String toString(T object) {\n                if (object == null) {\n                    return AppI18n.get(\"none\");\n                }\n\n                var found = range.getValue().get(object);\n                if (found == null) {\n                    return \"\";\n                }\n\n                return found.getValue();\n            }\n\n            @Override\n            public T fromString(String string) {\n                throw new UnsupportedOperationException();\n            }\n        });\n        range.subscribe(c -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                var list = FXCollections.observableArrayList(c.keySet());\n                if (!list.contains(null) && includeNone) {\n                    list.addFirst(null);\n                }\n\n                cb.getItems().setAll(list);\n\n                if (list.size() == 1) {\n                    value.setValue(list.getFirst());\n                } else if (list.isEmpty()) {\n                    value.setValue(null);\n                }\n            });\n        });\n\n        cb.valueProperty().addListener((observable, oldValue, newValue) -> {\n            value.setValue(newValue);\n        });\n        value.subscribe(val -> {\n            PlatformThread.runLaterIfNeeded(() -> cb.valueProperty().set(val));\n        });\n\n        cb.getStyleClass().add(\"choice-comp\");\n        cb.setMaxWidth(10000);\n        return cb;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/ChoicePaneComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.platform.MenuHelper;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.FXCollections;\nimport javafx.scene.control.ComboBox;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.VBox;\nimport javafx.util.StringConverter;\n\nimport lombok.Setter;\n\nimport java.util.List;\nimport java.util.function.Function;\n\npublic class ChoicePaneComp extends RegionBuilder<VBox> {\n\n    private final List<Entry> entries;\n    private final Property<Entry> selected;\n\n    @Setter\n    private Function<ComboBox<Entry>, Region> transformer = c -> c;\n\n    public ChoicePaneComp(List<Entry> entries, Property<Entry> selected) {\n        this.entries = entries;\n        this.selected = selected;\n    }\n\n    @Override\n    public VBox createSimple() {\n        var list = FXCollections.observableArrayList(entries);\n        var cb = MenuHelper.<Entry>createComboBox();\n        cb.setItems(list);\n        cb.setOnKeyPressed(event -> {\n            if (!cb.isShowing() && event.getCode().equals(KeyCode.ENTER)) {\n                cb.show();\n                event.consume();\n            }\n        });\n        cb.getSelectionModel().select(selected.getValue());\n        cb.setConverter(new StringConverter<>() {\n            @Override\n            public String toString(Entry object) {\n                if (object == null || object.name() == null) {\n                    return \"\";\n                }\n\n                return object.name().getValue();\n            }\n\n            @Override\n            public Entry fromString(String string) {\n                throw new UnsupportedOperationException();\n            }\n        });\n\n        var vbox = new VBox(transformer.apply(cb));\n        vbox.setFillWidth(true);\n        vbox.focusedProperty().addListener((observable, oldValue, newValue) -> {\n            if (newValue) {\n                vbox.getChildren().getFirst().requestFocus();\n            }\n        });\n\n        cb.prefWidthProperty().bind(vbox.widthProperty());\n        cb.valueProperty().subscribe(n -> {\n            if (n == null) {\n                if (vbox.getChildren().size() > 1) {\n                    vbox.getChildren().remove(1);\n                }\n            } else {\n                var region = n.comp().build();\n                if (vbox.getChildren().size() == 1) {\n                    vbox.getChildren().add(region);\n                } else {\n                    vbox.getChildren().set(1, region);\n                }\n            }\n        });\n\n        cb.showingProperty().addListener((observable, oldValue, newValue) -> {\n            if (!newValue && vbox.getChildren().size() > 1) {\n                vbox.getChildren().get(1).requestFocus();\n            }\n        });\n\n        cb.valueProperty().addListener((observable, oldValue, newValue) -> {\n            selected.setValue(newValue);\n        });\n        selected.subscribe(val -> {\n            PlatformThread.runLaterIfNeeded(() -> cb.valueProperty().set(val));\n        });\n\n        vbox.getStyleClass().add(\"choice-pane-comp\");\n\n        return vbox;\n    }\n\n    public record Entry(ObservableValue<String> name, BaseRegionBuilder<?, ?> comp) {\n\n        @Override\n        public int hashCode() {\n            return name.hashCode();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/ComboTextFieldComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.application.Platform;\nimport javafx.beans.property.Property;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.ObservableList;\nimport javafx.scene.control.ComboBox;\nimport javafx.scene.control.ListCell;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyEvent;\n\nimport lombok.Setter;\n\nimport java.util.Objects;\nimport java.util.function.Supplier;\n\npublic class ComboTextFieldComp extends RegionBuilder<ComboBox<String>> {\n\n    private final Property<String> value;\n    private final ObservableList<String> predefinedValues;\n    private final Supplier<ListCell<String>> customCellFactory;\n\n    @Setter\n    private ObservableValue<String> prompt;\n\n    public ComboTextFieldComp(\n            Property<String> value,\n            ObservableList<String> predefinedValues,\n            Supplier<ListCell<String>> customCellFactory) {\n        this.value = value;\n        this.predefinedValues = predefinedValues;\n        this.customCellFactory = customCellFactory;\n    }\n\n    @Override\n    public ComboBox<String> createSimple() {\n        var text = new ComboBox<>(predefinedValues);\n        text.addEventFilter(KeyEvent.ANY, event -> {\n            Platform.runLater(() -> {\n                text.commitValue();\n            });\n        });\n        text.setEditable(true);\n        text.setMaxWidth(20000);\n        text.setValue(value.getValue() != null ? value.getValue() : null);\n        text.valueProperty().addListener((c, o, n) -> {\n            value.setValue(n != null && !n.isBlank() ? n : null);\n        });\n\n        if (prompt != null) {\n            prompt.subscribe(filePath -> {\n                PlatformThread.runLaterIfNeeded(() -> {\n                    text.setPromptText(filePath);\n                });\n            });\n        }\n\n        value.addListener((c, o, n) -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                // Check if control value is the same. Then don't set it as that might cause bugs\n                if (Objects.equals(text.getValue(), n)\n                        || (n == null && text.getValue().isEmpty())) {\n                    return;\n                }\n\n                text.setValue(n);\n            });\n        });\n\n        if (customCellFactory != null) {\n            text.setCellFactory(param -> customCellFactory.get());\n            text.setButtonCell(customCellFactory.get());\n        }\n\n        text.setOnKeyPressed(ke -> {\n            if (ke.getCode().equals(KeyCode.ENTER)) {\n                text.getScene().getRoot().requestFocus();\n            }\n            ke.consume();\n        });\n\n        return text;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/ContextualFileReferenceChoiceComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.browser.BrowserFileChooserSessionComp;\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.ext.FileSystemStore;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.storage.ContextualFileReference;\nimport io.xpipe.app.storage.DataStorageSyncHandler;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.core.FilePath;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.FXCollections;\nimport javafx.scene.control.ListCell;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Priority;\n\nimport lombok.NonNull;\nimport lombok.Setter;\nimport lombok.Value;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Predicate;\n\npublic class ContextualFileReferenceChoiceComp extends RegionBuilder<HBox> {\n\n    private final Property<DataStoreEntryRef<? extends FileSystemStore>> fileSystem;\n    private final Property<FilePath> filePath;\n    private final ContextualFileReferenceSync sync;\n    private final List<PreviousFileReference> previousFileReferences;\n    private final Predicate<DataStoreEntry> filter;\n    private final boolean directory;\n\n    @Setter\n    private ObservableValue<FilePath> prompt;\n\n    public <T extends FileSystemStore> ContextualFileReferenceChoiceComp(\n            ObservableValue<DataStoreEntryRef<T>> fileSystem,\n            Property<FilePath> filePath,\n            ContextualFileReferenceSync sync,\n            List<PreviousFileReference> previousFileReferences,\n            Predicate<DataStoreEntry> filter,\n            boolean directory) {\n        this.sync = sync;\n        this.previousFileReferences = previousFileReferences;\n        this.filter = filter;\n        this.directory = directory;\n        this.fileSystem = new SimpleObjectProperty<>();\n        fileSystem.subscribe(val -> {\n            this.fileSystem.setValue(val);\n        });\n        this.filePath = filePath;\n    }\n\n    @Override\n    public HBox createSimple() {\n        var path = previousFileReferences.isEmpty() ? createTextField() : createComboBox();\n        var fileBrowseButton = new ButtonComp(null, new FontIcon(\"mdi2f-folder-open-outline\"), () -> {\n            var replacement = ProcessControlProvider.get().replace(fileSystem.getValue());\n            BrowserFileChooserSessionComp.open(\n                    () -> replacement,\n                    () -> filePath.getValue() != null ? filePath.getValue().getParent() : null,\n                    fileStore -> {\n                        if (fileStore != null) {\n                            filePath.setValue(fileStore.getPath());\n                        }\n                    },\n                    false,\n                    directory,\n                    filter);\n        });\n\n        var gitShareButton = new ButtonComp(null, new FontIcon(\"mdi2g-git\"), () -> {\n            if (!DataStorageSyncHandler.getInstance().supportsSync()) {\n                AppLayoutModel.get().selectSettings();\n                AppPrefs.get().selectCategory(\"vaultSync\");\n                return;\n            }\n\n            var currentPath = filePath.getValue();\n            if (currentPath == null) {\n                return;\n            }\n\n            if (ContextualFileReference.of(currentPath).isInDataDirectory()) {\n                return;\n            }\n\n            try {\n                var rawSource = currentPath.asLocalPathIfPossible();\n                if (rawSource.isEmpty() || !Files.exists(rawSource.get())) {\n                    ErrorEventFactory.fromMessage(\"Unable to resolve local file path \" + currentPath)\n                            .expected()\n                            .handle();\n                    return;\n                }\n\n                var source = rawSource.get();\n                var target = sync.getTargetLocation().apply(source);\n                var shouldCopy = AppDialog.confirm(\"confirmGitShare\");\n                if (!shouldCopy) {\n                    return;\n                }\n\n                var handler = DataStorageSyncHandler.getInstance();\n                var syncedTarget =\n                        handler.addDataFile(source, target, sync.getPerUser().get());\n\n                var sourceBase = source.toString().endsWith(\".pem\")\n                        ? Path.of(\n                                source.toString().substring(0, source.toString().length() - 4))\n                        : source;\n\n                var pubSource = Path.of(sourceBase + \".pub\");\n                if (Files.exists(pubSource)) {\n                    var pubTarget = Path.of(target.toString() + \".pub\");\n                    handler.addDataFile(pubSource, pubTarget, sync.getPerUser().get());\n                }\n\n                var ppkSource = Path.of(sourceBase + \".ppk\");\n                if (Files.exists(ppkSource)) {\n                    var ppkTarget = Path.of(target.toString() + \".ppk\");\n                    handler.addDataFile(ppkSource, ppkTarget, sync.getPerUser().get());\n                }\n\n                Platform.runLater(() -> {\n                    filePath.setValue(FilePath.of(syncedTarget));\n                });\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).handle();\n            }\n        });\n        gitShareButton.style(\"git-sync-file-button\");\n        gitShareButton.describe(d -> d.nameKey(\"gitShareFileTooltip\"));\n        gitShareButton.disable(Bindings.createBooleanBinding(\n                () -> {\n                    return filePath.getValue() == null\n                            || ContextualFileReference.of(filePath.getValue()).isInDataDirectory();\n                },\n                filePath));\n\n        var nodes = new ArrayList<BaseRegionBuilder<?, ?>>();\n        nodes.add(path);\n        nodes.add(fileBrowseButton);\n        if (sync != null) {\n            nodes.add(gitShareButton);\n        }\n        var layout = new InputGroupComp(nodes).setMainReference(path).apply(struc -> struc.setFillHeight(true));\n\n        layout.apply(struc -> {\n            struc.focusedProperty().addListener((observable, oldValue, newValue) -> {\n                struc.getChildren().getFirst().requestFocus();\n            });\n        });\n\n        return layout.build();\n    }\n\n    private BaseRegionBuilder<?, ?> createComboBox() {\n        var allFiles = new ArrayList<>(previousFileReferences);\n        allFiles.addAll(sync != null ? sync.getExistingFiles() : List.of());\n        var items = allFiles.stream()\n                .map(previousFileReference -> previousFileReference.getPath().toString())\n                .toList();\n        var prop = new SimpleStringProperty();\n        filePath.subscribe(s -> PlatformThread.runLaterIfNeeded(() -> {\n            prop.set(s != null ? s.toString() : null);\n        }));\n        prop.addListener((observable, oldValue, newValue) -> {\n            filePath.setValue(newValue != null ? FilePath.of(newValue) : null);\n        });\n        var combo = new ComboTextFieldComp(prop, FXCollections.observableList(items), () -> {\n            return new ListCell<>() {\n                @Override\n                protected void updateItem(String item, boolean empty) {\n                    super.updateItem(item, empty);\n                    if (empty) {\n                        return;\n                    }\n\n                    var display = allFiles.stream()\n                            .filter(ref -> ref.path.toString().equals(item))\n                            .findFirst()\n                            .map(previousFileReference -> previousFileReference.getDisplayName())\n                            .orElse(item);\n                    setText(display);\n                }\n            };\n        });\n        combo.setPrompt(Bindings.createStringBinding(\n                () -> {\n                    return filePath.getValue() != null ? filePath.getValue().toString() : null;\n                },\n                filePath));\n        combo.hgrow();\n        return combo;\n    }\n\n    private BaseRegionBuilder<?, ?> createTextField() {\n        var prop = new SimpleStringProperty();\n        filePath.subscribe(s -> PlatformThread.runLaterIfNeeded(() -> {\n            prop.set(s != null ? s.toString() : null);\n        }));\n        prop.addListener((observable, oldValue, newValue) -> {\n            filePath.setValue(newValue != null && !newValue.isBlank() ? FilePath.of(newValue.strip()) : null);\n        });\n        var fileNameComp = new TextFieldComp(prop).apply(struc -> HBox.setHgrow(struc, Priority.ALWAYS));\n\n        if (prompt != null) {\n            fileNameComp.apply(struc -> {\n                prompt.subscribe(filePath -> {\n                    PlatformThread.runLaterIfNeeded(() -> {\n                        struc.setPromptText(filePath != null ? filePath.toString() : null);\n                    });\n                });\n            });\n        }\n\n        return fileNameComp;\n    }\n\n    @Value\n    public static class PreviousFileReference {\n\n        String displayName;\n        Path path;\n\n        public static PreviousFileReference of(Path file) {\n            return new PreviousFileReference(file.toString(), file);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/ContextualFileReferenceSync.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.issue.ErrorAction;\nimport io.xpipe.app.issue.ErrorEvent;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.storage.DataStorageSyncHandler;\nimport io.xpipe.app.util.AsktextAlert;\n\nimport lombok.AccessLevel;\nimport lombok.AllArgsConstructor;\nimport lombok.Value;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\nimport java.util.function.UnaryOperator;\n\n@Value\n@AllArgsConstructor(access = AccessLevel.PRIVATE)\npublic class ContextualFileReferenceSync {\n\n    public static ContextualFileReferenceSync of(Path dir, Function<Path, String> fileName, Supplier<Boolean> perUser) {\n        return new ContextualFileReferenceSync(\n                dir,\n                path -> {\n                    String name = fileName.apply(path);\n                    while (true) {\n                        var target = dir.resolve(name);\n                        if (Files.exists(target)) {\n                            var rename = new AtomicBoolean(false);\n                            var event = ErrorEventFactory.fromMessage(AppI18n.get(\"syncFileExists\", target))\n                                    .customAction(new ErrorAction() {\n                                        @Override\n                                        public String getName() {\n                                            return AppI18n.get(\"replaceFile\");\n                                        }\n\n                                        @Override\n                                        public String getDescription() {\n                                            return AppI18n.get(\"replaceFileDescription\");\n                                        }\n\n                                        @Override\n                                        public boolean handle(ErrorEvent event) {\n                                            return true;\n                                        }\n                                    })\n                                    .customAction(new ErrorAction() {\n                                        @Override\n                                        public String getName() {\n                                            return AppI18n.get(\"renameFile\");\n                                        }\n\n                                        @Override\n                                        public String getDescription() {\n                                            return AppI18n.get(\"renameFileDescription\");\n                                        }\n\n                                        @Override\n                                        public boolean handle(ErrorEvent event) {\n                                            rename.set(true);\n                                            return true;\n                                        }\n                                    });\n                            event.expected();\n                            event.handle();\n\n                            if (rename.get()) {\n                                var newName = AsktextAlert.query(AppI18n.get(\"newFileName\"), name);\n                                if (newName.isEmpty()) {\n                                    continue;\n                                }\n\n                                name = newName.get();\n                                continue;\n                            }\n                        }\n\n                        return target;\n                    }\n                },\n                perUser);\n    }\n\n    Path targetDir;\n    UnaryOperator<Path> targetLocation;\n    Supplier<Boolean> perUser;\n\n    public List<ContextualFileReferenceChoiceComp.PreviousFileReference> getExistingFiles() {\n        var files = new ArrayList<ContextualFileReferenceChoiceComp.PreviousFileReference>();\n        DataStorageSyncHandler.getInstance().getSavedDataFiles().forEach(path -> {\n            if (!path.startsWith(targetDir)) {\n                return;\n            }\n\n            files.add(new ContextualFileReferenceChoiceComp.PreviousFileReference(\n                    path.getFileName().toString() + \" (Git)\", path));\n        });\n        return files;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/CountComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.value.ObservableIntegerValue;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.OverrunStyle;\n\nimport lombok.AllArgsConstructor;\n\nimport java.util.function.Function;\n\n@AllArgsConstructor\npublic class CountComp extends RegionBuilder<Label> {\n\n    private final ObservableIntegerValue sub;\n    private final ObservableIntegerValue all;\n    private final Function<String, String> transformation;\n\n    @Override\n    public Label createSimple() {\n        var label = new Label();\n        label.setTextOverrun(OverrunStyle.CLIP);\n        label.setAlignment(Pos.CENTER);\n        var binding = Bindings.createStringBinding(\n                () -> {\n                    if (sub.get() == all.get()) {\n                        return transformation.apply(all.get() + \"\");\n                    } else {\n                        return transformation.apply(sub.get() + \"/\" + all.get());\n                    }\n                },\n                sub,\n                all);\n        label.textProperty().bind(PlatformThread.sync(binding));\n        label.getStyleClass().add(\"count-comp\");\n        return label;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/DelayedInitComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.util.GlobalTimer;\n\nimport javafx.application.Platform;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\n\nimport lombok.AllArgsConstructor;\n\nimport java.time.Duration;\nimport java.util.function.Supplier;\n\n@AllArgsConstructor\npublic class DelayedInitComp extends SimpleRegionBuilder {\n\n    private final BaseRegionBuilder<?, ?> comp;\n    private final Supplier<Boolean> condition;\n\n    @Override\n    protected Region createSimple() {\n        var stack = new StackPane();\n        GlobalTimer.scheduleUntil(Duration.ofMillis(10), true, () -> {\n            if (!condition.get()) {\n                return false;\n            }\n\n            Platform.runLater(() -> {\n                var r = comp.build();\n                stack.getChildren().add(r);\n            });\n            return true;\n        });\n        stack.focusedProperty().addListener((observable, oldValue, newValue) -> {\n            if (newValue && !stack.getChildren().isEmpty()) {\n                stack.getChildren().getFirst().requestFocus();\n            }\n        });\n        return stack;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/FilterComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.RegionDescriptor;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppOpenArguments;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\nimport javafx.scene.Cursor;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyEvent;\nimport javafx.scene.input.MouseButton;\n\nimport atlantafx.base.controls.CustomTextField;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.List;\nimport java.util.Objects;\n\npublic class FilterComp extends RegionBuilder<CustomTextField> {\n\n    private final Property<String> filterText;\n\n    public FilterComp(Property<String> filterText) {\n        this.filterText = filterText;\n    }\n\n    @Override\n    public CustomTextField createSimple() {\n        var fi = new FontIcon(\"mdi2m-magnify\");\n        var clear = new FontIcon(\"mdi2c-close\");\n        clear.setCursor(Cursor.DEFAULT);\n        clear.setOnMousePressed(event -> {\n            if (event.getButton() == MouseButton.PRIMARY) {\n                filterText.setValue(null);\n                event.consume();\n            }\n        });\n        var filter = new CustomTextField();\n        filter.setMinHeight(0);\n        filter.setMaxHeight(20000);\n        filter.getStyleClass().add(\"filter-comp\");\n        filter.promptTextProperty().bind(AppI18n.observable(\"searchFilter\"));\n        filter.rightProperty()\n                .bind(Bindings.createObjectBinding(\n                        () -> {\n                            return filter.isFocused()\n                                            || (filter.getText() != null\n                                                    && !filter.getText().isEmpty())\n                                    ? clear\n                                    : fi;\n                        },\n                        filter.focusedProperty()));\n        RegionDescriptor.builder().nameKey(\"search\").build().apply(filter);\n\n        filter.addEventFilter(KeyEvent.KEY_PRESSED, event -> {\n            if (new KeyCodeCombination(KeyCode.ESCAPE).match(event)) {\n                filter.clear();\n                event.consume();\n            }\n        });\n\n        filterText.subscribe(val -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                clear.setVisible(val != null);\n                if (!Objects.equals(filter.getText(), val) && !(val == null && \"\".equals(filter.getText()))) {\n                    filter.setText(val);\n                }\n            });\n        });\n\n        filter.textProperty().addListener((observable, oldValue, n) -> {\n            // Handle pasted xpipe URLs\n            if (n != null && n.startsWith(\"xpipe://\")) {\n                AppOpenArguments.handle(List.of(n));\n                filter.setText(null);\n                return;\n            }\n\n            filterText.setValue(n != null && n.length() > 0 ? n : null);\n        });\n\n        return filter;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/FontIconComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionStructure;\nimport io.xpipe.app.comp.RegionStructureBuilder;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.layout.StackPane;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Value;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\n@AllArgsConstructor\npublic class FontIconComp extends RegionStructureBuilder<StackPane, FontIconComp.Structure> {\n\n    private final ObservableValue<String> icon;\n\n    @Override\n    public FontIconComp.Structure createBase() {\n        var fi = new FontIcon();\n        icon.subscribe(val -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                fi.setIconLiteral(val);\n            });\n        });\n\n        var pane = new StackPane(fi);\n        return new FontIconComp.Structure(fi, pane);\n    }\n\n    @Value\n    public static class Structure implements RegionStructure<StackPane> {\n\n        FontIcon icon;\n        StackPane pane;\n\n        @Override\n        public StackPane get() {\n            return pane;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/HorizontalComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.collections.FXCollections;\nimport javafx.collections.ListChangeListener;\nimport javafx.collections.ObservableList;\nimport javafx.geometry.Pos;\nimport javafx.scene.layout.HBox;\n\nimport java.util.List;\n\npublic class HorizontalComp extends RegionBuilder<HBox> {\n\n    private final ObservableList<BaseRegionBuilder<?, ?>> entries;\n\n    public HorizontalComp(List<BaseRegionBuilder<?, ?>> comps) {\n        entries = FXCollections.observableArrayList(List.copyOf(comps));\n    }\n\n    public HorizontalComp(ObservableList<BaseRegionBuilder<?, ?>> entries) {\n        this.entries = PlatformThread.sync(entries);\n    }\n\n    public RegionBuilder<HBox> spacing(double spacing) {\n        return apply(struc -> struc.setSpacing(spacing));\n    }\n\n    @Override\n    public HBox createSimple() {\n        var b = new HBox();\n        b.getStyleClass().add(\"horizontal-comp\");\n        entries.addListener((ListChangeListener<? super BaseRegionBuilder<?, ?>>) c -> {\n            b.getChildren().setAll(c.getList().stream().map(ab -> ab.build()).toList());\n        });\n        for (var entry : entries) {\n            b.getChildren().add(entry.build());\n        }\n        b.setAlignment(Pos.CENTER);\n        return b;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/IconButtonComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.css.Size;\nimport javafx.css.SizeUnits;\nimport javafx.scene.control.Button;\n\nimport atlantafx.base.theme.Styles;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\npublic class IconButtonComp extends RegionBuilder<Button> {\n\n    private final ObservableValue<? extends LabelGraphic> icon;\n    private final Runnable listener;\n\n    public IconButtonComp(String defaultVal) {\n        this(new SimpleObjectProperty<>(new LabelGraphic.IconGraphic(defaultVal)), null);\n    }\n\n    public IconButtonComp(String defaultVal, Runnable listener) {\n        this(new SimpleObjectProperty<>(new LabelGraphic.IconGraphic(defaultVal)), listener);\n    }\n\n    public IconButtonComp(ObservableValue<? extends LabelGraphic> icon) {\n        this.icon = icon;\n        this.listener = null;\n    }\n\n    public IconButtonComp(LabelGraphic defaultVal, Runnable listener) {\n        this(new SimpleObjectProperty<>(defaultVal), listener);\n    }\n\n    public IconButtonComp(ObservableValue<? extends LabelGraphic> icon, Runnable listener) {\n        this.icon = icon;\n        this.listener = listener;\n    }\n\n    @Override\n    public Button createSimple() {\n        var button = new Button();\n        button.getStyleClass().add(Styles.FLAT);\n        // AtlantaFX sets underline to true. This bugs out ikonli: https://github.com/kordamp/ikonli/issues/175\n        button.setUnderline(false);\n        icon.subscribe(labelGraphic -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                button.setGraphic(labelGraphic.createGraphicNode());\n                if (button.getGraphic() instanceof FontIcon fi) {\n                    fi.setIconSize((int) new Size(button.getFont().getSize(), SizeUnits.PT).pixels());\n                }\n            });\n        });\n        button.fontProperty().subscribe((n) -> {\n            if (button.getGraphic() instanceof FontIcon fi) {\n                fi.setIconSize((int) new Size(n.getSize(), SizeUnits.PT).pixels());\n            }\n        });\n        if (listener != null) {\n            button.setOnAction(e -> {\n                listener.run();\n                e.consume();\n            });\n        }\n        button.getStyleClass().add(\"icon-button-comp\");\n        return button;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/InputGroupComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\n\nimport javafx.geometry.Pos;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Priority;\nimport javafx.scene.layout.Region;\n\nimport atlantafx.base.layout.InputGroup;\n\nimport java.util.List;\n\npublic class InputGroupComp extends RegionBuilder<InputGroup> {\n\n    private final List<BaseRegionBuilder<?, ?>> entries;\n\n    private BaseRegionBuilder<?, ?> mainReference;\n\n    public InputGroupComp(List<BaseRegionBuilder<?, ?>> comps) {\n        entries = List.copyOf(comps);\n    }\n\n    public InputGroupComp setMainReference(BaseRegionBuilder<?, ?> mainReference) {\n        this.mainReference = mainReference;\n        return this;\n    }\n\n    public InputGroupComp setMainReference(int index) {\n        return setMainReference(entries.get(index));\n    }\n\n    @Override\n    public InputGroup createSimple() {\n        InputGroup b = new InputGroup();\n        b.getStyleClass().add(\"input-group-comp\");\n        for (var entry : entries) {\n            b.getChildren().add(entry.build());\n        }\n        b.setAlignment(Pos.CENTER);\n\n        if (mainReference != null && entries.contains(mainReference)) {\n            var refIndex = entries.indexOf(mainReference);\n            var ref = b.getChildren().get(refIndex);\n            HBox.setHgrow(ref, Priority.ALWAYS);\n            if (ref instanceof Region refR) {\n                for (int i = 0; i < entries.size(); i++) {\n                    if (i == refIndex) {\n                        continue;\n                    }\n\n                    var entry = b.getChildren().get(i);\n                    if (!(entry instanceof Region entryR)) {\n                        continue;\n                    }\n\n                    entryR.minHeightProperty().bind(refR.heightProperty());\n                    entryR.maxHeightProperty().bind(refR.heightProperty());\n                    entryR.prefHeightProperty().bind(refR.heightProperty());\n                }\n            }\n\n            b.focusedProperty().addListener((observable, oldValue, newValue) -> {\n                if (newValue) {\n                    ref.requestFocus();\n                }\n            });\n        }\n\n        return b;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/IntComboFieldComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.platform.MenuHelper;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.value.ChangeListener;\nimport javafx.collections.FXCollections;\nimport javafx.scene.control.ComboBox;\nimport javafx.scene.input.KeyEvent;\n\nimport lombok.AccessLevel;\nimport lombok.experimental.FieldDefaults;\n\nimport java.util.List;\n\n@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)\npublic class IntComboFieldComp extends RegionBuilder<ComboBox<String>> {\n\n    Property<Integer> value;\n    List<Integer> predefined;\n    boolean allowNegative;\n\n    public IntComboFieldComp(Property<Integer> value, List<Integer> predefined, boolean allowNegative) {\n        this.value = value;\n        this.predefined = predefined;\n        this.allowNegative = allowNegative;\n    }\n\n    @Override\n    public ComboBox<String> createSimple() {\n        var text = MenuHelper.<String>createComboBox();\n        text.setEditable(true);\n        text.setValue(value.getValue() != null ? value.getValue().toString() : null);\n        text.setItems(FXCollections.observableList(\n                predefined.stream().map(integer -> \"\" + integer).toList()));\n        text.setMaxWidth(20000);\n        text.getStyleClass().add(\"int-combo-field-comp\");\n        text.setVisibleRowCount(Math.min(10, predefined.size()));\n\n        value.addListener((ChangeListener<Number>) (observableValue, oldValue, newValue) -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                if (newValue == null) {\n                    text.setValue(\"\");\n                } else {\n                    text.setValue(newValue.toString());\n                }\n            });\n        });\n\n        text.addEventFilter(KeyEvent.KEY_TYPED, keyEvent -> {\n            if (allowNegative) {\n                if (!\"-0123456789\".contains(keyEvent.getCharacter())) {\n                    keyEvent.consume();\n                }\n            } else {\n                if (!\"0123456789\".contains(keyEvent.getCharacter())) {\n                    keyEvent.consume();\n                }\n            }\n        });\n\n        text.valueProperty().addListener((observableValue, oldValue, newValue) -> {\n            if (newValue == null\n                    || newValue.isEmpty()\n                    || (allowNegative && \"-\".equals(newValue))\n                    || !newValue.matches(\"-?\\\\d+\")) {\n                value.setValue(null);\n                return;\n            }\n\n            int intValue = Integer.parseInt(newValue);\n            value.setValue(intValue);\n        });\n\n        return text;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/IntFieldComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.application.Platform;\nimport javafx.beans.property.Property;\nimport javafx.beans.value.ChangeListener;\nimport javafx.scene.control.TextField;\nimport javafx.scene.input.KeyEvent;\n\nimport lombok.AccessLevel;\nimport lombok.experimental.FieldDefaults;\n\nimport java.util.Objects;\n\n@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)\npublic class IntFieldComp extends RegionBuilder<TextField> {\n\n    Property<Integer> value;\n    int minValue;\n    int maxValue;\n\n    public IntFieldComp(Property<Integer> value) {\n        this.value = value;\n        this.minValue = 0;\n        this.maxValue = Integer.MAX_VALUE;\n    }\n\n    public IntFieldComp(Property<Integer> value, int minValue, int maxValue) {\n        this.value = value;\n        this.minValue = minValue;\n        this.maxValue = maxValue;\n    }\n\n    @Override\n    public TextField createSimple() {\n        var field = new TextField(value.getValue() != null ? value.getValue().toString() : null);\n\n        value.addListener((ChangeListener<Number>) (observableValue, oldValue, newValue) -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                // Check if control value is the same. Then don't set it as that might cause bugs\n                if ((newValue == null && field.getText().isEmpty())\n                        || Objects.equals(field.getText(), newValue != null ? newValue.toString() : null)) {\n                    return;\n                }\n\n                if (newValue == null) {\n                    Platform.runLater(() -> {\n                        field.setText(null);\n                    });\n                    return;\n                }\n\n                if (newValue.intValue() < minValue) {\n                    value.setValue(minValue);\n                    return;\n                }\n\n                if (newValue.intValue() > maxValue) {\n                    value.setValue(maxValue);\n                    return;\n                }\n\n                field.setText(newValue.toString());\n            });\n        });\n\n        field.addEventFilter(KeyEvent.KEY_TYPED, keyEvent -> {\n            if (minValue < 0) {\n                if (!\"-0123456789\".contains(keyEvent.getCharacter())) {\n                    keyEvent.consume();\n                }\n            } else {\n                if (!\"0123456789\".contains(keyEvent.getCharacter())) {\n                    keyEvent.consume();\n                }\n            }\n        });\n\n        field.textProperty().addListener((observableValue, oldValue, newValue) -> {\n            if (newValue == null\n                    || newValue.isEmpty()\n                    || (minValue < 0 && \"-\".equals(newValue))\n                    || !newValue.matches(\"-?\\\\d+\")) {\n                value.setValue(null);\n                return;\n            }\n\n            try {\n                int intValue = Integer.parseInt(newValue);\n                if (minValue > intValue || intValue > maxValue) {\n                    field.textProperty().setValue(oldValue);\n                } else {\n                    value.setValue(intValue);\n                }\n            } catch (NumberFormatException ignored) {\n                // If you really try to break it, you can still insert non-integer text into this\n            }\n        });\n\n        return field;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/IntegratedTextAreaComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionStructure;\nimport io.xpipe.app.comp.RegionStructureBuilder;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.ext.StatefulDataStore;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.app.process.SystemState;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.FileOpener;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.control.TextArea;\nimport javafx.scene.layout.AnchorPane;\nimport javafx.scene.layout.Region;\n\nimport atlantafx.base.theme.Styles;\nimport lombok.Builder;\nimport lombok.Value;\n\npublic class IntegratedTextAreaComp extends RegionStructureBuilder<AnchorPane, IntegratedTextAreaComp.Structure> {\n\n    private final Property<String> value;\n    private final boolean lazy;\n    private final String identifier;\n    private final ObservableValue<String> fileType;\n\n    public IntegratedTextAreaComp(\n            Property<String> value, boolean lazy, String identifier, ObservableValue<String> fileType) {\n        this.value = value;\n        this.lazy = lazy;\n        this.identifier = identifier;\n        this.fileType = fileType;\n    }\n\n    public static IntegratedTextAreaComp script(\n            ObservableValue<DataStoreEntryRef<ShellStore>> host, Property<ShellScript> value) {\n        var type = Bindings.createStringBinding(\n                () -> {\n                    return host.getValue() != null\n                                    && host.getValue().getStore() instanceof StatefulDataStore<?> sd\n                                    && sd.getState() instanceof SystemState ss\n                                    && ss.getShellDialect() != null\n                            ? ss.getShellDialect().getScriptFileEnding()\n                            : \"sh\";\n                },\n                host);\n        return script(value, type);\n    }\n\n    public static IntegratedTextAreaComp script(Property<ShellScript> value, ObservableValue<String> fileType) {\n        var string = new SimpleStringProperty();\n        value.subscribe(shellScript -> {\n            string.set(shellScript != null ? shellScript.getValue() : null);\n        });\n        string.addListener((observable, oldValue, newValue) -> {\n            value.setValue(newValue != null ? new ShellScript(newValue) : null);\n        });\n        var i = new IntegratedTextAreaComp(string, false, \"script\", fileType);\n        return i;\n    }\n\n    private Region createOpenButton() {\n        return new IconButtonComp(\n                        \"mdal-edit\",\n                        () -> FileOpener.openString(\n                                identifier + (fileType.getValue() != null ? \".\" + fileType.getValue() : \"\"),\n                                this,\n                                value.getValue(),\n                                (s) -> {\n                                    Platform.runLater(() -> value.setValue(s));\n                                }))\n                .style(\"edit-button\")\n                .apply(struc -> struc.getStyleClass().remove(Styles.FLAT))\n                .describe(d -> d.nameKey(\"edit\"))\n                .build();\n    }\n\n    @Override\n    public Structure createBase() {\n        var textArea = new TextAreaComp(value, lazy);\n        textArea.applyStructure(struc -> {\n            struc.getTextArea()\n                    .prefRowCountProperty()\n                    .bind(Bindings.createIntegerBinding(\n                            () -> {\n                                var val = value.getValue() != null ? value.getValue() : \"\";\n                                var valCount = (int) val.lines().count() + (val.endsWith(\"\\n\") ? 1 : 0);\n\n                                var promptVal = struc.getTextArea().getPromptText() != null ? struc.getTextArea().getPromptText() : \"\";\n                                var promptValCount = (int) promptVal.lines().count() + (promptVal.endsWith(\"\\n\") ? 1 : 0);\n\n                                var count = Math.max(valCount, promptValCount);\n                                // Somehow the handling of trailing newlines is weird\n                                // This makes the handling better for JavaFX text areas\n                                count++;\n                                return Math.max(1, count);\n                            },\n                            value,\n                            struc.getTextArea().promptTextProperty()));\n        });\n        var textAreaStruc = textArea.buildStructure();\n        var copyButton = createOpenButton();\n        var pane = new AnchorPane(textAreaStruc.get(), copyButton);\n        pane.setPickOnBounds(false);\n        AnchorPane.setTopAnchor(copyButton, 7.0);\n        AnchorPane.setRightAnchor(copyButton, 7.0);\n        AnchorPane.setLeftAnchor(textAreaStruc.get(), 0.0);\n        AnchorPane.setRightAnchor(textAreaStruc.get(), 0.0);\n        pane.maxHeightProperty().bind(textAreaStruc.get().heightProperty());\n        return new Structure(pane, textAreaStruc.getTextArea());\n    }\n\n    @Value\n    @Builder\n    public static class Structure implements RegionStructure<AnchorPane> {\n        AnchorPane pane;\n        TextArea textArea;\n\n        @Override\n        public AnchorPane get() {\n            return pane;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/IntroComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport javafx.geometry.Pos;\nimport javafx.scene.control.Label;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\nimport javafx.scene.layout.VBox;\n\nimport atlantafx.base.theme.Styles;\nimport lombok.Setter;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\npublic class IntroComp extends SimpleRegionBuilder {\n\n    private final String translationsKey;\n    private final LabelGraphic graphic;\n\n    @Setter\n    private LabelGraphic buttonGraphic;\n\n    @Setter\n    private Runnable buttonAction;\n\n    @Setter\n    private boolean buttonDefault;\n\n    public IntroComp(String translationsKey, LabelGraphic graphic) {\n        this.translationsKey = translationsKey;\n        this.graphic = graphic;\n    }\n\n    @Override\n    public Region createSimple() {\n        var title = new Label();\n        title.textProperty().bind(AppI18n.observable(translationsKey + \"Header\"));\n        title.getStyleClass().add(Styles.TEXT_BOLD);\n        AppFontSizes.title(title);\n\n        var introDesc = new Label();\n        introDesc.textProperty().bind(AppI18n.observable(translationsKey + \"Content\"));\n        introDesc.setWrapText(true);\n        introDesc.setMaxWidth(470);\n\n        var img = graphic.createGraphicNode();\n        if (img instanceof FontIcon fontIcon) {\n            fontIcon.setIconSize(80);\n        }\n        var text = new VBox(title, introDesc);\n        text.setSpacing(5);\n        text.setAlignment(Pos.CENTER_LEFT);\n        var hbox = new HBox(img, text);\n        hbox.setSpacing(55);\n        hbox.setAlignment(Pos.CENTER);\n        var v = new VBox(hbox);\n\n        if (buttonAction != null) {\n            var button = new ButtonComp(AppI18n.observable(translationsKey + \"Button\"), buttonGraphic, buttonAction);\n            if (buttonDefault) {\n                button.style(Styles.ACCENT);\n            }\n            var buttonPane = new StackPane(button.build());\n            buttonPane.setAlignment(Pos.CENTER);\n            v.getChildren().add(buttonPane);\n        }\n\n        v.setMinWidth(Region.USE_PREF_SIZE);\n        v.setMaxWidth(Region.USE_PREF_SIZE);\n        v.setMinHeight(Region.USE_PREF_SIZE);\n        v.setMaxHeight(Region.USE_PREF_SIZE);\n\n        v.setSpacing(20);\n        v.getStyleClass().add(\"intro\");\n        return v;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/IntroListComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\n\nimport javafx.geometry.Insets;\nimport javafx.geometry.Pos;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\n\nimport java.util.List;\n\npublic class IntroListComp extends SimpleRegionBuilder {\n\n    private final List<IntroComp> intros;\n\n    public IntroListComp(List<IntroComp> intros) {\n        this.intros = intros;\n    }\n\n    @Override\n    public Region createSimple() {\n        var v = new VerticalComp(intros).build();\n        v.setSpacing(80);\n        v.setMinWidth(Region.USE_PREF_SIZE);\n        v.setMaxWidth(Region.USE_PREF_SIZE);\n        v.setMinHeight(Region.USE_PREF_SIZE);\n        v.setMaxHeight(Region.USE_PREF_SIZE);\n\n        var sp = new StackPane(v);\n        sp.setPadding(new Insets(40, 0, 0, 0));\n        sp.setAlignment(Pos.CENTER);\n        sp.setPickOnBounds(false);\n        return sp;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/LabelComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.Label;\n\nimport lombok.AllArgsConstructor;\n\n@AllArgsConstructor\npublic class LabelComp extends RegionBuilder<Label> {\n\n    private final ObservableValue<String> text;\n    private final ObservableValue<LabelGraphic> graphic;\n\n    public LabelComp(String text, LabelGraphic graphic) {\n        this(new SimpleStringProperty(text), new SimpleObjectProperty<>(graphic));\n    }\n\n    public LabelComp(String text) {\n        this(new SimpleStringProperty(text));\n    }\n\n    public LabelComp(ObservableValue<String> text) {\n        this(text, new SimpleObjectProperty<>());\n    }\n\n    @Override\n    public Label createSimple() {\n        var label = new Label();\n        text.subscribe(t -> {\n            PlatformThread.runLaterIfNeeded(() -> label.setText(t));\n        });\n        graphic.subscribe(t -> {\n            PlatformThread.runLaterIfNeeded(() -> label.setGraphic(t != null ? t.createGraphicNode() : null));\n        });\n        label.setAlignment(Pos.CENTER);\n        return label;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionStructure;\nimport io.xpipe.app.comp.RegionStructureBuilder;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.core.OsType;\n\nimport javafx.application.Platform;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.TextField;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.layout.StackPane;\n\nimport lombok.Builder;\nimport lombok.Value;\n\nimport java.util.Objects;\n\npublic class LazyTextFieldComp extends RegionStructureBuilder<StackPane, LazyTextFieldComp.Structure> {\n\n    private final Property<String> currentValue;\n    private final Property<String> appliedValue;\n\n    public LazyTextFieldComp(Property<String> appliedValue) {\n        this.appliedValue = appliedValue;\n        this.currentValue = new SimpleStringProperty(appliedValue.getValue());\n    }\n\n    @Override\n    public Structure createBase() {\n        var r = new TextField();\n\n        r.setOnKeyPressed(ke -> {\n            if (ke.getCode().equals(KeyCode.ESCAPE)) {\n                currentValue.setValue(appliedValue.getValue());\n            }\n\n            if (ke.getCode().equals(KeyCode.ENTER) || ke.getCode().equals(KeyCode.ESCAPE)) {\n                r.getParent().getParent().requestFocus();\n            }\n\n            ke.consume();\n        });\n\n        r.focusedProperty().addListener((c, o, n) -> {\n            if (n && OsType.ofLocal() != OsType.WINDOWS) {\n                Platform.runLater(() -> {\n                    r.selectEnd();\n                });\n            }\n\n            if (!n) {\n                appliedValue.setValue(currentValue.getValue());\n                r.setDisable(true);\n            }\n        });\n\n        // Handles external updates\n        appliedValue.addListener((observable, oldValue, n) -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                currentValue.setValue(n);\n            });\n        });\n\n        r.setMinWidth(0);\n        r.setDisable(true);\n        r.prefWidthProperty().bind(r.minWidthProperty());\n\n        currentValue.subscribe(n -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                // Check if control value is the same. Then don't set it as that might cause bugs\n                if (Objects.equals(r.getText(), n) || (n == null && r.getText().isEmpty())) {\n                    return;\n                }\n\n                r.setText(n);\n            });\n        });\n        r.textProperty().addListener((observable, oldValue, newValue) -> {\n            currentValue.setValue(newValue);\n        });\n\n        r.getStyleClass().add(\"lazy-text-field-comp\");\n\n        var sizeLabel = new Label();\n        sizeLabel.maxWidthProperty().bind(sizeLabel.prefWidthProperty());\n        sizeLabel.textProperty().bind(currentValue);\n        sizeLabel.setVisible(false);\n        sizeLabel.paddingProperty().bind(r.paddingProperty());\n\n        var stack = new StackPane();\n        stack.getChildren().addAll(sizeLabel, r);\n        stack.setAlignment(Pos.CENTER_LEFT);\n        stack.prefWidthProperty().bind(sizeLabel.prefWidthProperty());\n        stack.prefHeightProperty().bind(r.heightProperty());\n\n        stack.focusedProperty().addListener((observable, oldValue, n) -> {\n            if (n) {\n                r.setDisable(false);\n                r.requestFocus();\n            }\n        });\n\n        return new Structure(stack, r);\n    }\n\n    @Value\n    @Builder\n    public static class Structure implements RegionStructure<StackPane> {\n        StackPane pane;\n        TextField textField;\n\n        @Override\n        public StackPane get() {\n            return pane;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/LeftSplitPaneComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionStructure;\nimport io.xpipe.app.comp.RegionStructureBuilder;\n\nimport javafx.application.Platform;\nimport javafx.beans.property.SimpleDoubleProperty;\nimport javafx.beans.value.ChangeListener;\nimport javafx.scene.control.SplitPane;\nimport javafx.scene.layout.Region;\n\nimport lombok.Value;\n\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.function.Consumer;\n\npublic class LeftSplitPaneComp extends RegionStructureBuilder<SplitPane, LeftSplitPaneComp.Structure> {\n\n    private final BaseRegionBuilder<?, ?> left;\n    private final BaseRegionBuilder<?, ?> center;\n    private Double initialWidth;\n    private Consumer<Double> onDividerChange;\n\n    public LeftSplitPaneComp(BaseRegionBuilder<?, ?> left, BaseRegionBuilder<?, ?> center) {\n        this.left = left;\n        this.center = center;\n    }\n\n    @Override\n    public Structure createBase() {\n        var c = center.build();\n        var sidebar = left.build();\n        if (initialWidth != null) {\n            sidebar.setPrefWidth(initialWidth);\n        }\n        var r = new SplitPane(c);\n\n        AtomicBoolean setInitial = new AtomicBoolean(false);\n        r.widthProperty().addListener((observable, oldValue, newValue) -> {\n            if (newValue.doubleValue() <= 0 || !r.getItems().contains(sidebar)) {\n                return;\n            }\n\n            if (!setInitial.get() && initialWidth != null && r.getDividers().size() > 0) {\n                r.getDividers().getFirst().setPosition(initialWidth / newValue.doubleValue());\n                setInitial.set(true);\n            }\n        });\n\n        var dividerPosition = new SimpleDoubleProperty();\n        ChangeListener<Number> changeListener = (observable, oldValue, newValue) -> {\n            if (r.getWidth() <= 0 || !r.getItems().contains(sidebar)) {\n                return;\n            }\n\n            if (onDividerChange != null) {\n                onDividerChange.accept(newValue.doubleValue() * r.getWidth());\n            }\n\n            dividerPosition.set(newValue.doubleValue() * r.getWidth());\n        };\n\n        sidebar.managedProperty().subscribe(m -> {\n            var divs = r.getDividers();\n            if (!m) {\n                if (!divs.isEmpty()) {\n                    divs.getFirst().positionProperty().removeListener(changeListener);\n                }\n                r.getItems().remove(sidebar);\n                if (onDividerChange != null) {\n                    onDividerChange.accept(0.0);\n                }\n            } else if (!r.getItems().contains(sidebar)) {\n                r.getItems().addFirst(sidebar);\n                var oldPos = dividerPosition.get();\n                var d = oldPos / r.getWidth();\n                divs.getFirst().setPosition(d);\n                r.layout();\n                Platform.runLater(() -> {\n                    // Div might be removed again since last time\n                    if (divs.size() > 0) {\n                        divs.getFirst().setPosition(oldPos / r.getWidth());\n                    }\n                });\n                if (onDividerChange != null) {\n                    onDividerChange.accept(d);\n                }\n                divs.getFirst().positionProperty().addListener(changeListener);\n            }\n        });\n\n        SplitPane.setResizableWithParent(sidebar, false);\n        r.getStyleClass().add(\"side-split-pane-comp\");\n        return new Structure(sidebar, c, r);\n    }\n\n    public LeftSplitPaneComp withInitialWidth(double val) {\n        this.initialWidth = val;\n        return this;\n    }\n\n    public LeftSplitPaneComp withOnDividerChange(Consumer<Double> onDividerChange) {\n        this.onDividerChange = onDividerChange;\n        return this;\n    }\n\n    @Value\n    public static class Structure implements RegionStructure<SplitPane> {\n\n        Region left;\n        Region center;\n        SplitPane pane;\n\n        @Override\n        public SplitPane get() {\n            return pane;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/ListBoxViewComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.platform.DerivedObservableList;\n\nimport javafx.animation.AnimationTimer;\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.collections.ListChangeListener;\nimport javafx.collections.ObservableList;\nimport javafx.css.PseudoClass;\nimport javafx.scene.Node;\nimport javafx.scene.control.ScrollBar;\nimport javafx.scene.control.ScrollPane;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.VBox;\n\nimport lombok.Setter;\n\nimport java.util.*;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.function.Function;\n\npublic class ListBoxViewComp<T> extends RegionBuilder<ScrollPane> {\n\n    private static final PseudoClass ODD = PseudoClass.getPseudoClass(\"odd\");\n    private static final PseudoClass EVEN = PseudoClass.getPseudoClass(\"even\");\n    private static final PseudoClass FIRST = PseudoClass.getPseudoClass(\"first\");\n    private static final PseudoClass LAST = PseudoClass.getPseudoClass(\"last\");\n\n    private final ObservableList<T> shown;\n    private final ObservableList<T> all;\n\n    private final Function<T, BaseRegionBuilder<?, ?>> compFunction;\n    private final boolean scrollBar;\n\n    @Setter\n    private boolean visibilityControl = false;\n\n    public ListBoxViewComp(\n            ObservableList<T> shown,\n            ObservableList<T> all,\n            Function<T, BaseRegionBuilder<?, ?>> compFunction,\n            boolean scrollBar) {\n        this.shown = shown;\n        this.all = all;\n        this.compFunction = compFunction;\n        this.scrollBar = scrollBar;\n    }\n\n    @Override\n    public ScrollPane createSimple() {\n        Map<T, Region> cache = new IdentityHashMap<>();\n\n        VBox vbox = new VBox();\n        vbox.getStyleClass().add(\"list-box-content\");\n        vbox.setFocusTraversable(false);\n        var scroll = new ScrollPane(vbox);\n\n        refresh(scroll, vbox, shown, all, cache, false);\n\n        var hadScene = new AtomicBoolean(false);\n        scroll.sceneProperty().subscribe(scene -> {\n            if (scene != null) {\n                hadScene.set(true);\n                refresh(scroll, vbox, shown, all, cache, true);\n            }\n        });\n\n        shown.addListener((ListChangeListener<? super T>) (c) -> {\n            Platform.runLater(() -> {\n                if (scroll.getScene() == null && hadScene.get()) {\n                    return;\n                }\n\n                refresh(scroll, vbox, c.getList(), all, cache, true);\n            });\n        });\n\n        if (scrollBar) {\n            scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);\n            scroll.skinProperty().subscribe(newValue -> {\n                if (newValue != null) {\n                    ScrollBar bar = (ScrollBar) scroll.lookup(\".scroll-bar:vertical\");\n                    bar.opacityProperty()\n                            .bind(Bindings.createDoubleBinding(\n                                    () -> {\n                                        var v = bar.getVisibleAmount();\n                                        // Check for rounding and accuracy issues\n                                        // It might not be exactly equal to 1.0\n                                        return v < 0.99 ? 1.0 : 0.0;\n                                    },\n                                    bar.visibleAmountProperty()));\n                }\n            });\n        } else {\n            scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);\n            scroll.setFitToHeight(true);\n        }\n        scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);\n        scroll.setFitToWidth(true);\n        scroll.getStyleClass().add(\"list-box-view-comp\");\n\n        registerVisibilityListeners(scroll, vbox);\n\n        return scroll;\n    }\n\n    private void registerVisibilityListeners(ScrollPane scroll, VBox vbox) {\n        if (!visibilityControl) {\n            return;\n        }\n\n        var dirty = new SimpleBooleanProperty();\n        var animationTimer = new AnimationTimer() {\n            @Override\n            public void handle(long now) {\n                if (!dirty.get()) {\n                    return;\n                }\n\n                updateVisibilities(scroll, vbox);\n                dirty.set(false);\n            }\n        };\n\n        scroll.vvalueProperty().addListener((observable, oldValue, newValue) -> {\n            // Fix scrollbar resetting on fast scroll\n            // If one node within has focus and moves out of focus fast,\n            // the scrollbar will try to focus another one and move it into view\n            // This can result in flicker when scrolling fast enough\n            var hasWindowFocus = scroll.getScene().getWindow().isFocused();\n            if (scroll.isFocusWithin() || !hasWindowFocus) {\n                scroll.requestFocus();\n            }\n            dirty.set(true);\n        });\n        scroll.heightProperty().addListener((observable, oldValue, newValue) -> {\n            dirty.set(true);\n        });\n        vbox.heightProperty().addListener((observable, oldValue, newValue) -> {\n            dirty.set(true);\n        });\n\n        // We can't directly listen to any parent element changing visibility, so this is a compromise\n        if (AppLayoutModel.get() != null) {\n            AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> {\n                dirty.set(true);\n            });\n        }\n        BrowserFullSessionModel.DEFAULT.getSelectedEntry().addListener((observable, oldValue, newValue) -> {\n            dirty.set(true);\n        });\n        if (StoreViewState.get() != null) {\n            StoreViewState.get().getGlobalSortMode().addListener((observable, oldValue, newValue) -> {\n                // This is very ugly, but it just takes multiple iterations for the order to apply\n                Platform.runLater(() -> {\n                    Platform.runLater(() -> {\n                        Platform.runLater(() -> {\n                            dirty.set(true);\n                        });\n                    });\n                });\n            });\n\n            StoreViewState.get().getTieSortMode().addListener((observable, oldValue, newValue) -> {\n                // This is very ugly, but it just takes multiple iterations for the order to apply\n                Platform.runLater(() -> {\n                    Platform.runLater(() -> {\n                        Platform.runLater(() -> {\n                            dirty.set(true);\n                        });\n                    });\n                });\n            });\n\n            StoreViewState.get().getEntriesListUpdateObservable().addListener((observable, oldValue, newValue) -> {\n                // This is very ugly, but it just takes multiple iterations for the order to apply\n                Platform.runLater(() -> {\n                    Platform.runLater(() -> {\n                        Platform.runLater(() -> {\n                            dirty.set(true);\n                        });\n                    });\n                });\n            });\n        }\n\n        vbox.sceneProperty().addListener((observable, oldValue, newValue) -> {\n            dirty.set(true);\n\n            if (newValue != null) {\n                animationTimer.start();\n            } else {\n                animationTimer.stop();\n            }\n\n            Node c = vbox;\n            do {\n                c.boundsInParentProperty().addListener((change, oldBounds, newBounds) -> {\n                    dirty.set(true);\n                });\n                // Don't listen to root node changes, we don't need that\n            } while ((c = c.getParent()) != null && c.getParent() != null);\n\n            if (newValue != null) {\n                newValue.heightProperty().addListener((observable1, oldValue1, newValue1) -> {\n                    dirty.set(true);\n                });\n            }\n        });\n    }\n\n    private boolean isVisible(ScrollPane pane, VBox box, Node node) {\n        if (pane.getScene() == null || box.getScene() == null || node.getScene() == null) {\n            return false;\n        }\n\n        // Preload items at the edges by enlarging the height\n        var paneHeight = pane.getHeight() * 1.2;\n        var scrollCenter = box.getBoundsInLocal().getHeight() * pane.getVvalue();\n        var minBoundsHeight = scrollCenter - paneHeight;\n        var maxBoundsHeight = scrollCenter + paneHeight;\n\n        var nodeMinHeight = node.getBoundsInParent().getMinY();\n        var nodeMaxHeight = node.getBoundsInParent().getMaxY();\n\n        // There are some rounding errors when display scaling is enabled,\n        // so don't check for 0.0\n        if (paneHeight < 5.0\n                || box.getHeight() < 5.0\n                || ((Region) node).getHeight() < 5.0\n                || nodeMinHeight == nodeMaxHeight) {\n            return false;\n        }\n\n        if (nodeMaxHeight < minBoundsHeight) {\n            // Use soft buffer zone to keep items visible a bit if moved out\n            // This prevents jittering when display scaling causes rounding errors around the edges\n            var useBufferZone = node.isVisible() && minBoundsHeight - nodeMaxHeight < 30.0;\n            if (!useBufferZone) {\n                return false;\n            }\n        }\n\n        if (nodeMinHeight > maxBoundsHeight) {\n            // Use soft buffer zone to keep items visible a bit if moved out\n            // This prevents jittering when display scaling causes rounding errors around the edges\n            var useBufferZone = node.isVisible() && nodeMinHeight - maxBoundsHeight < 30.0;\n            if (!useBufferZone) {\n                return false;\n            }\n        }\n\n        if (pane.getScene().getHeight() > 200) {\n            var sceneNodeBounds = node.localToScene(node.getBoundsInLocal());\n            // Add some margin to preload\n            if (sceneNodeBounds.getMaxY() < -100\n                    || sceneNodeBounds.getMinY() > pane.getScene().getHeight() + 100) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    private void updateVisibilities(ScrollPane scroll, VBox vbox) {\n        if (!Platform.isFxApplicationThread()) {\n            throw new IllegalStateException(\"Not in FxApplication thread\");\n        }\n\n        if (!scroll.isVisible() || !vbox.isVisible()) {\n            return;\n        }\n\n        if (!visibilityControl) {\n            return;\n        }\n\n        int count = 0;\n        for (Node child : vbox.getChildren()) {\n            var v = isVisible(scroll, vbox, child);\n            child.setVisible(v);\n            if (v) {\n                count++;\n            }\n        }\n\n        //        if (count > 10) {\n        //            System.out.println(\"Visible: \" + count);\n        //        }\n    }\n\n    private void refresh(\n            ScrollPane scroll,\n            VBox listView,\n            List<? extends T> shown,\n            List<? extends T> all,\n            Map<T, Region> cache,\n            boolean refreshVisibilities) {\n        Runnable update = () -> {\n            if (!Platform.isFxApplicationThread()) {\n                throw new IllegalStateException(\"Not in FxApplication thread\");\n            }\n\n            var set = new HashSet<T>();\n            // These lists might diverge on updates, so add both\n            synchronized (shown) {\n                set.addAll(shown);\n            }\n            synchronized (all) {\n                set.addAll(all);\n            }\n            // Clear cache of unused values\n            cache.keySet().retainAll(set);\n\n            // Use copy to prevent concurrent modifications and to not synchronize to long\n            List<T> shownCopy;\n            synchronized (shown) {\n                shownCopy = new ArrayList<>(shown);\n            }\n            List<Region> newShown = shownCopy.stream()\n                    .map(v -> {\n                        if (!cache.containsKey(v)) {\n                            var comp = compFunction.apply(v);\n                            if (comp != null) {\n                                var r = comp.build();\n                                if (visibilityControl) {\n                                    r.setVisible(false);\n                                }\n                                cache.put(v, r);\n                            } else {\n                                cache.put(v, null);\n                            }\n                        }\n\n                        return cache.get(v);\n                    })\n                    .filter(region -> region != null)\n                    .toList();\n\n            if (listView.getChildren().equals(newShown)) {\n                return;\n            }\n\n            for (int i = 0; i < newShown.size(); i++) {\n                var r = newShown.get(i);\n                r.pseudoClassStateChanged(ODD, i % 2 != 0);\n                r.pseudoClassStateChanged(EVEN, i % 2 == 0);\n                r.pseudoClassStateChanged(FIRST, i == 0);\n                r.pseudoClassStateChanged(LAST, i == newShown.size() - 1);\n            }\n\n            var d = DerivedObservableList.wrap(listView.getChildren(), true);\n            d.setContent(newShown);\n            if (refreshVisibilities) {\n                updateVisibilities(scroll, listView);\n            }\n        };\n        update.run();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/ListSelectorComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.collections.ListChangeListener;\nimport javafx.collections.ObservableList;\nimport javafx.geometry.Orientation;\nimport javafx.geometry.Pos;\nimport javafx.scene.Node;\nimport javafx.scene.control.CheckBox;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.ScrollPane;\nimport javafx.scene.control.Separator;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.VBox;\n\nimport lombok.EqualsAndHashCode;\nimport lombok.Value;\nimport org.int4.fx.builders.pane.HBoxBuilder;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\n\n@Value\n@EqualsAndHashCode(callSuper = true)\npublic class ListSelectorComp<T> extends SimpleRegionBuilder {\n\n    ObservableList<T> values;\n    Function<T, String> toString;\n    Function<T, LabelGraphic> toGraphic;\n    ObservableList<T> selected;\n    Predicate<T> disable;\n    Supplier<Boolean> showAllSelector;\n\n    @Override\n    protected Region createSimple() {\n        var vbox = new VBox();\n        vbox.setSpacing(8);\n        vbox.getStyleClass().add(\"list-content\");\n        var cbs = new ArrayList<CheckBox>();\n        update(vbox, cbs);\n        values.addListener((ListChangeListener<? super T>) c -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                update(vbox, cbs);\n            });\n        });\n        var sp = new ScrollPane(vbox);\n        sp.setFitToWidth(true);\n        sp.getStyleClass().add(\"list-selector-comp\");\n        return sp;\n    }\n\n    private void update(VBox vbox, List<CheckBox> cbs) {\n        List<T> currentVals;\n        // Allow for the usage of a synchronized list\n        synchronized (values) {\n            currentVals = new ArrayList<>(values);\n        }\n        vbox.getChildren().clear();\n        cbs.clear();\n        var newChildren = new ArrayList<Node>();\n        for (var v : currentVals) {\n            var cb = new CheckBox(null);\n            if (disable.test(v)) {\n                cb.setDisable(true);\n            }\n            cbs.add(cb);\n\n            cb.setAccessibleText(toString.apply(v));\n            cb.setSelected(selected.contains(v));\n            cb.selectedProperty().addListener((c, o, n) -> {\n                if (n) {\n                    selected.add(v);\n                } else {\n                    selected.remove(v);\n                }\n            });\n\n            Region graphicRegion;\n            var graphic = toGraphic.apply(v);\n            if (graphic != null) {\n                graphicRegion = new HBoxBuilder()\n                        .apply(h -> h.setSpacing(10))\n                        .apply(h -> h.setAlignment(Pos.CENTER))\n                        .nodes(cb, graphic.createGraphicNode());\n            } else {\n                graphicRegion = cb;\n            }\n\n            var l = new Label(toString.apply(v), graphicRegion);\n            l.setAlignment(Pos.CENTER);\n            l.setGraphicTextGap(9);\n            l.setOnMouseClicked(event -> {\n                if (disable.test(v)) {\n                    return;\n                }\n\n                cb.setSelected(!cb.isSelected());\n                event.consume();\n            });\n            l.opacityProperty().bind(cb.opacityProperty());\n            newChildren.add(l);\n        }\n        vbox.getChildren().addAll(newChildren);\n\n        if (showAllSelector.get()) {\n            var allSelector = new CheckBox(null);\n            allSelector.setSelected(\n                    currentVals.stream().filter(t -> !disable.test(t)).count() == selected.size());\n            allSelector.selectedProperty().addListener((observable, oldValue, newValue) -> {\n                cbs.forEach(checkBox -> {\n                    if (checkBox.isDisabled()) {\n                        return;\n                    }\n\n                    checkBox.setSelected(newValue);\n                });\n            });\n            var l = new Label(null, allSelector);\n            l.textProperty().bind(AppI18n.observable(\"selectAll\"));\n            l.setGraphicTextGap(9);\n            l.setOnMouseClicked(event -> allSelector.setSelected(!allSelector.isSelected()));\n            vbox.getChildren().add(new Separator(Orientation.HORIZONTAL));\n            vbox.getChildren().add(l);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/LoadingIconComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.animation.AnimationTimer;\nimport javafx.beans.value.ObservableValue;\nimport javafx.geometry.Pos;\nimport javafx.scene.Node;\nimport javafx.scene.control.Label;\nimport javafx.scene.layout.Region;\n\nimport java.util.function.Consumer;\n\npublic class LoadingIconComp extends SimpleRegionBuilder {\n\n    private static final char[] chars = new char[] {'⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'};\n\n    private final ObservableValue<Boolean> show;\n    private final Consumer<Node> fontSize;\n\n    public LoadingIconComp(ObservableValue<Boolean> show, Consumer<Node> fontSize) {\n        this.show = show;\n        this.fontSize = fontSize;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var label = new Label();\n        fontSize.accept(label);\n        label.setAlignment(Pos.CENTER);\n        label.setText(Character.toString(chars[0]));\n        label.getStyleClass().add(\"loading-icon-comp\");\n        label.setEllipsisString(\"\");\n\n        label.setPrefWidth(16);\n        label.setPrefHeight(16);\n\n        var timer = new AnimationTimer() {\n\n            long init = 0;\n            int index = 0;\n\n            @Override\n            public void handle(long now) {\n                if (init == 0) {\n                    init = now;\n                }\n\n                var nowMs = now;\n                if ((nowMs - init) > 250 * 1_000_000L) {\n                    label.setText(Character.toString(chars[index]));\n                    init = nowMs;\n                    index = (index + 1) % chars.length;\n                }\n            }\n        };\n\n        show.subscribe(val -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                label.setVisible(val);\n                if (val) {\n                    timer.start();\n                } else {\n                    timer.stop();\n                }\n            });\n        });\n\n        return label;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/LoadingOverlayComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.value.ChangeListener;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.layout.StackPane;\n\npublic class LoadingOverlayComp extends RegionBuilder<StackPane> {\n\n    private final BaseRegionBuilder<?, ?> comp;\n    private final ObservableValue<Boolean> loading;\n    private final boolean showIcon;\n\n    public LoadingOverlayComp(BaseRegionBuilder<?, ?> comp, ObservableValue<Boolean> loading, boolean showIcon) {\n        this.comp = comp;\n        this.loading = loading;\n        this.showIcon = showIcon;\n    }\n\n    @Override\n    public StackPane createSimple() {\n        var r = comp.build();\n        var loadingOverlay = new StackPane();\n        if (showIcon) {\n            var loading = new LoadingIconComp(this.loading, AppFontSizes::title).build();\n            loading.prefWidthProperty()\n                    .bind(Bindings.createDoubleBinding(\n                            () -> {\n                                return Math.min(r.getHeight() - 20, 50);\n                            },\n                            r.heightProperty()));\n            loading.prefHeightProperty().bind(loading.prefWidthProperty());\n            loading.managedProperty().bind(loading.visibleProperty());\n            loadingOverlay.getChildren().add(loading);\n        }\n        loadingOverlay.getStyleClass().add(\"loading-comp\");\n        loadingOverlay.setVisible(showIcon && this.loading.getValue());\n        loadingOverlay.setManaged(showIcon && this.loading.getValue());\n\n        var listener = new ChangeListener<Boolean>() {\n            @Override\n            public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean busy) {\n                if (!busy) {\n                    // Reduce flickering for consecutive loads\n                    ThreadHelper.runAsync(() -> {\n                        try {\n                            Thread.sleep(50);\n                        } catch (InterruptedException ignored) {\n                        }\n\n                        if (!LoadingOverlayComp.this.loading.getValue()) {\n                            Platform.runLater(() -> {\n                                loadingOverlay.setVisible(false);\n                                loadingOverlay.setManaged(false);\n                            });\n                        }\n                    });\n                } else {\n                    ThreadHelper.runAsync(() -> {\n                        try {\n                            Thread.sleep(50);\n                        } catch (InterruptedException ignored) {\n                        }\n\n                        if (LoadingOverlayComp.this.loading.getValue()) {\n                            Platform.runLater(() -> {\n                                loadingOverlay.setVisible(true);\n                                loadingOverlay.setManaged(true);\n                            });\n                        }\n                    });\n                }\n            }\n        };\n        this.loading.addListener(listener);\n\n        var stack = new StackPane(r, loadingOverlay);\n\n        stack.prefWidthProperty().bind(r.prefWidthProperty());\n        stack.prefHeightProperty().bind(r.prefHeightProperty());\n\n        return stack;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.core.AppResources;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.MarkdownHelper;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.util.Hyperlinks;\nimport io.xpipe.core.OsType;\n\nimport javafx.application.Platform;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.control.TextArea;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\nimport javafx.scene.paint.Color;\nimport javafx.scene.web.WebEngine;\nimport javafx.scene.web.WebView;\n\nimport lombok.SneakyThrows;\nimport org.apache.commons.io.FileUtils;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.function.UnaryOperator;\n\npublic class MarkdownComp extends RegionBuilder<StackPane> {\n\n    private static Boolean WEB_VIEW_SUPPORTED;\n    private static Path DIR;\n    private final ObservableValue<String> markdown;\n    private final UnaryOperator<String> htmlTransformation;\n    private final boolean bodyPadding;\n\n    public MarkdownComp(String markdown, UnaryOperator<String> htmlTransformation, boolean bodyPadding) {\n        this.markdown = new SimpleStringProperty(markdown);\n        this.htmlTransformation = htmlTransformation;\n        this.bodyPadding = bodyPadding;\n    }\n\n    public MarkdownComp(\n            ObservableValue<String> markdown, UnaryOperator<String> htmlTransformation, boolean bodyPadding) {\n        this.markdown = markdown;\n        this.htmlTransformation = htmlTransformation;\n        this.bodyPadding = bodyPadding;\n    }\n\n    private Path getHtmlFile(String markdown) {\n        if (DIR == null) {\n            DIR = AppCache.getBasePath().resolve(\"md\");\n        }\n\n        if (markdown == null) {\n            return null;\n        }\n\n        int hash;\n        // Rebuild files for updates in case the css have been changed\n        if (AppProperties.get().isImage()) {\n            hash = markdown.hashCode() + AppProperties.get().getVersion().hashCode();\n        } else {\n            hash = markdown.hashCode();\n        }\n        var file = DIR.resolve(\"md-\" + hash + \".html\");\n        if (Files.exists(file)) {\n            return file;\n        }\n\n        var html = MarkdownHelper.toHtml(markdown, s -> s, htmlTransformation, bodyPadding ? \"padded\" : null);\n        try {\n            // Workaround for https://bugs.openjdk.org/browse/JDK-8199014\n            FileUtils.forceMkdir(file.getParent().toFile());\n            Files.writeString(file, html);\n            return file;\n        } catch (IOException e) {\n            // Any possible IO errors can occur here\n            ErrorEventFactory.fromThrowable(e).expected().handle();\n            return null;\n        }\n    }\n\n    @SneakyThrows\n    private WebView createWebView() {\n        var wv = new WebView();\n        wv.getEngine().setJavaScriptEnabled(false);\n        wv.setContextMenuEnabled(false);\n        wv.setPageFill(Color.TRANSPARENT);\n        wv.getEngine()\n                .setUserDataDirectory(AppCache.getBasePath().resolve(\"webview\").toFile());\n        var theme = AppPrefs.get() != null\n                        && AppPrefs.get().theme().getValue() != null\n                        && AppPrefs.get().theme().getValue().isDark()\n                ? \"misc/github-markdown-dark.css\"\n                : \"misc/github-markdown-light.css\";\n        var url = AppResources.getResourceURL(AppResources.MAIN_MODULE, theme).orElseThrow();\n        wv.getEngine().setUserStyleSheetLocation(url.toString());\n\n        markdown.subscribe(val -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                var file = getHtmlFile(val);\n                if (file != null) {\n                    var contentUrl = file.toUri();\n                    wv.getEngine().load(contentUrl.toString());\n                }\n            });\n        });\n\n        // Fix initial scrollbar size\n        wv.lookupAll(\".scroll-bar\").stream().findFirst().ifPresent(node -> {\n            Region region = (Region) node;\n            region.setMinWidth(0);\n            region.setPrefWidth(7);\n            region.setMaxWidth(7);\n        });\n\n        wv.getStyleClass().add(\"markdown-comp\");\n        addLinkHandler(wv.getEngine());\n        return wv;\n    }\n\n    private void addLinkHandler(WebEngine engine) {\n        engine.getLoadWorker()\n                .stateProperty()\n                .addListener((observable, oldValue, newValue) -> Platform.runLater(() -> {\n                    String toBeopen =\n                            engine.getLoadWorker().getMessage().strip().replace(\"Loading \", \"\");\n                    if (toBeopen.contains(\"http://\") || toBeopen.contains(\"https://\") || toBeopen.contains(\"mailto:\")) {\n                        engine.getLoadWorker().cancel();\n                        Hyperlinks.open(toBeopen);\n                    }\n                }));\n    }\n\n    @Override\n    public StackPane createSimple() {\n        var sp = new StackPane();\n\n        if (OsType.ofLocal() == OsType.WINDOWS && AppProperties.get().getArch().equals(\"arm64\")) {\n            WEB_VIEW_SUPPORTED = false;\n        }\n\n        if (WEB_VIEW_SUPPORTED == null || WEB_VIEW_SUPPORTED) {\n            try {\n                var wv = createWebView();\n                WEB_VIEW_SUPPORTED = true;\n                sp.getChildren().addAll(wv);\n            } catch (Throwable t) {\n                ErrorEventFactory.fromThrowable(t).handle();\n                WEB_VIEW_SUPPORTED = false;\n            }\n        }\n\n        if (!WEB_VIEW_SUPPORTED) {\n            var text = new TextArea();\n            text.setEditable(false);\n            text.setWrapText(true);\n            markdown.subscribe(s -> {\n                PlatformThread.runLaterIfNeeded(() -> {\n                    text.setText(s);\n                });\n            });\n            sp.getChildren().add(text);\n        }\n\n        return sp;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/MarkdownEditorComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionStructure;\nimport io.xpipe.app.comp.RegionStructureBuilder;\nimport io.xpipe.app.util.FileOpener;\n\nimport javafx.application.Platform;\nimport javafx.beans.property.Property;\nimport javafx.scene.control.Button;\nimport javafx.scene.control.TextArea;\nimport javafx.scene.layout.AnchorPane;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\n\nimport atlantafx.base.theme.Styles;\nimport lombok.Builder;\nimport lombok.Value;\n\npublic class MarkdownEditorComp extends RegionStructureBuilder<AnchorPane, MarkdownEditorComp.Structure> {\n\n    private final Property<String> value;\n    private final String identifier;\n\n    public MarkdownEditorComp(Property<String> value, String identifier) {\n        this.value = value;\n        this.identifier = identifier;\n    }\n\n    private Button createOpenButton() {\n        return new IconButtonComp(\n                        \"mdal-edit\",\n                        () -> FileOpener.openString(identifier + \".md\", this, value.getValue(), (s) -> {\n                            Platform.runLater(() -> value.setValue(s));\n                        }))\n                .style(\"edit-button\")\n                .apply(struc -> struc.getStyleClass().remove(Styles.FLAT))\n                .build();\n    }\n\n    @Override\n    public Structure createBase() {\n        var markdown = new MarkdownComp(value, s -> s, true).build();\n        var editButton = createOpenButton();\n        var pane = new AnchorPane(markdown, editButton);\n        pane.setPickOnBounds(false);\n        AnchorPane.setTopAnchor(editButton, 10.0);\n        AnchorPane.setRightAnchor(editButton, 10.0);\n        markdown.prefWidthProperty().bind(pane.prefWidthProperty());\n        markdown.prefHeightProperty().bind(pane.prefHeightProperty());\n        return new Structure(pane, markdown, editButton);\n    }\n\n    @Value\n    @Builder\n    public static class TextAreaStructure implements RegionStructure<StackPane> {\n        StackPane pane;\n        TextArea textArea;\n\n        @Override\n        public StackPane get() {\n            return pane;\n        }\n    }\n\n    @Value\n    @Builder\n    public static class Structure implements RegionStructure<AnchorPane> {\n        AnchorPane pane;\n        Region markdown;\n        Button editButton;\n\n        @Override\n        public AnchorPane get() {\n            return pane;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/ModalButton.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.core.mode.AppOperationMode;\n\nimport javafx.beans.property.Property;\nimport javafx.scene.control.Button;\n\nimport lombok.Value;\nimport lombok.experimental.NonFinal;\n\nimport java.util.function.Consumer;\n\n@Value\npublic class ModalButton {\n    String key;\n    Runnable action;\n    boolean close;\n    boolean defaultButton;\n\n    @NonFinal\n    Consumer<Button> augment;\n\n    public ModalButton(String key, Runnable action, boolean close, boolean defaultButton) {\n        this.key = key;\n        this.action = action;\n        this.close = close;\n        this.defaultButton = defaultButton;\n    }\n\n    public static ModalButton ok(Runnable action) {\n        return new ModalButton(\"ok\", action, true, true);\n    }\n\n    public static ModalButton ok() {\n        return new ModalButton(\"ok\", null, true, true);\n    }\n\n    public static ModalButton cancel() {\n        return cancel(null);\n    }\n\n    public static ModalButton cancel(Runnable action) {\n        return new ModalButton(\"cancel\", action, true, false);\n    }\n\n    public static ModalButton confirm(Runnable action) {\n        return new ModalButton(\"confirm\", action, true, true);\n    }\n\n    public static ModalButton quit() {\n        return new ModalButton(\n                \"quit\",\n                () -> {\n                    AppOperationMode.halt(1);\n                },\n                true,\n                false);\n    }\n\n    public static Runnable toggle(Property<Boolean> prop) {\n        return () -> {\n            prop.setValue(true);\n        };\n    }\n\n    public ModalButton augment(Consumer<Button> augment) {\n        this.augment = augment;\n        return this;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/ModalOverlay.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.*;\nimport lombok.experimental.NonFinal;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@Value\n@With\n@Builder(toBuilder = true)\npublic class ModalOverlay {\n\n    ObservableValue<String> title;\n    BaseRegionBuilder<?, ?> content;\n    LabelGraphic graphic;\n\n    @Singular\n    List<Object> buttons;\n\n    @NonFinal\n    @Setter\n    boolean hasCloseButton;\n\n    @NonFinal\n    @Setter\n    boolean requireCloseButtonForClose;\n\n    @NonFinal\n    @Setter\n    Runnable hideAction;\n\n    public static ModalOverlay of(BaseRegionBuilder<?, ?> content) {\n        return of((ObservableValue<String>) null, content, null);\n    }\n\n    public static ModalOverlay of(String titleKey, BaseRegionBuilder<?, ?> content) {\n        return of(titleKey, content, null);\n    }\n\n    public static ModalOverlay of(String titleKey, BaseRegionBuilder<?, ?> content, LabelGraphic graphic) {\n        return of(titleKey != null ? AppI18n.observable(titleKey) : null, content, graphic);\n    }\n\n    public static ModalOverlay of(\n            ObservableValue<String> title, BaseRegionBuilder<?, ?> content, LabelGraphic graphic) {\n        return new ModalOverlay(title, content, graphic, new ArrayList<>(), true, false, null);\n    }\n\n    public ModalOverlay withDefaultButtons(Runnable action) {\n        addButton(ModalButton.cancel());\n        addButton(ModalButton.ok(action));\n        return this;\n    }\n\n    public ModalButton addButton(ModalButton button) {\n        buttons.add(button);\n        return button;\n    }\n\n    public void hideable(AppLayoutModel.QueueEntry entry) {\n        setHideAction(() -> {\n            AppLayoutModel.get().getQueueEntries().add(entry);\n        });\n    }\n\n    public void addButtonBarComp(BaseRegionBuilder<?, ?> comp) {\n        buttons.add(comp);\n    }\n\n    public void persist() {\n        this.hasCloseButton = false;\n        this.requireCloseButtonForClose = true;\n    }\n\n    public void show() {\n        AppDialog.show(this, false);\n    }\n\n    public void hide() {\n        AppDialog.hide(this);\n    }\n\n    public boolean isShowing() {\n        return AppDialog.getModalOverlays().contains(this);\n    }\n\n    public void showAndWait() {\n        AppDialog.showAndWait(this);\n    }\n\n    public void close() {\n        AppDialog.closeDialog(this);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppLogs;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.util.BooleanScope;\nimport io.xpipe.core.OsType;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleDoubleProperty;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.value.ObservableDoubleValue;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.Button;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyEvent;\nimport javafx.scene.input.MouseEvent;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\nimport javafx.scene.layout.VBox;\nimport javafx.util.Duration;\n\nimport atlantafx.base.controls.ModalPane;\nimport atlantafx.base.controls.ModalPaneSkin;\nimport atlantafx.base.layout.ModalBox;\nimport atlantafx.base.theme.Styles;\nimport atlantafx.base.util.Animations;\nimport net.synedra.validatorfx.GraphicDecorationStackPane;\n\nimport java.time.Instant;\n\npublic class ModalOverlayComp extends RegionBuilder<Region> {\n\n    private final BaseRegionBuilder<?, ?> background;\n    private final Property<ModalOverlay> overlayContent;\n    private final BooleanScope actionRunning = new BooleanScope(new SimpleBooleanProperty()).exclusive();\n\n    public ModalOverlayComp(BaseRegionBuilder<?, ?> background, Property<ModalOverlay> overlayContent) {\n        this.background = background;\n        this.overlayContent = overlayContent;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var lastShow = new SimpleObjectProperty<Instant>();\n        var bgRegion = background.build();\n        var modal = new ModalPane();\n        modal.setSkin(new ModalPaneSkin(modal) {\n\n            @Override\n            protected void registerListeners() {\n                super.registerListeners();\n\n                scrollPane.removeEventFilter(MouseEvent.MOUSE_PRESSED, mouseHandler);\n                scrollPane.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {\n                    var lastShowValue = lastShow.getValue();\n                    if (lastShowValue != null\n                            && java.time.Duration.between(lastShowValue, Instant.now())\n                                            .toMillis()\n                                    > 500) {\n                        mouseHandler.handle(event);\n                    }\n                });\n            }\n        });\n        modal.setInTransitionFactory(\n                OsType.ofLocal() == OsType.LINUX ? null : node -> Animations.fadeIn(node, Duration.millis(150)));\n        modal.setOutTransitionFactory(\n                OsType.ofLocal() == OsType.LINUX ? null : node -> Animations.fadeOut(node, Duration.millis(50)));\n        modal.focusedProperty().addListener((observable, oldValue, newValue) -> {\n            var c = modal.getContent();\n            if (newValue && c != null) {\n                c.requestFocus();\n            }\n        });\n        modal.getStyleClass().add(\"modal-overlay-comp\");\n        var pane = new StackPane(bgRegion, modal);\n        pane.setAlignment(Pos.TOP_LEFT);\n        pane.setPickOnBounds(false);\n        pane.focusedProperty().addListener((observable, oldValue, newValue) -> {\n            if (newValue) {\n                if (modal.isDisplay()) {\n                    modal.requestFocus();\n                } else {\n                    bgRegion.requestFocus();\n                }\n            }\n        });\n\n        modal.contentProperty().addListener((observable, oldValue, newValue) -> {\n            if (newValue == null) {\n                overlayContent.setValue(null);\n                bgRegion.setDisable(false);\n                bgRegion.requestFocus();\n            }\n\n            if (newValue != null) {\n                bgRegion.setDisable(true);\n            }\n        });\n\n        modal.displayProperty().addListener((observable, oldValue, newValue) -> {\n            if (newValue) {\n                lastShow.setValue(Instant.now());\n            } else {\n                overlayContent.setValue(null);\n                bgRegion.setDisable(false);\n                bgRegion.requestFocus();\n            }\n        });\n\n        modal.addEventHandler(KeyEvent.KEY_PRESSED, event -> {\n            if (event.getCode() == KeyCode.ENTER || event.getCode() == KeyCode.SPACE) {\n                if (actionRunning.get()) {\n                    return;\n                }\n\n                var ov = overlayContent.getValue();\n                if (ov != null) {\n                    var def = ov.getButtons().stream()\n                            .filter(modalButton -> modalButton instanceof ModalButton mb && mb.isDefaultButton())\n                            .findFirst();\n                    if (def.isPresent()) {\n                        var mb = (ModalButton) def.get();\n                        if (mb.getAction() != null) {\n                            try (var ignored = actionRunning.start()) {\n                                mb.getAction().run();\n                            }\n                        }\n                        if (mb.isClose()) {\n                            overlayContent.setValue(null);\n                        }\n                        event.consume();\n                    }\n                }\n            }\n        });\n\n        overlayContent.addListener((observable, oldValue, newValue) -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                if (oldValue != null && modal.isDisplay()) {\n                    if (newValue == null) {\n                        modal.hide(true);\n                    }\n                }\n\n                if (oldValue != null) {\n                    if (oldValue.getContent() instanceof ModalOverlayContentComp mocc) {\n                        mocc.setModalOverlay(null);\n                    }\n                }\n\n                try {\n                    if (newValue != null) {\n                        if (newValue.getContent() instanceof ModalOverlayContentComp mocc) {\n                            mocc.setModalOverlay(newValue);\n                        }\n                        showModalBox(modal, newValue);\n                    }\n                } catch (Throwable t) {\n                    AppLogs.get().logException(null, t);\n                    Platform.runLater(() -> {\n                        overlayContent.setValue(null);\n                    });\n                }\n            });\n        });\n\n        var current = overlayContent.getValue();\n        if (current != null) {\n            showModalBox(modal, current);\n        }\n\n        return pane;\n    }\n\n    private void showModalBox(ModalPane modal, ModalOverlay overlay) {\n        var modalBox = toBox(modal, overlay);\n        modal.setPersistent(overlay.isRequireCloseButtonForClose());\n        modal.show(modalBox);\n        if (!overlay.isHasCloseButton() || overlay.getTitle() == null) {\n            var closeButton = modalBox.lookup(\".close-button\");\n            if (closeButton != null) {\n                closeButton.setVisible(false);\n            }\n        }\n        modal.requestFocus();\n    }\n\n    private Region toBox(ModalPane pane, ModalOverlay newValue) {\n        Region r = newValue.getContent().build();\n        var validatorPane = new GraphicDecorationStackPane();\n        validatorPane.getChildren().add(r);\n\n        var content = new VBox(validatorPane);\n        content.getStyleClass().add(\"content\");\n        content.focusedProperty().addListener((o, old, n) -> {\n            if (n) {\n                r.requestFocus();\n            }\n        });\n        content.setSpacing(20);\n\n        if (newValue.getTitle() != null) {\n            var l = new LabelComp(\n                    newValue.getTitle(),\n                    new SimpleObjectProperty<>(\n                            newValue.getGraphic() != null\n                                    ? newValue.getGraphic()\n                                    : new LabelGraphic.IconGraphic(\"mdi2i-information-outline\")));\n            l.apply(struc -> {\n                struc.setGraphicTextGap(8);\n                AppFontSizes.xl(struc);\n            });\n            content.getChildren().addFirst(l.build());\n        } else {\n            content.getChildren().addFirst(RegionBuilder.vspacer(0).build());\n        }\n\n        if (newValue.getButtons().size() > 0) {\n            var max = new SimpleDoubleProperty();\n            var buttonBar = new HBox();\n            buttonBar.getStyleClass().add(\"button-bar\");\n            buttonBar.setSpacing(10);\n            buttonBar.setAlignment(Pos.CENTER_RIGHT);\n            for (var o : newValue.getButtons()) {\n                var node = o instanceof ModalButton mb ? toButton(mb) : ((BaseRegionBuilder<?, ?>) o).build();\n                if (o instanceof ModalButton) {\n                    node.widthProperty().addListener((observable, oldValue, n) -> {\n                        var d = Math.min(Math.max(n.doubleValue(), 70.0), 200.0);\n                        if (d > max.get()) {\n                            max.set(d);\n                        }\n                    });\n                    node.minWidthProperty().bind(max);\n                    node.prefHeightProperty().bind(buttonBar.heightProperty());\n                }\n                buttonBar.getChildren().add(node);\n            }\n            content.getChildren().add(buttonBar);\n            AppFontSizes.apply(buttonBar, sizes -> {\n                if (sizes.getBase().equals(\"10.5\")) {\n                    return sizes.getBase();\n                } else {\n                    return sizes.getSm();\n                }\n            });\n        }\n\n        var modalBox = new ModalBox(pane, content) {\n\n            @Override\n            protected void setCloseButtonPosition() {\n                setTopAnchor(closeButton, 10d);\n                setRightAnchor(closeButton, 19d);\n            }\n        };\n        modalBox.setClearOnClose(true);\n        if (newValue.getHideAction() != null) {\n            modalBox.setOnMinimize(event -> {\n                newValue.getHideAction().run();\n                event.consume();\n            });\n        }\n        modalBox.setOnClose(event -> {\n            overlayContent.setValue(null);\n            event.consume();\n        });\n        content.maxHeightProperty().bind(pane.heightProperty().subtract(40));\n        modalBox.minHeightProperty().bind(content.heightProperty());\n\n        content.prefWidthProperty().bind(modalBox.widthProperty());\n        modalBox.setMinWidth(100);\n        modalBox.prefWidthProperty().bind(modalBoxWidth(pane, r));\n        modalBox.maxWidthProperty().bind(modalBox.prefWidthProperty());\n        modalBox.setMaxHeight(Region.USE_PREF_SIZE);\n        modalBox.focusedProperty().addListener((o, old, n) -> {\n            if (n) {\n                content.requestFocus();\n            }\n        });\n\n        if (newValue.getContent() instanceof ModalOverlayContentComp mocc) {\n            var busy = mocc.busy();\n            if (busy != null) {\n                var loading = new LoadingOverlayComp(RegionBuilder.of(() -> modalBox), busy, true);\n                return loading.build();\n            }\n        }\n\n        return modalBox;\n    }\n\n    private ObservableDoubleValue modalBoxWidth(ModalPane pane, Region r) {\n        return Bindings.createDoubleBinding(\n                () -> {\n                    var max = pane.getWidth() - 120;\n                    if (r.getPrefWidth() != Region.USE_COMPUTED_SIZE) {\n                        return Math.min(max, r.getPrefWidth() + 50);\n                    }\n                    return max;\n                },\n                pane.widthProperty(),\n                r.prefWidthProperty());\n    }\n\n    private Button toButton(ModalButton mb) {\n        var button = new Button(mb.getKey() != null ? AppI18n.get(mb.getKey()) : null);\n        if (mb.isDefaultButton()) {\n            button.getStyleClass().add(Styles.ACCENT);\n        }\n        if (mb.getAugment() != null) {\n            mb.getAugment().accept(button);\n        }\n        button.managedProperty().bind(button.visibleProperty());\n        button.setOnAction(event -> {\n            if (actionRunning.get()) {\n                return;\n            }\n\n            if (mb.getAction() != null) {\n                try (var ignored = actionRunning.start()) {\n                    mb.getAction().run();\n                }\n            }\n            if (mb.isClose()) {\n                overlayContent.setValue(null);\n            }\n            event.consume();\n        });\n        return button;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/ModalOverlayContentComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.Getter;\n\n@Getter\npublic abstract class ModalOverlayContentComp extends SimpleRegionBuilder {\n\n    protected ModalOverlay modalOverlay;\n\n    protected void setModalOverlay(ModalOverlay modalOverlay) {\n        this.modalOverlay = modalOverlay;\n    }\n\n    protected ObservableValue<Boolean> busy() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/ModalOverlayStackComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\n\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.collections.ListChangeListener;\nimport javafx.collections.ObservableList;\nimport javafx.scene.layout.Region;\n\nimport java.util.concurrent.atomic.AtomicInteger;\n\npublic class ModalOverlayStackComp extends SimpleRegionBuilder {\n\n    private final BaseRegionBuilder<?, ?> background;\n    private final ObservableList<ModalOverlay> modalOverlay;\n\n    public ModalOverlayStackComp(BaseRegionBuilder<?, ?> background, ObservableList<ModalOverlay> modalOverlay) {\n        this.background = background;\n        this.modalOverlay = modalOverlay;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var current = background;\n        for (var i = 0; i < 5; i++) {\n            current = buildModalOverlay(current, i);\n        }\n        return current.build();\n    }\n\n    private BaseRegionBuilder<?, ?> buildModalOverlay(BaseRegionBuilder<?, ?> current, int index) {\n        AtomicInteger currentIndex = new AtomicInteger(index);\n        var prop = new SimpleObjectProperty<>(modalOverlay.size() > index ? modalOverlay.get(index) : null);\n        modalOverlay.addListener((ListChangeListener<? super ModalOverlay>) c -> {\n            var ex = prop.get();\n            // Don't shift just for an index change\n            if (ex != null && modalOverlay.contains(ex)) {\n                currentIndex.set(modalOverlay.indexOf(ex));\n                return;\n            } else {\n                currentIndex.set(index);\n            }\n\n            prop.set(modalOverlay.size() > index ? modalOverlay.get(index) : null);\n        });\n        prop.addListener((observable, oldValue, newValue) -> {\n            if (newValue == null && modalOverlay.indexOf(oldValue) == currentIndex.get()) {\n                modalOverlay.remove(oldValue);\n            }\n        });\n        var comp = new ModalOverlayComp(current, prop);\n        comp.style(\"modal-overlay-stack-element\");\n        return comp;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/MultiContentComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.application.Platform;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.FXCollections;\nimport javafx.collections.MapChangeListener;\nimport javafx.collections.ObservableMap;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\n\nimport java.util.Map;\n\npublic class MultiContentComp extends SimpleRegionBuilder {\n\n    private final boolean requestFocus;\n    private final boolean log;\n    private final Map<BaseRegionBuilder<?, ?>, ObservableValue<Boolean>> content;\n\n    public MultiContentComp(\n            boolean requestFocus, Map<BaseRegionBuilder<?, ?>, ObservableValue<Boolean>> content, boolean log) {\n        this.requestFocus = requestFocus;\n        this.log = log;\n        this.content = FXCollections.observableMap(content);\n    }\n\n    @Override\n    protected Region createSimple() {\n        ObservableMap<BaseRegionBuilder<?, ?>, Region> m = FXCollections.observableHashMap();\n        var stack = new StackPane();\n        m.addListener((MapChangeListener<? super BaseRegionBuilder<?, ?>, Region>) change -> {\n            if (change.wasAdded()) {\n                stack.getChildren().add(change.getValueAdded());\n            } else {\n                stack.getChildren().remove(change.getValueRemoved());\n            }\n        });\n\n        stack.focusedProperty().addListener((observable, oldValue, newValue) -> {\n            if (newValue) {\n                var selected = content.entrySet().stream()\n                        .filter(e -> e.getValue().getValue())\n                        .map(e -> m.get(e.getKey()))\n                        .findFirst();\n                if (selected.isPresent()) {\n                    selected.get().requestFocus();\n                }\n            }\n        });\n\n        for (Map.Entry<BaseRegionBuilder<?, ?>, ObservableValue<Boolean>> e : content.entrySet()) {\n            var name = e.getKey().getClass().getSimpleName();\n            if (log) {\n                TrackEvent.trace(\"Creating content tab region for element \" + name);\n            }\n            var r = e.getKey().build();\n            if (log) {\n                TrackEvent.trace(\"Created content tab region for element \" + name);\n            }\n            e.getValue().subscribe(val -> {\n                PlatformThread.runLaterIfNeeded(() -> {\n                    r.setManaged(val);\n                    r.setVisible(val);\n                    if (requestFocus && val) {\n                        Platform.runLater(() -> {\n                            r.requestFocus();\n                        });\n                    }\n                });\n            });\n            m.put(e.getKey(), r);\n            if (log) {\n                TrackEvent.trace(\"Added content tab region for element \" + name);\n            }\n        }\n\n        return stack;\n    }\n\n    //    Lazy impl\n    //    @Override\n    //    protected Region createSimple() {\n    //        var stack = new StackPane();\n    //        for (Map.Entry<BaseRegionBuilder<?,?>, ObservableValue<Boolean>> e : content.entrySet()) {\n    //            var r = e.getKey().build();\n    //            e.getValue().subscribe(val -> {\n    //                PlatformThread.runLaterIfNeeded(() -> {\n    //                    r.setManaged(val);\n    //                    r.setVisible(val);\n    //                    if (val && !stack.getChildren().contains(r)) {\n    //                        stack.getChildren().add(r);\n    //                    } else {\n    //                        stack.getChildren().remove(r);\n    //                    }\n    //                });\n    //            });\n    //        }\n    //\n    //        return stack;\n    //    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/OptionsComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.platform.Check;\nimport io.xpipe.app.util.Hyperlinks;\n\nimport javafx.application.Platform;\nimport javafx.beans.Observable;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.geometry.Insets;\nimport javafx.geometry.Orientation;\nimport javafx.geometry.Pos;\nimport javafx.scene.AccessibleRole;\nimport javafx.scene.Node;\nimport javafx.scene.TraversalDirection;\nimport javafx.scene.control.Button;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.Tooltip;\nimport javafx.scene.layout.*;\nimport javafx.util.Duration;\n\nimport atlantafx.base.controls.Spacer;\nimport atlantafx.base.theme.Styles;\nimport lombok.Getter;\nimport org.int4.fx.builders.common.AbstractRegionBuilder;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\n\n@Getter\npublic class OptionsComp extends RegionBuilder<VBox> {\n\n    private final List<Entry> entries;\n    private final List<Check> checks;\n\n    public OptionsComp(List<Entry> entries, List<Check> checks) {\n        this.entries = entries;\n        this.checks = checks;\n    }\n\n    @Override\n    public VBox createSimple() {\n        VBox pane = new VBox();\n        pane.getStyleClass().add(\"options-comp\");\n\n        var nameRegions = new ArrayList<Region>();\n        var nameMap = new HashMap<Node, Node>();\n\n        Region firstComp = null;\n        for (var entry : getEntries()) {\n            Region compRegion = entry.comp() != null ? entry.comp().build() : new Region();\n\n            if (firstComp == null) {\n                compRegion.getStyleClass().add(\"first\");\n                firstComp = compRegion;\n            }\n\n            var showVertical = (entry.name() != null\n                    && (entry.description() != null || entry.comp() instanceof SimpleTitledPaneComp));\n            if (showVertical) {\n                var line = new VBox();\n                line.prefWidthProperty().bind(pane.widthProperty());\n\n                var name = new Label();\n                name.getStyleClass().add(\"name\");\n                name.textProperty().bind(entry.name());\n                name.setMinWidth(Region.USE_PREF_SIZE);\n                name.setMinHeight(Region.USE_PREF_SIZE);\n                name.setAlignment(Pos.CENTER_LEFT);\n                VBox.setVgrow(line, VBox.getVgrow(compRegion));\n                line.spacingProperty()\n                        .bind(Bindings.createDoubleBinding(\n                                () -> {\n                                    return name.isManaged() ? 2.0 : 0.0;\n                                },\n                                name.managedProperty()));\n                name.visibleProperty().bind(compRegion.visibleProperty());\n                name.managedProperty().bind(compRegion.managedProperty());\n                VBox.setMargin(name, new Insets(0, 0, 0, 1));\n\n                if (entry.description() != null) {\n                    var description = new Label();\n                    description.setWrapText(true);\n                    description.getStyleClass().add(\"description\");\n                    description.textProperty().bind(entry.description());\n                    description.setAlignment(Pos.CENTER_LEFT);\n                    description.setMinHeight(Region.USE_PREF_SIZE);\n                    description.visibleProperty().bind(compRegion.visibleProperty());\n                    description.managedProperty().bind(compRegion.managedProperty());\n\n                    var vbox = new VBox();\n                    vbox.getChildren().add(name);\n                    vbox.spacingProperty()\n                            .bind(Bindings.createDoubleBinding(\n                                    () -> {\n                                        return name.isManaged() ? 2.0 : 0.0;\n                                    },\n                                    name.managedProperty()));\n\n                    vbox.focusTraversableProperty()\n                            .bind(Platform.accessibilityActiveProperty().and(compRegion.visibleProperty()));\n                    vbox.setAccessibleRole(AccessibleRole.TEXT);\n                    var joined = Bindings.createStringBinding(\n                            () -> {\n                                return entry.name.getValue() + \"\\n\\n\"\n                                        + entry.description().getValue();\n                            },\n                            entry.name(),\n                            entry.description());\n                    vbox.accessibleTextProperty().bind(joined);\n\n                    if (entry.documentationLink() != null) {\n                        var link = new Button(\"... ?\");\n                        link.setMinWidth(Region.USE_PREF_SIZE);\n                        link.getStyleClass().add(Styles.BUTTON_OUTLINED);\n                        link.getStyleClass().add(Styles.ACCENT);\n                        link.getStyleClass().add(\"long-description\");\n                        link.accessibleTextProperty()\n                                .bind(Bindings.createStringBinding(\n                                        () -> {\n                                            return AppI18n.get(\n                                                    \"helpButton\", entry.name().getValue());\n                                        },\n                                        AppI18n.activeLanguage(),\n                                        entry.name()));\n                        AppFontSizes.xl(link);\n                        link.setOnAction(e -> {\n                            Hyperlinks.open(entry.documentationLink());\n                            e.consume();\n                        });\n\n                        var tt = TooltipHelper.create(new SimpleStringProperty(entry.documentationLink()));\n                        tt.setShowDelay(Duration.millis(1));\n                        Tooltip.install(link, tt);\n\n                        var descriptionBox = new HBox(description, new Spacer(Orientation.HORIZONTAL), link);\n                        descriptionBox.getStyleClass().add(\"description-box\");\n                        descriptionBox.setSpacing(5);\n                        HBox.setHgrow(descriptionBox, Priority.ALWAYS);\n                        descriptionBox.setAlignment(Pos.TOP_LEFT);\n                        vbox.getChildren().add(descriptionBox);\n                        VBox.setMargin(descriptionBox, new Insets(0, 0, 0, 1));\n                        descriptionBox.visibleProperty().bind(compRegion.visibleProperty());\n                        descriptionBox.managedProperty().bind(compRegion.managedProperty());\n                    } else {\n                        vbox.getChildren().add(description);\n                        vbox.getChildren().add(new Spacer(2, Orientation.VERTICAL));\n                        VBox.setMargin(description, new Insets(0, 0, 0, 1));\n                    }\n\n                    line.getChildren().add(vbox);\n                    nameMap.put(compRegion, vbox);\n                } else {\n                    line.getChildren().add(name);\n                    nameMap.put(compRegion, name);\n                }\n\n                line.getChildren().add(compRegion);\n                compRegion.getStyleClass().add(\"options-content\");\n\n                pane.getChildren().add(line);\n            } else if (entry.name() != null) {\n                var line = new HBox();\n                line.setFillHeight(true);\n                line.prefWidthProperty().bind(pane.widthProperty());\n                line.setSpacing(8);\n\n                var name = new Label();\n                name.textProperty().bind(entry.name());\n                name.prefHeightProperty().bind(line.heightProperty());\n                name.setMinWidth(Region.USE_PREF_SIZE);\n                name.setAlignment(Pos.CENTER_LEFT);\n                name.focusTraversableProperty()\n                        .bind(Platform.accessibilityActiveProperty().and(compRegion.visibleProperty()));\n                name.setAccessibleRole(AccessibleRole.TEXT);\n                name.accessibleTextProperty().bind(entry.name());\n                nameRegions.add(name);\n                name.visibleProperty().bind(compRegion.visibleProperty());\n                name.managedProperty().bind(compRegion.managedProperty());\n                line.getChildren().add(name);\n                nameMap.put(compRegion, name);\n\n                line.getChildren().add(compRegion);\n                HBox.setHgrow(compRegion, Priority.ALWAYS);\n\n                pane.getChildren().add(line);\n            } else {\n                pane.getChildren().add(compRegion);\n            }\n\n            var last = entry.equals(entries.getLast());\n            if (!last) {\n                Spacer spacer = new Spacer(7, Orientation.VERTICAL);\n                pane.getChildren().add(spacer);\n                spacer.visibleProperty().bind(compRegion.visibleProperty());\n                spacer.managedProperty().bind(compRegion.managedProperty());\n            }\n        }\n\n        if (entries.size() == 1 && firstComp != null) {\n            firstComp.visibleProperty().subscribe(v -> {\n                pane.setVisible(v);\n            });\n            firstComp.managedProperty().subscribe(v -> {\n                pane.setManaged(v);\n            });\n        }\n\n        for (Region nameRegion : nameRegions) {\n            nameRegion.setPrefWidth(Region.USE_COMPUTED_SIZE);\n        }\n\n        if (entries.stream().anyMatch(entry -> entry.name() != null && entry.description() == null)) {\n            var nameWidthBinding = Bindings.createDoubleBinding(\n                    () -> {\n                        return nameRegions.stream()\n                                .map(Region::getWidth)\n                                .filter(aDouble -> aDouble > 0.0)\n                                .max(Double::compareTo)\n                                .orElse(Region.USE_COMPUTED_SIZE);\n                    },\n                    nameRegions.stream().map(Region::widthProperty).toList().toArray(new Observable[0]));\n            BindingsHelper.preserve(pane, nameWidthBinding);\n            nameWidthBinding.addListener((observableValue, number, t1) -> {\n                Platform.runLater(() -> {\n                    for (Region nameRegion : nameRegions) {\n                        nameRegion.setPrefWidth(t1.doubleValue());\n                    }\n                });\n            });\n        }\n\n        Region finalFirstComp = firstComp;\n        pane.focusedProperty().addListener((observable, oldValue, newValue) -> {\n            if (!newValue) {\n                return;\n            }\n\n            var failed = checks.stream()\n                    .filter(check -> check.getValidationResult().getMessages().size() > 0)\n                    .findFirst();\n\n            Node target = null;\n            if (failed.isPresent()) {\n                var targets = failed.get().getTargets();\n                if (targets.size() > 0) {\n                    target = targets.getFirst();\n                }\n            } else {\n                if (finalFirstComp != null) {\n                    target = finalFirstComp;\n                }\n            }\n\n            if (target != null) {\n                var a18y = Platform.accessibilityActiveProperty().get();\n                if (a18y) {\n                    if (nameMap.containsKey(target)) {\n                        nameMap.get(target).requestFocus();\n                    } else {\n                        target.requestFocus();\n                        target.requestFocusTraversal(TraversalDirection.UP);\n                    }\n                } else {\n                    target.requestFocus();\n                }\n            }\n        });\n\n        return pane;\n    }\n\n    public record Entry(\n            ObservableValue<String> description,\n            String documentationLink,\n            ObservableValue<String> name,\n            AbstractRegionBuilder<?, ?> comp) {}\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/PrettyImageComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.core.AppImages;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.core.FilePath;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.geometry.Pos;\nimport javafx.scene.image.ImageView;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\n\nimport java.util.function.Consumer;\n\npublic class PrettyImageComp extends SimpleRegionBuilder {\n\n    private final ObservableValue<String> value;\n    private final double width;\n    private final double height;\n\n    public PrettyImageComp(ObservableValue<String> value, double width, double height) {\n        this.value = value;\n        this.width = width;\n        this.height = height;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var storeIcon = new ImageView();\n        var aspectRatioProperty = Bindings.createDoubleBinding(\n                () -> {\n                    if (storeIcon.getImage() == null) {\n                        return 1.0;\n                    }\n\n                    return storeIcon.getImage().getWidth()\n                            / storeIcon.getImage().getHeight();\n                },\n                storeIcon.imageProperty());\n        var widthProperty = Bindings.createDoubleBinding(\n                () -> {\n                    boolean widthLimited = width / height < aspectRatioProperty.doubleValue();\n                    if (widthLimited) {\n                        return width;\n                    } else {\n                        return height * aspectRatioProperty.doubleValue();\n                    }\n                },\n                aspectRatioProperty);\n        var heightProperty = Bindings.createDoubleBinding(\n                () -> {\n                    boolean widthLimited = width / height < aspectRatioProperty.doubleValue();\n                    if (widthLimited) {\n                        return width / aspectRatioProperty.doubleValue();\n                    } else {\n                        return height;\n                    }\n                },\n                aspectRatioProperty);\n        var image = new SimpleStringProperty();\n        var stack = new StackPane();\n\n        storeIcon.setFocusTraversable(false);\n        storeIcon\n                .imageProperty()\n                .bind(Bindings.createObjectBinding(\n                        () -> {\n                            if (image.get() == null) {\n                                return null;\n                            }\n\n                            var value = image.getValue();\n                            if (AppImages.hasImage(value)) {\n                                return AppImages.image(value);\n                            } else if (AppImages.hasImage(value.replace(\"-dark\", \"\"))) {\n                                return AppImages.image(value.replace(\"-dark\", \"\"));\n                            } else {\n                                TrackEvent.withWarn(\"Image file not found\")\n                                        .tag(\"file\", value)\n                                        .handle();\n                                return null;\n                            }\n                        },\n                        image));\n        storeIcon.fitWidthProperty().bind(widthProperty);\n        storeIcon.fitHeightProperty().bind(heightProperty);\n        storeIcon.setSmooth(true);\n        stack.getChildren().add(storeIcon);\n\n        Consumer<String> update = val -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                var useDark = AppPrefs.get() != null\n                        && AppPrefs.get().theme().getValue() != null\n                        && AppPrefs.get().theme().getValue().isDark();\n                var fixed = val != null\n                        ? FilePath.of(val).getBaseName() + (useDark ? \"-dark\" : \"\") + \".\"\n                                + FilePath.of(val).getExtension().orElseThrow()\n                        : null;\n                image.set(fixed);\n\n                if (val == null) {\n                    stack.getChildren().getFirst().setVisible(false);\n                } else {\n                    stack.getChildren().getFirst().setVisible(true);\n                }\n            });\n        };\n\n        value.subscribe(update);\n        if (AppPrefs.get() != null) {\n            AppPrefs.get().theme().addListener((observable, oldValue, newValue) -> {\n                update.accept(value.getValue());\n            });\n        }\n\n        stack.setFocusTraversable(false);\n        stack.setPrefWidth(width);\n        stack.setMinWidth(width);\n        stack.setPrefHeight(height);\n        stack.setMinHeight(height);\n        stack.setAlignment(Pos.CENTER);\n        stack.getStyleClass().add(\"stack\");\n        return stack;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/PrettyImageHelper.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.core.AppDisplayScale;\nimport io.xpipe.app.core.AppImages;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.core.FilePath;\n\nimport javafx.beans.property.ReadOnlyStringWrapper;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.Optional;\nimport java.util.stream.IntStream;\n\npublic class PrettyImageHelper {\n\n    private static Optional<String> rasterizedImageIfExists(String img, int height) {\n        if (img != null && img.endsWith(\".svg\")) {\n            var base = FilePath.of(img).getBaseName();\n            var renderedName = base + \"-\" + height + \".png\";\n            if (AppImages.hasImage(renderedName)) {\n                return Optional.of(renderedName);\n            }\n        }\n\n        if (img != null && img.endsWith(\".png\")) {\n            if (AppImages.hasImage(img)) {\n                return Optional.of(img);\n            }\n        }\n\n        return Optional.empty();\n    }\n\n    private static String rasterizedImageIfExistsScaled(String img, int height, int... availableSizes) {\n        if (img == null) {\n            return null;\n        }\n\n        if (!img.endsWith(\".svg\")) {\n            return rasterizedImageIfExists(img, height).orElse(null);\n        }\n\n        var scale = AppDisplayScale.getEffectiveDisplayScale();\n        var mult = Math.round(scale * height);\n        var base = FilePath.of(img).getBaseName();\n        var available = IntStream.of(availableSizes)\n                .filter(integer -> AppImages.hasImage(base + \"-\" + integer + \".png\"))\n                .boxed()\n                .toList();\n        var closest = available.stream()\n                .filter(integer -> integer >= mult)\n                .findFirst()\n                .orElse(available.size() > 0 ? available.getLast() : 0);\n        return rasterizedImageIfExists(img, closest).orElse(null);\n    }\n\n    public static BaseRegionBuilder<?, ?> ofFixedSizeSquare(String img, int size) {\n        return ofFixedSize(img, size, size);\n    }\n\n    public static BaseRegionBuilder<?, ?> ofFixedSize(String img, int w, int h) {\n        return ofFixedSize(new SimpleStringProperty(img), w, h);\n    }\n\n    public static BaseRegionBuilder<?, ?> ofFixedSize(ObservableValue<String> img, int w, int h) {\n        if (img == null) {\n            return new PrettyImageComp(new SimpleStringProperty(null), w, h);\n        }\n\n        var binding = BindingsHelper.map(img, s -> {\n            return rasterizedImageIfExistsScaled(s, h, 16, 24, 40, 80);\n        });\n        return new PrettyImageComp(binding, w, h);\n    }\n\n    public static BaseRegionBuilder<?, ?> ofSpecificFixedSize(String img, int w, int h) {\n        var b = rasterizedImageIfExistsScaled(img, h, h, h * 2);\n        return new PrettyImageComp(new ReadOnlyStringWrapper(b), w, h);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/ScrollComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.scene.control.ScrollBar;\nimport javafx.scene.control.ScrollPane;\nimport javafx.scene.control.skin.ScrollPaneSkin;\nimport javafx.scene.layout.StackPane;\n\npublic class ScrollComp extends RegionBuilder<ScrollPane> {\n\n    private final BaseRegionBuilder<?, ?> content;\n\n    public ScrollComp(BaseRegionBuilder<?, ?> content) {\n        this.content = content;\n    }\n\n    @Override\n    public ScrollPane createSimple() {\n        var r = content.build();\n        var stack = new StackPane(r);\n        stack.getStyleClass().add(\"scroll-comp-content\");\n\n        var sp = new ScrollPane(stack);\n        sp.setFitToWidth(true);\n        sp.getStyleClass().add(\"scroll-comp\");\n        sp.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);\n        sp.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);\n        sp.setSkin(new ScrollPaneSkin(sp));\n\n        ScrollBar bar = (ScrollBar) sp.lookup(\".scroll-bar:vertical\");\n        bar.opacityProperty()\n                .bind(Bindings.createDoubleBinding(\n                        () -> {\n                            var v = bar.getVisibleAmount();\n                            // Check for rounding and accuracy issues\n                            // It might not be exactly equal to 1.0\n                            return v < 0.99 ? 1.0 : 0.0;\n                        },\n                        bar.visibleAmountProperty()));\n\n        StackPane viewport = (StackPane) sp.lookup(\".viewport\");\n        var child = viewport.getChildren().getFirst();\n        child.getStyleClass().add(\"view\");\n        return sp;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/SecretFieldComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.RegionStructure;\nimport io.xpipe.app.comp.RegionStructureBuilder;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.ClipboardHelper;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.core.InPlaceSecretValue;\n\nimport javafx.application.Platform;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.geometry.Insets;\nimport javafx.scene.AccessibleAttribute;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.PasswordField;\nimport javafx.scene.control.TextField;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyEvent;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Priority;\n\nimport atlantafx.base.controls.Popover;\nimport atlantafx.base.layout.InputGroup;\nimport lombok.AllArgsConstructor;\nimport lombok.Getter;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\n\npublic class SecretFieldComp extends RegionStructureBuilder<InputGroup, SecretFieldComp.Structure> {\n\n    private final Property<InPlaceSecretValue> value;\n    private final boolean allowCopy;\n    private final List<BaseRegionBuilder<?, ?>> additionalButtons = new ArrayList<>();\n\n    public SecretFieldComp(Property<InPlaceSecretValue> value, boolean allowCopy) {\n        this.value = value;\n        this.allowCopy = allowCopy;\n    }\n\n    public static SecretFieldComp ofString(Property<String> s) {\n        var prop = new SimpleObjectProperty<>(s.getValue() != null ? InPlaceSecretValue.of(s.getValue()) : null);\n        prop.addListener((observable, oldValue, newValue) -> {\n            s.setValue(newValue != null ? new String(newValue.getSecret()) : null);\n        });\n        s.addListener((observableValue, s1, t1) -> {\n            prop.set(t1 != null ? InPlaceSecretValue.of(t1) : null);\n        });\n        return new SecretFieldComp(prop, false);\n    }\n\n    public void addButton(BaseRegionBuilder<?, ?> button) {\n        this.additionalButtons.add(button);\n    }\n\n    protected InPlaceSecretValue encrypt(char[] c) {\n        return InPlaceSecretValue.of(c);\n    }\n\n    @Override\n    public Structure createBase() {\n        var field = new PasswordField() {\n            @Override\n            public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {\n                switch (attribute) {\n                    case TEXT:\n                        return getAccessibleText();\n                    default:\n                        return super.queryAccessibleAttribute(attribute, parameters);\n                }\n            }\n        };\n        field.addEventFilter(KeyEvent.KEY_PRESSED, e -> {\n            if (e.isControlDown() && e.getCode() == KeyCode.BACK_SPACE) {\n                var sel = field.getSelection();\n                if (sel.getEnd() > 0) {\n                    field.setText(field.getText().substring(sel.getEnd()));\n                    e.consume();\n                }\n            }\n        });\n        field.setText(value.getValue() != null ? value.getValue().getSecretValue() : null);\n        field.textProperty().addListener((c, o, n) -> {\n            value.setValue(n != null && n.length() > 0 ? encrypt(n.toCharArray()) : null);\n        });\n\n        var capsPopover = new Popover();\n        var label = new Label();\n        label.textProperty().bind(AppI18n.observable(\"capslockWarning\"));\n        label.setGraphic(new FontIcon(\"mdi2i-information-outline\"));\n        label.setPadding(new Insets(0, 10, 0, 10));\n        capsPopover.setContentNode(label);\n        capsPopover.setArrowLocation(Popover.ArrowLocation.BOTTOM_CENTER);\n        capsPopover.setDetachable(false);\n\n        value.addListener((c, o, n) -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                // Check if control value is the same. Then don't set it as that might cause bugs\n                if ((n == null && field.getText().isEmpty())\n                        || Objects.equals(field.getText(), n != null ? n.getSecretValue() : null)) {\n                    return;\n                }\n\n                field.setText(n != null ? n.getSecretValue() : null);\n\n                var capslock = Platform.isKeyLocked(KeyCode.CAPS);\n                if (!capslock.orElse(false)) {\n                    capsPopover.hide();\n                    return;\n                }\n                if (!capsPopover.isShowing() && field.getScene() != null) {\n                    capsPopover.show(field);\n                }\n            });\n        });\n        HBox.setHgrow(field, Priority.ALWAYS);\n\n        var copyButton = new ButtonComp(null, new FontIcon(\"mdi2c-clipboard-multiple-outline\"), () -> {\n                    ClipboardHelper.copyPassword(value.getValue());\n                })\n                .describe(d -> d.nameKey(\"copy\"));\n\n        var list = new ArrayList<BaseRegionBuilder<?, ?>>();\n        var fieldComp = RegionBuilder.of(() -> field);\n        list.add(fieldComp);\n        if (allowCopy) {\n            list.add(copyButton);\n        }\n        list.addAll(additionalButtons);\n\n        var ig = new InputGroupComp(list);\n        ig.setMainReference(fieldComp);\n        ig.style(\"secret-field-comp\");\n        ig.apply(struc -> {\n            struc.focusedProperty().addListener((c, o, n) -> {\n                if (n) {\n                    field.requestFocus();\n                }\n            });\n        });\n\n        return new Structure(ig.build(), field);\n    }\n\n    @AllArgsConstructor\n    public static class Structure implements RegionStructure<InputGroup> {\n\n        private final InputGroup inputGroup;\n\n        @Getter\n        private final TextField field;\n\n        @Override\n        public InputGroup get() {\n            return inputGroup;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.update.AppDistributionType;\nimport io.xpipe.app.update.UpdateAvailableDialog;\nimport io.xpipe.app.util.Hyperlinks;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\nimport javafx.collections.ListChangeListener;\nimport javafx.collections.ObservableList;\nimport javafx.css.PseudoClass;\nimport javafx.geometry.Insets;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.Button;\nimport javafx.scene.layout.*;\nimport javafx.scene.paint.Color;\n\nimport lombok.AllArgsConstructor;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@AllArgsConstructor\npublic class SideMenuBarComp extends RegionBuilder<VBox> {\n\n    private final Property<AppLayoutModel.Entry> value;\n    private final List<AppLayoutModel.Entry> entries;\n    private final ObservableList<AppLayoutModel.QueueEntry> queueEntries;\n\n    @Override\n    public VBox createSimple() {\n        var vbox = new VBox();\n        vbox.setFillWidth(true);\n\n        for (AppLayoutModel.Entry e : entries) {\n            var b = new IconButtonComp(e.icon(), () -> {\n                // Don't allow switching prior to startup\n                if (AppOperationMode.isInStartup() || AppOperationMode.isInShutdown()) {\n                    return;\n                }\n\n                if (e.action() != null) {\n                    e.action().run();\n                }\n\n                if (e.comp() != null) {\n                    value.setValue(e);\n                }\n            });\n            b.describe(d -> d.name(e.name()));\n\n            var stack = createStyle(e, b);\n            var shortcut = e.combination();\n            if (shortcut != null) {\n                stack.apply(struc -> struc.getProperties().put(\"shortcut\", shortcut));\n            }\n            vbox.getChildren().add(stack.build());\n        }\n\n        {\n            var b = new IconButtonComp(\"mdi2u-update\", () -> UpdateAvailableDialog.showIfNeeded(false));\n            b.describe(d -> d.nameKey(\"updateAvailableTooltip\"));\n            var stack = createStyle(null, b);\n            stack.hide(Bindings.createBooleanBinding(\n                    () -> {\n                        return AppDistributionType.get()\n                                        .getUpdateHandler()\n                                        .getPreparedUpdate()\n                                        .getValue()\n                                == null;\n                    },\n                    AppDistributionType.get().getUpdateHandler().getPreparedUpdate()));\n            vbox.getChildren().add(stack.build());\n        }\n\n        if (!AppProperties.get().isStaging()) {\n            var b = new IconButtonComp(\"mdoal-insights\", () -> Hyperlinks.open(Hyperlinks.GITHUB_PTB));\n            b.describe(d -> d.nameKey(\"ptbAvailableTooltip\"));\n            var stack = createStyle(null, b);\n            stack.hide(AppLayoutModel.get().getPtbAvailable().not());\n            vbox.getChildren().add(stack.build());\n        }\n\n        var filler = new Button();\n        filler.setDisable(true);\n        filler.setMaxHeight(3000);\n        vbox.getChildren().add(filler);\n        VBox.setVgrow(filler, Priority.ALWAYS);\n        vbox.getStyleClass().add(\"sidebar-comp\");\n\n        var queueButtons = new VBox();\n        queueEntries.addListener((ListChangeListener<? super AppLayoutModel.QueueEntry>) c -> {\n            var l = new ArrayList<>(c.getList());\n            PlatformThread.runLaterIfNeeded(() -> {\n                queueButtons.getChildren().clear();\n                for (int i = l.size() - 1; i >= 0; i--) {\n                    var item = l.get(i);\n                    var b = new IconButtonComp(item.getIcon(), null);\n                    b.describe(d -> d.name(item.getName()));\n                    b.apply(struc -> {\n                        struc.setOnAction(e -> {\n                            struc.setDisable(true);\n                            item.execute();\n                            struc.setDisable(false);\n                            e.consume();\n                        });\n                    });\n                    var stack = createStyle(null, b);\n                    queueButtons.getChildren().add(stack.build());\n                }\n            });\n        });\n        vbox.getChildren().add(queueButtons);\n        vbox.setMinHeight(0);\n        vbox.setPrefHeight(0);\n\n        return vbox;\n    }\n\n    private BaseRegionBuilder<?, ?> createStyle(AppLayoutModel.Entry e, IconButtonComp b) {\n        var selected = PseudoClass.getPseudoClass(\"selected\");\n\n        b.apply(struc -> {\n            AppFontSizes.lg(struc);\n            struc.setAlignment(Pos.CENTER);\n\n            struc.pseudoClassStateChanged(selected, value.getValue().equals(e));\n            value.addListener((c, o, n) -> {\n                PlatformThread.runLaterIfNeeded(() -> {\n                    struc.pseudoClassStateChanged(selected, n.equals(e));\n                });\n            });\n        });\n\n        var selectedBorder = Bindings.createObjectBinding(\n                () -> {\n                    var c = Platform.getPreferences()\n                            .getAccentColor()\n                            .desaturate()\n                            .desaturate();\n                    return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(17, 1, 15, 2)));\n                },\n                Platform.getPreferences().accentColorProperty());\n        var hoverBorder = Bindings.createObjectBinding(\n                () -> {\n                    var c = Platform.getPreferences()\n                            .getAccentColor()\n                            .darker()\n                            .desaturate()\n                            .desaturate();\n                    return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(17, 1, 15, 2)));\n                },\n                Platform.getPreferences().accentColorProperty());\n        var noneBorder = Bindings.createObjectBinding(\n                () -> {\n                    return Background.fill(Color.TRANSPARENT);\n                },\n                Platform.getPreferences().accentColorProperty());\n\n        var indicator = RegionBuilder.empty().style(\"indicator\");\n        var stack = new StackComp(List.of(indicator, b)).apply(struc -> struc.setAlignment(Pos.CENTER_RIGHT));\n        stack.apply(struc -> {\n            var indicatorRegion = (Region) struc.getChildren().getFirst();\n            var buttonRegion = (Region) struc.getChildren().get(1);\n            indicatorRegion.setMaxWidth(7);\n            indicatorRegion.prefHeightProperty().bind(buttonRegion.heightProperty());\n            indicatorRegion\n                    .backgroundProperty()\n                    .bind(Bindings.createObjectBinding(\n                            () -> {\n                                if (value.getValue().equals(e)) {\n                                    return selectedBorder.get();\n                                }\n\n                                if (struc.isHover()) {\n                                    return hoverBorder.get();\n                                }\n\n                                return noneBorder.get();\n                            },\n                            struc.hoverProperty(),\n                            value,\n                            hoverBorder,\n                            selectedBorder,\n                            noneBorder));\n        });\n        return stack;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/SimpleTitledPaneComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\n\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.control.TitledPane;\n\npublic class SimpleTitledPaneComp extends RegionBuilder<TitledPane> {\n\n    private final ObservableValue<String> name;\n    private final BaseRegionBuilder<?, ?> content;\n    private final boolean collapsible;\n\n    public SimpleTitledPaneComp(ObservableValue<String> name, BaseRegionBuilder<?, ?> content, boolean collapsible) {\n        this.name = name;\n        this.content = content;\n        this.collapsible = collapsible;\n    }\n\n    @Override\n    public TitledPane createSimple() {\n        var r = content.build();\n        r.getStyleClass().add(\"content\");\n        var tp = new TitledPane(null, r);\n        tp.textProperty().bind(name);\n        tp.getStyleClass().add(\"simple-titled-pane-comp\");\n        tp.setExpanded(true);\n        tp.setCollapsible(collapsible);\n        return tp;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/StackComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionBuilder;\n\nimport javafx.geometry.Pos;\nimport javafx.scene.layout.StackPane;\n\nimport org.int4.fx.builders.common.AbstractRegionBuilder;\n\nimport java.util.List;\n\npublic class StackComp extends RegionBuilder<StackPane> {\n\n    private final List<AbstractRegionBuilder<?, ?>> comps;\n\n    public StackComp(List<AbstractRegionBuilder<?, ?>> comps) {\n        this.comps = List.copyOf(comps);\n    }\n\n    @Override\n    public StackPane createSimple() {\n        var pane = new StackPane();\n        for (var c : comps) {\n            pane.getChildren().add(c.build());\n        }\n        pane.setAlignment(Pos.CENTER);\n        return pane;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/TextAreaComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionStructure;\nimport io.xpipe.app.comp.RegionStructureBuilder;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.scene.control.TextArea;\nimport javafx.scene.layout.AnchorPane;\n\nimport lombok.Builder;\nimport lombok.Value;\n\nimport java.util.Objects;\n\npublic class TextAreaComp extends RegionStructureBuilder<AnchorPane, TextAreaComp.Structure> {\n\n    private final Property<String> currentValue;\n    private final Property<String> lastAppliedValue;\n    private final boolean lazy;\n\n    public TextAreaComp(Property<String> value) {\n        this(value, false);\n    }\n\n    public TextAreaComp(Property<String> value, boolean lazy) {\n        this.lastAppliedValue = value;\n        this.currentValue = new SimpleStringProperty(value.getValue());\n        this.lazy = lazy;\n        if (!lazy) {\n            currentValue.subscribe(val -> {\n                if (!Objects.equals(val, value.getValue())) {\n                    value.setValue(val);\n                }\n            });\n        }\n        lastAppliedValue.subscribe(val -> {\n            currentValue.setValue(val);\n        });\n    }\n\n    @Override\n    public Structure createBase() {\n        var text = new TextArea(currentValue.getValue() != null ? currentValue.getValue() : null);\n        text.setPrefRowCount(5);\n        text.textProperty().addListener((c, o, n) -> {\n            currentValue.setValue(n != null && n.length() > 0 ? n : null);\n        });\n        lastAppliedValue.addListener((c, o, n) -> {\n            currentValue.setValue(n);\n            PlatformThread.runLaterIfNeeded(() -> {\n                // Check if control value is the same. Then don't set it as that might cause bugs\n                if (Objects.equals(text.getText(), n)\n                        || (n == null && text.getText().isEmpty())) {\n                    return;\n                }\n\n                text.setText(n);\n            });\n        });\n\n        text.focusedProperty().addListener((observable, oldValue, newValue) -> {\n            if (!newValue && !Objects.equals(currentValue.getValue(), lastAppliedValue.getValue())) {\n                lastAppliedValue.setValue(currentValue.getValue());\n            }\n        });\n\n        var anchorPane = new AnchorPane(text);\n        AnchorPane.setBottomAnchor(text, 0.0);\n        AnchorPane.setTopAnchor(text, 0.0);\n        AnchorPane.setLeftAnchor(text, 0.0);\n        AnchorPane.setRightAnchor(text, 0.0);\n\n        if (lazy) {\n            var isEqual = Bindings.createBooleanBinding(\n                    () -> Objects.equals(lastAppliedValue.getValue(), currentValue.getValue()),\n                    currentValue,\n                    lastAppliedValue);\n            var button = new IconButtonComp(\"mdi2c-checkbox-marked-outline\")\n                    .hide(isEqual)\n                    .build();\n            anchorPane.getChildren().add(button);\n            AnchorPane.setBottomAnchor(button, 10.0);\n            AnchorPane.setRightAnchor(button, 10.0);\n\n            text.prefWidthProperty().bind(anchorPane.widthProperty());\n            text.prefHeightProperty().bind(anchorPane.heightProperty());\n        }\n\n        return new Structure(anchorPane, text);\n    }\n\n    @Value\n    @Builder\n    public static class Structure implements RegionStructure<AnchorPane> {\n        AnchorPane pane;\n        TextArea textArea;\n\n        @Override\n        public AnchorPane get() {\n            return pane;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/TextFieldComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.scene.control.TextField;\nimport javafx.scene.input.KeyCode;\n\nimport java.util.Objects;\n\npublic class TextFieldComp extends RegionBuilder<TextField> {\n\n    private final Property<String> lastAppliedValue;\n    private final Property<String> currentValue;\n    private final boolean lazy;\n\n    public TextFieldComp(Property<String> value) {\n        this(value, false);\n    }\n\n    public TextFieldComp(Property<String> value, boolean lazy) {\n        this.lastAppliedValue = value;\n        this.currentValue = new SimpleStringProperty(value.getValue());\n        this.lazy = lazy;\n        if (!lazy) {\n            currentValue.subscribe(val -> {\n                if (!Objects.equals(val, value.getValue())) {\n                    value.setValue(val);\n                }\n            });\n        }\n        lastAppliedValue.addListener((c, o, n) -> {\n            currentValue.setValue(n);\n        });\n    }\n\n    @Override\n    public TextField createSimple() {\n        var text = new TextField(currentValue.getValue() != null ? currentValue.getValue() : \"\");\n        text.textProperty().addListener((c, o, n) -> {\n            currentValue.setValue(n != null && n.length() > 0 ? n : null);\n        });\n        lastAppliedValue.addListener((c, o, n) -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                // Check if control value is the same. Then don't set it as that might cause bugs\n                if (Objects.equals(text.getText(), n)\n                        || (n == null && text.getText().isEmpty())) {\n                    return;\n                }\n\n                text.setText(n);\n            });\n        });\n\n        text.setOnKeyPressed(ke -> {\n            if (ke.getCode().equals(KeyCode.ENTER)) {\n                text.getScene().getRoot().requestFocus();\n            }\n\n            if (lazy && ke.getCode().equals(KeyCode.ENTER)) {\n                lastAppliedValue.setValue(currentValue.getValue());\n            }\n        });\n\n        text.focusedProperty().addListener((observable, oldValue, newValue) -> {\n            if (!newValue && lazy) {\n                lastAppliedValue.setValue(currentValue.getValue());\n            }\n        });\n\n        return text;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/TileButtonComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionDescriptor;\nimport io.xpipe.app.comp.RegionStructure;\nimport io.xpipe.app.comp.RegionStructureBuilder;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.event.ActionEvent;\nimport javafx.scene.control.Button;\nimport javafx.scene.control.Label;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.VBox;\n\nimport atlantafx.base.controls.Spacer;\nimport lombok.*;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.function.Consumer;\n\n@Getter\npublic class TileButtonComp extends RegionStructureBuilder<Button, TileButtonComp.Structure> {\n\n    private final ObservableValue<String> name;\n    private final ObservableValue<String> description;\n    private final ObservableValue<String> icon;\n    private final Consumer<ActionEvent> action;\n\n    @Setter\n    private double iconSize = 0.55;\n\n    @Setter\n    private BaseRegionBuilder<?, ?> right;\n\n    public TileButtonComp(String nameKey, String descriptionKey, String icon, Consumer<ActionEvent> action) {\n        this.name = AppI18n.observable(nameKey);\n        this.description = AppI18n.observable(descriptionKey);\n        this.icon = new SimpleStringProperty(icon);\n        this.action = action;\n    }\n\n    public TileButtonComp(\n            ObservableValue<String> name,\n            ObservableValue<String> description,\n            ObservableValue<String> icon,\n            Consumer<ActionEvent> action) {\n        this.name = name;\n        this.description = description;\n        this.icon = icon;\n        this.action = action;\n    }\n\n    @Override\n    public Structure createBase() {\n        var bt = new Button();\n        RegionDescriptor.builder().name(name).description(description).build().apply(bt);\n        bt.getStyleClass().add(\"tile-button-comp\");\n        bt.setOnAction(e -> {\n            if (action != null) {\n                action.accept(e);\n            }\n        });\n\n        var header = new Label();\n        name.subscribe(value -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                header.setText(value);\n            });\n        });\n        var desc = new Label();\n        description.subscribe(value -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                desc.setText(value);\n            });\n        });\n        AppFontSizes.xs(desc);\n        desc.setOpacity(0.8);\n        var text = new VBox(header, desc);\n        text.setSpacing(2);\n\n        var fi = new FontIconComp(icon).buildStructure();\n        var pane = fi.getPane();\n        var hbox = new HBox(pane, text);\n        Region rightRegion = right != null ? right.build() : null;\n        if (rightRegion != null) {\n            hbox.getChildren().add(new Spacer());\n            hbox.getChildren().add(rightRegion);\n        }\n        hbox.setSpacing(8);\n        pane.prefWidthProperty()\n                .bind(Bindings.createDoubleBinding(\n                        () -> (header.getHeight() + desc.getHeight()) * 0.6,\n                        header.heightProperty(),\n                        desc.heightProperty()));\n        pane.prefHeightProperty()\n                .bind(Bindings.createDoubleBinding(\n                        () -> header.getHeight() + desc.getHeight() + 2,\n                        header.heightProperty(),\n                        desc.heightProperty()));\n        pane.prefHeightProperty().addListener((c, o, n) -> {\n            var size = Math.min(n.intValue(), 100);\n            fi.getIcon().setIconSize((int) (size * iconSize));\n        });\n        bt.setGraphic(hbox);\n        return Structure.builder()\n                .graphic(fi.getIcon())\n                .button(bt)\n                .content(hbox)\n                .name(header)\n                .description(desc)\n                .right(rightRegion)\n                .build();\n    }\n\n    @Value\n    @Builder\n    public static class Structure implements RegionStructure<Button> {\n        Button button;\n        HBox content;\n        FontIcon graphic;\n        Label name;\n        Label description;\n        Region right;\n\n        @Override\n        public Button get() {\n            return button;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/ToggleGroupComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.control.ToggleButton;\nimport javafx.scene.control.ToggleGroup;\nimport javafx.scene.layout.HBox;\n\nimport atlantafx.base.theme.Styles;\n\nimport java.util.Map;\nimport java.util.Objects;\n\npublic class ToggleGroupComp<T> extends RegionBuilder<HBox> {\n\n    private final Property<T> value;\n    private final ObservableValue<Map<T, ObservableValue<String>>> range;\n\n    public ToggleGroupComp(Property<T> value, ObservableValue<Map<T, ObservableValue<String>>> range) {\n        this.value = value;\n        this.range = range;\n    }\n\n    @Override\n    public HBox createSimple() {\n        var box = new HBox();\n        box.getStyleClass().add(\"toggle-group-comp\");\n        ToggleGroup group = new ToggleGroup();\n        range.subscribe(val -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                if (!val.containsKey(value.getValue())) {\n                    this.value.setValue(null);\n                }\n\n                box.getChildren().clear();\n                for (var entry : val.entrySet()) {\n                    var b = new ToggleButton(entry.getValue().getValue());\n                    if (entry.getKey() == null) {\n                        b.disableProperty().bind(b.selectedProperty());\n                    }\n                    b.setOnAction(e -> {\n                        if (Objects.equals(entry.getKey(), value.getValue())) {\n                            value.setValue(null);\n                        } else {\n                            value.setValue(entry.getKey());\n                        }\n                        e.consume();\n                    });\n                    group.getToggles().add(b);\n                    box.getChildren().add(b);\n                    if (Objects.equals(entry.getKey(), value.getValue())) {\n                        b.setSelected(true);\n                    }\n\n                    value.addListener((observable, oldValue, newValue) -> {\n                        if (Objects.equals(newValue, entry.getKey())) {\n                            b.setSelected(true);\n                        }\n                    });\n                }\n\n                if (box.getChildren().size() > 0) {\n                    box.getChildren().getFirst().getStyleClass().add(Styles.LEFT_PILL);\n                    for (int i = 1; i < box.getChildren().size() - 1; i++) {\n                        box.getChildren().get(i).getStyleClass().add(Styles.CENTER_PILL);\n                    }\n                    box.getChildren().getLast().getStyleClass().add(Styles.RIGHT_PILL);\n                }\n            });\n        });\n\n        return box;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/ToggleSwitchComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.ReadOnlyObjectWrapper;\nimport javafx.beans.value.ObservableValue;\nimport javafx.css.PseudoClass;\nimport javafx.geometry.Pos;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyEvent;\n\nimport atlantafx.base.controls.ToggleSwitch;\nimport lombok.EqualsAndHashCode;\nimport lombok.Value;\n\n@Value\n@EqualsAndHashCode(callSuper = true)\npublic class ToggleSwitchComp extends RegionBuilder<ToggleSwitch> {\n\n    Property<Boolean> selected;\n    ObservableValue<String> name;\n    ObservableValue<LabelGraphic> graphic;\n\n    @Override\n    public ToggleSwitch createSimple() {\n        var s = new ToggleSwitch();\n        s.addEventFilter(KeyEvent.KEY_PRESSED, event -> {\n            if (event.getCode() == KeyCode.SPACE || event.getCode() == KeyCode.ENTER) {\n                s.setSelected(!s.isSelected());\n                event.consume();\n            }\n        });\n        s.accessibleTextProperty()\n                .bind(Bindings.createStringBinding(\n                        () -> {\n                            if (name != null && name.getValue() != null) {\n                                return name.getValue();\n                            }\n\n                            return AppI18n.get(\"toggleButton\");\n                        },\n                        name != null ? name : new ReadOnlyObjectWrapper<>(),\n                        AppI18n.activeLanguage()));\n        s.setAlignment(Pos.CENTER);\n        s.getStyleClass().add(\"toggle-switch-comp\");\n        s.setSelected(selected.getValue() != null ? selected.getValue() : false);\n        s.selectedProperty().addListener((observable, oldValue, newValue) -> {\n            selected.setValue(newValue);\n        });\n        selected.addListener((observable, oldValue, newValue) -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                s.setSelected(newValue);\n            });\n        });\n        if (name != null) {\n            name.subscribe(value -> {\n                PlatformThread.runLaterIfNeeded(() -> {\n                    s.setText(value);\n                });\n            });\n        }\n        if (graphic != null) {\n            graphic.subscribe(value -> {\n                PlatformThread.runLaterIfNeeded(() -> {\n                    s.setGraphic(value.createGraphicNode());\n                });\n            });\n            s.setAlignment(Pos.CENTER);\n            s.pseudoClassStateChanged(PseudoClass.getPseudoClass(\"has-graphic\"), true);\n        }\n\n        s.setOnKeyPressed(keyEvent -> {\n            if (keyEvent.getCode() == KeyCode.SPACE || keyEvent.getCode() == KeyCode.ENTER) {\n                s.setSelected(!s.isSelected());\n                keyEvent.consume();\n            }\n        });\n\n        return s;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/TooltipHelper.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.core.AppFontSizes;\n\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.control.Tooltip;\nimport javafx.stage.Window;\n\npublic class TooltipHelper {\n\n    public static Tooltip create(ObservableValue<String> text) {\n        var tt = new FixedTooltip();\n        tt.textProperty().bind(text);\n        AppFontSizes.base(tt.getStyleableNode());\n        tt.setWrapText(true);\n        tt.setMaxWidth(400);\n        tt.getStyleClass().add(\"fancy-tooltip\");\n        return tt;\n    }\n\n    private static class FixedTooltip extends Tooltip {\n\n        public FixedTooltip() {\n            super();\n        }\n\n        @Override\n        protected void show() {\n            Window owner = getOwnerWindow();\n            if (owner.isFocused()) {\n                super.show();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/comp/base/VerticalComp.java",
    "content": "package io.xpipe.app.comp.base;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.collections.FXCollections;\nimport javafx.collections.ListChangeListener;\nimport javafx.collections.ObservableList;\nimport javafx.scene.layout.VBox;\n\nimport org.int4.fx.builders.common.AbstractRegionBuilder;\n\nimport java.util.List;\n\npublic class VerticalComp extends RegionBuilder<VBox> {\n\n    private final ObservableList<? extends AbstractRegionBuilder<?, ?>> entries;\n\n    public VerticalComp(List<? extends AbstractRegionBuilder<?, ?>> comps) {\n        entries = FXCollections.observableArrayList(List.copyOf(comps));\n    }\n\n    public VerticalComp(ObservableList<? extends AbstractRegionBuilder<?, ?>> entries) {\n        this.entries = PlatformThread.sync(entries);\n    }\n\n    public RegionBuilder<VBox> spacing(double spacing) {\n        return apply(struc -> struc.setSpacing(spacing));\n    }\n\n    @Override\n    public VBox createSimple() {\n        VBox b = new VBox();\n        b.getStyleClass().add(\"vertical-comp\");\n        entries.addListener((ListChangeListener<? super AbstractRegionBuilder<?, ?>>) c -> {\n            b.getChildren()\n                    .setAll(c.getList().stream()\n                            .map(AbstractRegionBuilder::build)\n                            .toList());\n        });\n        for (var entry : entries) {\n            b.getChildren().add(entry.build());\n        }\n        return b;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/App.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.issue.TrackEvent;\n\nimport javafx.application.Application;\nimport javafx.stage.Stage;\n\nimport lombok.Getter;\nimport lombok.SneakyThrows;\n\n@Getter\npublic class App extends Application {\n\n    private static App APP;\n    private Stage stage;\n\n    public static App getApp() {\n        return APP;\n    }\n\n    @Override\n    @SneakyThrows\n    public void start(Stage primaryStage) {\n        TrackEvent.info(\"Platform application started\");\n        APP = this;\n        stage = primaryStage;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppAotTrain.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.OsType;\n\npublic class AppAotTrain {\n\n    public static void runTrainingMode() throws Throwable {\n        // Linux runners don't support graphics\n        if (OsType.ofLocal() == OsType.LINUX) {\n            return;\n        }\n\n        AppOperationMode.switchToSyncOrThrow(AppOperationMode.GUI);\n        ThreadHelper.sleep(5000);\n        BrowserFullSessionModel.DEFAULT.openFileSystemSync(\n                DataStorage.get().local().ref(),\n                null,\n                m -> m.getFileSystem().getShell().orElseThrow().view().userHome(),\n                null,\n                true);\n        AppLayoutModel.get().selectSettings();\n        ThreadHelper.sleep(1000);\n        AppLayoutModel.get().selectLicense();\n        ThreadHelper.sleep(1000);\n        AppLayoutModel.get().selectBrowser();\n        ThreadHelper.sleep(5000);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppArguments.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.LogErrorHandler;\nimport io.xpipe.core.XPipeDaemonMode;\n\nimport lombok.Value;\nimport picocli.CommandLine;\n\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.concurrent.Callable;\nimport java.util.regex.Pattern;\n\n@Value\npublic class AppArguments {\n\n    private static final Pattern PROPERTY_PATTERN = Pattern.compile(\"^-[DP](.+)=(.+)$\");\n    List<String> rawArgs;\n    List<String> resolvedArgs;\n    List<String> openArgs;\n\n    public static AppArguments init(String[] args) {\n        var rawArgs = Arrays.asList(args);\n        var resolvedArgs = Arrays.asList(parseProperties(args));\n        var command = LauncherCommand.resolveLauncher(resolvedArgs.toArray(String[]::new));\n        return new AppArguments(rawArgs, resolvedArgs, command.inputs);\n    }\n\n    private static String[] parseProperties(String[] args) {\n        List<String> newArgs = new ArrayList<>();\n        for (var a : args) {\n            var m = PROPERTY_PATTERN.matcher(a);\n            if (m.matches()) {\n                var k = m.group(1);\n                var v = m.group(2);\n                System.setProperty(k, v);\n            } else {\n                newArgs.add(a);\n            }\n        }\n        return newArgs.toArray(String[]::new);\n    }\n\n    public static class ModeConverter implements CommandLine.ITypeConverter<XPipeDaemonMode> {\n\n        @Override\n        public XPipeDaemonMode convert(String value) {\n            return XPipeDaemonMode.get(value);\n        }\n    }\n\n    @CommandLine.Command()\n    public static class LauncherCommand implements Callable<Integer> {\n\n        @CommandLine.Parameters(paramLabel = \"<input>\")\n        final List<String> inputs = List.of();\n\n        public static LauncherCommand resolveLauncher(String[] args) {\n            var cmd = new CommandLine(new LauncherCommand());\n            cmd.setExecutionExceptionHandler((ex, commandLine, parseResult) -> {\n                var event = ErrorEventFactory.fromThrowable(ex).term().build();\n                // Print error in case we launched from the command-line\n                new LogErrorHandler().handle(event);\n                event.handle();\n                return 1;\n            });\n            cmd.setParameterExceptionHandler((ex, args1) -> {\n                var event =\n                        ErrorEventFactory.fromThrowable(ex).term().expected().build();\n                // Print error in case we launched from the command-line\n                new LogErrorHandler().handle(event);\n                event.handle();\n                return 1;\n            });\n\n            if (AppLogs.get() != null) {\n                // Use original output streams for command output\n                cmd.setOut(new PrintWriter(AppLogs.get().getOriginalSysOut()));\n                cmd.setErr(new PrintWriter(AppLogs.get().getOriginalSysErr()));\n            }\n\n            try {\n                cmd.parseArgs(args);\n            } catch (Throwable t) {\n                // Fix serialization issues with exception class\n                var converted = t instanceof CommandLine.UnmatchedArgumentException u\n                        ? new IllegalArgumentException(u.getMessage())\n                        : t;\n                var e = ErrorEventFactory.fromThrowable(converted)\n                        .expected()\n                        .term()\n                        .build();\n                // Print error in case we launched from the command-line\n                new LogErrorHandler().handle(e);\n                e.handle();\n            }\n\n            return cmd.getCommand();\n        }\n\n        @Override\n        public Integer call() {\n            return 0;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppCache.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.core.JacksonMapper;\n\nimport com.fasterxml.jackson.databind.JavaType;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.type.TypeFactory;\nimport lombok.Getter;\nimport lombok.Setter;\nimport org.apache.commons.io.FileUtils;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Instant;\nimport java.util.Optional;\nimport java.util.function.Supplier;\n\npublic class AppCache {\n\n    @Getter\n    @Setter\n    private static Path basePath;\n\n    private static Path getPath(String key) {\n        var name = key + \".cache\";\n        return getBasePath().resolve(name);\n    }\n\n    public static void clear() {\n        if (!Files.exists(getBasePath())) {\n            return;\n        }\n\n        try {\n            FileUtils.cleanDirectory(getBasePath().toFile());\n        } catch (IOException e) {\n            ErrorEventFactory.fromThrowable(e).expected().handle();\n        }\n    }\n\n    public static void clear(String key) {\n        var path = getPath(key);\n        if (Files.exists(path)) {\n            FileUtils.deleteQuietly(path.toFile());\n        }\n    }\n\n    public static <T> T getNonNull(String key, Class<?> type, Supplier<T> notPresent) {\n        return getNonNull(key, TypeFactory.defaultInstance().constructType(type), notPresent);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <T> T getNonNull(String key, JavaType type, Supplier<T> notPresent) {\n        var path = getPath(key);\n        if (Files.exists(path)) {\n            try {\n                ObjectMapper o = JacksonMapper.getDefault();\n                var tree = o.readTree(path.toFile());\n                if (tree == null || tree.isMissingNode() || tree.isNull()) {\n                    FileUtils.deleteQuietly(path.toFile());\n                    return notPresent.get();\n                }\n\n                var r = (T) JacksonMapper.getDefault().treeToValue(tree, type);\n                if (r == null) {\n                    FileUtils.deleteQuietly(path.toFile());\n                    return notPresent.get();\n                } else {\n                    return r;\n                }\n            } catch (Exception ex) {\n                ErrorEventFactory.fromThrowable(\"Could not parse cached data for key \" + key, ex)\n                        .omit()\n                        .expected()\n                        .handle();\n                FileUtils.deleteQuietly(path.toFile());\n            }\n        }\n        return notPresent.get();\n    }\n\n    public static boolean getBoolean(String key, boolean notPresent) {\n        var path = getPath(key);\n        if (Files.exists(path)) {\n            try {\n                ObjectMapper o = JacksonMapper.getDefault();\n                var tree = o.readTree(path.toFile());\n                if (tree == null || !tree.isBoolean()) {\n                    FileUtils.deleteQuietly(path.toFile());\n                    return notPresent;\n                }\n\n                return tree.asBoolean();\n            } catch (Exception ex) {\n                ErrorEventFactory.fromThrowable(\"Could not parse cached data for key \" + key, ex)\n                        .omit()\n                        .expected()\n                        .handle();\n                FileUtils.deleteQuietly(path.toFile());\n            }\n        }\n        return notPresent;\n    }\n\n    public static <T> void update(String key, T val) {\n        var path = getPath(key);\n\n        try {\n            FileUtils.forceMkdirParent(path.toFile());\n            JacksonMapper.getDefault().writeValue(path.toFile(), val);\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(\"Could not write cache data for key \" + key, e)\n                    .omitted(true)\n                    .expected()\n                    .build()\n                    .handle();\n        }\n    }\n\n    public static Optional<Instant> getModifiedTime(String key) {\n        var path = getPath(key);\n        if (Files.exists(path)) {\n            try {\n                var t = Files.getLastModifiedTime(path);\n                return Optional.of(t.toInstant());\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(\"Could not get modified date for \" + key, e)\n                        .omitted(true)\n                        .expected()\n                        .build()\n                        .handle();\n                return Optional.empty();\n            }\n        } else {\n            return Optional.empty();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppConfigurationDialog.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.comp.base.ScrollComp;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.prefs.EditorCategory;\nimport io.xpipe.app.prefs.PersonalizationCategory;\nimport io.xpipe.app.prefs.TerminalCategory;\nimport io.xpipe.app.util.DocumentationLink;\n\nimport javafx.scene.layout.Region;\n\npublic class AppConfigurationDialog {\n\n    public static void showIfNeeded() {\n        if (!AppProperties.get().isInitialLaunch() || AppProperties.get().isTest()) {\n            return;\n        }\n\n        var options = new OptionsBuilder()\n                .sub(PersonalizationCategory.languageChoice())\n                .sub(PersonalizationCategory.themeChoice())\n                .sub(TerminalCategory.terminalChoice(false))\n                .sub(EditorCategory.editorChoice())\n                .buildComp();\n        options.style(\"initial-setup\");\n        options.style(\"prefs-container\");\n\n        var scroll = new ScrollComp(options);\n        scroll.apply(struc -> {\n            struc.prefHeightProperty().bind(((Region) struc.getContent()).heightProperty());\n        });\n        scroll.minWidth(650);\n        scroll.prefWidth(650);\n\n        var modal = ModalOverlay.of(\"initialSetup\", scroll);\n        modal.addButton(new ModalButton(\n                \"docs\",\n                () -> {\n                    DocumentationLink.INTRO.open();\n                },\n                false,\n                false));\n        modal.addButton(ModalButton.ok());\n        AppDialog.show(modal);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppDataLock.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\n\nimport org.apache.commons.io.FileUtils;\n\nimport java.io.RandomAccessFile;\nimport java.nio.channels.FileChannel;\nimport java.nio.channels.FileLock;\nimport java.nio.file.FileAlreadyExistsException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic class AppDataLock {\n\n    private static FileChannel channel;\n    private static FileLock lock;\n\n    private static Path getLockFile() {\n        return AppProperties.get().getDataDir().resolve(\"lock\");\n    }\n\n    public static boolean lock() {\n        try {\n            var file = getLockFile().toFile();\n            FileUtils.forceMkdir(file.getParentFile());\n            if (!Files.exists(file.toPath())) {\n                try {\n                    // It is possible that another instance creates the lock at almost the same time\n                    // If it already exists, we lost the race\n                    Files.createFile(file.toPath());\n                } catch (FileAlreadyExistsException f) {\n                    return false;\n                }\n            }\n            channel = new RandomAccessFile(file, \"rw\").getChannel();\n            lock = channel.tryLock();\n            return lock != null;\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).build().handle();\n            return false;\n        }\n    }\n\n    public static void unlock() {\n        if (channel == null || lock == null) {\n            return;\n        }\n\n        try {\n            lock.release();\n            channel.close();\n            lock = null;\n            channel = null;\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).build().handle();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppDesktopIntegration.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.Main;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.PlatformState;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.core.OsType;\n\nimport java.awt.*;\nimport java.awt.desktop.*;\nimport java.util.List;\nimport javax.imageio.ImageIO;\n\npublic class AppDesktopIntegration {\n\n    public static void init() {\n        try {\n            if (Desktop.isDesktopSupported()) {\n                Desktop.getDesktop().addAppEventListener(new SystemSleepListener() {\n                    @Override\n                    public void systemAboutToSleep(SystemSleepEvent e) {\n                        if (AppPrefs.get() == null) {\n                            return;\n                        }\n\n                        var b = AppPrefs.get().hibernateBehaviour().getValue();\n                        if (b == null) {\n                            return;\n                        }\n\n                        b.runOnSleep();\n                    }\n\n                    @Override\n                    public void systemAwoke(SystemSleepEvent e) {\n                        if (AppPrefs.get() == null) {\n                            return;\n                        }\n\n                        var b = AppPrefs.get().hibernateBehaviour().getValue();\n                        if (b == null) {\n                            return;\n                        }\n\n                        b.runOnWake();\n                    }\n                });\n            }\n\n            // This will initialize the toolkit on macOS and create the dock icon\n            // macOS does not like applications that run fully in the background, so always do it\n            if (OsType.ofLocal() == OsType.MACOS && Desktop.isDesktopSupported()) {\n                Desktop.getDesktop().setPreferencesHandler(e -> {\n                    if (PlatformState.getCurrent() != PlatformState.RUNNING) {\n                        return;\n                    }\n\n                    if (AppLayoutModel.get() != null) {\n                        AppLayoutModel.get().selectSettings();\n                    }\n                });\n\n                // URL open operations have to be handled in a special way on macOS!\n                Desktop.getDesktop().setOpenURIHandler(e -> {\n                    AppOpenArguments.handle(List.of(e.getURI().toString()));\n                });\n\n                Desktop.getDesktop().addAppEventListener(new AppReopenedListener() {\n                    @Override\n                    public void appReopened(AppReopenedEvent e) {\n                        AppOperationMode.switchToAsync(AppOperationMode.GUI);\n                    }\n                });\n\n                // Set dock icon explicitly on macOS\n                // This is necessary in case the app was started through a script as it will have no icon otherwise\n                if (AppProperties.get().isDeveloperMode()\n                        && AppLogs.get().isWriteToSysout()\n                        && Taskbar.isTaskbarSupported()) {\n                    try {\n                        var iconUrl = Main.class.getResourceAsStream(\"resources/logo/padded/logo_128x128.png\");\n                        if (iconUrl != null) {\n                            var awtIcon = ImageIO.read(iconUrl);\n                            Taskbar.getTaskbar().setIconImage(awtIcon);\n                        }\n                    } catch (Exception ex) {\n                        ErrorEventFactory.fromThrowable(ex)\n                                .omitted(true)\n                                .build()\n                                .handle();\n                    }\n                }\n            }\n        } catch (Throwable ex) {\n            ErrorEventFactory.fromThrowable(ex).term().handle();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppDisplayScale.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.AppPrefs;\n\nimport javafx.stage.Screen;\n\npublic class AppDisplayScale {\n\n    private static Double screenOutputScale;\n    private static Boolean defaultDisplayScale;\n\n    public static void init() {\n        try {\n            Screen primary = Screen.getPrimary();\n            if (primary != null) {\n                screenOutputScale = primary.getOutputScaleX();\n            }\n\n            var s = AppPrefs.get().uiScale().getValue();\n            if (s != null && s == 100) {\n                defaultDisplayScale = true;\n            }\n\n            var allScreensDefault = Screen.getScreens().stream().allMatch(screen -> screen.getOutputScaleX() == 1.0);\n            if (allScreensDefault) {\n                defaultDisplayScale = true;\n            }\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).omit().expected().handle();\n        }\n    }\n\n    public static boolean hasOnlyDefaultDisplayScale() {\n        return defaultDisplayScale != null ? defaultDisplayScale : false;\n    }\n\n    public static double getEffectiveDisplayScale() {\n        if (AppPrefs.get() != null) {\n            var s = AppPrefs.get().uiScale().getValue();\n            if (s != null) {\n                var i = Math.min(300, Math.max(25, s));\n                var percent = i / 100.0;\n                return percent;\n            }\n        }\n\n        return screenOutputScale != null ? screenOutputScale : 1.0;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppExecutableCache.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.util.Hyperlinks;\nimport io.xpipe.core.FailableConsumer;\nimport io.xpipe.core.OsType;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Duration;\nimport java.time.Instant;\n\npublic class AppExecutableCache {\n\n    public static synchronized Path getOrInstall(String name, String url, FailableConsumer<Path, Exception> function)\n            throws Exception {\n        // We want a recent version installed\n        // Just checking the path might result in old packages that don't support certain options\n        var file =\n                AppProperties.get().getDataBinDir().resolve(name + (OsType.ofLocal() == OsType.WINDOWS ? \".exe\" : \"\"));\n        var exists = Files.exists(file);\n        if (exists) {\n            var date = Files.getLastModifiedTime(file).toInstant();\n            var elapsed = Duration.between(date, Instant.now());\n            if (elapsed.toDays() < 30) {\n                return file;\n            }\n        }\n\n        var queueEntry = new AppLayoutModel.QueueEntry(\n                AppI18n.observable(\"downloadInProgress\", name), new LabelGraphic.IconGraphic(\"mdi2d-download\"), () -> {\n                    Hyperlinks.open(url);\n                    return true;\n                });\n        AppLayoutModel.get().getQueueEntries().add(queueEntry);\n\n        try {\n            function.accept(file);\n            return file;\n        } catch (Exception e) {\n            // We can still use the old file\n            if (exists) {\n                ErrorEventFactory.fromThrowable(e).omit().expected().handle();\n                return file;\n            } else {\n                throw e;\n            }\n        } finally {\n            AppLayoutModel.get().getQueueEntries().remove(queueEntry);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppExtensionManager.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.ext.ExtensionException;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.util.LocalExec;\nimport io.xpipe.app.util.ModuleAccess;\nimport io.xpipe.core.ModuleLayerLoader;\n\nimport lombok.Getter;\n\nimport java.lang.module.Configuration;\nimport java.lang.module.ModuleFinder;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class AppExtensionManager {\n\n    private static AppExtensionManager INSTANCE;\n    private final List<Module> loadedModules = new ArrayList<>();\n    private final List<ModuleLayer> leafModuleLayers = new ArrayList<>();\n    private final List<Path> extensionBaseDirectories = new ArrayList<>();\n    private ModuleLayer baseLayer = ModuleLayer.boot();\n\n    @Getter\n    private ModuleLayer extendedLayer;\n\n    public static synchronized void init() throws Exception {\n        if (INSTANCE != null) {\n            return;\n        }\n\n        INSTANCE = new AppExtensionManager();\n        INSTANCE.determineExtensionDirectories();\n        INSTANCE.loadBaseExtension();\n        INSTANCE.loadAllExtensions();\n        try {\n            ProcessControlProvider.init(INSTANCE.extendedLayer);\n            ModuleLayerLoader.loadAll(INSTANCE.extendedLayer, t -> {\n                ErrorEventFactory.fromThrowable(t).handle();\n            });\n        } catch (Throwable t) {\n            throw ExtensionException.corrupt(\"Service provider initialization failed\", t);\n        }\n    }\n\n    public static void reset() {\n        INSTANCE = null;\n    }\n\n    public static AppExtensionManager getInstance() {\n        return INSTANCE;\n    }\n\n    private static String getLocalInstallVersion(AppInstallation localInstallation) {\n        var exec = localInstallation.getCliExecutablePath();\n        var out = LocalExec.readStdoutIfPossible(exec.toString(), \"version\");\n        var s = out.orElseThrow().strip();\n        return !s.isEmpty() ? s : \"?\";\n    }\n\n    private void loadBaseExtension() {\n        var baseModule = findAndParseExtension(\"base\", ModuleLayer.boot());\n        if (baseModule.isEmpty()) {\n            throw ExtensionException.corrupt(\"Missing base module\");\n        }\n\n        baseLayer = baseModule.get().getLayer();\n        loadedModules.add(baseModule.get());\n    }\n\n    private void determineExtensionDirectories() throws Exception {\n        if (!AppProperties.get().isFullVersion()) {\n            var localInstallation =\n                    !AppProperties.get().isStaging() && AppProperties.get().isLocatePtb()\n                            ? AppInstallation.ofDefault(true)\n                            : AppInstallation.ofCurrent();\n            Path p = localInstallation.getBaseInstallationPath();\n            if (!Files.exists(p)) {\n                throw new IllegalStateException(\n                        \"Required local \" + AppNames.ofCurrent().getName()\n                                + \" installation was not found but is required for development. See https://github\"\n                                + \".com/xpipe-io/xpipe/blob/master/CONTRIBUTING.md#development-setup\");\n            }\n\n            if (AppProperties.get().isLocatorVersionCheck()) {\n                var iv = getLocalInstallVersion(localInstallation);\n                var installVersion = AppVersion.parse(iv)\n                        .orElseThrow(() -> new IllegalArgumentException(\"Invalid installation version: \" + iv));\n                var sv = !AppProperties.get().isImage()\n                        ? Files.readString(Path.of(\"version\")).strip()\n                        : AppProperties.get().getVersion();\n                var sourceVersion = AppVersion.parse(sv)\n                        .orElseThrow(() -> new IllegalArgumentException(\"Invalid source version: \" + sv));\n                if (!installVersion.equals(sourceVersion)) {\n                    throw new IllegalStateException(\"Incompatible development version. Source: \" + sv\n                            + \", Installation: \"\n                            + iv\n                            + \"\\n\\nPlease try to check out the matching release version in the repository. See https://github\"\n                            + \".com/xpipe-io/xpipe/blob/master/CONTRIBUTING.md#development-setup\");\n                }\n            }\n\n            var extensions = localInstallation.getExtensionsPath();\n            extensionBaseDirectories.add(extensions);\n        }\n    }\n\n    public Set<Module> getContentModules() {\n        return Stream.concat(\n                        Stream.of(ModuleLayer.boot()\n                                .findModule(AppNames.packageName())\n                                .orElseThrow()),\n                        loadedModules.stream())\n                .collect(Collectors.toSet());\n    }\n\n    private void loadAllExtensions() throws Exception {\n        for (var ext : List.of(\"system\", \"proc\", \"uacc\")) {\n            var extension = findAndParseExtension(ext, baseLayer)\n                    .orElseThrow(() -> ExtensionException.corrupt(\"Missing module \" + ext));\n            loadedModules.add(extension);\n            leafModuleLayers.add(extension.getLayer());\n        }\n\n        var scl = ClassLoader.getSystemClassLoader();\n        var cfs = leafModuleLayers.stream().map(ModuleLayer::configuration).toList();\n        var finder = ModuleFinder.ofSystem();\n        var cf = Configuration.resolve(finder, cfs, finder, List.of());\n        extendedLayer = ModuleLayer.defineModulesWithOneLoader(cf, leafModuleLayers, scl)\n                .layer();\n\n        if (!AppProperties.get().isFullVersion()) {\n            ModuleAccess.exportAndOpen(\n                    ModuleLayer.boot().findModule(\"java.base\").orElseThrow(),\n                    \"java.io\",\n                    extendedLayer.findModule(AppNames.extModuleName(\"proc\")).orElseThrow());\n            ModuleAccess.exportAndOpen(\n                    ModuleLayer.boot().findModule(\"org.apache.commons.io\").orElseThrow(),\n                    \"org.apache.commons.io.input\",\n                    extendedLayer.findModule(AppNames.extModuleName(\"proc\")).orElseThrow());\n        }\n    }\n\n    private Optional<Module> findAndParseExtension(String name, ModuleLayer parent) {\n        var inModulePath = ModuleLayer.boot().findModule(AppNames.extModuleName(name));\n        if (inModulePath.isPresent()) {\n            TrackEvent.info(\"Loaded extension \" + name + \" from boot module path\");\n            return inModulePath;\n        }\n\n        for (Path extensionBaseDirectory : extensionBaseDirectories) {\n            var extensionDir = extensionBaseDirectory.resolve(name);\n            var found = parseExtensionDirectory(extensionDir, parent);\n            if (found.isPresent()) {\n                TrackEvent.info(\"Loaded extension \" + name + \" from module \" + extensionDir);\n                return found;\n            }\n        }\n\n        TrackEvent.info(\"Unable to locate module \" + name);\n        return Optional.empty();\n    }\n\n    private Optional<Module> parseExtensionDirectory(Path dir, ModuleLayer parent) {\n        if (!Files.exists(dir) || !Files.isDirectory(dir)) {\n            return Optional.empty();\n        }\n\n        TrackEvent.trace(String.format(\"Scanning directory %s for extensions\", dir));\n        try {\n            ModuleFinder finder = ModuleFinder.of(dir);\n            var found = finder.findAll();\n            var hasModules = found.size() > 0;\n\n            TrackEvent.withTrace(\"Found modules\").elements(found).handle();\n\n            if (hasModules) {\n                Configuration cf = parent.configuration()\n                        .resolve(\n                                finder,\n                                ModuleFinder.of(),\n                                found.stream().map(r -> r.descriptor().name()).collect(Collectors.toSet()));\n                ClassLoader scl = ClassLoader.getSystemClassLoader();\n                var layer = ModuleLayer.defineModulesWithOneLoader(cf, List.of(parent), scl)\n                        .layer();\n                var mod = layer.modules().iterator().next();\n                return Optional.of(mod);\n            }\n        } catch (Throwable t) {\n            ErrorEventFactory.fromThrowable(t)\n                    .description(\"Unable to load extension from \" + dir + \". Is the installation corrupted?\")\n                    .handle();\n        }\n        return Optional.empty();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppFileWatcher.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport lombok.Getter;\n\nimport java.io.IOException;\nimport java.nio.file.*;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.concurrent.CopyOnWriteArraySet;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.BiConsumer;\n\nimport static java.nio.file.StandardWatchEventKinds.*;\n\npublic class AppFileWatcher {\n\n    private static AppFileWatcher INSTANCE;\n\n    private final Set<WatchedDirectory> watchedDirectories = new CopyOnWriteArraySet<>();\n    private WatchService watchService;\n    private Thread watcherThread;\n    private boolean active;\n\n    public static AppFileWatcher getInstance() {\n        return INSTANCE;\n    }\n\n    public static void init() {\n        INSTANCE = new AppFileWatcher();\n        INSTANCE.startWatcher();\n    }\n\n    public static void reset() {\n        INSTANCE.stopWatcher();\n        INSTANCE = null;\n    }\n\n    public void startWatchersInDirectories(List<Path> dirs, BiConsumer<Path, WatchEvent.Kind<Path>> listener) {\n        // Check in case initialization failed\n        if (watchService == null) {\n            return;\n        }\n\n        dirs.forEach(d -> watchedDirectories.add(new WatchedDirectory(d, listener)));\n    }\n\n    private void startWatcher() {\n        try {\n            watchService = FileSystems.getDefault().newWatchService();\n        } catch (IOException e) {\n            ErrorEventFactory.fromThrowable(\n                            \"Unable to initialize file watcher. Watching and updating files in the file browser will be unavailable.\",\n                            e)\n                    .expected()\n                    .handle();\n            return;\n        }\n\n        active = true;\n        watcherThread = ThreadHelper.createPlatformThread(\"file watcher\", true, () -> {\n            while (active) {\n                WatchKey key;\n                try {\n                    key = AppFileWatcher.this.watchService.poll(10, TimeUnit.MILLISECONDS);\n                    if (key == null) {\n                        continue;\n                    }\n\n                    for (var wd : new HashSet<>(watchedDirectories)) {\n                        wd.update(key);\n                    }\n                } catch (ClosedWatchServiceException ex) {\n                    // Exit loop if watch service is closed\n                    break;\n                } catch (Exception ex) {\n                    // Catch all other exceptions to not terminate this thread if an error occurs!\n                    ErrorEventFactory.fromThrowable(ex).handle();\n                }\n\n                // Don't sleep, since polling the directories always sleeps for some ms\n            }\n        });\n        watcherThread.start();\n    }\n\n    private void stopWatcher() {\n        // Check in case initialization failed\n        if (watchService == null) {\n            return;\n        }\n\n        active = false;\n        watchedDirectories.clear();\n\n        try {\n            watchService.close();\n        } catch (IOException e) {\n            ErrorEventFactory.fromThrowable(e).omit().handle();\n        }\n\n        try {\n            watcherThread.join();\n        } catch (InterruptedException e) {\n            ErrorEventFactory.fromThrowable(e).omit().handle();\n        }\n    }\n\n    private class WatchedDirectory {\n        private final BiConsumer<Path, WatchEvent.Kind<Path>> listener;\n\n        @Getter\n        private final Path baseDir;\n\n        private WatchedDirectory(Path dir, BiConsumer<Path, WatchEvent.Kind<Path>> listener) {\n            this.baseDir = dir;\n            this.listener = listener;\n            createRecursiveWatchers(dir);\n            TrackEvent.withTrace(\"Added watched directory\").tag(\"location\", dir).handle();\n        }\n\n        private void createRecursiveWatchers(Path dir) {\n            if (!Files.isDirectory(dir)) {\n                return;\n            }\n\n            try {\n                dir.register(AppFileWatcher.this.watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);\n                Files.list(dir).filter(Files::isDirectory).forEach(this::createRecursiveWatchers);\n            } catch (IOException e) {\n                ErrorEventFactory.fromThrowable(e).omit().handle();\n            }\n        }\n\n        public void update(WatchKey key) {\n            Path dir = (Path) key.watchable();\n            if (!dir.startsWith(getBaseDir())) {\n                return;\n            }\n\n            while (true) {\n                var baseDir = key.watchable();\n\n                var events = key.pollEvents();\n                if (events.size() == 0) {\n                    break;\n                }\n\n                for (WatchEvent<?> event : events) {\n                    handleWatchEvent((Path) baseDir, event);\n                }\n\n                boolean valid = key.reset();\n                if (!valid) {\n                    break;\n                }\n            }\n        }\n\n        private void handleWatchEvent(Path path, WatchEvent<?> event) {\n            @SuppressWarnings(\"unchecked\")\n            WatchEvent<Path> ev = (WatchEvent<Path>) event;\n\n            // Context may be null for whatever reason\n            if (ev.context() == null) {\n                return;\n            }\n            Path file = path.resolve(ev.context());\n\n            // Check for outdated info\n            if (ev.kind() == StandardWatchEventKinds.ENTRY_CREATE\n                    || ev.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {\n                var ex = Files.exists(file);\n                if (!ex) {\n                    return;\n                }\n            }\n\n            // Add new watcher for directory\n            if (ev.kind().equals(ENTRY_CREATE) && Files.isDirectory(file)) {\n                try {\n                    file.register(AppFileWatcher.this.watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);\n                } catch (IOException e) {\n                    ErrorEventFactory.fromThrowable(e).omit().handle();\n                }\n            }\n\n            // Handle event\n            TrackEvent.withTrace(\"Watch event\")\n                    .tag(\"baseDir\", baseDir)\n                    .tag(\"file\", baseDir.relativize(file))\n                    .tag(\"kind\", event.kind().name())\n                    .handle();\n            listener.accept(file, ev.kind());\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppFont.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\n\nimport javafx.scene.text.Font;\n\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.nio.file.FileVisitResult;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.SimpleFileVisitor;\nimport java.nio.file.attribute.BasicFileAttributes;\n\npublic class AppFont {\n\n    public static void init() {\n        // Load ikonli fonts\n        TrackEvent.info(\"Loading ikonli fonts ...\");\n        new FontIcon(\"mdi2s-stop\");\n        new FontIcon(\"mdal-360\");\n        new FontIcon(\"bi-alarm\");\n\n        TrackEvent.info(\"Loading bundled fonts ...\");\n        AppResources.with(\n                AppResources.MAIN_MODULE,\n                \"fonts\",\n                path -> Files.walkFileTree(path, new SimpleFileVisitor<>() {\n                    @Override\n                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {\n                        try (var in = Files.newInputStream(file)) {\n                            Font.loadFont(in, 11);\n                        } catch (Throwable t) {\n                            // Font loading can fail in rare cases. This is however not important, so we can just ignore\n                            // it\n                            ErrorEventFactory.fromThrowable(t).expected().omit().handle();\n                        }\n                        return FileVisitResult.CONTINUE;\n                    }\n                }));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppFontSizes.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.core.OsType;\n\nimport javafx.scene.Node;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Value;\n\nimport java.lang.ref.WeakReference;\nimport java.util.function.Function;\nimport java.util.regex.Pattern;\n\n@Value\n@AllArgsConstructor\npublic class AppFontSizes {\n\n    public static final AppFontSizes BASE_10 = ofBase(\"10\");\n    public static final AppFontSizes BASE_10_5 = ofBase(\"10.5\");\n    public static final AppFontSizes BASE_11 = ofBase(\"11\");\n\n    private static final Pattern FONT_SIZE_PATTERN = Pattern.compile(\"-fx-font-size: \\\\d+(\\\\.\\\\d+)?pt;\");\n    // -1.0pt\n    String xs;\n    // -0.5pt\n    String sm;\n    // 0pt\n    String base;\n    // +0.5pt\n    String lg;\n    // +1.0pt\n    String xl;\n    // +2.0pt\n    String xxl;\n    // +3.0pt\n    String xxxl;\n    // +7.0pt\n    String title;\n\n    public static void xs(Node node) {\n        apply(node, AppFontSizes::getXs);\n    }\n\n    public static void sm(Node node) {\n        apply(node, AppFontSizes::getSm);\n    }\n\n    public static void base(Node node) {\n        apply(node, AppFontSizes::getBase);\n    }\n\n    public static void lg(Node node) {\n        apply(node, AppFontSizes::getLg);\n    }\n\n    public static void xl(Node node) {\n        apply(node, AppFontSizes::getXl);\n    }\n\n    public static void xxl(Node node) {\n        apply(node, AppFontSizes::getXxl);\n    }\n\n    public static void xxxl(Node node) {\n        apply(node, AppFontSizes::getXxxl);\n    }\n\n    public static void title(Node node) {\n        apply(node, AppFontSizes::getTitle);\n    }\n\n    public static void apply(Node node, Function<AppFontSizes, String> function) {\n        if (AppPrefs.get() == null) {\n            setFont(node, function.apply(getDefault()));\n            return;\n        }\n\n        var ref = new WeakReference<>(node);\n        AppPrefs.get().theme().subscribe((newValue) -> {\n            var refNode = ref.get();\n            if (refNode != null) {\n                var effective = newValue != null ? newValue.getFontSizes().get() : getDefault();\n                setFont(refNode, function.apply(effective));\n            }\n        });\n\n        AppPrefs.get().useSystemFont().addListener((ignored, ignored2, newValue) -> {\n            var refNode = ref.get();\n            if (refNode != null) {\n                var effective = AppPrefs.get().theme().getValue() != null ? AppPrefs.get().theme().getValue().getFontSizes().get() : getDefault();\n                setFont(refNode, function.apply(effective));\n            }\n        });\n    }\n\n    private static void setFont(Node node, String fontSize) {\n        var s = node.getStyle();\n        var matcher = FONT_SIZE_PATTERN.matcher(s);\n        s = matcher.replaceAll(\"\");\n        node.setStyle(\"-fx-font-size: \" + fontSize + \"pt;\" + s);\n    }\n\n    public static AppFontSizes ofBase(String base) {\n        if (base.contains(\".\")) {\n            var l = Integer.parseInt(base.split(\"\\\\.\")[0]);\n            var r = \".\" + base.split(\"\\\\.\")[1];\n            return new AppFontSizes(\n                    (l - 1) + r, (l - 1) + \"\", base, (l + 1) + \"\", (l + 1) + r, (l + 2) + r, (l + 3) + r, (l + 7) + r);\n        } else {\n            var l = Integer.parseInt(base);\n            return new AppFontSizes(\n                    (l - 1) + \"\", (l - 1) + \".5\", l + \"\", l + \".5\", l + 1 + \"\", l + 2 + \"\", l + 3 + \"\", l + 7 + \"\");\n        }\n    }\n\n    private static AppFontSizes fallbackFontSize(AppFontSizes s) {\n        if (s == BASE_10) {\n            return BASE_10;\n        } else if (s == BASE_10_5) {\n            return BASE_10_5;\n        } else if (s == BASE_11) {\n            return BASE_10_5;\n        } else {\n            return s;\n        }\n    }\n\n    public static AppFontSizes forOs(AppFontSizes windows, AppFontSizes linux, AppFontSizes mac) {\n        var inter = AppPrefs.get() != null && !AppPrefs.get().useSystemFont().getValue();\n        var r =\n                switch (OsType.ofLocal()) {\n                    case OsType.Linux ignored -> linux;\n                    case OsType.MacOs ignored -> mac;\n                    case OsType.Windows ignored -> windows;\n                };\n        return inter ? fallbackFontSize(r) : r;\n    }\n\n    public static AppFontSizes getDefault() {\n        return forOs(AppFontSizes.BASE_10_5, AppFontSizes.BASE_10, AppFontSizes.BASE_11);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppGreetingsDialog.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.MarkdownComp;\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.core.window.AppDialog;\n\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.geometry.Insets;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.*;\nimport javafx.scene.layout.BorderPane;\n\nimport java.nio.file.Files;\nimport java.util.List;\nimport java.util.function.UnaryOperator;\n\npublic class AppGreetingsDialog {\n\n    public static TitledPane createIntroduction() {\n        var tp = new TitledPane();\n        tp.setExpanded(true);\n        tp.setText(AppI18n.get(\"introduction\"));\n        tp.setAlignment(Pos.CENTER_LEFT);\n\n        AppResources.with(AppResources.MAIN_MODULE, \"misc/welcome.md\", file -> {\n            var md = Files.readString(file);\n            var markdown = new MarkdownComp(md, UnaryOperator.identity(), true).build();\n            tp.setContent(markdown);\n        });\n\n        return tp;\n    }\n\n    private static TitledPane createEula() {\n        var tp = new TitledPane();\n        tp.setExpanded(false);\n        tp.setText(AppI18n.get(\"eula\"));\n        tp.setAlignment(Pos.CENTER_LEFT);\n\n        AppResources.with(AppResources.MAIN_MODULE, \"misc/eula.md\", file -> {\n            var md = Files.readString(file);\n            var markdown = new MarkdownComp(md, UnaryOperator.identity(), true).build();\n            tp.setContent(markdown);\n        });\n\n        return tp;\n    }\n\n    public static void showAndWaitIfNeeded() {\n        boolean set = AppCache.getBoolean(\"legalAccepted\", false);\n        if (set\n                || AppProperties.get().isDevelopmentEnvironment()\n                || AppProperties.get().isTest()) {\n            return;\n        }\n\n        if (AppProperties.get().isAutoAcceptEula()) {\n            AppCache.update(\"legalAccepted\", true);\n            return;\n        }\n\n        var read = new SimpleBooleanProperty();\n        var accepted = new SimpleBooleanProperty();\n\n        var modal = ModalOverlay.of(RegionBuilder.of(() -> {\n            var content = List.of(createIntroduction(), createEula());\n            var accordion = new Accordion(content.toArray(TitledPane[]::new));\n            accordion.setExpandedPane(content.get(0));\n            accordion.expandedPaneProperty().addListener((observable, oldValue, newValue) -> {\n                if (content.get(1).equals(newValue)) {\n                    read.set(true);\n                }\n            });\n\n            var acceptanceBox = RegionBuilder.of(() -> {\n                        var cb = new CheckBox();\n                        cb.selectedProperty().bindBidirectional(accepted);\n\n                        var label = new Label(AppI18n.get(\"legalAccept\"));\n                        label.setGraphic(cb);\n                        label.setPadding(new Insets(20, 0, 10, 0));\n                        label.setOnMouseClicked(event -> accepted.set(!accepted.get()));\n                        label.setGraphicTextGap(10);\n                        return label;\n                    })\n                    .build();\n\n            var layout = new BorderPane();\n            layout.setCenter(accordion);\n            layout.setBottom(acceptanceBox);\n            layout.setPrefWidth(600);\n            layout.setPrefHeight(600);\n            return layout;\n        }));\n        modal.persist();\n        modal.addButton(ModalButton.quit());\n        modal.addButton(ModalButton.confirm(() -> {\n                    AppCache.update(\"legalAccepted\", true);\n                }))\n                .augment(button -> button.disableProperty().bind(accepted.not()));\n        AppDialog.showAndWait(modal);\n\n        if (!AppCache.getBoolean(\"legalAccepted\", false)) {\n            AppProperties.get().resetInitialLaunch();\n            AppOperationMode.halt(1);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppI18n.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.platform.GlobalObjectProperty;\nimport io.xpipe.app.platform.PlatformState;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.prefs.SupportedLocale;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.*;\n\npublic class AppI18n {\n\n    private static AppI18n INSTANCE;\n    private final Property<AppI18nData> currentLanguage = new GlobalObjectProperty<>();\n    private final Property<SupportedLocale> currentLocale = new GlobalObjectProperty<>();\n    private final Map<String, ObservableValue<String>> observableCache = new HashMap<>();\n    private AppI18nData english;\n\n    public AppI18n() {\n        currentLocale.bind(BindingsHelper.map(\n                currentLanguage,\n                appI18nData -> appI18nData != null ? appI18nData.getLocale() : SupportedLocale.getEnglish()));\n    }\n\n    public static ObservableValue<SupportedLocale> activeLanguage() {\n        if (INSTANCE == null) {\n            return new GlobalObjectProperty<>(SupportedLocale.getEnglish());\n        }\n\n        return INSTANCE.currentLocale;\n    }\n\n    public static void init() throws Exception {\n        if (INSTANCE == null) {\n            INSTANCE = new AppI18n();\n        }\n        INSTANCE.load();\n    }\n\n    public static AppI18n get() {\n        return INSTANCE;\n    }\n\n    public static ObservableValue<String> observable(String s, Object... vars) {\n        return INSTANCE.observableImpl(s, vars);\n    }\n\n    public static ObservableValue<String> observable(ObservableValue<String> s, Object... vars) {\n        return BindingsHelper.flatMap(s, v -> INSTANCE.observableImpl(v, vars));\n    }\n\n    public static String get(String s, Object... vars) {\n        return INSTANCE.getLocalised(s, vars);\n    }\n\n    private ObservableValue<String> observableImpl(String s, Object... vars) {\n        if (s == null) {\n            return null;\n        }\n\n        synchronized (this) {\n            var key = getKey(s);\n\n            // Don't cache vars\n            if (vars.length > 0) {\n                var binding = Bindings.createStringBinding(\n                        () -> {\n                            return getLocalised(key, vars);\n                        },\n                        currentLanguage);\n                return binding;\n            }\n\n            var found = observableCache.get(key);\n            if (found != null) {\n                return found;\n            }\n\n            var binding = Bindings.createStringBinding(\n                    () -> {\n                        return getLocalised(key, vars);\n                    },\n                    currentLanguage);\n            observableCache.put(key, binding);\n            return binding;\n        }\n    }\n\n    private void load() throws Exception {\n        if (english == null) {\n            english = AppI18nData.load(SupportedLocale.getEnglish());\n            Locale.setDefault(Locale.ENGLISH);\n\n            // Load bundled JDK locale resources by initializing the classes\n            for (var value : SupportedLocale.values()) {\n                value.getLocale().getDisplayName();\n            }\n        }\n\n        if (currentLanguage.getValue() == null && PlatformState.getCurrent() == PlatformState.RUNNING) {\n            if (AppPrefs.get() != null) {\n                // Perform initial update on platform thread\n                PlatformThread.runLaterIfNeededBlocking(() -> {\n                    AppPrefs.get().language().subscribe(n -> {\n                        try {\n                            var newValue = n != null ? AppI18nData.load(n) : null;\n                            PlatformThread.runLaterIfNeeded(() -> {\n                                currentLanguage.setValue(newValue);\n                                Locale.setDefault(n != null ? n.getLocale() : Locale.ENGLISH);\n                            });\n                        } catch (Exception e) {\n                            ErrorEventFactory.fromThrowable(e).handle();\n                        }\n                    });\n                });\n            }\n        }\n    }\n\n    private String getKey(String s) {\n        var key = s;\n        if (s.startsWith(\"app.\")) {\n            key = key.substring(key.indexOf(\".\") + 1);\n        }\n        return key;\n    }\n\n    private String getLocalised(String s, Object... vars) {\n        var key = getKey(s);\n        if (english == null) {\n            return key;\n        }\n\n        if (currentLanguage.getValue() != null) {\n            var localisedString = currentLanguage.getValue().getLocalised(key, vars);\n            if (localisedString.isPresent()) {\n                return localisedString.get();\n            }\n        }\n\n        if (english != null) {\n            var localisedString = english.getLocalised(key, vars);\n            if (localisedString.isPresent()) {\n                return localisedString.get();\n            }\n        }\n\n        TrackEvent.warn(\"Translation key not found for \" + key);\n        return key;\n    }\n\n    public String getMarkdownTranslation(String name) {\n        if (name.contains(\":\")) {\n            name = name.substring(name.indexOf(\":\") + 1);\n        }\n\n        if (currentLanguage.getValue() != null\n                && currentLanguage.getValue().getMarkdownTranslations().containsKey(name)) {\n            var localisedString =\n                    currentLanguage.getValue().getMarkdownTranslations().get(name);\n            return localisedString;\n        }\n\n        if (english.getMarkdownTranslations().containsKey(name)) {\n            var localisedString = english.getMarkdownTranslations().get(name);\n            return localisedString;\n        }\n\n        TrackEvent.withWarn(\"Markdown translation for key \" + name + \" not found\")\n                .handle();\n        return \"\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppI18nData.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.ext.ExtensionException;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.prefs.SupportedLocale;\n\nimport lombok.Value;\nimport org.apache.commons.io.FilenameUtils;\n\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.FileVisitResult;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.SimpleFileVisitor;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.util.*;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.regex.Pattern;\n\n@Value\npublic class AppI18nData {\n\n    private static final Pattern VAR_PATTERN = Pattern.compile(\"\\\\$\\\\w+?\\\\$\");\n\n    SupportedLocale locale;\n    Map<String, String> translations;\n    Map<String, String> markdownTranslations;\n\n    static AppI18nData load(SupportedLocale l) throws Exception {\n        TrackEvent.info(\"Loading translations ...\");\n\n        var translations = new HashMap<String, String>();\n        {\n            var basePath = AppInstallation.ofCurrent().getLangPath().resolve(\"strings\");\n            if (!Files.exists(basePath)) {\n                throw ExtensionException.corrupt(\"Failed to locate translation data\");\n            }\n\n            AtomicInteger fileCounter = new AtomicInteger();\n            AtomicInteger lineCounter = new AtomicInteger();\n            Files.walkFileTree(basePath, new SimpleFileVisitor<>() {\n                @Override\n                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {\n                    if (!matchesLocale(file, l)) {\n                        return FileVisitResult.CONTINUE;\n                    }\n\n                    if (!file.getFileName().toString().endsWith(\".properties\")) {\n                        return FileVisitResult.CONTINUE;\n                    }\n\n                    fileCounter.incrementAndGet();\n                    try (var in = Files.newInputStream(file)) {\n                        var props = new Properties();\n                        props.load(new InputStreamReader(in, StandardCharsets.UTF_8));\n                        props.forEach((key, value) -> {\n                            translations.put(key.toString(), value.toString());\n                            lineCounter.incrementAndGet();\n                        });\n                    } catch (IOException ex) {\n                        ErrorEventFactory.fromThrowable(ex)\n                                .omitted(true)\n                                .build()\n                                .handle();\n                    }\n                    return FileVisitResult.CONTINUE;\n                }\n            });\n        }\n\n        var markdownDocumentations = new HashMap<String, String>();\n        {\n            var basePath = AppInstallation.ofCurrent().getLangPath().resolve(\"texts\");\n            if (!Files.exists(basePath)) {\n                throw ExtensionException.corrupt(\"Failed to locate translation data\");\n            }\n\n            Files.walkFileTree(basePath, new SimpleFileVisitor<>() {\n                @Override\n                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {\n                    if (!matchesLocale(file, l)) {\n                        return FileVisitResult.CONTINUE;\n                    }\n\n                    if (!file.getFileName().toString().endsWith(\".md\")) {\n                        return FileVisitResult.CONTINUE;\n                    }\n\n                    var name = file.getFileName()\n                            .toString()\n                            .substring(0, file.getFileName().toString().lastIndexOf(\"_\"));\n                    try (var in = Files.newInputStream(file)) {\n                        markdownDocumentations.put(name, new String(in.readAllBytes(), StandardCharsets.UTF_8));\n                    } catch (IOException ex) {\n                        ErrorEventFactory.fromThrowable(ex)\n                                .omitted(true)\n                                .build()\n                                .handle();\n                    }\n                    return FileVisitResult.CONTINUE;\n                }\n            });\n        }\n\n        return new AppI18nData(l, translations, markdownDocumentations);\n    }\n\n    private static boolean matchesLocale(Path f, SupportedLocale l) {\n        var name = FilenameUtils.getBaseName(f.getFileName().toString());\n        var ending = \"_\" + l.getId();\n        return name.endsWith(ending);\n    }\n\n    Optional<String> getLocalised(String s, Object... vars) {\n        if (getTranslations().containsKey(s)) {\n            var localisedString = getTranslations().get(s);\n            return Optional.ofNullable(getValue(localisedString, vars));\n        }\n        return Optional.empty();\n    }\n\n    private String getValue(String s, Object... vars) {\n        s = s.replace(\"\\\\n\", \"\\n\");\n        if (vars.length > 0) {\n            var matcher = VAR_PATTERN.matcher(s);\n            for (var v : vars) {\n                v = v != null ? v : \"null\";\n                if (matcher.find()) {\n                    var group = matcher.group();\n                    s = s.replace(group, v.toString());\n                } else {\n                    TrackEvent.warn(\"No match found for value \" + v + \" in string \" + s);\n                }\n            }\n        }\n        return s;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppImages.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\n\nimport javafx.scene.image.Image;\nimport javafx.scene.image.WritableImage;\n\nimport org.apache.commons.io.FilenameUtils;\n\nimport java.awt.image.BufferedImage;\nimport java.io.IOException;\nimport java.nio.file.FileVisitResult;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.SimpleFileVisitor;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.Predicate;\n\npublic class AppImages {\n\n    public static final Image DEFAULT_IMAGE = new WritableImage(1, 1);\n    private static final Map<String, Image> images = new HashMap<>();\n\n    public static void remove(Predicate<String> filter) {\n        images.keySet().removeIf(filter);\n    }\n\n    public static void init() {\n        loadBundledProviderIcons();\n        loadOsIcons();\n        loadWelcomeImages();\n        loadMiscImages();\n        loadLogos();\n    }\n\n    private static void loadBundledProviderIcons() {\n        var exts = AppExtensionManager.getInstance().getContentModules();\n        for (Module ext : exts) {\n            AppResources.with(ext.getName(), \"img/\", basePath -> {\n                if (!Files.exists(basePath)) {\n                    return;\n                }\n\n                var skipLarge = AppDisplayScale.hasOnlyDefaultDisplayScale();\n                Files.walkFileTree(basePath, new SimpleFileVisitor<>() {\n                    @Override\n                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {\n                        var name = file.getFileName().toString();\n                        if (name.contains(\"-80\") && skipLarge) {\n                            return FileVisitResult.CONTINUE;\n                        }\n\n                        AppImages.loadImage(file, name);\n                        return FileVisitResult.CONTINUE;\n                    }\n                });\n            });\n        }\n    }\n\n    private static void loadOsIcons() {\n        AppResources.with(AppResources.MAIN_MODULE, \"os\", basePath -> {\n            var skipLarge = AppDisplayScale.hasOnlyDefaultDisplayScale();\n            Files.walkFileTree(basePath, new SimpleFileVisitor<>() {\n                @Override\n                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {\n                    var name = file.getFileName().toString();\n                    if (name.contains(\"-40\") && skipLarge) {\n                        return FileVisitResult.CONTINUE;\n                    }\n\n                    AppImages.loadImage(file, \"os/\" + name);\n                    return FileVisitResult.CONTINUE;\n                }\n            });\n        });\n    }\n\n    private static void loadWelcomeImages() {\n        AppResources.with(AppResources.MAIN_MODULE, \"welcome\", basePath -> {\n            var skipLarge = AppDisplayScale.hasOnlyDefaultDisplayScale();\n            Files.walkFileTree(basePath, new SimpleFileVisitor<>() {\n                @Override\n                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {\n                    var name = file.getFileName().toString();\n                    if (name.contains(\"-244\") && skipLarge) {\n                        return FileVisitResult.CONTINUE;\n                    }\n\n                    AppImages.loadImage(file, \"welcome/\" + name);\n                    return FileVisitResult.CONTINUE;\n                }\n            });\n        });\n    }\n\n    private static void loadLogos() {\n        AppResources.with(AppResources.MAIN_MODULE, \"logo\", basePath -> {\n            Files.walkFileTree(basePath, new SimpleFileVisitor<>() {\n                @Override\n                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {\n                    var relativeFileName = FilenameUtils.separatorsToUnix(\n                            basePath.getParent().relativize(file).toString());\n                    AppImages.loadImage(file, relativeFileName);\n                    return FileVisitResult.CONTINUE;\n                }\n            });\n        });\n    }\n\n    private static void loadMiscImages() {\n        AppResources.with(AppResources.MAIN_MODULE, \"\", basePath -> {\n            loadImage(basePath.resolve(\"action.png\"), \"action.png\");\n            loadImage(basePath.resolve(\"error.png\"), \"error.png\");\n        });\n    }\n\n    public static boolean hasImage(String file) {\n        if (file == null) {\n            return false;\n        }\n\n        if (images.containsKey(file)) {\n            return true;\n        }\n\n        var key = file.contains(\":\") ? file.split(\":\", 2)[1] : file;\n        if (images.containsKey(key)) {\n            return true;\n        }\n\n        return false;\n    }\n\n    public static Image image(String file) {\n        if (file == null) {\n            return DEFAULT_IMAGE;\n        }\n\n        if (images.containsKey(file)) {\n            return images.get(file);\n        }\n\n        var key = file.contains(\":\") ? file.split(\":\", 2)[1] : file;\n        if (images.containsKey(key)) {\n            return images.get(key);\n        }\n\n        TrackEvent.warn(\"Image \" + key + \" not found\");\n        return DEFAULT_IMAGE;\n    }\n\n    public static BufferedImage toAwtImage(Image fxImage) {\n        BufferedImage img =\n                new BufferedImage((int) fxImage.getWidth(), (int) fxImage.getHeight(), BufferedImage.TYPE_INT_ARGB);\n        for (int x = 0; x < fxImage.getWidth(); x++) {\n            for (int y = 0; y < fxImage.getHeight(); y++) {\n                int rgb = fxImage.getPixelReader().getArgb(x, y);\n                img.setRGB(x, y, rgb);\n            }\n        }\n        return img;\n    }\n\n    public static void loadImage(Path p, String key) {\n        if (p == null) {\n            return;\n        }\n\n        images.put(key, loadImage(p));\n    }\n\n    public static Image loadImage(Path p) {\n        if (p == null) {\n            return DEFAULT_IMAGE;\n        }\n\n        if (!Files.isRegularFile(p)) {\n            TrackEvent.error(\"Image file \" + p + \" not found.\");\n            return DEFAULT_IMAGE;\n        }\n\n        try (var in = Files.newInputStream(p)) {\n            return new Image(in, -1, -1, true, true);\n        } catch (IOException e) {\n            ErrorEventFactory.fromThrowable(e).omitted(true).build().handle();\n            return DEFAULT_IMAGE;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppInstallation.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.core.OsType;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic abstract class AppInstallation {\n\n    private static final Windows WINDOWS = AppProperties.get().isImage()\n            ? new Windows(determineCurrentInstallationBasePath())\n            : new WindowsDev(\n                    determineDefaultInstallationBasePath(AppProperties.get().isStaging()),\n                    determineCurrentInstallationBasePath());\n    private static final Linux LINUX = AppProperties.get().isImage()\n            ? new Linux(determineCurrentInstallationBasePath())\n            : new LinuxDev(\n                    determineDefaultInstallationBasePath(AppProperties.get().isStaging()),\n                    determineCurrentInstallationBasePath());\n    private static final MacOs MACOS = AppProperties.get().isImage()\n            ? new MacOs(determineCurrentInstallationBasePath())\n            : new MacOsDev(\n                    determineDefaultInstallationBasePath(AppProperties.get().isStaging()),\n                    determineCurrentInstallationBasePath());\n    private final Path base;\n\n    private AppInstallation(Path base) {\n        this.base = base;\n    }\n\n    public static AppInstallation ofCurrent() {\n        return switch (OsType.ofLocal()) {\n            case OsType.Windows ignored -> WINDOWS;\n            case OsType.Linux ignored -> LINUX;\n            case OsType.MacOs ignored -> MACOS;\n        };\n    }\n\n    public static AppInstallation ofDefault() {\n        return ofDefault(AppProperties.get().isStaging());\n    }\n\n    public static AppInstallation ofDefault(boolean stage) {\n        var def = determineDefaultInstallationBasePath(stage);\n        return switch (OsType.ofLocal()) {\n            case OsType.Windows ignored -> new Windows(def);\n            case OsType.Linux ignored -> new Linux(def);\n            case OsType.MacOs ignored -> new MacOs(def);\n        };\n    }\n\n    private static Path determineDefaultInstallationBasePath(boolean stage) {\n        return switch (OsType.ofLocal()) {\n            case OsType.Linux ignored -> {\n                yield Path.of(stage ? \"/opt/xpipe-ptb\" : \"/opt/xpipe\");\n            }\n            case OsType.MacOs ignored -> {\n                yield Path.of(stage ? \"/Applications/XPipe PTB.app\" : \"/Applications/XPipe.app\");\n            }\n            case OsType.Windows ignored -> {\n                var pg = AppSystemInfo.ofWindows().getProgramFiles();\n                var systemPath = pg.resolve(AppNames.ofCurrent().getName());\n                if (Files.exists(systemPath)) {\n                    yield systemPath;\n                }\n\n                var ad = AppSystemInfo.ofWindows().getLocalAppData();\n                yield ad.resolve(AppNames.ofCurrent().getName());\n            }\n        };\n    }\n\n    private static Path determineCurrentInstallationBasePath() {\n        var command = ProcessHandle.current().info().command();\n        // We should always have a command associated with the current process, otherwise something went seriously wrong\n        if (command.isEmpty()) {\n            var javaHome = System.getProperty(\"java.home\");\n            var javaExec = toRealPathIfPossible(Path.of(javaHome, \"bin\", \"java\"));\n            var path = getInstallationBasePathForJavaExecutable(javaExec);\n            return path;\n        }\n\n        // Resolve any possible links to a real path\n        Path path = toRealPathIfPossible(Path.of(command.get()));\n        // Check if the process was started using a relative path, and adapt it if necessary\n        if (!path.isAbsolute()) {\n            path = toRealPathIfPossible(Path.of(System.getProperty(\"user.dir\")).resolve(path));\n        }\n\n        var name = path.getFileName().toString();\n        // Check if we launched the JVM via a start script instead of the native executable\n        if (name.endsWith(\"java\") || name.endsWith(\"java.exe\")) {\n            // If we are not an image, we are probably running in a development environment where we want to use the\n            // working directory\n            var isImage = AppProperties.get().isImage();\n            if (!isImage) {\n                var cd = Path.of(System.getProperty(\"user.dir\"));\n                var valid = Files.exists(cd.resolve(\"app\")) && Files.exists(cd.resolve(\"lang\"));\n                if (!valid) {\n                    throw new IllegalArgumentException(\n                            \"Development build launched in wrong working directory, expected project root but got \"\n                                    + cd);\n                }\n                return cd;\n            }\n            return getInstallationBasePathForJavaExecutable(path);\n        } else {\n            return getInstallationBasePathForDaemonExecutable(path);\n        }\n    }\n\n    private static Path getInstallationBasePathForDaemonExecutable(Path executable) {\n        // Resolve root path of installation relative to executable in a JPackage installation\n        return switch (OsType.ofLocal()) {\n            case OsType.Linux ignored -> {\n                yield executable.getParent().getParent();\n            }\n            case OsType.MacOs ignored -> {\n                yield executable.getParent().getParent().getParent();\n            }\n            case OsType.Windows ignored -> {\n                yield executable.getParent();\n            }\n        };\n    }\n\n    private static Path getInstallationBasePathForJavaExecutable(Path executable) {\n        // Resolve root path of installation relative to executable in a JPackage installation\n        return switch (OsType.ofLocal()) {\n            case OsType.Linux ignored -> {\n                yield executable.getParent().getParent().getParent().getParent();\n            }\n            case OsType.MacOs ignored -> {\n                yield executable\n                        .getParent()\n                        .getParent()\n                        .getParent()\n                        .getParent()\n                        .getParent()\n                        .getParent();\n            }\n            case OsType.Windows ignored -> {\n                yield executable.getParent().getParent().getParent();\n            }\n        };\n    }\n\n    private static Path toRealPathIfPossible(Path p) {\n        try {\n            // Under certain conditions, e.g. when running on a ramdisk, path resolution might fail.\n            // This is however not a big problem in that case, so we ignore it\n            return p.toRealPath();\n        } catch (IOException e) {\n            return p;\n        }\n    }\n\n    public Path getBaseInstallationPath() {\n        return base;\n    }\n\n    public abstract Path getDaemonDebugScriptPath();\n\n    public abstract Path getLangPath();\n\n    public abstract Path getCliExecutablePath();\n\n    public abstract Path getDaemonExecutablePath();\n\n    public abstract Path getExtensionsPath();\n\n    public abstract Path getLogoPath();\n\n    public static class Windows extends AppInstallation {\n\n        private Windows(Path base) {\n            super(base);\n        }\n\n        @Override\n        public Path getDaemonDebugScriptPath() {\n            return getBaseInstallationPath()\n                    .resolve(\"scripts\", AppNames.ofCurrent().getExecutableName() + \"_debug.bat\");\n        }\n\n        @Override\n        public Path getLangPath() {\n            return getBaseInstallationPath().resolve(\"lang\");\n        }\n\n        @Override\n        public Path getCliExecutablePath() {\n            return getBaseInstallationPath().resolve(\"bin\", \"xpipe.exe\");\n        }\n\n        @Override\n        public Path getDaemonExecutablePath() {\n            return getBaseInstallationPath().resolve(AppNames.ofCurrent().getExecutableName() + \".exe\");\n        }\n\n        @Override\n        public Path getExtensionsPath() {\n            return getBaseInstallationPath().resolve(\"extensions\");\n        }\n\n        @Override\n        public Path getLogoPath() {\n            return getBaseInstallationPath().resolve(\"logo.ico\");\n        }\n    }\n\n    public static class WindowsDev extends Windows {\n\n        private final Path devBase;\n\n        private WindowsDev(Path base, Path devBase) {\n            super(base);\n            this.devBase = devBase;\n        }\n\n        @Override\n        public Path getLangPath() {\n            return devBase.resolve(\"lang\");\n        }\n\n        @Override\n        public Path getLogoPath() {\n            return devBase.resolve(\"dist\").resolve(\"logo\").resolve(\"logo.ico\");\n        }\n    }\n\n    public static class Linux extends AppInstallation {\n\n        private Linux(Path base) {\n            super(base);\n        }\n\n        @Override\n        public Path getDaemonDebugScriptPath() {\n            return getBaseInstallationPath()\n                    .resolve(\"scripts\", AppNames.ofCurrent().getExecutableName() + \"_debug.sh\");\n        }\n\n        @Override\n        public Path getLangPath() {\n            return getBaseInstallationPath().resolve(\"lang\");\n        }\n\n        @Override\n        public Path getCliExecutablePath() {\n            return getBaseInstallationPath().resolve(\"bin\", \"xpipe\");\n        }\n\n        @Override\n        public Path getDaemonExecutablePath() {\n            return getBaseInstallationPath().resolve(\"bin\", AppNames.ofCurrent().getExecutableName());\n        }\n\n        @Override\n        public Path getExtensionsPath() {\n            return getBaseInstallationPath().resolve(\"extensions\");\n        }\n\n        @Override\n        public Path getLogoPath() {\n            if (!AppProperties.get().isImage()) {\n                return getBaseInstallationPath().resolve(\"dist\").resolve(\"logo\").resolve(\"logo.png\");\n            }\n\n            return getBaseInstallationPath().resolve(\"logo.png\");\n        }\n    }\n\n    public static class LinuxDev extends Linux {\n\n        private final Path devBase;\n\n        private LinuxDev(Path base, Path devBase) {\n            super(base);\n            this.devBase = devBase;\n        }\n\n        @Override\n        public Path getLangPath() {\n            return devBase.resolve(\"lang\");\n        }\n    }\n\n    public static class MacOs extends AppInstallation {\n\n        private MacOs(Path base) {\n            super(base);\n        }\n\n        @Override\n        public Path getDaemonDebugScriptPath() {\n            return getBaseInstallationPath()\n                    .resolve(\n                            \"Contents\",\n                            \"Resources\",\n                            \"scripts\",\n                            AppNames.ofCurrent().getExecutableName() + \"_debug.sh\");\n        }\n\n        @Override\n        public Path getLangPath() {\n            if (!AppProperties.get().isImage()) {\n                return getBaseInstallationPath().resolve(\"lang\");\n            }\n\n            return getBaseInstallationPath().resolve(\"Contents\", \"Resources\", \"lang\");\n        }\n\n        @Override\n        public Path getCliExecutablePath() {\n            return getBaseInstallationPath().resolve(\"Contents\", \"MacOS\", \"xpipe\");\n        }\n\n        @Override\n        public Path getDaemonExecutablePath() {\n            return getBaseInstallationPath()\n                    .resolve(\"Contents\", \"MacOS\", AppNames.ofCurrent().getExecutableName());\n        }\n\n        @Override\n        public Path getExtensionsPath() {\n            return getBaseInstallationPath().resolve(\"Contents\", \"Resources\", \"extensions\");\n        }\n\n        @Override\n        public Path getLogoPath() {\n            if (!AppProperties.get().isImage()) {\n                return getBaseInstallationPath().resolve(\"dist\").resolve(\"logo\").resolve(\"logo.icns\");\n            }\n\n            return getBaseInstallationPath()\n                    .resolve(\"Contents\")\n                    .resolve(\"Resources\")\n                    .resolve(\"xpipe.icns\");\n        }\n    }\n\n    public static class MacOsDev extends MacOs {\n\n        private final Path devBase;\n\n        private MacOsDev(Path base, Path devBase) {\n            super(base);\n            this.devBase = devBase;\n        }\n\n        @Override\n        public Path getLangPath() {\n            return devBase.resolve(\"lang\");\n        }\n\n        @Override\n        public Path getLogoPath() {\n            return devBase.resolve(\"dist\").resolve(\"logo\").resolve(\"logo.icns\");\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppInstance.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.beacon.AppBeaconServer;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.beacon.BeaconClient;\nimport io.xpipe.beacon.BeaconClientInformation;\nimport io.xpipe.beacon.BeaconServer;\nimport io.xpipe.beacon.api.DaemonFocusExchange;\nimport io.xpipe.beacon.api.DaemonOpenExchange;\nimport io.xpipe.core.OsType;\n\nimport java.awt.*;\nimport java.util.List;\nimport java.util.Optional;\n\npublic class AppInstance {\n\n    public static void init() {\n        checkStart(0);\n    }\n\n    public static Optional<BeaconClient> tryEstablishConnection(int port) {\n        try {\n            return Optional.of(BeaconClient.establishConnection(\n                    port, BeaconClientInformation.Daemon.builder().build()));\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).omit().expected().handle();\n            return Optional.empty();\n        }\n    }\n\n    private static void checkStart(int attemptCounter) {\n        var port = AppBeaconServer.get().getPort();\n        var reachable = BeaconServer.isReachable(port);\n        if (!reachable) {\n            // Even in case we are unable to reach another beacon server\n            // there might be another instance running, for example\n            // starting up or listening on another port\n            if (!AppDataLock.lock()) {\n                TrackEvent.info(\n                        \"Data directory \" + AppProperties.get().getDataDir().toString()\n                                + \" is already locked. Is another instance running?\");\n                AppOperationMode.halt(1);\n            }\n\n            // We are good to start up!\n            return;\n        }\n\n        var client = tryEstablishConnection(port);\n        if (client.isEmpty()) {\n            // If an instance is running as another user, we cannot connect to it as the xpipe_auth file is inaccessible\n            // Therefore the beacon client is not present.\n            // We still should check whether it is somehow occupied, otherwise beacon server startup will fail\n            TrackEvent.info(\n                    \"Another instance is already running on this port as another user or is not reachable. Quitting ...\");\n            AppOperationMode.halt(1);\n            return;\n        }\n\n        try {\n            var inputs = AppProperties.get().getArguments().getOpenArgs();\n            // Assume that we want to open the GUI if we launched again\n            client.get().performRequest(DaemonFocusExchange.Request.builder().build());\n            if (!inputs.isEmpty()) {\n                client.get()\n                        .performRequest(DaemonOpenExchange.Request.builder()\n                                .arguments(inputs)\n                                .build());\n            }\n        } catch (Exception ex) {\n            // Wait until shutdown has completed\n            if (ex.getMessage() != null\n                    && ex.getMessage().contains(\"Daemon is currently in shutdown\")\n                    && attemptCounter < 10) {\n                ThreadHelper.sleep(1000);\n                checkStart(++attemptCounter);\n                return;\n            }\n\n            var cli = AppInstallation.ofCurrent().getCliExecutablePath();\n            ErrorEventFactory.fromThrowable(\n                            \"Unable to connect to existing running daemon instance as it did not respond.\"\n                                    + \" Either try to kill the process xpiped manually or use the command \\\"\"\n                                    + cli\n                                    + \"\\\" daemon stop --force.\",\n                            ex)\n                    .term()\n                    .expected()\n                    .handle();\n        }\n\n        if (OsType.ofLocal() == OsType.MACOS) {\n            Desktop.getDesktop().setOpenURIHandler(e -> {\n                try {\n                    client.get()\n                            .performRequest(DaemonOpenExchange.Request.builder()\n                                    .arguments(List.of(e.getURI().toString()))\n                                    .build());\n                } catch (Exception ex) {\n                    ErrorEventFactory.fromThrowable(ex).expected().omit().handle();\n                }\n            });\n            ThreadHelper.sleep(1000);\n        }\n        TrackEvent.info(\"Another instance is already running on this port. Quitting ...\");\n        AppOperationMode.halt(1);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppLayoutModel.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.browser.BrowserFullSessionComp;\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.hub.comp.StoreLayoutComp;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.prefs.AppPrefsComp;\nimport io.xpipe.app.terminal.TerminalDockHubManager;\nimport io.xpipe.app.update.AppDistributionType;\nimport io.xpipe.app.util.*;\n\nimport javafx.beans.property.BooleanProperty;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.FXCollections;\nimport javafx.collections.ObservableList;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\n\nimport lombok.*;\nimport lombok.experimental.NonFinal;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Supplier;\n\n@Getter\npublic class AppLayoutModel {\n\n    private static AppLayoutModel INSTANCE;\n\n    private final SavedState savedState;\n\n    private final List<Entry> entries;\n\n    private final Property<Entry> selected;\n\n    private final ObservableList<QueueEntry> queueEntries;\n\n    private final BooleanProperty ptbAvailable = new SimpleBooleanProperty();\n\n    public AppLayoutModel(SavedState savedState) {\n        this.savedState = savedState;\n        this.entries = createEntryList();\n        this.selected = new SimpleObjectProperty<>(entries.getFirst());\n        this.queueEntries = FXCollections.observableArrayList();\n    }\n\n    public static AppLayoutModel get() {\n        return INSTANCE;\n    }\n\n    public static void init() {\n        var state = AppCache.getNonNull(\"layoutState\", SavedState.class, () -> new SavedState(270, 300));\n        INSTANCE = new AppLayoutModel(state);\n    }\n\n    public static void reset() {\n        if (INSTANCE == null) {\n            return;\n        }\n\n        AppCache.update(\"layoutState\", INSTANCE.savedState);\n        INSTANCE = null;\n    }\n\n    public synchronized void showQueueEntry(QueueEntry entry, Duration duration, boolean allowDuplicates) {\n        if (!allowDuplicates && queueEntries.contains(entry)) {\n            return;\n        }\n\n        queueEntries.add(entry);\n        if (duration != null) {\n            GlobalTimer.delay(\n                    () -> {\n                        synchronized (this) {\n                            queueEntries.remove(entry);\n                        }\n                    },\n                    duration);\n        }\n    }\n\n    public void selectBrowser() {\n        PlatformThread.runLaterIfNeeded(() -> {\n            selected.setValue(entries.get(1));\n        });\n    }\n\n    public void selectSettings() {\n        PlatformThread.runLaterIfNeeded(() -> {\n            selected.setValue(entries.get(2));\n        });\n    }\n\n    public void selectLicense() {\n        PlatformThread.runLaterIfNeeded(() -> {\n            selected.setValue(entries.get(3));\n        });\n    }\n\n    public void selectConnections() {\n        PlatformThread.runLaterIfNeeded(() -> {\n            selected.setValue(entries.getFirst());\n        });\n    }\n\n    private List<Entry> createEntryList() {\n        var l = new ArrayList<>(List.of(\n                new Entry(\n                        AppI18n.observable(\"connections\"),\n                        new LabelGraphic.IconGraphic(\"mdi2c-connection\"),\n                        new StoreLayoutComp(),\n                        () -> {\n                            TerminalDockHubManager.get().hideDock();\n                        },\n                        new KeyCodeCombination(KeyCode.DIGIT1, KeyCombination.SHORTCUT_DOWN)),\n                new Entry(\n                        AppI18n.observable(\"browser\"),\n                        new LabelGraphic.IconGraphic(\"mdi2f-file-cabinet\"),\n                        new BrowserFullSessionComp(BrowserFullSessionModel.DEFAULT),\n                        null,\n                        new KeyCodeCombination(KeyCode.DIGIT2, KeyCombination.SHORTCUT_DOWN)),\n                new Entry(\n                        AppI18n.observable(\"settings\"),\n                        new LabelGraphic.IconGraphic(\"mdsmz-miscellaneous_services\"),\n                        new AppPrefsComp(),\n                        null,\n                        new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.SHORTCUT_DOWN)),\n                new Entry(\n                        AppI18n.observable(\"explorePlans\"),\n                        new LabelGraphic.IconGraphic(\"mdi2p-professional-hexagon\"),\n                        LicenseProvider.get().overviewPage(),\n                        null,\n                        null),\n                new Entry(\n                        AppI18n.observable(\"docs\"),\n                        new LabelGraphic.IconGraphic(\"mdi2b-book-open-variant\"),\n                        null,\n                        () -> Hyperlinks.open(DocumentationLink.getRoot()),\n                        null),\n                new Entry(\n                        AppI18n.observable(\"visitGithubRepository\"),\n                        new LabelGraphic.IconGraphic(\"mdi2g-github\"),\n                        null,\n                        () -> Hyperlinks.open(Hyperlinks.GITHUB),\n                        null),\n                new Entry(\n                        AppI18n.observable(\"discord\"),\n                        new LabelGraphic.IconGraphic(\"bi-discord\"),\n                        null,\n                        () -> Hyperlinks.open(Hyperlinks.DISCORD),\n                        null)));\n        if (AppDistributionType.get() != AppDistributionType.WEBTOP) {\n            l.add(new Entry(\n                    AppI18n.observable(\"webtop\"),\n                    new LabelGraphic.IconGraphic(\"mdal-desktop_mac\"),\n                    null,\n                    () -> Hyperlinks.open(Hyperlinks.GITHUB_WEBTOP),\n                    null));\n        }\n        return l;\n    }\n\n    @Data\n    @Builder\n    @Jacksonized\n    public static class SavedState {\n\n        double sidebarWidth;\n        double browserConnectionsWidth;\n    }\n\n    public record Entry(\n            ObservableValue<String> name,\n            LabelGraphic icon,\n            BaseRegionBuilder<?, ?> comp,\n            Runnable action,\n            KeyCombination combination) {}\n\n    @Value\n    @NonFinal\n    public static class QueueEntry {\n\n        public static QueueEntry ofNotification(String key, String value) {\n            return new QueueEntry(AppI18n.observable(key), new LabelGraphic.IconGraphic(value), () -> true);\n        }\n\n        ObservableValue<String> name;\n        LabelGraphic icon;\n        Supplier<Boolean> action;\n\n        public void execute() {\n            ThreadHelper.runAsync(() -> {\n                executeSync();\n            });\n        }\n\n        public void executeSync() {\n            try {\n                var r = getAction().get();\n                if (r) {\n                    PlatformThread.runLaterIfNeeded(() -> {\n                        AppLayoutModel.get().getQueueEntries().remove(this);\n                    });\n                }\n            } catch (Throwable t) {\n                PlatformThread.runLaterIfNeeded(() -> {\n                    AppLayoutModel.get().getQueueEntries().remove(this);\n                });\n                throw t;\n            }\n        }\n\n        public void hide() {\n            AppLayoutModel.get().getQueueEntries().remove(this);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppLocalTemp.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.core.OsType;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.attribute.PosixFilePermissions;\n\npublic class AppLocalTemp {\n\n    public static Path getLocalTempDataDirectory() {\n        var temp =\n                AppSystemInfo.ofCurrent().getTemp().resolve(AppNames.ofCurrent().getKebapName());\n        // On Windows and macOS, we already have user specific temp directories\n        // Even on macOS as root we will have a unique directory (in contrast to shell controls)\n        if (OsType.ofLocal() == OsType.LINUX) {\n            try {\n                Files.createDirectories(temp);\n                // We did not set this in earlier versions. If we are running as a different user, it might fail\n                Files.setPosixFilePermissions(temp, PosixFilePermissions.fromString(\"rwxrwxrwx\"));\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).omit().expected().handle();\n            }\n\n            var user = AppSystemInfo.ofCurrent().getUser();\n            temp = temp.resolve(user);\n\n            try {\n                Files.createDirectories(temp);\n                // We did not set this in earlier versions. If we are running as a different user, it might fail\n                Files.setPosixFilePermissions(temp, PosixFilePermissions.fromString(\"rwx------\"));\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).omit().expected().handle();\n            }\n        } else {\n            try {\n                Files.createDirectories(temp);\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).omit().expected().handle();\n            }\n        }\n\n        return temp;\n    }\n\n    public static Path getLocalTempDataDirectory(String sub) {\n        var path = getLocalTempDataDirectory().resolve(sub);\n        try {\n            Files.createDirectories(path);\n        } catch (IOException e) {\n            ErrorEventFactory.fromThrowable(e).expected().omit().handle();\n        }\n        return path;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppLogs.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.util.Deobfuscator;\n\nimport lombok.Getter;\nimport org.apache.commons.io.FileUtils;\nimport org.slf4j.ILoggerFactory;\nimport org.slf4j.IMarkerFactory;\nimport org.slf4j.Logger;\nimport org.slf4j.Marker;\nimport org.slf4j.event.Level;\nimport org.slf4j.helpers.AbstractLogger;\nimport org.slf4j.helpers.NOPLogger;\nimport org.slf4j.spi.MDCAdapter;\nimport org.slf4j.spi.SLF4JServiceProvider;\n\nimport java.io.*;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\nimport java.time.temporal.ChronoField;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.concurrent.ConcurrentHashMap;\n\npublic class AppLogs {\n\n    public static final List<String> LOG_LEVELS = List.of(\"error\", \"warn\", \"info\", \"debug\", \"trace\");\n\n    private static final DateTimeFormatter NAME_FORMATTER =\n            DateTimeFormatter.ofPattern(\"yyyy-MM-dd_HH-mm-ss\").withZone(ZoneId.systemDefault());\n    private static final DateTimeFormatter MESSAGE_FORMATTER =\n            DateTimeFormatter.ofPattern(\"HH:mm:ss:SSS\").withZone(ZoneId.systemDefault());\n\n    private static AppLogs INSTANCE;\n\n    @Getter\n    private final PrintStream originalSysOut;\n\n    @Getter\n    private final PrintStream originalSysErr;\n\n    private final Path logDir;\n\n    @Getter\n    private final boolean writeToSysout;\n\n    @Getter\n    private final boolean writeToFile;\n\n    @Getter\n    private final String logLevel;\n\n    private final PrintStream outFileStream;\n\n    public AppLogs(\n            Path logDir, boolean writeToSysout, boolean writeToFile, String logLevel, PrintStream outFileStream) {\n        this.logDir = logDir;\n        this.writeToSysout = writeToSysout;\n        this.writeToFile = writeToFile;\n        this.logLevel = logLevel;\n        this.outFileStream = outFileStream;\n\n        this.originalSysOut = System.out;\n        this.originalSysErr = System.err;\n\n        setLogLevels();\n        hookUpSystemOut();\n        hookUpSystemErr();\n    }\n\n    public static void init() {\n        if (INSTANCE != null) {\n            return;\n        }\n\n        var logDir = AppProperties.get().getDataDir().resolve(\"logs\");\n\n        // Regularly clean logs dir\n        if (AppProperties.get().isNewBuildSession() && Files.exists(logDir)) {\n            try {\n                List<Path> all;\n                try (var s = Files.list(logDir)) {\n                    all = s.toList();\n                }\n                for (Path path : all) {\n                    // Don't delete installer logs\n                    if (path.getFileName().toString().contains(\"installer\")) {\n                        continue;\n                    }\n\n                    FileUtils.forceDelete(path.toFile());\n                }\n            } catch (Exception ex) {\n                // It can happen that another instance is running that is locking a log file\n                // Since we initialized before checking for another instance, this might fail\n                ErrorEventFactory.fromThrowable(ex).expected().omit().handle();\n            }\n        }\n\n        var now = Instant.now();\n        var name = NAME_FORMATTER.format(now);\n        Path usedLogsDir = logDir.resolve(name);\n\n        // When two instances are being launched within the same second, add milliseconds\n        if (Files.exists(usedLogsDir)) {\n            usedLogsDir = logDir.resolve(name + \"_\" + now.get(ChronoField.MILLI_OF_SECOND));\n        }\n\n        PrintStream outFileStream = null;\n        var shouldLogToFile = AppProperties.get().isLogToFile();\n        if (shouldLogToFile) {\n            try {\n                FileUtils.forceMkdir(usedLogsDir.toFile());\n                var file = usedLogsDir.resolve(AppNames.ofMain().getKebapName() + \".log\");\n                var fos = new FileOutputStream(file.toFile(), true);\n                var buf = new BufferedOutputStream(fos);\n                outFileStream = new PrintStream(buf, false);\n            } catch (Exception ex) {\n                ErrorEventFactory.fromThrowable(ex).build().handle();\n            }\n        }\n\n        var shouldLogToSysout = AppProperties.get().isLogToSysOut();\n\n        if (shouldLogToFile && outFileStream == null) {\n            TrackEvent.info(\"Log file initialization failed. Writing to standard out\");\n            shouldLogToSysout = true;\n            shouldLogToFile = false;\n        }\n\n        if (shouldLogToFile && !shouldLogToSysout) {\n            TrackEvent.info(\"Writing log output to \" + usedLogsDir + \" from now on\");\n        }\n\n        var level = AppProperties.get().getLogLevel();\n        INSTANCE = new AppLogs(usedLogsDir, shouldLogToSysout, shouldLogToFile, level, outFileStream);\n    }\n\n    public static void teardown() {\n        if (AppLogs.get() == null) {\n            return;\n        }\n\n        AppLogs.get().close();\n        INSTANCE = null;\n    }\n\n    public static AppLogs get() {\n        return INSTANCE;\n    }\n\n    public void flush() {\n        if (outFileStream != null) {\n            outFileStream.flush();\n        }\n    }\n\n    private void close() {\n        if (outFileStream != null) {\n            outFileStream.close();\n        }\n    }\n\n    private void hookUpSystemOut() {\n        System.setOut(new PrintStream(new OutputStream() {\n            private final ByteArrayOutputStream baos = new ByteArrayOutputStream(1000);\n\n            @Override\n            public void write(int b) {\n                if (b == '\\r' || b == '\\n') {\n                    String line = baos.toString();\n                    if (line.length() == 0) {\n                        return;\n                    }\n\n                    TrackEvent.builder().type(\"info\").message(line).build().handle();\n                    baos.reset();\n                } else {\n                    baos.write(b);\n                }\n            }\n        }));\n    }\n\n    private void hookUpSystemErr() {\n        System.setErr(new PrintStream(new OutputStream() {\n            private final ByteArrayOutputStream baos = new ByteArrayOutputStream(1000);\n\n            @Override\n            public void write(int b) {\n                if (b == '\\r' || b == '\\n') {\n                    String line = baos.toString();\n                    if (line.length() == 0) {\n                        return;\n                    }\n\n                    TrackEvent.builder().type(\"error\").message(line).build().handle();\n                    baos.reset();\n                } else {\n                    baos.write(b);\n                }\n            }\n        }));\n    }\n\n    public void logException(String description, Throwable e) {\n        var deob = Deobfuscator.deobfuscateToString(e);\n        var event = TrackEvent.builder()\n                .type(\"error\")\n                .message((description != null ? description : \"\") + \"\\n\" + deob)\n                .build();\n        logEvent(event);\n    }\n\n    public synchronized void logEvent(TrackEvent event) {\n        var li = LOG_LEVELS.indexOf(AppProperties.get().getLogLevel());\n        int i = li == -1 ? 5 : li;\n        int current = LOG_LEVELS.indexOf(event.getType());\n        if (current <= i) {\n            if (writeToSysout) {\n                logSysOut(event);\n            }\n            if (writeToFile) {\n                logToFile(event);\n            }\n        }\n    }\n\n    private synchronized void logSysOut(TrackEvent event) {\n        var time = MESSAGE_FORMATTER.format(event.getInstant());\n        var string =\n                new StringBuilder(time).append(\" - \").append(event.getType()).append(\": \");\n        string.append(event);\n        var toLog = string.toString();\n        this.originalSysOut.println(toLog);\n    }\n\n    private void logToFile(TrackEvent event) {\n        var time = MESSAGE_FORMATTER.format(event.getInstant());\n        var string =\n                new StringBuilder(time).append(\" - \").append(event.getType()).append(\": \");\n        string.append(event);\n        var toLog = string.toString();\n        outFileStream.println(toLog);\n    }\n\n    private void setLogLevels() {\n        // Debug output for platform\n        if (AppProperties.get().isLogPlatformDebug()) {\n            System.setProperty(\"prism.verbose\", \"true\");\n            System.setProperty(\"prism.debug\", \"true\");\n            // System.setProperty(\"prism.trace\", \"true\");\n            // System.setProperty(\"sun.perflog\", \"results.log\");\n            //            System.setProperty(\"quantum.verbose\", \"true\");\n            //            System.setProperty(\"quantum.debug\", \"true\");\n            //            System.setProperty(\"quantum.pulse\", \"true\");\n        }\n    }\n\n    public Path getSessionLogsDirectory() {\n        return logDir;\n    }\n\n    public static final class Slf4jProvider implements SLF4JServiceProvider {\n\n        private static final String REQUESTED_API_VERSION = \"2.0\";\n\n        private final ILoggerFactory factory = new ILoggerFactory() {\n\n            private final Map<String, Logger> loggers = new ConcurrentHashMap<>();\n\n            public Logger getLogger(String name) {\n                // Only change this when debugging the logs of other libraries\n                return NOPLogger.NOP_LOGGER;\n\n                //                                // Don't use fully qualified class names\n                //                                var normalizedName = FilenameUtils.getExtension(name);\n                //                                if (normalizedName == null || normalizedName.isEmpty()) {\n                //                                    normalizedName = name;\n                //                                }\n                //\n                //                                return loggers.computeIfAbsent(normalizedName, s -> new\n                // Slf4jLogger());\n            }\n        };\n\n        @Override\n        public ILoggerFactory getLoggerFactory() {\n            return factory;\n        }\n\n        @Override\n        public IMarkerFactory getMarkerFactory() {\n            return null;\n        }\n\n        @Override\n        public MDCAdapter getMDCAdapter() {\n            return null;\n        }\n\n        @Override\n        public String getRequestedApiVersion() {\n            return REQUESTED_API_VERSION;\n        }\n\n        @Override\n        public void initialize() {}\n    }\n\n    public static final class Slf4jLogger extends AbstractLogger {\n\n        @Override\n        protected String getFullyQualifiedCallerName() {\n            return \"logger\";\n        }\n\n        @Override\n        protected void handleNormalizedLoggingCall(\n                Level level, Marker marker, String msg, Object[] arguments, Throwable throwable) {\n            if (arguments != null) {\n                for (var arg : arguments) {\n                    msg = msg.replaceFirst(\"\\\\{}\", Objects.toString(arg));\n                }\n            }\n            TrackEvent.builder()\n                    .type(level.toString().toLowerCase())\n                    .message(msg)\n                    .build()\n                    .handle();\n        }\n\n        @Override\n        public boolean isTraceEnabled() {\n            // return LOG_LEVELS.indexOf(\"trace\") <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());\n\n            // You almost never want trace output, javafx will spam everything\n            return false;\n        }\n\n        @Override\n        public boolean isTraceEnabled(Marker marker) {\n            // return LOG_LEVELS.indexOf(\"trace\") <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());\n\n            // You almost never want trace output, javafx will spam everything\n            return false;\n        }\n\n        @Override\n        public boolean isDebugEnabled() {\n            return LOG_LEVELS.indexOf(\"debug\")\n                    <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());\n        }\n\n        @Override\n        public boolean isDebugEnabled(Marker marker) {\n            return LOG_LEVELS.indexOf(\"debug\")\n                    <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());\n        }\n\n        @Override\n        public boolean isInfoEnabled() {\n            return LOG_LEVELS.indexOf(\"info\")\n                    <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());\n        }\n\n        @Override\n        public boolean isInfoEnabled(Marker marker) {\n            return LOG_LEVELS.indexOf(\"info\")\n                    <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());\n        }\n\n        @Override\n        public boolean isWarnEnabled() {\n            return LOG_LEVELS.indexOf(\"warn\")\n                    <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());\n        }\n\n        @Override\n        public boolean isWarnEnabled(Marker marker) {\n            return LOG_LEVELS.indexOf(\"warn\")\n                    <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());\n        }\n\n        @Override\n        public boolean isErrorEnabled() {\n            return LOG_LEVELS.indexOf(\"error\")\n                    <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());\n        }\n\n        @Override\n        public boolean isErrorEnabled(Marker marker) {\n            return LOG_LEVELS.indexOf(\"error\")\n                    <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppNames.java",
    "content": "package io.xpipe.app.core;\n\npublic abstract class AppNames {\n\n    public static AppNames ofMain() {\n        return new Main();\n    }\n\n    public static AppNames ofCurrent() {\n        if (AppProperties.get() != null && AppProperties.get().isStaging()) {\n            return new Ptb();\n        } else {\n            return new Main();\n        }\n    }\n\n    public static String propertyName(String name) {\n        return ofCurrent().getGroupName() + \".app.\" + name;\n    }\n\n    public static String packageName() {\n        return packageName(null);\n    }\n\n    public static String packageName(String name) {\n        return ofCurrent().getGroupName() + \".app\" + (name != null ? \".\" + name : \"\");\n    }\n\n    public static String extModuleName(String name) {\n        return ofCurrent().getGroupName() + \".ext.\" + name;\n    }\n\n    public abstract String getName();\n\n    public abstract String getKebapName();\n\n    public abstract String getSnakeName();\n\n    public abstract String getUppercaseName();\n\n    public abstract String getGroupName();\n\n    public abstract String getExecutableName();\n\n    private static class Main extends AppNames {\n\n        @Override\n        public String getName() {\n            return \"XPipe\";\n        }\n\n        @Override\n        public String getKebapName() {\n            return \"xpipe\";\n        }\n\n        @Override\n        public String getSnakeName() {\n            return \"xpipe\";\n        }\n\n        @Override\n        public String getUppercaseName() {\n            return \"XPIPE\";\n        }\n\n        @Override\n        public String getGroupName() {\n            return \"io.xpipe\";\n        }\n\n        @Override\n        public String getExecutableName() {\n            return \"xpiped\";\n        }\n    }\n\n    private static class Ptb extends AppNames {\n\n        @Override\n        public String getName() {\n            return \"XPipe PTB\";\n        }\n\n        @Override\n        public String getKebapName() {\n            return \"xpipe-ptb\";\n        }\n\n        @Override\n        public String getSnakeName() {\n            return \"xpipe_ptb\";\n        }\n\n        @Override\n        public String getUppercaseName() {\n            return \"XPIPE_PTB\";\n        }\n\n        @Override\n        public String getGroupName() {\n            return \"io.xpipe\";\n        }\n\n        @Override\n        public String getExecutableName() {\n            return \"xpiped\";\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppOpenArguments.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.action.LauncherUrlProvider;\nimport io.xpipe.app.browser.action.impl.OpenDirectoryActionProvider;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.core.FilePath;\n\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.InvalidPathException;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class AppOpenArguments {\n\n    private static final List<String> bufferedArguments = new ArrayList<>();\n\n    public static synchronized void init() {\n        handleImpl(AppProperties.get().getArguments().getOpenArgs());\n        handleImpl(bufferedArguments);\n        bufferedArguments.clear();\n    }\n\n    public static synchronized void handle(List<String> arguments) {\n        if (AppOperationMode.isInShutdown()) {\n            return;\n        }\n\n        if (AppOperationMode.isInStartup()) {\n            TrackEvent.withDebug(\"Buffering open arguments\").elements(arguments).handle();\n            bufferedArguments.addAll(arguments);\n            return;\n        }\n\n        handleImpl(arguments);\n    }\n\n    private static synchronized void handleImpl(List<String> arguments) {\n        if (arguments.size() == 0) {\n            return;\n        }\n\n        TrackEvent.withDebug(\"Handling arguments\").elements(arguments).handle();\n\n        var all = new ArrayList<AbstractAction>();\n        arguments.forEach(s -> {\n            try {\n                all.addAll(parseActions(s));\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).omit().handle();\n            }\n        });\n\n        //        var requiresPlatform = all.stream().anyMatch(launcherInput -> launcherInput.requiresJavaFXPlatform());\n        //        if (requiresPlatform) {\n        //            OperationMode.switchToSyncIfPossible(OperationMode.GUI);\n        //        }\n        //        var hasGui = OperationMode.get() == OperationMode.GUI;\n\n        all.forEach(launcherInput -> {\n            launcherInput.executeAsync();\n        });\n    }\n\n    public static List<? extends AbstractAction> parseActions(String input) {\n        if (input == null || input.isBlank()) {\n            return List.of();\n        }\n\n        if (input.startsWith(\"\\\"\") && input.endsWith(\"\\\"\")) {\n            input = input.substring(1, input.length() - 1);\n        }\n\n        try {\n            var uri = URI.create(input);\n            var scheme = uri.getScheme();\n            if (scheme != null) {\n                var action = uri.getScheme();\n                var found = ActionProvider.ALL.stream()\n                        .filter(actionProvider -> actionProvider instanceof LauncherUrlProvider lcs\n                                && lcs.getScheme().equalsIgnoreCase(action))\n                        .findFirst();\n                if (found.isPresent()) {\n                    AbstractAction a;\n                    try {\n                        a = ((LauncherUrlProvider) found.get()).createAction(uri);\n                    } catch (Exception e) {\n                        ErrorEventFactory.fromThrowable(e).omit().expected().handle();\n                        return List.of();\n                    }\n                    return a != null ? List.of(a) : List.of();\n                }\n            }\n        } catch (IllegalArgumentException ignored) {\n        }\n\n        try {\n            var path = Path.of(input);\n            if (Files.isRegularFile(path)) {\n                path = path.getParent();\n            }\n\n            if (Files.exists(path)) {\n                return List.of(OpenDirectoryActionProvider.Action.builder()\n                        .ref(DataStorage.get().local().ref())\n                        .files(List.of(FilePath.of(path)))\n                        .build());\n            }\n        } catch (InvalidPathException ignored) {\n        }\n\n        return List.of();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppPreloader.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.core.OsType;\n\nimport javafx.application.Preloader;\nimport javafx.stage.Stage;\n\nimport lombok.Getter;\nimport lombok.SneakyThrows;\n\n@Getter\npublic class AppPreloader extends Preloader {\n\n    @Override\n    @SneakyThrows\n    public void start(Stage primaryStage) {\n        if (OsType.ofLocal() != OsType.LINUX) {\n            return;\n        }\n\n        // Do it this way to prevent IDE inspections from complaining\n        var c = Class.forName(\n                ModuleLayer.boot().findModule(\"javafx.graphics\").orElseThrow(), \"com.sun.glass.ui.Application\");\n        var m = c.getDeclaredMethod(\"setName\", String.class);\n        m.invoke(\n                c.getMethod(\"GetApplication\").invoke(null), AppNames.ofCurrent().getName());\n        TrackEvent.info(\"Application preloader run\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppProperties.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.core.check.AppDirectoryPermissionsCheck;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.core.XPipeDaemonMode;\n\nimport lombok.Value;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.*;\n\n@Value\npublic class AppProperties {\n\n    private static AppProperties INSTANCE;\n\n    boolean fullVersion;\n    String version;\n    String build;\n    UUID buildUuid;\n    String sentryUrl;\n    String arch;\n    boolean image;\n    boolean staging;\n    boolean useVirtualThreads;\n    boolean debugThreads;\n    Path dataDir;\n    Path defaultDataDir;\n    Path dataBinDir;\n    boolean showcase;\n    AppVersion canonicalVersion;\n    boolean locatePtb;\n    boolean locatorVersionCheck;\n    boolean isTest;\n    boolean autoAcceptEula;\n    UUID uuid;\n    boolean initialLaunch;\n    boolean restarted;\n    UUID sessionId;\n    boolean developerMode;\n    boolean newBuildSession;\n    boolean aotTrainMode;\n    boolean debugPlatformThreadAccess;\n    boolean persistData;\n    AppArguments arguments;\n    XPipeDaemonMode explicitMode;\n    String devLoginPassword;\n    boolean logToSysOut;\n    boolean logToFile;\n    boolean logPlatformDebug;\n    String logLevel;\n    String loginTarget;\n\n    public AppProperties(String[] args) {\n        var appDir = Path.of(System.getProperty(\"user.dir\")).resolve(\"app\");\n        Path propsFile = appDir.resolve(\"dev.properties\");\n        if (Files.exists(propsFile)) {\n            try {\n                Properties props = new Properties();\n                props.load(Files.newInputStream(propsFile));\n                props.forEach((key, value) -> {\n                    // Don't overwrite existing properties\n                    if (System.getProperty(key.toString()) == null) {\n                        System.setProperty(key.toString(), value.toString());\n                    }\n                });\n            } catch (IOException e) {\n                ErrorEventFactory.fromThrowable(e).handle();\n            }\n        }\n        var referenceDir = Files.exists(appDir) ? appDir : Path.of(System.getProperty(\"user.dir\"));\n\n        image = AppProperties.class\n                .getProtectionDomain()\n                .getCodeSource()\n                .getLocation()\n                .getProtocol()\n                .equals(\"jrt\");\n        arguments = AppArguments.init(args);\n        fullVersion = Optional.ofNullable(System.getProperty(AppNames.propertyName(\"fullVersion\")))\n                .map(Boolean::parseBoolean)\n                .orElse(false);\n        version = Optional.ofNullable(System.getProperty(AppNames.propertyName(\"version\")))\n                .orElse(\"dev\");\n        build = Optional.ofNullable(System.getProperty(AppNames.propertyName(\"build\")))\n                .orElse(\"unknown\");\n        buildUuid = Optional.ofNullable(System.getProperty(AppNames.propertyName(\"buildId\")))\n                .map(UUID::fromString)\n                .orElse(UUID.randomUUID());\n        sentryUrl = System.getProperty(AppNames.propertyName(\"sentryUrl\"));\n        arch = System.getProperty(\"os.arch\").equals(\"amd64\") ? \"x86_64\" : \"arm64\";\n        staging = Optional.ofNullable(System.getProperty(AppNames.propertyName(\"staging\")))\n                .map(Boolean::parseBoolean)\n                .orElse(false);\n        devLoginPassword = System.getProperty(AppNames.propertyName(\"loginPassword\"));\n        useVirtualThreads = Optional.ofNullable(System.getProperty(AppNames.propertyName(\"useVirtualThreads\")))\n                .map(Boolean::parseBoolean)\n                .orElse(true);\n        debugThreads = Optional.ofNullable(System.getProperty(AppNames.propertyName(\"debugThreads\")))\n                .map(Boolean::parseBoolean)\n                .orElse(false);\n        debugPlatformThreadAccess = Optional.ofNullable(\n                        System.getProperty(AppNames.propertyName(\"debugPlatformThreadAccess\")))\n                .map(Boolean::parseBoolean)\n                .orElse(false);\n        defaultDataDir = AppSystemInfo.ofCurrent().getUserHome().resolve(isStaging() ? \".xpipe-ptb\" : \".xpipe\");\n        dataDir = Optional.ofNullable(System.getProperty(AppNames.propertyName(\"dataDir\")))\n                .map(s -> {\n                    var p = Path.of(s);\n                    if (!p.isAbsolute()) {\n                        p = referenceDir.resolve(p);\n                    }\n                    return p;\n                })\n                .orElse(defaultDataDir);\n        showcase = Optional.ofNullable(System.getProperty(AppNames.propertyName(\"showcase\")))\n                .map(Boolean::parseBoolean)\n                .orElse(false);\n        canonicalVersion = AppVersion.parse(version).orElse(null);\n        locatePtb = Optional.ofNullable(System.getProperty(AppNames.propertyName(\"locatorUsePtbInstallation\")))\n                .map(Boolean::parseBoolean)\n                .orElse(false);\n        locatorVersionCheck = Optional.ofNullable(\n                        System.getProperty(AppNames.propertyName(\"locatorDisableInstallationVersionCheck\")))\n                .map(s -> !Boolean.parseBoolean(s))\n                .orElse(true);\n        isTest = isJUnitTest();\n        autoAcceptEula = Optional.ofNullable(System.getProperty(AppNames.propertyName(\"acceptEula\")))\n                .map(Boolean::parseBoolean)\n                .orElse(false);\n        restarted = Optional.ofNullable(System.getProperty(AppNames.propertyName(\"restarted\")))\n                .map(Boolean::parseBoolean)\n                .orElse(false);\n        developerMode = Optional.ofNullable(System.getProperty(AppNames.propertyName(\"developerMode\")))\n                .map(Boolean::parseBoolean)\n                .orElse(false);\n        persistData = Optional.ofNullable(System.getProperty(AppNames.propertyName(\"persistData\")))\n                .map(Boolean::parseBoolean)\n                .orElse(true);\n        logToSysOut = Optional.ofNullable(System.getProperty(AppNames.propertyName(\"writeSysOut\")))\n                .map(Boolean::parseBoolean)\n                .orElse(false);\n        logToFile = Optional.ofNullable(System.getProperty(AppNames.propertyName(\"writeLogs\")))\n                .map(Boolean::parseBoolean)\n                .orElse(true);\n        logPlatformDebug = Optional.ofNullable(System.getProperty(AppNames.propertyName(\"debugPlatform\")))\n                .map(Boolean::parseBoolean)\n                .orElse(false);\n        logLevel = Optional.ofNullable(System.getProperty(AppNames.propertyName(\"logLevel\")))\n                .filter(s -> AppLogs.LOG_LEVELS.contains(s))\n                .orElse(\"info\");\n        loginTarget = Optional.ofNullable(System.getProperty(AppNames.propertyName(\"login\")))\n                .orElse(null);\n\n        // We require the user dir from here\n        AppDirectoryPermissionsCheck.checkDirectory(dataDir);\n        AppCache.setBasePath(dataDir.resolve(\"cache\"));\n        dataBinDir = dataDir.resolve(\"cache\", \"bin\");\n        UUID id = AppCache.getNonNull(\"uuid\", UUID.class, () -> null);\n        if (id == null) {\n            uuid = UUID.randomUUID();\n            AppCache.update(\"uuid\", uuid);\n        } else {\n            uuid = id;\n        }\n        initialLaunch = AppCache.getNonNull(\"lastBuildId\", String.class, () -> null) == null;\n        sessionId = UUID.randomUUID();\n        var cachedBuildId = AppCache.getNonNull(\"lastBuildId\", String.class, () -> null);\n        newBuildSession = !buildUuid.toString().equals(cachedBuildId);\n        AppCache.update(\"lastBuildId\", buildUuid);\n        aotTrainMode = Optional.ofNullable(System.getProperty(AppNames.propertyName(\"aotTrainMode\")))\n                .map(Boolean::parseBoolean)\n                .orElse(false);\n        explicitMode = XPipeDaemonMode.getIfPresent(System.getProperty(AppNames.propertyName(\"mode\")))\n                .orElse(null);\n    }\n\n    private static boolean isJUnitTest() {\n        for (StackTraceElement element : Thread.currentThread().getStackTrace()) {\n            if (element.getClassName().startsWith(\"org.junit.\")) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public static void init() {\n        init(new String[0]);\n    }\n\n    public static void init(String[] args) {\n        if (INSTANCE != null) {\n            return;\n        }\n\n        INSTANCE = new AppProperties(args);\n    }\n\n    public static AppProperties get() {\n        return INSTANCE;\n    }\n\n    public void resetInitialLaunch() {\n        AppCache.clear(\"lastBuildId\");\n    }\n\n    public void logArguments() {\n        TrackEvent.withInfo(\"Loaded properties\")\n                .tag(\"version\", version)\n                .tag(\"build\", build)\n                .tag(\"dataDir\", dataDir)\n                .tag(\"fullVersion\", fullVersion)\n                .handle();\n\n        TrackEvent.withInfo(\"Received arguments\")\n                .tag(\"raw\", arguments.getRawArgs())\n                .tag(\"resolved\", arguments.getResolvedArgs())\n                .tag(\"resolvedCommand\", arguments.getOpenArgs())\n                .handle();\n\n        for (var e : System.getProperties().entrySet()) {\n            if (e.getKey().toString().contains(AppNames.ofCurrent().getGroupName())) {\n                TrackEvent.debug(\"Detected app property \" + e.getKey() + \"=\" + e.getValue());\n            }\n        }\n    }\n\n    public boolean isDevelopmentEnvironment() {\n        return !isImage() && isDeveloperMode();\n    }\n\n    public Optional<AppVersion> getCanonicalVersion() {\n        return Optional.ofNullable(canonicalVersion);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppResources.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.core.FailableConsumer;\nimport io.xpipe.modulefs.ModuleFileSystem;\n\nimport java.io.IOException;\nimport java.net.JarURLConnection;\nimport java.net.URI;\nimport java.net.URL;\nimport java.nio.file.FileSystems;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\n\npublic class AppResources {\n\n    public static final String MAIN_MODULE = AppNames.packageName();\n\n    private static final Map<String, ModuleFileSystem> fileSystems = new HashMap<>();\n\n    public static void reset() {\n        synchronized (fileSystems) {\n            fileSystems.forEach((s, moduleFileSystem) -> {\n                try {\n                    moduleFileSystem.close();\n                } catch (IOException ex) {\n                    // Usually when updating, an exit signal is sent to this application.\n                    // However, it takes a while to shut down but the installer is deleting files meanwhile.\n                    // It can happen that the jar does not exist anymore\n                    ErrorEventFactory.fromThrowable(ex).expected().omit().handle();\n                }\n            });\n            fileSystems.clear();\n        }\n    }\n\n    private static ModuleFileSystem openFileSystemIfNeeded(String module) throws IOException {\n        var layer = AppExtensionManager.getInstance() != null\n                ? AppExtensionManager.getInstance().getExtendedLayer()\n                : null;\n\n        synchronized (fileSystems) {\n            // Only cache file systems with extended layer\n            if (layer != null && fileSystems.containsKey(module)) {\n                var v = fileSystems.get(module);\n                if (v == null) {\n                    throw new IOException(\"Unable to load module file system \" + module);\n                }\n\n                return v;\n            }\n\n            if (layer == null) {\n                layer = ModuleLayer.boot();\n            }\n\n            try {\n                var fs = (ModuleFileSystem)\n                        FileSystems.newFileSystem(URI.create(\"module:/\" + module), Map.of(\"layer\", layer));\n                if (AppExtensionManager.getInstance() != null) {\n                    fileSystems.put(module, fs);\n                }\n                return fs;\n            } catch (Throwable ex) {\n                fileSystems.put(module, null);\n                throw ex;\n            }\n        }\n    }\n\n    public static Optional<URL> getResourceURL(String module, String file) {\n        try {\n            var fs = openFileSystemIfNeeded(module);\n            var f = fs.getPath(module.replace('.', '/') + \"/resources/\" + file);\n            var url = f.getWrappedPath().toUri().toURL();\n            return Optional.of(url);\n        } catch (IOException e) {\n            ErrorEventFactory.fromThrowable(e).omitted(true).build().handle();\n            return Optional.empty();\n        }\n    }\n\n    public static void with(String module, String file, FailableConsumer<Path, IOException> con) {\n        if (AppProperties.get() != null && AppProperties.get().isDevelopmentEnvironment()) {\n            // Check if resource was found. If we use external processed resources, we can't use local dev resources\n            if (withLocalDevResource(module, file, con)) {\n                return;\n            }\n        }\n\n        withResource(module, file, con);\n    }\n\n    private static void withResource(String module, String file, FailableConsumer<Path, IOException> con) {\n        var path = module.startsWith(AppNames.ofCurrent().getGroupName())\n                ? module.replace('.', '/') + \"/resources/\" + file\n                : file;\n        try {\n            var fs = openFileSystemIfNeeded(module);\n            var f = fs.getPath(path);\n            con.accept(f);\n        } catch (IOException e) {\n            ErrorEventFactory.fromThrowable(e).omitted(true).build().handle();\n        }\n    }\n\n    private static boolean withLocalDevResource(String module, String file, FailableConsumer<Path, IOException> con) {\n        try {\n            var fs = openFileSystemIfNeeded(module);\n            var url = fs.getPath(\"\").getWrappedPath().toUri().toURL();\n            if (!url.getProtocol().equals(\"jar\")) {\n                return false;\n            }\n\n            JarURLConnection connection = (JarURLConnection) url.openConnection();\n            URL fileUrl = connection.getJarFileURL();\n            var jarFile = Path.of(fileUrl.toURI());\n            var resDir = jarFile.getParent()\n                    .getParent()\n                    .getParent()\n                    .resolve(\"src\")\n                    .resolve(\"main\")\n                    .resolve(\"resources\");\n            var f = resDir.resolve(module.replace('.', '/') + \"/resources/\" + file);\n            if (!Files.exists(f)) {\n                return false;\n            }\n\n            con.accept(f);\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).omitted(true).build().handle();\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppRestart.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ShellDialect;\nimport io.xpipe.app.process.ShellDialects;\nimport io.xpipe.app.update.AppDistributionType;\nimport io.xpipe.core.OsType;\n\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class AppRestart {\n\n    private static String createTerminalLaunchCommand(List<String> arguments, ShellDialect dialect) {\n        var loc = AppProperties.get().isDevelopmentEnvironment()\n                ? AppInstallation.ofDefault()\n                : AppInstallation.ofCurrent();\n        var suffix = (arguments.size() > 0 ? \" \" + String.join(\" \", arguments) : \"\");\n        if (AppDistributionType.get() == AppDistributionType.APP_IMAGE) {\n            var exec = System.getenv(\"APPIMAGE\");\n            return \"nohup \\\"\" + exec + \"\\\"\" + suffix + \" </dev/null >/dev/null 2>&1 & disown\";\n        } else if (OsType.ofLocal() == OsType.LINUX) {\n            var exec = loc.getCliExecutablePath();\n            return \"\\\"\" + exec + \"\\\" open\" + suffix;\n        } else if (OsType.ofLocal() == OsType.MACOS) {\n            var exec = loc.getCliExecutablePath();\n            return \"\\\"\" + exec + \"\\\" open\" + suffix;\n        } else {\n            var exe = loc.getDaemonDebugScriptPath();\n            if (ShellDialects.isPowershell(dialect)) {\n                var escapedList =\n                        arguments.stream().map(s -> s.replaceAll(\"\\\"\", \"`\\\"\")).toList();\n                var argumentList = String.join(\" \", escapedList);\n                return \"Start-Process -FilePath \\\"\" + exe + \"\\\" -ArgumentList \\\"\" + argumentList + \"\\\"\";\n            } else {\n                var base = \"\\\"\" + exe + \"\\\"\" + suffix;\n                return \"start \\\"\\\" \" + base;\n            }\n        }\n    }\n\n    private static String createBackgroundLaunchCommand(List<String> arguments, ShellDialect dialect) {\n        var loc = AppProperties.get().isDevelopmentEnvironment()\n                ? AppInstallation.ofDefault()\n                : AppInstallation.ofCurrent();\n        var suffix = (arguments.size() > 0 ? \" \" + String.join(\" \", arguments) : \"\");\n        if (AppDistributionType.get() == AppDistributionType.APP_IMAGE) {\n            var exec = System.getenv(\"APPIMAGE\");\n            return \"nohup \\\"\" + exec + \"\\\"\" + suffix + \" </dev/null >/dev/null 2>&1 & disown\";\n        } else if (OsType.ofLocal() == OsType.LINUX) {\n            return \"nohup \\\"\" + loc.getDaemonExecutablePath() + \"\\\"\" + suffix + \" </dev/null >/dev/null 2>&1 & disown\";\n        } else if (OsType.ofLocal() == OsType.MACOS) {\n            return \"(sleep 1;open \\\"\" + loc.getBaseInstallationPath() + \"\\\" --args\" + suffix\n                    + \" </dev/null &>/dev/null) & disown\";\n        } else {\n            var exe = loc.getDaemonExecutablePath();\n            if (ShellDialects.isPowershell(dialect)) {\n                var escapedList =\n                        arguments.stream().map(s -> s.replaceAll(\"\\\"\", \"`\\\"\")).toList();\n                var argumentList = String.join(\" \", escapedList);\n                return \"Start-Process -FilePath \\\"\" + exe + \"\\\" -ArgumentList \\\"\" + argumentList + \"\\\"\";\n            } else {\n                var base = \"\\\"\" + exe + \"\\\"\" + suffix;\n                return \"start \\\"\\\" \" + base;\n            }\n        }\n    }\n\n    public static String getBackgroundRestartCommand(Path dataDir, String user, ShellDialect dialect) {\n        var l = new ArrayList<String>();\n        l.addAll(List.of(\n                \"-Dio.xpipe.app.mode=gui\",\n                \"-Dio.xpipe.app.acceptEula=true\",\n                \"-Dio.xpipe.app.dataDir=\\\"\" + dataDir + \"\\\"\",\n                \"-Dio.xpipe.app.restarted=true\"));\n        if (user != null) {\n            l.add(\"-Dio.xpipe.app.login=\\\"\" + user + \"\\\"\");\n        }\n        var exec = createBackgroundLaunchCommand(l, dialect);\n        return exec;\n    }\n\n    public static String getBackgroundRestartCommand() {\n        return getBackgroundRestartCommand(AppProperties.get().getDataDir(), null, LocalShell.getDialect());\n    }\n\n    public static String getTerminalRestartCommand(ShellDialect dialect) {\n        var dataDir = AppProperties.get().getDataDir();\n        var exec = createTerminalLaunchCommand(\n                List.of(\n                        \"-Dio.xpipe.app.mode=gui\",\n                        \"-Dio.xpipe.app.acceptEula=true\",\n                        \"-Dio.xpipe.app.dataDir=\\\"\" + dataDir + \"\\\"\",\n                        \"-Dio.xpipe.app.restarted=true\"),\n                dialect);\n        return exec;\n    }\n\n    public static String getTerminalRestartCommand() {\n        return getTerminalRestartCommand(LocalShell.getDialect());\n    }\n\n    public static void restart() {\n        AppOperationMode.executeAfterShutdown(() -> {\n            try (var sc = LocalShell.getShell().start()) {\n                sc.command(getBackgroundRestartCommand()).execute();\n            }\n        });\n    }\n\n    public static void restart(Path dataDir) {\n        AppOperationMode.executeAfterShutdown(() -> {\n            try (var sc = LocalShell.getShell().start()) {\n                sc.command(getBackgroundRestartCommand(dataDir, null, sc.getShellDialect()))\n                        .execute();\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppSid.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.core.OsType;\n\nimport com.sun.jna.Function;\nimport lombok.Getter;\n\nimport java.util.concurrent.TimeUnit;\n\npublic class AppSid {\n\n    @Getter\n    private static boolean hasSetsid;\n\n    public static void init() {\n        if (OsType.ofLocal() == OsType.WINDOWS) {\n            return;\n        }\n\n        var checkProcess = new ProcessBuilder(\"which\", \"setsid\")\n                .redirectErrorStream(true)\n                .redirectOutput(ProcessBuilder.Redirect.DISCARD);\n        try {\n            var p = checkProcess.start();\n            if (p.waitFor(1000, TimeUnit.MILLISECONDS)) {\n                hasSetsid = p.exitValue() == 0;\n            }\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).omit().expected().handle();\n        }\n\n        if (hasSetsid) {\n            TrackEvent.info(\"Found setsid command\");\n            return;\n        }\n\n        // Don't set this in development mode or debug mode\n        if (AppProperties.get().isDevelopmentEnvironment()\n                || AppLogs.get().getLogLevel().equals(\"trace\")) {\n            return;\n        }\n\n        try {\n            // If there is no setsid command, we can't fully prevent commands from accessing any potential parent tty\n            // We can however set the pid to prevent this happening when launched from the cli command\n            // If we launched the daemon executable itself, this has no effect\n            var func = Function.getFunction(\"c\", \"setsid\");\n            func.invoke(new Object[0]);\n            TrackEvent.info(\"Successfully set process sid\");\n        } catch (Throwable t) {\n            ErrorEventFactory.fromThrowable(t).omit().handle();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppStyle.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.prefs.AppPrefs;\n\nimport javafx.application.Platform;\nimport javafx.scene.Scene;\n\nimport atlantafx.base.theme.Styles;\n\nimport java.io.IOException;\nimport java.nio.file.FileVisitResult;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.SimpleFileVisitor;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.util.*;\n\npublic class AppStyle {\n\n    private static final Map<Path, String> STYLESHEET_CONTENTS = new LinkedHashMap<>();\n    private static final Map<AppTheme.Theme, String> THEME_SPECIFIC_STYLESHEET_CONTENTS = new LinkedHashMap<>();\n    private static final Map<AppTheme.Theme, String> THEME_PREFERENCES_STYLESHEET_CONTENTS = new LinkedHashMap<>();\n    private static final WeakHashMap<Scene, Object> scenes = new WeakHashMap<>();\n    private static String FONT_CONTENTS = null;\n\n    public static void init() {\n        if (STYLESHEET_CONTENTS.size() > 0) {\n            return;\n        }\n\n        TrackEvent.info(\"Loading stylesheets ...\");\n        loadStylesheets();\n\n        if (AppPrefs.get() != null) {\n            AppPrefs.get().useSystemFont().addListener((c, o, n) -> {\n                changeFontUsage(n);\n            });\n            AppPrefs.get().theme().addListener((c, o, n) -> {\n                changeTheme(n);\n            });\n        }\n\n        var fxPrefs = Platform.getPreferences();\n        fxPrefs.accentColorProperty().addListener((c, o, n) -> {\n            changePlatformPreferences();\n        });\n    }\n\n    private static void add(Scene scene, String stylesheet) {\n        if (stylesheet != null && !stylesheet.isBlank()) {\n            scene.getStylesheets().add(stylesheet);\n        }\n    }\n\n    private static void loadStylesheets() {\n        AppResources.with(AppResources.MAIN_MODULE, \"font-config/font.css\", path -> {\n            var bytes = Files.readAllBytes(path);\n            FONT_CONTENTS = \"data:text/css;base64,\" + Base64.getEncoder().encodeToString(bytes);\n        });\n\n        for (var module : AppExtensionManager.getInstance().getContentModules()) {\n            // Use data URLs because module path URLs are not accepted\n            // by JavaFX as it does not use Path objects to load stylesheets\n            AppResources.with(module.getName(), \"style\", path -> {\n                if (!Files.exists(path)) {\n                    return;\n                }\n\n                TrackEvent.trace(\"Loading styles for module \" + module.getName());\n                Files.walkFileTree(path, new SimpleFileVisitor<>() {\n                    @Override\n                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {\n                        try {\n                            var bytes = Files.readAllBytes(file);\n                            if (file.getFileName().toString().endsWith(\".bss\")) {\n                                var s = \"data:application/octet-stream;base64,\"\n                                        + Base64.getEncoder().encodeToString(bytes);\n                                STYLESHEET_CONTENTS.put(file, s);\n                            } else if (file.getFileName().toString().endsWith(\".css\")) {\n                                var s = \"data:text/css;base64,\"\n                                        + Base64.getEncoder().encodeToString(bytes);\n                                STYLESHEET_CONTENTS.put(file, s);\n                            }\n                        } catch (IOException ex) {\n                            ErrorEventFactory.fromThrowable(ex)\n                                    .omitted(true)\n                                    .build()\n                                    .handle();\n                        }\n                        return FileVisitResult.CONTINUE;\n                    }\n                });\n            });\n        }\n\n        AppResources.with(AppResources.MAIN_MODULE, \"theme\", path -> {\n            if (!Files.exists(path)) {\n                return;\n            }\n\n            for (AppTheme.Theme theme : AppTheme.Theme.ALL) {\n                var file = path.resolve(theme.getId() + \".css\");\n                var bytes = Files.readAllBytes(file);\n                var s = \"data:text/css;base64,\" + Base64.getEncoder().encodeToString(bytes);\n                THEME_SPECIFIC_STYLESHEET_CONTENTS.put(theme, s);\n                THEME_PREFERENCES_STYLESHEET_CONTENTS.put(\n                        theme, Styles.toDataURI(theme.getPlatformPreferencesStylesheet()));\n            }\n        });\n    }\n\n    private static void changeFontUsage(boolean use) {\n        if (!use) {\n            scenes.keySet().forEach(scene -> {\n                add(scene, FONT_CONTENTS);\n            });\n        } else {\n            scenes.keySet().forEach(scene -> {\n                scene.getStylesheets().remove(FONT_CONTENTS);\n            });\n        }\n    }\n\n    private static void changePlatformPreferences() {\n        if (AppPrefs.get() == null) {\n            return;\n        }\n\n        var t = AppPrefs.get().theme().getValue();\n        if (t == null) {\n            return;\n        }\n\n        scenes.keySet().forEach(scene -> {\n            scene.getStylesheets().remove(THEME_PREFERENCES_STYLESHEET_CONTENTS.get(t));\n        });\n        THEME_PREFERENCES_STYLESHEET_CONTENTS.clear();\n        for (AppTheme.Theme theme : AppTheme.Theme.ALL) {\n            THEME_PREFERENCES_STYLESHEET_CONTENTS.put(\n                    theme, Styles.toDataURI(theme.getPlatformPreferencesStylesheet()));\n        }\n        scenes.keySet().forEach(scene -> {\n            add(scene, THEME_PREFERENCES_STYLESHEET_CONTENTS.get(t));\n        });\n    }\n\n    private static void changeTheme(AppTheme.Theme theme) {\n        scenes.keySet().forEach(scene -> {\n            scene.getStylesheets().removeAll(THEME_SPECIFIC_STYLESHEET_CONTENTS.values());\n            scene.getStylesheets().removeAll(THEME_PREFERENCES_STYLESHEET_CONTENTS.values());\n            add(scene, THEME_SPECIFIC_STYLESHEET_CONTENTS.get(theme));\n            add(scene, THEME_PREFERENCES_STYLESHEET_CONTENTS.get(theme));\n        });\n    }\n\n    public static void reloadStylesheets(Scene scene) {\n        STYLESHEET_CONTENTS.clear();\n        THEME_SPECIFIC_STYLESHEET_CONTENTS.clear();\n        THEME_PREFERENCES_STYLESHEET_CONTENTS.clear();\n        FONT_CONTENTS = \"\";\n\n        init();\n        scene.getStylesheets().clear();\n        addStylesheets(scene);\n    }\n\n    public static void addStylesheets(Scene scene) {\n        if (AppPrefs.get() != null && !AppPrefs.get().useSystemFont().getValue()) {\n            add(scene, FONT_CONTENTS);\n        }\n\n        STYLESHEET_CONTENTS.values().forEach(s -> {\n            add(scene, s);\n        });\n        if (AppPrefs.get() != null) {\n            var t = AppPrefs.get().theme().getValue();\n            if (t != null) {\n                add(scene, THEME_SPECIFIC_STYLESHEET_CONTENTS.get(t));\n                add(scene, THEME_PREFERENCES_STYLESHEET_CONTENTS.get(t));\n            }\n        }\n        TrackEvent.debug(\"Added stylesheets for scene\");\n\n        scenes.put(scene, null);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppSystemInfo.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.util.LocalExec;\nimport io.xpipe.core.OsType;\n\nimport com.sun.jna.platform.win32.*;\n\nimport java.nio.file.Files;\nimport java.nio.file.InvalidPathException;\nimport java.nio.file.Path;\n\npublic abstract class AppSystemInfo {\n\n    private static final Windows WINDOWS = new Windows();\n    private static final Linux LINUX = new Linux();\n    private static final MacOs MACOS = new MacOs();\n\n    public static AppSystemInfo ofCurrent() {\n        return switch (OsType.ofLocal()) {\n            case OsType.Linux ignored -> ofLinux();\n            case OsType.MacOs ignored -> ofMacOs();\n            case OsType.Windows ignored -> ofWindows();\n        };\n    }\n\n    public static Windows ofWindows() {\n        if (OsType.ofLocal() != OsType.WINDOWS) {\n            throw new IllegalStateException();\n        }\n\n        return WINDOWS;\n    }\n\n    public static Linux ofLinux() {\n        if (OsType.ofLocal() != OsType.LINUX) {\n            throw new IllegalStateException();\n        }\n\n        return LINUX;\n    }\n\n    public static MacOs ofMacOs() {\n        if (OsType.ofLocal() != OsType.MACOS) {\n            throw new IllegalStateException();\n        }\n\n        return MACOS;\n    }\n\n    private static Path parsePath(String path) {\n        if (path == null || path.isEmpty()) {\n            return null;\n        }\n\n        try {\n            return Path.of(path);\n        } catch (InvalidPathException ignored) {\n            return null;\n        }\n    }\n\n    public abstract Path getUserHome();\n\n    public abstract Path getDownloads();\n\n    public abstract Path getDesktop();\n\n    public abstract Path getTemp();\n\n    public abstract String getUser();\n\n    public static final class Windows extends AppSystemInfo {\n\n        private Path userHome;\n        private Path localAppData;\n        private Path roamingAppData;\n        private Path temp;\n        private Path downloads;\n        private Path desktop;\n        private Path documents;\n\n        public Path getSystemRoot() {\n            var root = AppSystemInfo.parsePath(System.getenv(\"SystemRoot\"));\n            if (root == null) {\n                return Path.of(\"C:\\\\Windows\");\n            }\n            return root;\n        }\n\n        public Path getTemp() {\n            if (temp != null) {\n                return temp;\n            }\n\n            var dir = AppSystemInfo.parsePath(System.getenv(\"TEMP\"));\n            if (dir == null) {\n                dir = AppSystemInfo.parsePath(System.getenv(\"TMP\"));\n            }\n\n            if (dir == null) {\n                return (temp = getLocalAppData().resolve(\"Temp\"));\n            }\n\n            // Don't use system temp dir\n            if (dir.startsWith(Path.of(\"C:\\\\Windows\"))) {\n                return (temp = getLocalAppData().resolve(\"Temp\"));\n            }\n\n            try {\n                // Replace 8.3 filename\n                dir = dir.toRealPath();\n            } catch (Exception ignored) {\n            }\n            return (temp = dir);\n        }\n\n        @Override\n        public String getUser() {\n            var username = System.getenv(\"USERNAME\");\n            if (username == null) {\n                username = System.getProperty(\"user.name\");\n            }\n            if (username == null) {\n                username = \"User\";\n            }\n            return username;\n        }\n\n        public Path getProgramFiles() {\n            var env = AppSystemInfo.parsePath(System.getenv(\"ProgramFiles\"));\n            if (env != null) {\n                return env;\n            }\n\n            var def = Path.of(\"C:\\\\ProgramFiles\");\n            return def;\n        }\n\n        public Path getLocalAppData() {\n            if (localAppData != null) {\n                return localAppData;\n            }\n\n            var dir = AppSystemInfo.parsePath(System.getenv(\"LOCALAPPDATA\"));\n            if (dir != null) {\n                try {\n                    // Replace 8.3 filename\n                    dir = dir.toRealPath();\n                } catch (Exception ignored) {\n                }\n                return (localAppData = dir);\n            }\n\n            var def = getUserHome().resolve(\"AppData\").resolve(\"Local\");\n            return def;\n        }\n\n        public Path getRoamingAppData() {\n            if (roamingAppData != null) {\n                return roamingAppData;\n            }\n\n            var dir = AppSystemInfo.parsePath(System.getenv(\"APPDATA\"));\n            if (dir != null) {\n                try {\n                    // Replace 8.3 filename\n                    dir = dir.toRealPath();\n                } catch (Exception ignored) {\n                }\n                return (roamingAppData = dir);\n            }\n\n            var def = getUserHome().resolve(\"AppData\").resolve(\"Roaming\");\n            return def;\n        }\n\n        public Path getUserHome() {\n            if (userHome != null) {\n                return userHome;\n            }\n\n            var dir = AppSystemInfo.parsePath(System.getenv(\"USERPROFILE\"));\n            if (dir == null) {\n                dir = AppSystemInfo.parsePath(System.getProperty(\"user.home\"));\n            }\n            if (dir == null) {\n                var username = System.getenv(\"USERNAME\");\n                if (username == null) {\n                    username = System.getProperty(\"user.name\");\n                }\n                if (username == null) {\n                    username = \"User\";\n                }\n                dir = Path.of(\"C:\\\\Users\\\\\" + username);\n            }\n\n            try {\n                // Replace 8.3 filename\n                userHome = dir.toRealPath();\n            } catch (Exception ignored) {\n                userHome = dir;\n            }\n            return dir;\n        }\n\n        @Override\n        public Path getDownloads() {\n            if (downloads != null) {\n                return downloads;\n            }\n\n            try {\n                var r = Shell32Util.getKnownFolderPath(KnownFolders.FOLDERID_Downloads);\n                // Replace 8.3 filename\n                return (downloads = Path.of(r).toRealPath());\n            } catch (Throwable e) {\n                ErrorEventFactory.fromThrowable(e).handle();\n                var fallback = getUserHome().resolve(\"Downloads\");\n                return (downloads = fallback);\n            }\n        }\n\n        public Path getDocuments() {\n            if (documents != null) {\n                return documents;\n            }\n\n            try {\n                var r = Shell32Util.getKnownFolderPath(KnownFolders.FOLDERID_Documents);\n                // Replace 8.3 filename\n                return (documents = Path.of(r).toRealPath());\n            } catch (Throwable e) {\n                ErrorEventFactory.fromThrowable(e).handle();\n                var fallback = getUserHome().resolve(\"Documents\");\n                return (documents = fallback);\n            }\n        }\n\n        @Override\n        public Path getDesktop() {\n            if (desktop != null) {\n                return desktop;\n            }\n\n            try {\n                var r = Shell32Util.getKnownFolderPath(KnownFolders.FOLDERID_Desktop);\n                // Replace 8.3 filename\n                return (desktop = Path.of(r).toRealPath());\n            } catch (Throwable e) {\n                ErrorEventFactory.fromThrowable(e).handle();\n                var fallback = getUserHome().resolve(\"Desktop\");\n                return (desktop = fallback);\n            }\n        }\n    }\n\n    public static class Linux extends AppSystemInfo {\n\n        private Path downloads;\n        private Path desktop;\n        private Boolean vm;\n\n        public boolean isDebianBased() {\n            return Files.exists(Path.of(\"/etc/debian_version\"));\n        }\n\n        public boolean isVirtualMachine() {\n            if (vm != null) {\n                return vm;\n            }\n\n            var out = LocalExec.readStdoutIfPossible(\"cat\", \"/proc/cpuinfo\");\n            vm = out.map(s -> s.contains(\"hypervisor\")).orElse(false);\n            return vm;\n        }\n\n        @Override\n        public String getUser() {\n            var env = System.getenv(\"USER\");\n            if (env != null) {\n                return env;\n            }\n\n            // This can actually fail and return ?\n            // See https://stackoverflow.com/questions/70010648/system-getpropertyuser-name-returns\n            // The env variable is actually better\n            var username = System.getProperty(\"user.name\");\n            if (username == null || username.equals(\"?\")) {\n                username = \"user\";\n            }\n            return username;\n        }\n\n        @Override\n        public Path getUserHome() {\n            var env = System.getenv(\"HOME\");\n            if (env != null) {\n                try {\n                    return Path.of(env);\n                } catch (InvalidPathException ignored) {\n                }\n            }\n\n            // This can actually fail and return ?\n            // See https://stackoverflow.com/questions/70010648/system-getpropertyuser-name-returns\n            // The env variable is actually better\n            var dir = System.getProperty(\"user.home\");\n            try {\n                Path.of(dir);\n            } catch (InvalidPathException ignored) {\n                dir = null;\n            }\n            if (dir == null || dir.equals(\"?\")) {\n                dir = \"/\";\n            }\n            return Path.of(dir);\n        }\n\n        @Override\n        public Path getDownloads() {\n            if (downloads != null) {\n                return downloads;\n            }\n\n            try (var sc = LocalShell.getShell().start()) {\n                var out = sc.command(\"xdg-user-dir DOWNLOAD\").readStdoutIfPossible();\n                if (out.isPresent() && !out.get().isBlank()) {\n                    return (downloads = Path.of(out.get()));\n                }\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).handle();\n            }\n\n            var fallback = getUserHome().resolve(\"Desktop\");\n            return (downloads = fallback);\n        }\n\n        @Override\n        public Path getDesktop() {\n            if (desktop != null) {\n                return desktop;\n            }\n\n            try (var sc = LocalShell.getShell().start()) {\n                var out = sc.command(\"xdg-user-dir DESKTOP\").readStdoutIfPossible();\n                if (out.isPresent() && !out.get().isBlank()) {\n                    return (desktop = Path.of(out.get()));\n                }\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).handle();\n            }\n\n            var fallback = getUserHome().resolve(\"Desktop\");\n            return (desktop = fallback);\n        }\n\n        @Override\n        public Path getTemp() {\n            return Path.of(System.getProperty(\"java.io.tmpdir\"));\n        }\n    }\n\n    public static class MacOs extends AppSystemInfo {\n\n        @Override\n        public String getUser() {\n            var username = System.getProperty(\"user.name\");\n            if (username == null) {\n                username = \"user\";\n            }\n            return username;\n        }\n\n        @Override\n        public Path getUserHome() {\n            return Path.of(System.getProperty(\"user.home\"));\n        }\n\n        @Override\n        public Path getDownloads() {\n            return getUserHome().resolve(\"Downloads\");\n        }\n\n        @Override\n        public Path getDesktop() {\n            return getUserHome().resolve(\"Desktop\");\n        }\n\n        @Override\n        public Path getTemp() {\n            return Path.of(System.getProperty(\"java.io.tmpdir\"));\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppTheme.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.core.window.AppMainWindow;\nimport io.xpipe.app.ext.PrefsChoiceValue;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.platform.ColorHelper;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.core.OsType;\n\nimport javafx.animation.Interpolator;\nimport javafx.animation.KeyFrame;\nimport javafx.animation.KeyValue;\nimport javafx.animation.Timeline;\nimport javafx.application.Application;\nimport javafx.application.ColorScheme;\nimport javafx.application.Platform;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.MapChangeListener;\nimport javafx.css.PseudoClass;\nimport javafx.scene.Node;\nimport javafx.scene.image.Image;\nimport javafx.scene.image.ImageView;\nimport javafx.scene.layout.Pane;\nimport javafx.scene.paint.Color;\nimport javafx.stage.Stage;\nimport javafx.util.Duration;\n\nimport atlantafx.base.theme.*;\nimport lombok.AllArgsConstructor;\nimport lombok.Getter;\nimport lombok.SneakyThrows;\n\nimport java.lang.ref.WeakReference;\nimport java.nio.file.Files;\nimport java.util.List;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\n\npublic class AppTheme {\n\n    private static final PseudoClass LIGHT = PseudoClass.getPseudoClass(\"light\");\n    private static final PseudoClass DARK = PseudoClass.getPseudoClass(\"dark\");\n    private static final PseudoClass PRETTY = PseudoClass.getPseudoClass(\"pretty\");\n    private static final PseudoClass PERFORMANCE = PseudoClass.getPseudoClass(\"performance\");\n    private static boolean init;\n\n    public static void initThemeHandlers(Stage stage) {\n        if (stage.getScene() == null) {\n            return;\n        }\n\n        if (AppPrefs.get() == null) {\n            var root = stage.getScene().getRoot();\n            applyClasses(root, Theme.getDefaultLightTheme(), false);\n            return;\n        }\n\n        stage.getScene().rootProperty().subscribe(parent -> {\n            applyClasses(\n                    parent,\n                    AppPrefs.get().theme().getValue(),\n                    AppPrefs.get().performanceMode().getValue());\n        });\n\n        // Allow for GC\n        var ref = new WeakReference<>(stage);\n\n        AppPrefs.get().theme().subscribe(t -> {\n            var val = ref.get();\n            if (val != null) {\n                var scene = val.getScene();\n                if (scene != null) {\n                    var r = scene.getRoot();\n                    applyClasses(r, t, AppPrefs.get().performanceMode().get());\n                }\n            }\n        });\n\n        AppPrefs.get().performanceMode().subscribe(pm -> {\n            var val = ref.get();\n            if (val != null) {\n                var scene = val.getScene();\n                if (scene != null) {\n                    var r = scene.getRoot();\n                    applyClasses(r, AppPrefs.get().theme().getValue(), pm);\n                }\n            }\n        });\n    }\n\n    private static void applyClasses(Node r, Theme t, boolean perf) {\n        if (r == null) {\n            return;\n        }\n\n        r.pseudoClassStateChanged(PseudoClass.getPseudoClass(OsType.ofLocal().getId()), true);\n\n        Theme.ALL.forEach(theme -> {\n            r.pseudoClassStateChanged(\n                    PseudoClass.getPseudoClass(theme.getCssId()),\n                    theme.getCssId().equals(t.getCssId()));\n        });\n\n        if (t != null) {\n            r.pseudoClassStateChanged(LIGHT, !t.isDark());\n            r.pseudoClassStateChanged(DARK, t.isDark());\n        }\n\n        r.pseudoClassStateChanged(PRETTY, !perf);\n        r.pseudoClassStateChanged(PERFORMANCE, perf);\n    }\n\n    public static void init() {\n        if (init) {\n            TrackEvent.trace(\"Theme init requested again\");\n            return;\n        }\n\n        if (AppPrefs.get() == null) {\n            TrackEvent.trace(\"Theme init prior to prefs init, setting theme to default\");\n            Theme.getDefaultLightTheme().apply();\n            return;\n        }\n\n        try {\n            var lastSystemDark = AppCache.getBoolean(\"lastDarkTheme\", false);\n            var nowDark = isDarkMode();\n            AppCache.update(\"lastDarkTheme\", nowDark);\n            if (AppPrefs.get().theme().getValue() == null || lastSystemDark != nowDark) {\n                TrackEvent.trace(\"Updating theme to system theme\");\n                setDefault();\n            }\n\n            Platform.getPreferences().addListener((MapChangeListener<? super String, ? super Object>) change -> {\n                TrackEvent.withTrace(\"Platform preference changed\")\n                        .tag(\"change\", change.toString())\n                        .handle();\n            });\n\n            Platform.getPreferences().addListener((MapChangeListener<? super String, ? super Object>) change -> {\n                if (change.getKey().equals(\"GTK.theme_name\")) {\n                    Platform.runLater(() -> {\n                        updateThemeToThemeName(change.getValueRemoved(), change.getValueAdded());\n                    });\n                }\n            });\n\n            Platform.getPreferences().colorSchemeProperty().addListener((observableValue, colorScheme, t1) -> {\n                Platform.runLater(() -> {\n                    updateThemeToColorScheme(t1);\n                });\n            });\n        } catch (IllegalStateException ex) {\n            // The platform preferences are sometimes not initialized yet\n            ErrorEventFactory.fromThrowable(ex).expected().omit().handle();\n        } catch (Throwable t) {\n            ErrorEventFactory.fromThrowable(t).omit().handle();\n        }\n\n        var t = AppPrefs.get().theme().getValue();\n        t.apply();\n        TrackEvent.debug(\"Set theme \" + t.getId() + \" for scene\");\n\n        AppPrefs.get().theme().addListener((c, o, n) -> {\n            changeTheme(n);\n        });\n\n        init = true;\n    }\n\n    private static void updateThemeToThemeName(Object oldName, Object newName) {\n        if (OsType.ofLocal() == OsType.LINUX && newName != null) {\n            var toDark = (oldName == null || !oldName.toString().contains(\"-dark\"))\n                    && newName.toString().contains(\"-dark\");\n            var toLight = (oldName == null || oldName.toString().contains(\"-dark\"))\n                    && !newName.toString().contains(\"-dark\");\n            if (toDark) {\n                updateThemeToColorScheme(ColorScheme.DARK);\n            } else if (toLight) {\n                updateThemeToColorScheme(ColorScheme.LIGHT);\n            }\n        }\n    }\n\n    private static boolean isDarkMode() {\n        var nowDark = Platform.getPreferences().getColorScheme() == ColorScheme.DARK;\n        if (nowDark) {\n            return true;\n        }\n\n        var gtkTheme = Platform.getPreferences().get(\"GTK.theme_name\");\n        return gtkTheme != null && gtkTheme.toString().contains(\"-dark\");\n    }\n\n    private static void updateThemeToColorScheme(ColorScheme colorScheme) {\n        if (colorScheme == null) {\n            return;\n        }\n\n        if (colorScheme == ColorScheme.DARK\n                && !AppPrefs.get().theme().getValue().isDark()) {\n            AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());\n        }\n\n        if (colorScheme != ColorScheme.DARK && AppPrefs.get().theme().getValue().isDark()) {\n            AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());\n        }\n    }\n\n    public static void reset() {\n        if (!init) {\n            return;\n        }\n\n        PlatformThread.runLaterIfNeededBlocking(() -> {\n            var nowDark = isDarkMode();\n            AppCache.update(\"lastDarkTheme\", nowDark);\n        });\n    }\n\n    private static void setDefault() {\n        try {\n            var colorScheme = Platform.getPreferences().getColorScheme();\n            if (colorScheme == ColorScheme.DARK) {\n                AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());\n            } else {\n                AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());\n            }\n        } catch (IllegalStateException ex) {\n            // The platform preferences are sometimes not initialized yet\n            ErrorEventFactory.fromThrowable(ex).expected().omit().handle();\n        } catch (Exception ex) {\n            // The color scheme query can fail if the toolkit is not initialized properly\n            AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());\n        }\n    }\n\n    private static void changeTheme(Theme newTheme) {\n        if (newTheme == null) {\n            return;\n        }\n\n        PlatformThread.runLaterIfNeeded(() -> {\n            var window = AppMainWindow.get();\n            if (window == null) {\n                return;\n            }\n\n            TrackEvent.debug(\"Setting theme \" + newTheme.getId() + \" for scene\");\n\n            // Don't animate transition in performance mode\n            if (AppPrefs.get() == null || AppPrefs.get().performanceMode().get()) {\n                newTheme.apply();\n                return;\n            }\n\n            var stage = window.getStage();\n            var scene = stage.getScene();\n            Pane root = (Pane) scene.getRoot();\n            Image snapshot = null;\n            try {\n                scene.snapshot(null);\n            } catch (Exception ex) {\n                // This can fail if there is no window / screen I guess?\n                ErrorEventFactory.fromThrowable(ex).expected().omit().handle();\n                return;\n            }\n            ImageView imageView = new ImageView(snapshot);\n            root.getChildren().add(imageView);\n            newTheme.apply();\n\n            Platform.runLater(() -> {\n                // Animate!\n                var transition = new Timeline(\n                        new KeyFrame(\n                                Duration.millis(0),\n                                new KeyValue(imageView.opacityProperty(), 1, Interpolator.EASE_OUT)),\n                        new KeyFrame(\n                                Duration.millis(600),\n                                new KeyValue(imageView.opacityProperty(), 0, Interpolator.EASE_OUT)));\n                transition.setOnFinished(e -> {\n                    root.getChildren().remove(imageView);\n                });\n                transition.play();\n            });\n        });\n    }\n\n    public static class DerivedTheme extends Theme {\n\n        private final String name;\n        private final int skipLines;\n\n        public DerivedTheme(\n                String id,\n                String cssId,\n                String name,\n                atlantafx.base.theme.Theme theme,\n                Supplier<AppFontSizes> sizes,\n                Color baseColor,\n                Color borderColor,\n                Supplier<Color> contextMenuColor,\n                Supplier<Color> emphasisColor,\n                int displayBorderRadius,\n                int skipLines) {\n            super(\n                    id,\n                    cssId,\n                    theme,\n                    sizes,\n                    baseColor,\n                    borderColor,\n                    contextMenuColor,\n                    emphasisColor,\n                    displayBorderRadius);\n            this.name = name;\n            this.skipLines = skipLines;\n        }\n\n        @Override\n        @SneakyThrows\n        public void apply() {\n            var builder = new StringBuilder();\n            AppResources.with(AppResources.MAIN_MODULE, \"theme/\" + id + \".css\", path -> {\n                var content = Files.readString(path);\n                builder.append(content);\n            });\n\n            // Watch out for the leading slash\n            AppResources.with(\"atlantafx.base\", theme.getUserAgentStylesheet().substring(1), path -> {\n                var baseStyleContent = Files.readString(path);\n                builder.append(\"\\n\")\n                        .append(baseStyleContent.lines().skip(skipLines).collect(Collectors.joining(\"\\n\")));\n            });\n\n            Application.setUserAgentStylesheet(Styles.toDataURI(builder.toString()));\n        }\n\n        @Override\n        public ObservableValue<String> toTranslatedString() {\n            return new SimpleStringProperty(name);\n        }\n    }\n\n    @AllArgsConstructor\n    public static class Theme implements PrefsChoiceValue {\n\n        public static final Theme PRIMER_LIGHT = new Theme(\n                \"light\",\n                \"primer\",\n                new PrimerLight(),\n                () -> AppFontSizes.forOs(AppFontSizes.BASE_10_5, AppFontSizes.BASE_10_5, AppFontSizes.BASE_11),\n                Color.WHITE,\n                Color.web(\"#24292f\"),\n                () -> ColorHelper.withOpacity(\n                        Platform.getPreferences()\n                                .getAccentColor()\n                                .darker()\n                                .desaturate()\n                                .brighter(),\n                        0.3),\n                () -> Platform.getPreferences().getAccentColor(),\n                4);\n        public static final Theme PRIMER_DARK = new Theme(\n                \"dark\",\n                \"primer\",\n                new PrimerDark(),\n                () -> AppFontSizes.forOs(AppFontSizes.BASE_11, AppFontSizes.BASE_10_5, AppFontSizes.BASE_11),\n                Color.web(\"#0d1117\"),\n                Color.web(\"#c9d1d9\"),\n                () -> ColorHelper.withOpacity(\n                        Platform.getPreferences()\n                                .getAccentColor()\n                                .desaturate()\n                                .desaturate()\n                                .darker(),\n                        0.2),\n                () -> Platform.getPreferences().getAccentColor(),\n                4);\n        public static final Theme NORD_LIGHT = new Theme(\n                \"nordLight\",\n                \"nord\",\n                new NordLight(),\n                () -> AppFontSizes.forOs(AppFontSizes.BASE_10_5, AppFontSizes.BASE_10_5, AppFontSizes.BASE_11),\n                Color.web(\"#dadadc\"),\n                Color.web(\"#2E3440\"),\n                () -> ColorHelper.withOpacity(\n                        Platform.getPreferences()\n                                .getAccentColor()\n                                .darker()\n                                .desaturate()\n                                .brighter(),\n                        0.3),\n                () -> Platform.getPreferences().getAccentColor(),\n                0);\n        public static final Theme NORD_DARK = new Theme(\n                \"nordDark\",\n                \"nord\",\n                new NordDark(),\n                () -> AppFontSizes.forOs(AppFontSizes.BASE_11, AppFontSizes.BASE_10_5, AppFontSizes.BASE_11),\n                Color.web(\"#2d3137\"),\n                Color.web(\"#24292f\"),\n                () -> ColorHelper.withOpacity(\n                        Platform.getPreferences()\n                                .getAccentColor()\n                                .desaturate()\n                                .desaturate()\n                                .darker(),\n                        0.2),\n                () -> Platform.getPreferences().getAccentColor(),\n                0);\n        public static final Theme CUPERTINO_LIGHT = new Theme(\n                \"cupertinoLight\",\n                \"cupertino\",\n                new CupertinoLight(),\n                () -> AppFontSizes.forOs(AppFontSizes.BASE_10_5, AppFontSizes.BASE_10_5, AppFontSizes.BASE_11),\n                Color.WHITE,\n                Color.BLACK,\n                () -> ColorHelper.withOpacity(\n                        Platform.getPreferences()\n                                .getAccentColor()\n                                .darker()\n                                .desaturate()\n                                .brighter(),\n                        0.3),\n                () -> Platform.getPreferences().getAccentColor(),\n                4);\n        public static final Theme CUPERTINO_DARK = new Theme(\n                \"cupertinoDark\",\n                \"cupertino\",\n                new CupertinoDark(),\n                () -> AppFontSizes.forOs(AppFontSizes.BASE_11, AppFontSizes.BASE_10_5, AppFontSizes.BASE_11),\n                Color.BLACK,\n                Color.WHITE,\n                () -> ColorHelper.withOpacity(\n                        Platform.getPreferences()\n                                .getAccentColor()\n                                .desaturate()\n                                .desaturate()\n                                .darker(),\n                        0.2),\n                () -> Platform.getPreferences().getAccentColor(),\n                4);\n        public static final Theme DRACULA = new Theme(\n                \"dracula\",\n                \"dracula\",\n                new Dracula(),\n                () -> AppFontSizes.forOs(AppFontSizes.BASE_11, AppFontSizes.BASE_10_5, AppFontSizes.BASE_11),\n                Color.web(\"#383f49\"),\n                Color.web(\"#9580ff\"),\n                () -> ColorHelper.withOpacity(\n                        Platform.getPreferences()\n                                .getAccentColor()\n                                .desaturate()\n                                .desaturate()\n                                .darker(),\n                        0.2),\n                () -> Platform.getPreferences().getAccentColor(),\n                6);\n        public static final Theme MOCHA = new DerivedTheme(\n                \"mocha\",\n                \"mocha\",\n                \"Mocha\",\n                new PrimerDark(),\n                () -> AppFontSizes.forOs(AppFontSizes.BASE_11, AppFontSizes.BASE_10_5, AppFontSizes.BASE_11),\n                Color.web(\"#2E2E4EFF\"),\n                Color.web(\"#CDD6F4FF\"),\n                () -> ColorHelper.withOpacity(\n                        Platform.getPreferences()\n                                .getAccentColor()\n                                .desaturate()\n                                .desaturate()\n                                .darker(),\n                        0.2),\n                () -> Platform.getPreferences().getAccentColor(),\n                4,\n                91);\n\n        // Adjust this to create your own theme\n        @SuppressWarnings(\"unused\")\n        public static final Theme CUSTOM = new DerivedTheme(\n                \"custom\",\n                \"primer\",\n                \"Custom\",\n                new PrimerDark(),\n                () -> AppFontSizes.forOs(AppFontSizes.BASE_10_5, AppFontSizes.BASE_10_5, AppFontSizes.BASE_11),\n                Color.web(\"#0d1117\"),\n                Color.web(\"#24292f\"),\n                () -> ColorHelper.withOpacity(\n                        Platform.getPreferences().getAccentColor().desaturate().desaturate(), 0.2),\n                () -> Platform.getPreferences().getAccentColor(),\n                4,\n                91);\n\n        // Also include your custom theme here\n        public static final List<Theme> ALL = List.of(\n                PRIMER_LIGHT, PRIMER_DARK, NORD_LIGHT, NORD_DARK, CUPERTINO_LIGHT, CUPERTINO_DARK, DRACULA, MOCHA);\n        protected final String id;\n\n        @Getter\n        protected final String cssId;\n\n        protected final atlantafx.base.theme.Theme theme;\n\n        @Getter\n        protected final Supplier<AppFontSizes> fontSizes;\n\n        @Getter\n        protected final Color baseColor;\n\n        @Getter\n        protected final Color borderColor;\n\n        @Getter\n        protected final Supplier<Color> contextMenuColor;\n\n        @Getter\n        protected final Supplier<Color> emphasisColor;\n\n        @Getter\n        protected final int displayBorderRadius;\n\n        static Theme getDefaultLightTheme() {\n            return switch (OsType.ofLocal()) {\n                case OsType.Windows ignored -> PRIMER_LIGHT;\n                case OsType.Linux ignored -> PRIMER_LIGHT;\n                case OsType.MacOs ignored -> CUPERTINO_LIGHT;\n            };\n        }\n\n        static Theme getDefaultDarkTheme() {\n            return switch (OsType.ofLocal()) {\n                case OsType.Windows ignored -> PRIMER_DARK;\n                case OsType.Linux ignored -> PRIMER_DARK;\n                case OsType.MacOs ignored -> CUPERTINO_DARK;\n            };\n        }\n\n        public boolean isDark() {\n            return theme.isDarkMode();\n        }\n\n        public void apply() {\n            Application.setUserAgentStylesheet(theme.getUserAgentStylesheetBSS());\n        }\n\n        protected String getPlatformPreferencesStylesheet() {\n            var s = \"\"\"\n                    * {\n                        -color-context-menu: %s;\n                        -color-accent-fg: %s;\n                        -color-accent-emphasis: %s;\n                        -color-accent-muted: %s;\n                        -color-accent-subtle: %s;\n                    }\n                    \"\"\".formatted(\n                            ColorHelper.toWeb(contextMenuColor.get()),\n                            ColorHelper.toWeb(emphasisColor.get()),\n                            ColorHelper.toWeb(emphasisColor.get().darker()),\n                            ColorHelper.toWeb(emphasisColor.get().desaturate()),\n                            ColorHelper.toWeb(ColorHelper.withOpacity(\n                                    emphasisColor.get().darker().desaturate().desaturate(), 0.2)));\n            return s;\n        }\n\n        @Override\n        public ObservableValue<String> toTranslatedString() {\n            return new SimpleStringProperty(theme.getName());\n        }\n\n        @Override\n        public String getId() {\n            return id;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppTray.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.issue.ErrorEvent;\nimport io.xpipe.app.issue.ErrorHandler;\n\nimport javafx.application.Platform;\n\nimport lombok.Getter;\nimport lombok.SneakyThrows;\n\nimport java.awt.*;\nimport java.time.Duration;\nimport java.time.Instant;\n\npublic class AppTray {\n\n    private static AppTray INSTANCE;\n    private final AppTrayIcon icon;\n\n    @Getter\n    private final ErrorHandler errorHandler;\n\n    @SneakyThrows\n    private AppTray() {\n        this.icon = new AppTrayIcon();\n        this.errorHandler = new TrayErrorHandler();\n    }\n\n    public static void init() {\n        INSTANCE = new AppTray();\n    }\n\n    public static AppTray get() {\n        return INSTANCE;\n    }\n\n    @SneakyThrows\n    public void show() {\n        // Even though we check at startup, it seems like the support can change at runtime\n        if (!SystemTray.isSupported()) {\n            return;\n        }\n\n        icon.show();\n    }\n\n    public void hide() {\n        icon.hide();\n    }\n\n    private class TrayErrorHandler implements ErrorHandler {\n\n        private Instant lastErrorShown = Instant.MIN;\n\n        @Override\n        public void handle(ErrorEvent event) {\n            if (event.isOmitted()) {\n                return;\n            }\n\n            var title = AppI18n.get(event.isTerminal() ? \"terminalErrorOccured\" : \"errorOccured\");\n            var desc = event.getDescription();\n            if (desc == null && event.getThrowable() != null) {\n                var tName = event.getThrowable().getClass().getSimpleName();\n                desc = AppI18n.get(\"errorTypeOccured\", tName);\n            }\n            if (desc == null) {\n                desc = AppI18n.get(\"errorNoDetail\");\n            }\n\n            String finalDesc = desc;\n            Platform.runLater(() -> {\n                if (Duration.between(lastErrorShown, Instant.now()).getSeconds() < 10) {\n                    return;\n                }\n\n                lastErrorShown = Instant.now();\n                AppTray.this.icon.showErrorMessage(title, finalDesc);\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppTrayIcon.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.core.OsType;\n\nimport java.awt.*;\nimport java.io.IOException;\nimport java.net.URL;\nimport javax.imageio.ImageIO;\n\npublic class AppTrayIcon {\n\n    private final SystemTray tray;\n    private final TrayIcon trayIcon;\n\n    public AppTrayIcon() {\n        ensureSystemTraySupported();\n\n        tray = SystemTray.getSystemTray();\n\n        var image =\n                switch (OsType.ofLocal()) {\n                    case OsType.Windows ignored -> \"logo/full/logo_16x16.png\";\n                    case OsType.Linux ignored -> \"logo/full/logo_24x24.png\";\n                    case OsType.MacOs ignored -> \"logo/padded/logo_24x24.png\";\n                };\n        var url = AppResources.getResourceURL(AppResources.MAIN_MODULE, image).orElseThrow();\n\n        PopupMenu popupMenu = new PopupMenu();\n        this.trayIcon =\n                new TrayIcon(loadImageFromURL(url), App.getApp().getStage().getTitle(), popupMenu);\n        this.trayIcon.setToolTip(AppNames.ofCurrent().getName());\n        this.trayIcon.setImageAutoSize(true);\n\n        {\n            var open = new MenuItem(AppI18n.get(\"open\"));\n            open.addActionListener(e -> {\n                tray.remove(trayIcon);\n                AppOperationMode.switchToAsync(AppOperationMode.GUI);\n            });\n            popupMenu.add(open);\n        }\n\n        {\n            var quit = new MenuItem(AppI18n.get(\"quit\"));\n            quit.addActionListener(e -> {\n                tray.remove(trayIcon);\n                AppOperationMode.close();\n            });\n            popupMenu.add(quit);\n        }\n\n        trayIcon.addActionListener(e -> {\n            if (OsType.ofLocal() != OsType.MACOS) {\n                tray.remove(trayIcon);\n                AppOperationMode.switchToAsync(AppOperationMode.GUI);\n            }\n        });\n    }\n\n    private static Image loadImageFromURL(URL iconImagePath) {\n        try {\n            return ImageIO.read(iconImagePath);\n        } catch (IOException e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n            return AppImages.toAwtImage(AppImages.DEFAULT_IMAGE);\n        }\n    }\n\n    private void ensureSystemTraySupported() {\n        if (!SystemTray.isSupported()) {\n            throw new UnsupportedOperationException(\n                    \"SystemTray icons are not \" + \"supported by the current desktop environment.\");\n        }\n    }\n\n    public void show() {\n        EventQueue.invokeLater(() -> {\n            try {\n                tray.add(this.trayIcon);\n            } catch (Exception e) {\n                // This can sometimes fail on Linux\n                ErrorEventFactory.fromThrowable(\"Unable to add TrayIcon\", e)\n                        .expected()\n                        .handle();\n            }\n        });\n    }\n\n    public void hide() {\n        EventQueue.invokeLater(() -> {\n            tray.remove(trayIcon);\n        });\n    }\n\n    public void showErrorMessage(String title, String message) {\n        if (OsType.ofLocal() == OsType.MACOS) {\n            showMacAlert(title, message, \"Error\");\n        } else {\n            EventQueue.invokeLater(() -> this.trayIcon.displayMessage(title, message, TrayIcon.MessageType.ERROR));\n        }\n    }\n\n    private void showMacAlert(String subTitle, String message, String title) {\n        String execute = String.format(\n                \"display notification \\\"%s\\\"\" + \" with title \\\"%s\\\"\" + \" subtitle \\\"%s\\\"\",\n                message != null ? message : \"\", title != null ? title : \"\", subTitle != null ? subTitle : \"\");\n        try {\n            Runtime.getRuntime().exec(new String[] {\"osascript\", \"-e\", execute});\n        } catch (IOException e) {\n            throw new UnsupportedOperationException(\"Cannot run osascript with given parameters.\");\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppVersion.java",
    "content": "package io.xpipe.app.core;\n\nimport lombok.NonNull;\nimport lombok.Value;\n\nimport java.util.Optional;\n\n@Value\npublic class AppVersion implements Comparable<AppVersion> {\n\n    int major;\n    int minor;\n    int patch;\n\n    public static Optional<AppVersion> parse(String version) {\n        try {\n            var releaseSplit = version.split(\"-\");\n            var split = releaseSplit[0].split(\"\\\\.\");\n            var major = Integer.parseInt(split[0]);\n            var minor = split.length > 1 ? Integer.parseInt(split[1]) : 0;\n            var patch = split.length > 2 ? Integer.parseInt(split[2]) : 0;\n            return Optional.of(new AppVersion(major, minor, patch));\n        } catch (Exception ex) {\n            // This can happen on number format exceptions\n            // It shouldn't happen if the version is correctly formatted though\n            return Optional.empty();\n        }\n    }\n\n    public String toString() {\n        return major + \".\" + minor + \".\" + patch;\n    }\n\n    public boolean greaterThan(AppVersion other) {\n        return compareTo(other) > 0;\n    }\n\n    @Override\n    public int compareTo(@NonNull AppVersion o) {\n        var majorCompare = major - o.major;\n        if (majorCompare != 0) {\n            return majorCompare;\n        }\n\n        var minorCompare = minor - o.minor;\n        if (minorCompare != 0) {\n            return minorCompare;\n        }\n\n        return patch - o.patch;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppWindowsLock.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport com.sun.jna.Pointer;\nimport com.sun.jna.platform.win32.*;\nimport com.sun.jna.win32.StdCallLibrary;\nimport io.xpipe.app.util.User32Ex;\nimport lombok.RequiredArgsConstructor;\nimport lombok.Setter;\n\npublic class AppWindowsLock {\n\n    private static final int GWLP_WNDPROC = -4;\n\n    // Prevent GC\n    private static final WinLockMsgProc PROC = new WinLockMsgProc();\n\n    public static void registerHook(WinDef.HWND hwnd) {\n        try {\n            int windowThreadID = User32.INSTANCE.GetWindowThreadProcessId(hwnd, null);\n            if (windowThreadID == 0) {\n                return;\n            }\n\n            Wtsapi32.INSTANCE.WTSRegisterSessionNotification(hwnd, Wtsapi32.NOTIFY_FOR_ALL_SESSIONS);\n            PROC.oldWindowProc =\n                    User32.INSTANCE.GetWindowLongPtr(hwnd, GWLP_WNDPROC).toPointer();\n            User32Ex.INSTANCE.SetWindowLongPtr(hwnd, GWLP_WNDPROC, PROC);\n        } catch (Throwable t) {\n            ErrorEventFactory.fromThrowable(t).omit().handle();\n        }\n    }\n\n    public static void unregisterHook(WinDef.HWND hwnd) {\n        try {\n            int windowThreadID = User32.INSTANCE.GetWindowThreadProcessId(hwnd, null);\n            if (windowThreadID == 0) {\n                return;\n            }\n\n            if (PROC.oldWindowProc == null) {\n                return;\n            }\n\n            User32Ex.INSTANCE.SetWindowLongPtr(hwnd, GWLP_WNDPROC, PROC.oldWindowProc);\n            PROC.oldWindowProc = null;\n\n            Wtsapi32.INSTANCE.WTSUnRegisterSessionNotification(hwnd);\n        } catch (Throwable t) {\n            ErrorEventFactory.fromThrowable(t).omit().handle();\n        }\n    }\n\n    public interface WinMsgProc extends StdCallLibrary.StdCallCallback {\n\n        @SuppressWarnings(\"unused\")\n        WinDef.LRESULT callback(WinDef.HWND hwnd, int uMsg, WinDef.WPARAM wParam, WinDef.LPARAM lParam);\n    }\n\n    @Setter\n    @RequiredArgsConstructor\n    public static final class WinLockMsgProc implements WinMsgProc {\n\n        private Pointer oldWindowProc;\n\n        @Override\n        public WinDef.LRESULT callback(WinDef.HWND hwnd, int uMsg, WinDef.WPARAM wParam, WinDef.LPARAM lParam) {\n            // The awt UserSessionListener does not work, so do it manually\n            if (uMsg == WinUser.WM_SESSION_CHANGE) {\n                var type = wParam.longValue();\n                TrackEvent.withInfo(\"Received WM_SESSION_CHANGE event with lock state\")\n                        .tag(\"type\", type)\n                        .handle();\n                if (type == Wtsapi32.WTS_SESSION_LOCK) {\n                    if (AppPrefs.get() != null) {\n                        var b = AppPrefs.get().hibernateBehaviour().getValue();\n                        if (b != null) {\n                            ThreadHelper.runAsync(() -> {\n                                b.runOnSleep();\n                                b.runOnWake();\n                            });\n                            return new WinDef.LRESULT(0);\n                        }\n                    }\n                }\n            }\n\n            return User32.INSTANCE.CallWindowProc(oldWindowProc, hwnd, uMsg, wParam, lParam);\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/AppWindowsShutdown.java",
    "content": "package io.xpipe.app.core;\n\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.platform.PlatformState;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport com.sun.jna.*;\nimport com.sun.jna.platform.win32.User32;\nimport com.sun.jna.platform.win32.WinDef;\nimport com.sun.jna.platform.win32.WinUser;\nimport lombok.RequiredArgsConstructor;\nimport lombok.Setter;\n\nimport java.util.List;\n\npublic class AppWindowsShutdown {\n\n    private static final int WH_CALLWNDPROC = 0x4;\n    private static final int WM_ENDSESSION = 0x16;\n    private static final int WM_QUERYENDSESSION = 0x11;\n    private static final long ENDSESSION_CRITICAL = 0x40000000L;\n\n    // Prevent GC\n    private static final WinShutdownHookProc PROC = new WinShutdownHookProc();\n\n    public static void registerHook(WinDef.HWND hwnd) {\n        try {\n            int windowThreadID = User32.INSTANCE.GetWindowThreadProcessId(hwnd, null);\n            if (windowThreadID == 0) {\n                return;\n            }\n\n            PROC.hwnd = hwnd;\n            PROC.hhook = User32.INSTANCE.SetWindowsHookEx(WH_CALLWNDPROC, PROC, null, windowThreadID);\n        } catch (Throwable t) {\n            ErrorEventFactory.fromThrowable(t).omit().handle();\n        }\n    }\n\n    public static void unregisterHook(WinDef.HWND hwnd) {\n        try {\n            int windowThreadID = User32.INSTANCE.GetWindowThreadProcessId(hwnd, null);\n            if (windowThreadID == 0) {\n                return;\n            }\n\n            if (PROC.hhook == null) {\n                return;\n            }\n\n            User32.INSTANCE.UnhookWindowsHookEx(PROC.hhook);\n            PROC.hhook = null;\n            PROC.hwnd = null;\n        } catch (Throwable t) {\n            ErrorEventFactory.fromThrowable(t).omit().handle();\n        }\n    }\n\n    public interface WinHookProc extends WinUser.HOOKPROC {\n\n        @SuppressWarnings(\"unused\")\n        WinDef.LRESULT callback(int nCode, WinDef.WPARAM wParam, CWPSSTRUCT hookProcStruct);\n    }\n\n    @SuppressWarnings(\"unused\")\n    public static class CWPSSTRUCT extends Structure {\n        public WinDef.LPARAM lParam;\n        public WinDef.WPARAM wParam;\n        public WinDef.DWORD message;\n        public WinDef.HWND hwnd;\n\n        @Override\n        protected List<String> getFieldOrder() {\n            return List.of(\"lParam\", \"wParam\", \"message\", \"hwnd\");\n        }\n    }\n\n    @Setter\n    @RequiredArgsConstructor\n    public static final class WinShutdownHookProc implements WinHookProc {\n\n        private WinUser.HHOOK hhook;\n        private WinDef.HWND hwnd;\n\n        @Override\n        public WinDef.LRESULT callback(int nCode, WinDef.WPARAM wParam, CWPSSTRUCT hookProcStruct) {\n            if (nCode >= 0 && hookProcStruct.hwnd.equals(hwnd)) {\n                if (hookProcStruct.message.longValue() == WM_QUERYENDSESSION) {\n                    TrackEvent.info(\"Received window shutdown callback WM_QUERYENDSESSION\");\n\n                    // We don't always receive an exit signal with a queryendsession, e.g. in case an .msi wants to shut\n                    // it down\n                    // Guarantee that the shutdown is run regardless\n                    ThreadHelper.runAsync(() -> {\n                        AppOperationMode.externalShutdown();\n                    });\n\n                    // Indicates that we need to run the endsession case blocking\n                    return new WinDef.LRESULT(0);\n                }\n\n                if (hookProcStruct.message.longValue() == WM_ENDSESSION) {\n                    var type = hookProcStruct.lParam.longValue();\n                    TrackEvent.withInfo(\"Received window shutdown callback WM_ENDSESSION\")\n                            .tag(\"type\", type)\n                            .handle();\n\n                    // Instant exit for critical shutdowns\n                    if (type == ENDSESSION_CRITICAL) {\n                        AppOperationMode.halt(0);\n                    }\n\n                    // A shutdown hook will be started in parallel while we exit\n                    // The only thing we have to do is wait for it to exit the platform\n                    while (PlatformState.getCurrent() != PlatformState.EXITED) {\n                        ThreadHelper.sleep(10);\n                    }\n\n                    TrackEvent.withInfo(\"Wait for shutdown for WM_ENDSESSION finished\")\n                            .tag(\"type\", type)\n                            .handle();\n\n                    return new WinDef.LRESULT(0);\n                }\n            }\n            return User32.INSTANCE.CallNextHookEx(\n                    hhook, nCode, wParam, new WinDef.LPARAM(Pointer.nativeValue(hookProcStruct.getPointer())));\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/check/AppAndroidLinuxTerminalCheck.java",
    "content": "package io.xpipe.app.core.check;\n\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.util.LocalExec;\nimport io.xpipe.core.OsType;\n\npublic class AppAndroidLinuxTerminalCheck {\n\n    public static void check() {\n        if (OsType.ofLocal() != OsType.LINUX) {\n            return;\n        }\n\n        if (!AppProperties.get().isNewBuildSession()) {\n            return;\n        }\n\n        var uname = LocalExec.readStdoutIfPossible(\"uname\", \"-a\");\n        if (uname.isPresent() && uname.get().contains(\"android\") && uname.get().contains(\"aarch64\")) {\n            AppPrefs.get().setFromExternal(AppPrefs.get().limitedTouchscreenMode(), true);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/check/AppAvCheck.java",
    "content": "package io.xpipe.app.core.check;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.MarkdownComp;\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppNames;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.core.AppResources;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.util.WindowsRegistry;\nimport io.xpipe.core.OsType;\n\nimport javafx.scene.layout.Region;\n\nimport lombok.Getter;\n\nimport java.nio.file.Files;\nimport java.util.Optional;\nimport java.util.concurrent.atomic.AtomicReference;\n\npublic class AppAvCheck {\n\n    private static Optional<AvType> detect() {\n        for (AvType value : AvType.values()) {\n            if (value.isActive()) {\n                return Optional.of(value);\n            }\n        }\n        return Optional.empty();\n    }\n\n    public static void check() {\n        // Only show this on first launch on windows\n        if (OsType.ofLocal() != OsType.WINDOWS || !AppProperties.get().isInitialLaunch()) {\n            return;\n        }\n\n        AvType found;\n        try {\n            var detected = detect();\n            if (detected.isEmpty()) {\n                return;\n            }\n            found = detected.get();\n        } catch (Throwable t) {\n            ErrorEventFactory.fromThrowable(t).omit().handle();\n            return;\n        }\n\n        var modal = ModalOverlay.of(RegionBuilder.of(() -> {\n            AtomicReference<Region> markdown = new AtomicReference<>();\n            AppResources.with(AppResources.MAIN_MODULE, \"misc/antivirus.md\", file -> {\n                markdown.set(new MarkdownComp(\n                                Files.readString(file),\n                                s -> {\n                                    var t = found;\n                                    return s.formatted(\n                                            t.getName(),\n                                            t.getName(),\n                                            t.getDescription(),\n                                            AppProperties.get().getVersion(),\n                                            AppProperties.get().getVersion(),\n                                            t.getName());\n                                },\n                                false)\n                        .prefWidth(550)\n                        .prefHeight(600)\n                        .build());\n            });\n            return markdown.get();\n        }));\n        modal.addButton(ModalButton.quit());\n        modal.addButton(ModalButton.ok());\n        modal.showAndWait();\n    }\n\n    @Getter\n    public enum AvType {\n        BITDEFENDER(\"Bitdefender\") {\n            @Override\n            public String getDescription() {\n                return \"Bitdefender sometimes isolates \" + AppNames.ofCurrent().getName()\n                        + \" and some shell programs, effectively making it unusable.\";\n            }\n\n            @Override\n            public boolean isActive() {\n                return WindowsRegistry.local()\n                        .valueExists(WindowsRegistry.HKEY_LOCAL_MACHINE, \"SOFTWARE\\\\Bitdefender\", \"InstallDir\");\n            }\n        },\n        MALWAREBYTES(\"Malwarebytes\") {\n            @Override\n            public String getDescription() {\n                return \"The free Malwarebytes version performs less invasive scans, so it shouldn't be a problem. If you are running the paid \"\n                        + \"Malwarebytes Pro version, you will have access to the `Exploit Protection` under the `Real-time Protection` mode. When \"\n                        + \"this setting is active, any shell access is slowed down, resulting in XPipe becoming very slow.\";\n            }\n\n            @Override\n            public boolean isActive() {\n                return WindowsRegistry.local()\n                        .valueExists(WindowsRegistry.HKEY_LOCAL_MACHINE, \"SOFTWARE\\\\Malwarebytes\", \"id\");\n            }\n        },\n        MCAFEE(\"McAfee\") {\n            @Override\n            public String getDescription() {\n                return \"McAfee slows down XPipe considerably. It also sometimes preemptively disables some Win32 commands that XPipe depends on, \"\n                        + \"leading to errors.\";\n            }\n\n            @Override\n            public boolean isActive() {\n                return WindowsRegistry.local()\n                        .valueExists(WindowsRegistry.HKEY_LOCAL_MACHINE, \"SOFTWARE\\\\McAfee\", \"mi\");\n            }\n        };\n\n        private final String name;\n\n        AvType(String name) {\n            this.name = name;\n        }\n\n        public abstract String getDescription();\n\n        public abstract boolean isActive();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/check/AppDebugModeCheck.java",
    "content": "package io.xpipe.app.core.check;\n\nimport io.xpipe.app.core.AppLogs;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.util.ThreadHelper;\n\npublic class AppDebugModeCheck {\n\n    public static void printIfNeeded() {\n        if (!AppProperties.get().isImage() || !AppLogs.get().getLogLevel().equals(\"trace\")) {\n            return;\n        }\n\n        var out = AppLogs.get().getOriginalSysOut();\n        var msg = \"\"\"\n\n                  ****************************************\n                  * You are running in debug mode!       *\n                  * The debug console output can contain *\n                  * sensitive information and secrets.   *\n                  * Don't share this output via an       *\n                  * untrusted website or service.        *\n                  ****************************************\n                  \"\"\";\n        out.println(msg);\n        ThreadHelper.sleep(1000);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/check/AppDirectoryPermissionsCheck.java",
    "content": "package io.xpipe.app.core.check;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.core.OsType;\n\nimport org.apache.commons.io.FileUtils;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic class AppDirectoryPermissionsCheck {\n\n    public static void checkDirectory(Path dataDirectory) {\n        try {\n            FileUtils.forceMkdir(dataDirectory.toFile());\n            var testDirectory = dataDirectory.resolve(\"permissions_check\");\n            FileUtils.forceMkdir(testDirectory.toFile());\n            if (!Files.exists(testDirectory)) {\n                throw new IOException(\"Directory creation in \" + dataDirectory + \" failed silently\");\n            }\n            Files.delete(testDirectory);\n\n            // For cloud providers like OneDrive, we need another check to guarantee that all files are synced\n            // The file operation might fail if the directory is designated to sync but the actual file is not\n            // downloaded yet\n            var testFile = dataDirectory.resolve(\"sync_file\");\n            if (!Files.exists(testFile)) {\n                Files.createFile(testFile);\n            }\n            Files.readString(testFile);\n        } catch (IOException e) {\n            var message = \"Unable to access directory \" + dataDirectory + \".\";\n            if (OsType.ofLocal() == OsType.WINDOWS) {\n                message +=\n                        \" Please make sure that you have the appropriate permissions and no Antivirus program is blocking the access. \"\n                                + \"In case you use cloud storage, verify that your cloud storage is working and you are logged in.\";\n            }\n            ErrorEventFactory.fromThrowable(message, e).term().expected().handle();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/check/AppGpuCheck.java",
    "content": "package io.xpipe.app.core.check;\n\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.platform.PlatformState;\nimport io.xpipe.app.prefs.AppPrefs;\n\nimport javafx.application.ConditionalFeature;\nimport javafx.application.Platform;\n\npublic class AppGpuCheck {\n\n    public static void check() {\n        if (!AppProperties.get().isInitialLaunch()) {\n            return;\n        }\n\n        // We might launch the platform due to an error early\n        if (AppPrefs.get() == null) {\n            return;\n        }\n\n        if (PlatformState.getCurrent() != PlatformState.RUNNING) {\n            return;\n        }\n\n        if (Platform.isSupported(ConditionalFeature.SCENE3D)) {\n            return;\n        }\n\n        AppPrefs.get().performanceMode.setValue(true);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/check/AppJavaOptionsCheck.java",
    "content": "package io.xpipe.app.core.check;\n\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.AppNames;\nimport io.xpipe.app.issue.ErrorEventFactory;\n\npublic class AppJavaOptionsCheck {\n\n    public static void check() {\n        if (AppCache.getBoolean(\"javaOptionsWarningShown\", false)) {\n            return;\n        }\n\n        var env = System.getenv(\"_JAVA_OPTIONS\");\n        if (env == null || env.isBlank()) {\n            return;\n        }\n\n        ErrorEventFactory.fromMessage(\n                        \"You have configured the global environment variable _JAVA_OPTIONS=%s on your system.\"\n                                        .formatted(env)\n                                + \" This will forcefully apply all custom JVM options to \"\n                                + AppNames.ofCurrent().getName() + \" and can cause a variety of different issues.\"\n                                + \" Please remove this global environment variable and use local configuration instead for your other JVM programs.\")\n                .expected()\n                .handle();\n        AppCache.update(\"javaOptionsWarningShown\", true);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/check/AppPathCorruptCheck.java",
    "content": "package io.xpipe.app.core.check;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.util.LocalExec;\nimport io.xpipe.core.OsType;\n\npublic class AppPathCorruptCheck {\n\n    public static void check() {\n        if (OsType.ofLocal() != OsType.WINDOWS) {\n            return;\n        }\n\n        var where = LocalExec.readStdoutIfPossible(\"where\", \"powershell\");\n        if (where.isPresent()) {\n            return;\n        }\n\n        ErrorEventFactory.fromMessage(\n                        \"Your system PATH looks to be corrupt, essential system tools are not available. This will cause XPipe to not function \"\n                                + \"correctly. Please fix your PATH environment variable to include the base Windows tool directories like C:\\\\Windows\\\\system32 and others.\")\n                .expected()\n                .handle();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/check/AppPtbDialog.java",
    "content": "package io.xpipe.app.core.check;\n\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.core.window.AppDialog;\n\npublic class AppPtbDialog {\n\n    public static void showIfNeeded() {\n        if (!AppProperties.get().isStaging()) {\n            return;\n        }\n\n        if (AppProperties.get().isAotTrainMode()) {\n            return;\n        }\n\n        if (!AppProperties.get().isNewBuildSession()) {\n            return;\n        }\n\n        var content = AppDialog.dialogText(\n                \"You are running a PTB build of XPipe.\" + \" This version is unstable and might contain bugs.\"\n                        + \" You should not use it as a daily driver.\"\n                        + \" It will also not receive regular updates after its testing period.\"\n                        + \" You will have to install and launch the normal XPipe release for that.\");\n        var modal = ModalOverlay.of(\"ptbNotice\", content);\n        modal.persist();\n        modal.addButton(ModalButton.quit());\n        modal.addButton(ModalButton.ok());\n        AppDialog.showAndWait(modal);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/check/AppRosettaCheck.java",
    "content": "package io.xpipe.app.core.check;\n\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.core.OsType;\n\npublic class AppRosettaCheck {\n\n    public static void check() throws Exception {\n        if (OsType.ofLocal() != OsType.MACOS) {\n            return;\n        }\n\n        if (!AppProperties.get().getArch().equals(\"x86_64\")) {\n            return;\n        }\n\n        var ret = LocalShell.getShell()\n                .command(\"sysctl -n sysctl.proc_translated\")\n                .readStdoutIfPossible();\n        if (ret.isEmpty()) {\n            return;\n        }\n\n        if (ret.get().equals(\"1\")) {\n            ErrorEventFactory.fromMessage(\"You are running the Intel version of XPipe on an Apple Silicon system.\"\n                            + \" There is a native build available that comes with much better performance.\"\n                            + \" Please install that one instead.\")\n                    .documentationLink(DocumentationLink.MACOS_SETUP)\n                    .expected()\n                    .handle();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/check/AppShellCheck.java",
    "content": "package io.xpipe.app.core.check;\n\nimport io.xpipe.app.core.AppNames;\nimport io.xpipe.core.OsType;\n\npublic class AppShellCheck {\n\n    public static void check() throws Exception {\n        var checker =\n                switch (OsType.ofLocal()) {\n                    case OsType.Linux ignored ->\n                        new AppShellChecker() {\n\n                            @Override\n                            protected boolean fallBackInstantly() {\n                                return false;\n                            }\n                        };\n                    case OsType.MacOs ignored ->\n                        new AppShellChecker() {\n\n                            @Override\n                            protected boolean shouldAttemptFallbackForProcessStartFail() {\n                                // We don't want to fall back on macOS as occasional zsh spawn issues would cause many\n                                // users\n                                // to use sh\n                                return false;\n                            }\n\n                            @Override\n                            protected boolean fallBackInstantly() {\n                                return false;\n                            }\n                        };\n                    case OsType.Windows ignored ->\n                        new AppShellChecker() {\n\n                            @Override\n                            protected String modifyOutput(String output) {\n                                if (output.contains(\"is not recognized as an internal or external command\")\n                                        && output.contains(\"exec-\")) {\n                                    return \"Unable to create temporary script files. \"\n                                            + AppNames.ofCurrent().getName()\n                                            + \" needs to be able to create shell script files that can be launched \"\n                                            + \"by a terminal emulator to make terminal launches work.\";\n                                }\n\n                                return super.modifyOutput(output);\n                            }\n\n                            @Override\n                            protected boolean fallBackInstantly() {\n                                // In theory, this prevents cmd issues with unsupported characters\n                                // However, due to workarounds, this should still work\n                                // Falling back to powershell would make it slower and introduce other potential issues\n                                //                            var complex =\n                                // ShellDialects.CMD.requiresScript(System.getenv(\"USERPROFILE\")) ||\n                                //\n                                // ShellDialects.CMD.requiresScript(System.getenv(\"TEMP\"));\n                                //                            return complex;\n                                return false;\n                            }\n                        };\n                };\n        checker.check();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/check/AppShellChecker.java",
    "content": "package io.xpipe.app.core.check;\n\nimport io.xpipe.app.core.AppNames;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.issue.ErrorAction;\nimport io.xpipe.app.issue.ErrorEvent;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.*;\nimport io.xpipe.app.util.DocumentationLink;\n\nimport javafx.beans.property.BooleanProperty;\nimport javafx.beans.property.SimpleBooleanProperty;\n\nimport lombok.Value;\n\nimport java.util.Optional;\n\npublic abstract class AppShellChecker {\n\n    public void check() throws Exception {\n        var isDefaultShell = ProcessControlProvider.get()\n                .getAvailableLocalDialects()\n                .getFirst()\n                .equals(ProcessControlProvider.get().getEffectiveLocalDialect());\n        if (isDefaultShell && fallBackInstantly()) {\n            toggleFallback();\n        }\n\n        var originalErr = selfTestErrorCheck();\n        if (originalErr.isEmpty()) {\n            return;\n        }\n\n        // If we are already in fallback mode and can somehow continue, we should do so instantly\n        if (originalErr.get().isCanContinue() && !isDefaultShell) {\n            return;\n        }\n\n        var attemptFallback =\n                shouldAttemptFallbackForProcessStartFail() || !originalErr.get().isProcessSpawnIssue();\n        if (!attemptFallback) {\n            // Sometimes we don't want to fall back\n            // The local shell init will fail terminally if it still does not work\n            return;\n        }\n\n        var msg = formatMessage(originalErr.get().getMessage());\n        var fallBack = new SimpleBooleanProperty();\n        var newDialect = ProcessControlProvider.get().getNextFallbackDialect();\n        var switchAction = createFallbackAction(fallBack, newDialect);\n        ErrorEventFactory.fromThrowable(new IllegalStateException(msg))\n                .customAction(switchAction)\n                .documentationLink(DocumentationLink.LOCAL_SHELL_ERROR)\n                .expected()\n                .handle();\n        if (!fallBack.get() && originalErr.get().isCanContinue()) {\n            return;\n        }\n\n        toggleFallback();\n        var fallbackErr = selfTestErrorCheck();\n        if (fallbackErr.isEmpty()) {\n            return;\n        }\n\n        msg = formatMessage(fallbackErr.get().getMessage());\n        var event = ErrorEventFactory.fromThrowable(new IllegalStateException(msg))\n                .documentationLink(DocumentationLink.LOCAL_SHELL_ERROR);\n        // Only make it terminal if both shells can't continue\n        if (!fallbackErr.get().isCanContinue()) {\n            event.term();\n        }\n        event.handle();\n    }\n\n    private ErrorAction createFallbackAction(BooleanProperty set, ShellDialect dialect) {\n        return new ErrorAction() {\n            @Override\n            public String getName() {\n                return \"Fall back to \" + dialect.getDisplayName() + \" as an alternative shell\";\n            }\n\n            @Override\n            public String getDescription() {\n                return \"Attempt to handle all operations only using \" + dialect.getDisplayName();\n            }\n\n            @Override\n            public boolean handle(ErrorEvent event) {\n                set.set(true);\n                return true;\n            }\n        };\n    }\n\n    protected boolean shouldAttemptFallbackForProcessStartFail() {\n        return true;\n    }\n\n    protected String modifyOutput(String output) {\n        return output;\n    }\n\n    private String formatMessage(String output) {\n        var isDefaultShell = ProcessControlProvider.get()\n                .getAvailableLocalDialects()\n                .getFirst()\n                .equals(ProcessControlProvider.get().getEffectiveLocalDialect());\n        var fallback = isDefaultShell\n                ? AppNames.ofCurrent().getName() + \" will now attempt to fall back to another shell.\"\n                : \"\";\n        return \"\"\"\n               Shell self-test failed for %s:\n               %s\n\n               This indicates that something is seriously wrong and certain shell functionality will not work as expected. Some features like the terminal launcher have to create shell scripts for your external terminal emulator to launch.\n\n               %s\n               \"\"\".formatted(LocalShell.getDialect().getDisplayName(), modifyOutput(output), fallback)\n                .strip();\n    }\n\n    protected abstract boolean fallBackInstantly();\n\n    private void toggleFallback() throws Exception {\n        LocalShell.reset(true);\n        ProcessControlProvider.get().toggleFallbackShell();\n        LocalShell.init();\n    }\n\n    private Optional<FailureResult> selfTestErrorCheck() {\n        try (var sc = LocalShell.init()) {\n            var scriptContent = \"echo test\";\n            var scriptFile = ScriptHelper.createExecScript(sc, scriptContent);\n            var out = sc.command(sc.getShellDialect().runScriptCommand(sc, scriptFile.toString()))\n                    .readStdoutOrThrow();\n            if (!out.equals(\"test\")) {\n                return Optional.of(new FailureResult(\n                        \"Expected output \\\"test\\\", got output \\\"\" + out + \"\\\" when running test script\", false, true));\n            }\n        } catch (ProcessOutputException ex) {\n            return Optional.of(\n                    new FailureResult(ex.getOutput() != null ? ex.getOutput() : ex.getMessage(), false, true));\n        } catch (ShellSpawnException ex) {\n            return Optional.of(new FailureResult(ex.getMessage(), true, true));\n        } catch (Throwable t) {\n            return Optional.of(new FailureResult(t.getMessage() != null ? t.getMessage() : t.toString(), false, false));\n        }\n        return Optional.empty();\n    }\n\n    @Value\n    public static class FailureResult {\n\n        String message;\n        boolean processSpawnIssue;\n        boolean canContinue;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/check/AppSystemFontCheck.java",
    "content": "package io.xpipe.app.core.check;\n\nimport io.xpipe.app.core.AppInstallation;\nimport io.xpipe.core.OsType;\n\nimport java.util.concurrent.TimeUnit;\n\npublic class AppSystemFontCheck {\n\n    public static void init() {\n        if (OsType.ofLocal() != OsType.LINUX) {\n            return;\n        }\n\n        if (hasFonts()) {\n            return;\n        }\n\n        System.setProperty(\n                \"prism.fontdir\",\n                AppInstallation.ofCurrent()\n                        .getBaseInstallationPath()\n                        .resolve(\"fonts\")\n                        .toString());\n        System.setProperty(\"prism.embeddedfonts\", \"true\");\n    }\n\n    private static boolean hasFonts() {\n        var fc = new ProcessBuilder(\"fc-match\").redirectError(ProcessBuilder.Redirect.DISCARD);\n        try {\n            var proc = fc.start();\n            var out = new String(proc.getInputStream().readAllBytes());\n            proc.waitFor(1, TimeUnit.SECONDS);\n            return proc.exitValue() == 0 && !out.isBlank();\n        } catch (Exception e) {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/check/AppTestCommandCheck.java",
    "content": "package io.xpipe.app.core.check;\n\nimport io.xpipe.app.core.AppInstallation;\nimport io.xpipe.app.core.AppNames;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ProcessOutputException;\nimport io.xpipe.app.update.AppDistributionType;\nimport io.xpipe.core.OsType;\n\npublic class AppTestCommandCheck {\n\n    public static void check() throws Exception {\n        if (OsType.ofLocal() == OsType.WINDOWS) {\n            return;\n        }\n\n        if (AppDistributionType.get() == AppDistributionType.DEVELOPMENT) {\n            return;\n        }\n\n        try (var sc = LocalShell.getShell().start()) {\n            try {\n                sc.getShellDialect()\n                        .directoryExists(\n                                sc,\n                                AppInstallation.ofCurrent()\n                                        .getBaseInstallationPath()\n                                        .toString())\n                        .execute();\n            } catch (ProcessOutputException ex) {\n                throw ProcessOutputException.withPrefix(\n                        \"Installation self test failed. Is your \\\"test\\\" shell command working as expected and is the \"\n                                + AppNames.ofCurrent().getName() + \" installation directory \" + \"accessible?\",\n                        ex);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/check/AppWindowsArmCheck.java",
    "content": "package io.xpipe.app.core.check;\n\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.core.OsType;\n\npublic class AppWindowsArmCheck {\n\n    public static void check() {\n        if (OsType.ofLocal() != OsType.WINDOWS) {\n            return;\n        }\n\n        if (!AppProperties.get().getArch().equals(\"x86_64\")) {\n            return;\n        }\n\n        var armProgramFiles = System.getenv(\"ProgramFiles(Arm)\");\n        if (armProgramFiles != null) {\n            ErrorEventFactory.fromMessage(\"You are running the x86-64 version of XPipe on an ARM64 system.\"\n                            + \" There is a native build available that comes with much better performance.\"\n                            + \" Please install that one instead.\")\n                    .documentationLink(DocumentationLink.WINDOWS_SETUP)\n                    .expected()\n                    .handle();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/check/AppWindowsTempCheck.java",
    "content": "package io.xpipe.app.core.check;\n\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.core.OsType;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.InvalidPathException;\nimport java.nio.file.Path;\n\npublic class AppWindowsTempCheck {\n\n    private static void checkTemp(String tmpdir) {\n        Path dir = null;\n        if (tmpdir != null) {\n            try {\n                dir = Path.of(tmpdir);\n            } catch (InvalidPathException ignored) {\n            }\n        }\n\n        if (dir == null || !Files.exists(dir) || !Files.isDirectory(dir)) {\n            ErrorEventFactory.fromThrowable(new IOException(\"Specified temporary directory \" + tmpdir\n                            + \", set via the environment variable %TEMP% is invalid.\"))\n                    .term()\n                    .expected()\n                    .handle();\n        }\n    }\n\n    public static void check() {\n        if (OsType.ofLocal() != OsType.WINDOWS) {\n            return;\n        }\n\n        checkTemp(AppSystemInfo.ofWindows().getTemp().toString());\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/mode/AppBaseMode.java",
    "content": "package io.xpipe.app.core.mode;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.beacon.AppBeaconServer;\nimport io.xpipe.app.beacon.BlobManager;\nimport io.xpipe.app.beacon.mcp.AppMcpServer;\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.browser.file.BrowserLocalFileSystem;\nimport io.xpipe.app.browser.icon.BrowserIconManager;\nimport io.xpipe.app.core.*;\nimport io.xpipe.app.core.check.*;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.core.window.AppMainWindow;\nimport io.xpipe.app.core.window.AppWindowTitle;\nimport io.xpipe.app.ext.DataStoreProviders;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.ext.StartOnInitStore;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.icon.SystemIconManager;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.platform.PlatformInit;\nimport io.xpipe.app.platform.PlatformState;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.pwman.KeePassXcPasswordManager;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStorageSyncHandler;\nimport io.xpipe.app.terminal.TerminalDockHubManager;\nimport io.xpipe.app.terminal.TerminalLauncherManager;\nimport io.xpipe.app.terminal.TerminalView;\nimport io.xpipe.app.update.UpdateAvailableDialog;\nimport io.xpipe.app.update.UpdateChangelogDialog;\nimport io.xpipe.app.update.UpdateNagDialog;\nimport io.xpipe.app.util.*;\nimport io.xpipe.core.XPipeDaemonMode;\n\nimport java.util.concurrent.CountDownLatch;\n\npublic class AppBaseMode extends AppOperationMode {\n\n    private boolean initialized;\n\n    @Override\n    public boolean isSupported() {\n        return true;\n    }\n\n    @Override\n    public String getId() {\n        return \"background\";\n    }\n\n    @Override\n    public void onSwitchTo() {\n        if (initialized) {\n            return;\n        }\n\n        // For debugging error handling\n        // if (true) throw new IllegalStateException();\n\n        TrackEvent.info(\"Initializing base mode components ...\");\n        AppMainWindow.loadingText(\"checkingLicense\");\n        LicenseProvider.get().init();\n        AppMainWindow.loadingText(\"initializingApp\");\n        AppWindowTitle.init();\n        AppPathCorruptCheck.check();\n        AppWindowsTempCheck.check();\n        AppDirectoryPermissionsCheck.checkDirectory(AppSystemInfo.ofCurrent().getTemp());\n        WindowsRegistry.init();\n        AppAvCheck.check();\n        AppJavaOptionsCheck.check();\n        AppSid.init();\n        AppBeaconServer.init();\n        AppLayoutModel.init();\n\n        if (AppOperationMode.getStartupMode() == XPipeDaemonMode.GUI) {\n            AppPtbDialog.showIfNeeded();\n        }\n\n        // If we downloaded an update, and decided to no longer automatically update, don't remind us!\n        // You can still update manually in the about tab\n        if (AppPrefs.get().automaticallyUpdate().get()\n                || AppPrefs.get().checkForSecurityUpdates().get()) {\n            UpdateAvailableDialog.showIfNeeded(true);\n        } else {\n            UpdateNagDialog.showAndWaitIfNeeded();\n        }\n\n        var imagesLoaded = new CountDownLatch(1);\n        var iconsInit = new CountDownLatch(1);\n        var iconsLoaded = new CountDownLatch(1);\n        var browserLoaded = new CountDownLatch(1);\n        var shellLoaded = new CountDownLatch(1);\n        var storageLoaded = new CountDownLatch(1);\n        var localPrefsLoaded = new CountDownLatch(1);\n        var syncPrefsLoaded = new CountDownLatch(1);\n        ThreadHelper.load(\n                true,\n                () -> {\n                    AppShellCheck.check();\n                    LocalShell.init();\n                    shellLoaded.countDown();\n                    AppRosettaCheck.check();\n                    AppWindowsArmCheck.check();\n                    AppTestCommandCheck.check();\n                    // This might be slow on macOS and might take longer than the platform init\n                    AppPrefs.get().initDefaultValues();\n                    localPrefsLoaded.countDown();\n                    PlatformInit.init(true);\n                    TrackEvent.info(\"Shell initialization thread completed\");\n                },\n                () -> {\n                    shellLoaded.await();\n                    DataStorageSyncHandler.getInstance().init();\n                    if (DataStorageSyncHandler.getInstance().supportsSync()) {\n                        AppMainWindow.loadingText(\"loadingGpg\");\n                        DataStorageSyncHandler.getInstance().prepareGpgIfNeeded();\n                        AppMainWindow.loadingText(\"loadingGit\");\n                    }\n                    DataStorageSyncHandler.getInstance().retrieveSyncedData();\n                    AppMainWindow.loadingText(\"loadingSettings\");\n                    AppPrefs.initSynced();\n                    syncPrefsLoaded.countDown();\n                    AppMainWindow.loadingText(\"loadingConnections\");\n                    DataStorage.init();\n                    AppPrefs.initStorage();\n                    storageLoaded.countDown();\n                    AppMcpServer.init();\n                    iconsInit.await();\n                    StoreViewState.init();\n                    AppMainWindow.loadingText(\"loadingSettings\");\n                    TrackEvent.info(\"Connection storage initialization thread completed\");\n                },\n                () -> {\n                    PlatformInit.init(true);\n                    imagesLoaded.await();\n                    browserLoaded.await();\n                    iconsLoaded.await();\n                    localPrefsLoaded.await();\n                    AppMainWindow.loadingText(\"loadingUserInterface\");\n                    AppMainWindow.initContent();\n                    TrackEvent.info(\"Window content initialization thread completed\");\n                },\n                () -> {\n                    AppFileWatcher.init();\n                    FileBridge.init();\n                    BlobManager.init();\n                    TerminalView.init();\n                    TerminalLauncherManager.init();\n                    TerminalDockHubManager.init();\n                    TrackEvent.info(\"File/Terminal initialization thread completed\");\n                },\n                () -> {\n                    PlatformInit.init(true);\n                    AppImages.init();\n                    imagesLoaded.countDown();\n                    SystemIconManager.init();\n                    syncPrefsLoaded.await();\n                    SystemIconManager.initAdditional();\n                    iconsInit.countDown();\n                    storageLoaded.await();\n                    SystemIconManager.prepareUsedIconImages();\n                    iconsLoaded.countDown();\n                    TrackEvent.info(\"Platform initialization thread completed\");\n                },\n                () -> {\n                    BrowserIconManager.init();\n                    shellLoaded.await();\n                    BrowserLocalFileSystem.init();\n                    storageLoaded.await();\n                    BrowserFullSessionModel.init();\n                    browserLoaded.countDown();\n                    TrackEvent.info(\"Browser initialization thread completed\");\n                });\n\n        AppGreetingsDialog.showAndWaitIfNeeded();\n        TrackEvent.info(\"Waiting for startup dialogs to close\");\n        AppDialog.waitForAllDialogsClose();\n        UpdateChangelogDialog.showIfNeeded();\n\n        ActionProvider.initProviders();\n        DataStoreProviders.init();\n        StartOnInitStore.init();\n\n        AppConfigurationDialog.showIfNeeded();\n\n        TrackEvent.info(\"Finished base components initialization\");\n        initialized = true;\n    }\n\n    @Override\n    public void onSwitchFrom() {}\n\n    @Override\n    public void finalTeardown() throws Exception {\n        TrackEvent.withInfo(\"Base mode shutdown started\").build();\n        AbstractAction.reset();\n        AppMcpServer.reset();\n        AppPrefs.reset();\n        DataStorage.reset();\n        DataStorageSyncHandler.getInstance().reset();\n        SshLocalBridge.reset();\n        BrowserFullSessionModel.DEFAULT.reset();\n        LocalShell.reset(false);\n        BrowserLocalFileSystem.reset();\n        ProcessControlProvider.get().reset();\n        AppBeaconServer.reset();\n        KeePassXcPasswordManager.reset();\n        StoreViewState.reset();\n        AppLayoutModel.reset();\n        AppTheme.reset();\n        PlatformState.teardown();\n        DataStoreProviders.reset();\n        AppResources.reset();\n        AppExtensionManager.reset();\n        AppDataLock.unlock();\n        BlobManager.reset();\n        FileBridge.reset();\n        AppFileWatcher.reset();\n        GlobalTimer.reset();\n        LocalFileTracker.reset();\n        TrackEvent.info(\"Base mode shutdown finished\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/mode/AppGuiMode.java",
    "content": "package io.xpipe.app.core.mode;\n\nimport io.xpipe.app.core.window.AppMainWindow;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.platform.PlatformInit;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.util.LicenseProvider;\nimport io.xpipe.core.OsType;\n\nimport javafx.stage.Stage;\n\npublic class AppGuiMode extends AppOperationMode {\n\n    @Override\n    public boolean isSupported() {\n        // We force GUI to be supported and fail with a terminal\n        // exception if we can't initialize the platform\n        return true;\n    }\n\n    @Override\n    public String getId() {\n        return \"gui\";\n    }\n\n    @Override\n    public void onSwitchFrom() {\n        // If we are in an externally started shutdown hook, don't close the windows until the platform exits\n        // That way, it is kept open to block for shutdowns on Windows systems\n        if (OsType.ofLocal() != OsType.WINDOWS || !AppOperationMode.isInShutdownHook()) {\n            PlatformThread.runLaterIfNeededBlocking(() -> {\n                TrackEvent.info(\"Closing windows\");\n                Stage.getWindows().stream().toList().forEach(w -> {\n                    w.hide();\n                });\n            });\n        }\n    }\n\n    @Override\n    public void onSwitchTo() throws Throwable {\n        AppOperationMode.BACKGROUND.onSwitchTo();\n        PlatformInit.init(true);\n\n        // Refresh license check\n        // In case our exit behavior is set to continue in background,\n        // this will apply a new license properly\n        LicenseProvider.get().init();\n\n        PlatformThread.runLaterIfNeededBlocking(() -> {\n            AppMainWindow.get().show();\n        });\n    }\n\n    @Override\n    public void finalTeardown() throws Throwable {\n        onSwitchFrom();\n        BACKGROUND.finalTeardown();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/mode/AppOperationMode.java",
    "content": "package io.xpipe.app.core.mode;\n\nimport io.xpipe.app.beacon.AppBeaconServer;\nimport io.xpipe.app.core.*;\nimport io.xpipe.app.core.check.AppDebugModeCheck;\nimport io.xpipe.app.core.window.AppMainWindow;\nimport io.xpipe.app.issue.*;\nimport io.xpipe.app.platform.PlatformInit;\nimport io.xpipe.app.platform.PlatformState;\nimport io.xpipe.app.platform.PlatformThreadWatcher;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.prefs.CloseBehaviour;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.update.AppDistributionType;\nimport io.xpipe.app.util.*;\nimport io.xpipe.core.FailableRunnable;\nimport io.xpipe.core.XPipeDaemonMode;\n\nimport javafx.application.Platform;\n\nimport lombok.Getter;\nimport lombok.SneakyThrows;\n\nimport java.awt.*;\nimport java.time.Duration;\nimport java.util.List;\n\npublic abstract class AppOperationMode {\n\n    public static final AppOperationMode BACKGROUND = new AppBaseMode();\n    public static final AppOperationMode TRAY = new AppTrayMode();\n    public static final AppOperationMode GUI = new AppGuiMode();\n    private static final List<AppOperationMode> ALL = List.of(BACKGROUND, TRAY, GUI);\n    private static final Object HALT_LOCK = new Object();\n\n    @Getter\n    private static boolean inStartup;\n\n    @Getter\n    private static boolean inShutdown;\n\n    @Getter\n    private static boolean inShutdownHook;\n\n    private static AppOperationMode CURRENT = null;\n\n    public static AppOperationMode map(XPipeDaemonMode mode) {\n        return switch (mode) {\n            case BACKGROUND -> BACKGROUND;\n            case TRAY -> TRAY;\n            case GUI -> GUI;\n        };\n    }\n\n    public static void externalShutdown() {\n        // If we used System.exit(), we don't want to do this\n        if (AppOperationMode.isInShutdown()) {\n            return;\n        }\n\n        inShutdownHook = true;\n        TrackEvent.info(\"Received SIGTERM externally\");\n        AppOperationMode.shutdown(false);\n    }\n\n    private static void setup(String[] args) {\n        try {\n            // Only for handling SIGTERM\n            Runtime.getRuntime().addShutdownHook(new Thread(() -> {\n                externalShutdown();\n            }));\n\n            // Handle uncaught exceptions\n            Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> {\n                // It seems like a few exceptions are thrown in the quantum renderer\n                // when in shutdown. We can ignore these\n                if (AppOperationMode.isInShutdown()\n                        && Platform.isFxApplicationThread()\n                        && ex instanceof NullPointerException) {\n                    return;\n                }\n\n                // It seems like a few exceptions are thrown in the quantum renderer\n                // when the screen configuration changes\n                if (Platform.isFxApplicationThread()\n                        && ex instanceof IllegalArgumentException\n                        && ex.getStackTrace()[0].toString().contains(\"Rectangle2D\")) {\n                    return;\n                }\n\n                // Some random AWT errors are thrown sometimes\n                if (ex instanceof AWTError) {\n                    return;\n                }\n\n                // Handle any startup uncaught errors\n                if (AppOperationMode.isInStartup() && thread.threadId() == 1) {\n                    ex.printStackTrace();\n                    AppOperationMode.halt(1);\n                }\n\n                if (ex instanceof OutOfMemoryError) {\n                    ex.printStackTrace();\n                    AppOperationMode.halt(1);\n                }\n\n                ErrorEventFactory.fromThrowable(ex).unhandled(true).build().handle();\n            });\n\n            TrackEvent.info(\"Initial setup\");\n            AppMainWindow.loadingText(\"initializingApp\");\n            GlobalTimer.init();\n            AppProperties.init(args);\n            PlatformThreadWatcher.init();\n            AppLogs.init();\n            AppDebugModeCheck.printIfNeeded();\n            AppProperties.get().logArguments();\n            AppDistributionType.init();\n            AppExtensionManager.init();\n            AppI18n.init();\n            AppPrefs.initLocal();\n            AppBeaconServer.setupPort();\n            AppInstance.init();\n            // Initialize early to load in parallel\n            PlatformInit.init(false);\n            ThreadHelper.runAsync(() -> {\n                PlatformInit.init(true);\n                AppMainWindow.init(AppOperationMode.getStartupMode() == XPipeDaemonMode.GUI);\n            });\n            TrackEvent.info(\"Finished initial setup\");\n        } catch (Throwable ex) {\n            ErrorEventFactory.fromThrowable(ex).term().handle();\n        }\n    }\n\n    public static XPipeDaemonMode getStartupMode() {\n        var event = TrackEvent.withInfo(\"Startup mode determined\");\n        if (AppMainWindow.get() != null && AppMainWindow.get().getStage().isShowing()) {\n            event.tag(\"mode\", \"gui\").tag(\"reason\", \"windowShowing\").handle();\n            return XPipeDaemonMode.GUI;\n        }\n\n        var prop = AppProperties.get().getExplicitMode();\n        if (prop != null) {\n            event.tag(\"mode\", prop.getDisplayName())\n                    .tag(\"reason\", \"modePropertyPassed\")\n                    .handle();\n            return prop;\n        }\n\n        if (AppPrefs.get() != null) {\n            var pref = AppPrefs.get().startupBehaviour().getValue().getMode();\n            event.tag(\"mode\", pref.getDisplayName())\n                    .tag(\"reason\", \"prefSetting\")\n                    .handle();\n            return pref;\n        }\n\n        event.tag(\"mode\", \"gui\").tag(\"reason\", \"fallback\").handle();\n        return XPipeDaemonMode.GUI;\n    }\n\n    public static void init(String[] args) {\n        inStartup = true;\n        setup(args);\n\n        try {\n            if (AppProperties.get().isAotTrainMode()) {\n                AppOperationMode.switchToSyncOrThrow(BACKGROUND);\n                inStartup = false;\n                AppAotTrain.runTrainingMode();\n                AppOperationMode.shutdown(false);\n                return;\n            }\n\n            var startupMode = getStartupMode();\n            switchToSyncOrThrow(map(startupMode));\n            // If it doesn't find time, the JVM will not gc the startup workload\n            System.gc();\n            inStartup = false;\n            AppOpenArguments.init();\n            ThreadHelper.runAsync(() -> {\n                DataStorage.get().generateCaches();\n            });\n            AppMainWindow.postInit();\n        } catch (Throwable ex) {\n            ErrorEventFactory.fromThrowable(ex).term().handle();\n        }\n    }\n\n    public static void switchToAsync(AppOperationMode newMode) {\n        ThreadHelper.createPlatformThread(\"mode switcher\", false, () -> {\n                    switchToSyncIfPossible(newMode);\n                })\n                .start();\n    }\n\n    public static void switchToSyncOrThrow(AppOperationMode newMode) throws Throwable {\n        TrackEvent.info(\"Attempting to switch mode to \" + newMode.getId());\n\n        if (!newMode.isSupported()) {\n            throw PlatformState.getLastError() != null\n                    ? PlatformState.getLastError()\n                    : new IllegalStateException(\"Unsupported operation mode: \" + newMode.getId());\n        }\n\n        set(newMode);\n    }\n\n    public static boolean switchToSyncIfPossible(AppOperationMode newMode) {\n        TrackEvent.info(\"Attempting to switch mode to \" + newMode.getId());\n\n        if (newMode.equals(TRAY) && !TRAY.isSupported()) {\n            TrackEvent.info(\"Tray is not available, using base instead\");\n            set(BACKGROUND);\n            return false;\n        }\n\n        if (newMode.equals(GUI) && !GUI.isSupported()) {\n            TrackEvent.info(\"Gui is not available, using base instead\");\n            set(BACKGROUND);\n            return false;\n        }\n\n        set(newMode);\n        return true;\n    }\n\n    public static void close() {\n        set(null);\n    }\n\n    public static List<AppOperationMode> getAll() {\n        return ALL;\n    }\n\n    public static void executeAfterShutdown(FailableRunnable<Exception> r) {\n        Runnable exec = () -> {\n            if (inShutdown) {\n                return;\n            }\n\n            try {\n                if (!isInStartup()) {\n                    inShutdown = true;\n                    if (CURRENT != null) {\n                        CURRENT.finalTeardown();\n                    }\n                    CURRENT = null;\n                }\n\n                // Restart local shell\n                LocalShell.init();\n                r.run();\n            } catch (Throwable ex) {\n                ErrorEventFactory.fromThrowable(ex).handle();\n                AppOperationMode.halt(1);\n            }\n\n            // In case we perform any operations such as opening a terminal\n            // give it some time to open while this process is still alive\n            // Otherwise it might quit because the parent process is dead already\n            ThreadHelper.sleep(100);\n            AppOperationMode.halt(0);\n        };\n\n        // Creates separate non daemon thread to force execution after shutdown even if current thread is a daemon\n        var t = new Thread(exec);\n        t.setDaemon(false);\n        t.start();\n    }\n\n    public static void halt(int code) {\n        synchronized (HALT_LOCK) {\n            TrackEvent.info(\"Halting now!\");\n            AppLogs.teardown();\n            Runtime.getRuntime().halt(code);\n        }\n    }\n\n    public static void onWindowClose() {\n        CloseBehaviour action;\n        if (AppPrefs.get() != null && !isInStartup() && !isInShutdown()) {\n            action = AppPrefs.get().closeBehaviour().getValue();\n        } else {\n            action = CloseBehaviour.QUIT;\n        }\n        ThreadHelper.runAsync(() -> {\n            action.run();\n        });\n    }\n\n    @SneakyThrows\n    public static void shutdown(boolean hasError) {\n        if (isInStartup()) {\n            TrackEvent.info(\"Received shutdown request while in startup. Halting ...\");\n            AppOperationMode.halt(1);\n        }\n\n        TrackEvent.info(\"Starting shutdown ...\");\n\n        synchronized (AppOperationMode.class) {\n            if (inShutdown) {\n                return;\n            }\n\n            inShutdown = true;\n        }\n\n        // Keep a non-daemon thread running\n        var thread = ThreadHelper.createPlatformThread(\"shutdown\", false, () -> {\n            try {\n                if (CURRENT != null) {\n                    CURRENT.finalTeardown();\n                }\n                CURRENT = null;\n            } catch (Throwable t) {\n                ErrorEventFactory.fromThrowable(t).term().handle();\n                AppOperationMode.halt(1);\n            }\n\n            AppOperationMode.halt(hasError ? 1 : 0);\n        });\n        thread.start();\n\n        // Use a timer to always exit after some time in case we get stuck\n        var limit = !hasError && !AppProperties.get().isDevelopmentEnvironment() ? 25000 : Integer.MAX_VALUE;\n        var exited = thread.join(Duration.ofMillis(limit));\n        if (!exited) {\n            TrackEvent.info(\"Shutdown took too long. Halting ...\");\n            AppOperationMode.halt(1);\n        }\n    }\n\n    private static synchronized void set(AppOperationMode newMode) {\n        if (inShutdown) {\n            return;\n        }\n\n        if (CURRENT == null && newMode == null) {\n            return;\n        }\n\n        if (CURRENT != null && CURRENT.equals(newMode)) {\n            return;\n        }\n\n        try {\n            if (newMode == null) {\n                shutdown(false);\n                return;\n            }\n\n            if (CURRENT != null && CURRENT != BACKGROUND) {\n                CURRENT.onSwitchFrom();\n            }\n\n            BACKGROUND.onSwitchTo();\n            if (newMode != GUI\n                    && AppMainWindow.get() != null\n                    && AppMainWindow.get().getStage().isShowing()) {\n                GUI.onSwitchTo();\n                newMode = GUI;\n            } else {\n                newMode.onSwitchTo();\n            }\n            CURRENT = newMode;\n        } catch (Throwable ex) {\n            ErrorEventFactory.fromThrowable(ex).terminal(true).build().handle();\n        }\n    }\n\n    public static AppOperationMode get() {\n        return CURRENT;\n    }\n\n    public abstract boolean isSupported();\n\n    public abstract String getId();\n\n    public abstract void onSwitchTo() throws Throwable;\n\n    public abstract void onSwitchFrom();\n\n    public abstract void finalTeardown() throws Throwable;\n\n    public ErrorHandler getErrorHandler() {\n        return new SyncErrorHandler(new GuiErrorHandler());\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/mode/AppTrayMode.java",
    "content": "package io.xpipe.app.core.mode;\n\nimport io.xpipe.app.core.AppTray;\nimport io.xpipe.app.issue.*;\nimport io.xpipe.app.platform.PlatformInit;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.core.OsType;\n\nimport java.awt.*;\n\npublic class AppTrayMode extends AppOperationMode {\n\n    @Override\n    public boolean isSupported() {\n        return OsType.ofLocal() == OsType.WINDOWS && Desktop.isDesktopSupported() && SystemTray.isSupported();\n    }\n\n    @Override\n    public void onSwitchTo() throws Throwable {\n        AppOperationMode.BACKGROUND.onSwitchTo();\n        PlatformInit.init(true);\n\n        PlatformThread.runLaterIfNeededBlocking(() -> {\n            if (AppTray.get() == null) {\n                TrackEvent.info(\"Initializing tray\");\n                AppTray.init();\n            }\n\n            AppTray.get().show();\n            TrackEvent.info(\"Finished tray initialization\");\n        });\n    }\n\n    @Override\n    public String getId() {\n        return \"tray\";\n    }\n\n    @Override\n    public void onSwitchFrom() {\n        if (AppTray.get() != null) {\n            TrackEvent.info(\"Closing tray\");\n            PlatformThread.runLaterIfNeededBlocking(() -> AppTray.get().hide());\n        }\n    }\n\n    @Override\n    public ErrorHandler getErrorHandler() {\n        var log = new LogErrorHandler();\n        return new SyncErrorHandler(event -> {\n            // Check if tray initialization is finished\n            if (AppTray.get() != null) {\n                AppTray.get().getErrorHandler().handle(event);\n            }\n            log.handle(event);\n            ErrorAction.ignore().handle(event);\n        });\n    }\n\n    @Override\n    public void finalTeardown() throws Throwable {\n        onSwitchFrom();\n        BACKGROUND.finalTeardown();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/window/AppDialog.java",
    "content": "package io.xpipe.app.core.window;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.PlatformInit;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.animation.PauseTransition;\nimport javafx.application.Platform;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.FXCollections;\nimport javafx.collections.ListChangeListener;\nimport javafx.collections.ObservableList;\nimport javafx.scene.layout.StackPane;\nimport javafx.scene.text.Text;\nimport javafx.util.Duration;\n\nimport lombok.Getter;\n\nimport java.util.Optional;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\npublic class AppDialog {\n\n    @Getter\n    private static final ObservableList<ModalOverlay> modalOverlays = FXCollections.observableArrayList();\n\n    public static Optional<ModalOverlay> getCurrentModalOverlay() {\n        if (modalOverlays.isEmpty()) {\n            return Optional.empty();\n        }\n\n        return Optional.of(modalOverlays.getLast());\n    }\n\n    public static void closeDialog(ModalOverlay overlay) {\n        PlatformThread.runLaterIfNeeded(() -> {\n            synchronized (modalOverlays) {\n                modalOverlays.remove(overlay);\n            }\n        });\n    }\n\n    public static void waitForAllDialogsClose() {\n        while (!modalOverlays.isEmpty()) {\n            ThreadHelper.sleep(10);\n        }\n    }\n\n    private static void waitForDialogClose(ModalOverlay overlay) {\n        while (modalOverlays.contains(overlay)) {\n            ThreadHelper.sleep(10);\n        }\n    }\n\n    public static void show(ModalOverlay o) {\n        show(o, false);\n    }\n\n    public static void hide(ModalOverlay o) {\n        PlatformThread.runLaterIfNeeded(() -> {\n            modalOverlays.remove(o);\n        });\n    }\n\n    public static void showAndWait(ModalOverlay o) {\n        show(o, true);\n    }\n\n    public static void show(ModalOverlay o, boolean wait) {\n        PlatformInit.init(true);\n        AppMainWindow.init(true);\n\n        if (!Platform.isFxApplicationThread()) {\n            PlatformThread.runLaterIfNeededBlocking(() -> {\n                synchronized (modalOverlays) {\n                    modalOverlays.add(o);\n                }\n            });\n            if (wait) {\n                waitForDialogClose(o);\n            }\n            ThreadHelper.sleep(200);\n        } else {\n            var key = new Object();\n            PlatformThread.runLaterIfNeededBlocking(() -> {\n                synchronized (modalOverlays) {\n                    modalOverlays.add(o);\n                    modalOverlays.addListener(new ListChangeListener<>() {\n                        @Override\n                        public void onChanged(Change<? extends ModalOverlay> c) {\n                            if (!c.getList().contains(o)) {\n                                var transition = new PauseTransition(Duration.millis(200));\n                                transition.setOnFinished(e -> {\n                                    if (wait) {\n                                        PlatformThread.exitNestedEventLoop(key);\n                                    }\n                                });\n                                transition.play();\n                                modalOverlays.removeListener(this);\n                            }\n                        }\n                    });\n                }\n            });\n            if (wait) {\n                PlatformThread.enterNestedEventLoop(key);\n                waitForDialogClose(o);\n            }\n        }\n    }\n\n    public static BaseRegionBuilder<?, ?> dialogTextKey(String s) {\n        return dialogText(AppI18n.observable(s));\n    }\n\n    public static BaseRegionBuilder<?, ?> dialogText(String s) {\n        return RegionBuilder.of(() -> {\n                    var text = new Text(s);\n                    text.getStyleClass().add(\"dialog-text\");\n                    var sp = new StackPane(text);\n                    text.wrappingWidthProperty().bind(sp.prefWidthProperty());\n                    return sp;\n                })\n                .prefWidth(450);\n    }\n\n    public static BaseRegionBuilder<?, ?> dialogText(ObservableValue<String> s) {\n        return RegionBuilder.of(() -> {\n                    var text = new Text();\n                    text.getStyleClass().add(\"dialog-text\");\n                    text.textProperty().bind(s);\n                    var sp = new StackPane(text);\n                    text.wrappingWidthProperty().bind(sp.prefWidthProperty());\n                    return sp;\n                })\n                .prefWidth(450);\n    }\n\n    public static boolean confirm(String translationKey) {\n        var confirmed = new AtomicBoolean(false);\n        var content = dialogTextKey(translationKey + \"Content\");\n        var modal = ModalOverlay.of(translationKey + \"Title\", content);\n        modal.addButton(ModalButton.cancel());\n        modal.addButton(ModalButton.ok(() -> confirmed.set(true)));\n        showAndWait(modal);\n        return confirmed.get();\n    }\n\n    public static boolean confirm(String titleKey, ObservableValue<String> content) {\n        var confirmed = new AtomicBoolean(false);\n        var modal = ModalOverlay.of(titleKey, dialogText(content));\n        modal.addButton(ModalButton.cancel());\n        modal.addButton(ModalButton.ok(() -> confirmed.set(true)));\n        showAndWait(modal);\n        return confirmed.get();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/window/AppMainWindow.java",
    "content": "package io.xpipe.app.core.window;\n\nimport io.xpipe.app.comp.base.AppLayoutComp;\nimport io.xpipe.app.comp.base.AppMainWindowContentComp;\nimport io.xpipe.app.core.*;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.platform.NativeWinWindowControl;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.prefs.CloseBehaviourDialog;\nimport io.xpipe.app.update.AppDistributionType;\nimport io.xpipe.app.util.GlobalTimer;\nimport io.xpipe.core.OsType;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.*;\nimport javafx.geometry.Rectangle2D;\nimport javafx.scene.Scene;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\nimport javafx.scene.input.KeyEvent;\nimport javafx.scene.paint.Color;\nimport javafx.stage.Screen;\nimport javafx.stage.Stage;\n\nimport lombok.Builder;\nimport lombok.Getter;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.io.IOException;\nimport java.time.Duration;\nimport java.time.Instant;\nimport javax.imageio.ImageIO;\n\npublic class AppMainWindow {\n\n    @Getter\n    private static final Property<AppLayoutComp.Structure> loadedContent = new SimpleObjectProperty<>();\n\n    @Getter\n    private static final Property<String> loadingText = new SimpleObjectProperty<>();\n\n    private static AppMainWindow INSTANCE;\n\n    @Getter\n    private final Stage stage;\n\n    private final BooleanProperty windowActive = new SimpleBooleanProperty(false);\n    private volatile Instant lastUpdate;\n    private boolean shown = false;\n\n    private AppMainWindow(Stage stage) {\n        this.stage = stage;\n    }\n\n    public static void init(boolean show) {\n        if (INSTANCE != null\n                && INSTANCE.getStage() != null\n                && (!show || INSTANCE.getStage().isShowing())) {\n            return;\n        }\n\n        PlatformThread.runLaterIfNeededBlocking(() -> {\n            initEmpty(show);\n        });\n    }\n\n    public static void postInit() {\n        if (INSTANCE == null || INSTANCE.getStage() == null) {\n            return;\n        }\n\n        // Ugly solution to only start tracking kb input after we are finished starting up\n        // Otherwise, any typed vault password will always make it think that kb input is active\n        Platform.runLater(() -> {\n            AppWindowStyle.addNavigationPseudoClasses(INSTANCE.getStage().getScene());\n        });\n    }\n\n    private static synchronized void initEmpty(boolean show) {\n        if (INSTANCE != null) {\n            if (show) {\n                INSTANCE.show();\n            }\n            return;\n        }\n\n        var stage = App.getApp().getStage();\n        stage.setMinWidth(500);\n        stage.setMinHeight(400);\n        INSTANCE = new AppMainWindow(stage);\n        AppModifiedStage.prepareStage(stage);\n\n        var content = new AppMainWindowContentComp(stage).build();\n        content.opacityProperty()\n                .bind(Bindings.createDoubleBinding(\n                        () -> {\n                            if (OsType.ofLocal() != OsType.MACOS) {\n                                return 1.0;\n                            }\n                            return stage.isFocused() ? 1.0 : 0.8;\n                        },\n                        stage.focusedProperty()));\n        var scene = new Scene(content, -1, -1, false);\n        content.prefWidthProperty().bind(scene.widthProperty());\n        content.prefHeightProperty().bind(scene.heightProperty());\n        scene.setFill(Color.TRANSPARENT);\n\n        stage.setScene(scene);\n        if (AppPrefs.get() != null) {\n            stage.opacityProperty().bind(PlatformThread.sync(AppPrefs.get().windowOpacity()));\n        }\n        AppWindowStyle.addIcons(stage);\n        AppWindowStyle.addStylesheets(stage.getScene());\n        AppWindowStyle.addClickShield(stage);\n        AppWindowStyle.addMaximizedPseudoClass(stage);\n        AppWindowStyle.addFontSize(stage);\n        AppTheme.initThemeHandlers(stage);\n\n        AppWindowTitle.getTitle().subscribe(s -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                stage.setTitle(s);\n            });\n        });\n\n        var state = INSTANCE.loadState();\n        TrackEvent.withDebug(\"Window state loaded\").tag(\"state\", state).handle();\n        INSTANCE.initializeWindow(state);\n        INSTANCE.setupListeners();\n        INSTANCE.windowActive.set(true);\n\n        if (show) {\n            INSTANCE.show();\n        }\n    }\n\n    public static void loadingText(String key) {\n        loadingText.setValue(key != null && AppI18n.get() != null ? AppI18n.get(key) : \"?\");\n    }\n\n    public static synchronized void initContent() {\n        PlatformThread.runLaterIfNeededBlocking(() -> {\n            try {\n                TrackEvent.info(\"Window content node creation started\");\n                var content = new AppLayoutComp();\n                var s = content.createBase();\n                TrackEvent.info(\"Window content node structure created\");\n                loadedContent.setValue(s);\n            } catch (Throwable t) {\n                ErrorEventFactory.fromThrowable(t).term().handle();\n            }\n        });\n    }\n\n    public static synchronized void resetContent() {\n        PlatformThread.runLaterIfNeededBlocking(() -> {\n            loadingText.setValue(AppI18n.get(\"savingChanges\"));\n            loadedContent.setValue(null);\n        });\n    }\n\n    public static AppMainWindow get() {\n        return INSTANCE;\n    }\n\n    public void show() {\n        stage.show();\n\n        if (OsType.ofLocal() == OsType.WINDOWS) {\n            var currentControl = new NativeWinWindowControl(stage);\n\n            // Clean up old window as the HWND might not be reused\n            var oldControl = NativeWinWindowControl.MAIN_WINDOW;\n            if (oldControl != null && !oldControl.getWindowHandle().equals(currentControl.getWindowHandle())) {\n                AppWindowsLock.unregisterHook(oldControl.getWindowHandle());\n                AppWindowsShutdown.unregisterHook(oldControl.getWindowHandle());\n                NativeWinWindowControl.MAIN_WINDOW = null;\n            }\n\n            if (NativeWinWindowControl.MAIN_WINDOW == null) {\n                var ctrl = new NativeWinWindowControl(stage);\n                NativeWinWindowControl.MAIN_WINDOW = ctrl;\n                AppWindowsLock.registerHook(ctrl.getWindowHandle());\n                AppWindowsShutdown.registerHook(ctrl.getWindowHandle());\n            }\n        }\n\n        shown = true;\n    }\n\n    public void focus() {\n        if (AppPrefs.get() != null\n                && !AppPrefs.get().focusWindowOnNotifications().get()) {\n            return;\n        }\n\n        PlatformThread.runLaterIfNeeded(() -> {\n            if (!stage.isShowing()) {\n                return;\n            }\n\n            stage.setIconified(false);\n            stage.requestFocus();\n        });\n    }\n\n    private synchronized void onChange() {\n        var timestamp = Instant.now();\n        lastUpdate = timestamp;\n        // Reduce printed window updates\n        GlobalTimer.delay(\n                () -> {\n                    if (!timestamp.equals(lastUpdate)) {\n                        return;\n                    }\n\n                    synchronized (AppMainWindow.this) {\n                        logChange();\n                    }\n                },\n                Duration.ofSeconds(1));\n    }\n\n    private void logChange() {\n        TrackEvent.withDebug(\"Window resize\")\n                .tag(\"x\", stage.getX())\n                .tag(\"y\", stage.getY())\n                .tag(\"width\", stage.getWidth())\n                .tag(\"height\", stage.getHeight())\n                .tag(\"maximized\", stage.isMaximized())\n                .build()\n                .handle();\n    }\n\n    private void initializeWindow(WindowState state) {\n        applyState(state);\n\n        TrackEvent.withDebug(\"Window initialized\")\n                .tag(\"x\", stage.getX())\n                .tag(\"y\", stage.getY())\n                .tag(\"width\", stage.getWidth())\n                .tag(\"height\", stage.getHeight())\n                .tag(\"maximized\", stage.isMaximized())\n                .build()\n                .handle();\n    }\n\n    private void setupListeners() {\n        AppWindowBounds.fixInvalidStagePosition(stage);\n        stage.xProperty().addListener((c, o, n) -> {\n            if (windowActive.get()) {\n                onChange();\n            }\n        });\n        stage.yProperty().addListener((c, o, n) -> {\n            if (windowActive.get()) {\n                onChange();\n            }\n        });\n        stage.widthProperty().addListener((c, o, n) -> {\n            if (windowActive.get()) {\n                onChange();\n            }\n        });\n        stage.heightProperty().addListener((c, o, n) -> {\n            if (windowActive.get()) {\n                onChange();\n            }\n        });\n        stage.maximizedProperty().addListener((c, o, n) -> {\n            if (windowActive.get()) {\n                onChange();\n            }\n        });\n\n        stage.setOnHiding(e -> {\n            saveState();\n        });\n\n        stage.setOnHidden(e -> {\n            windowActive.set(false);\n        });\n\n        stage.setOnCloseRequest(e -> {\n            if (!AppOperationMode.isInStartup()\n                    && !AppOperationMode.isInShutdown()\n                    && !CloseBehaviourDialog.showIfNeeded()) {\n                e.consume();\n                return;\n            }\n\n            // Close dialogs\n            AppDialog.getModalOverlays().clear();\n\n            // Close other windows\n            Stage.getWindows().stream().filter(w -> !w.equals(stage)).toList().forEach(w -> w.fireEvent(e));\n\n            // Iconifying stages on Windows will break if the window is closed\n            // Work around this issue it by re-showing it immediately before hiding it again\n            if (OsType.ofLocal() == OsType.WINDOWS) {\n                stage.setIconified(false);\n            }\n\n            // Close self\n            stage.close();\n            AppOperationMode.onWindowClose();\n            e.consume();\n        });\n\n        stage.addEventHandler(KeyEvent.KEY_PRESSED, event -> {\n            if (new KeyCodeCombination(KeyCode.Q, KeyCombination.SHORTCUT_DOWN).match(event)) {\n                stage.close();\n                AppOperationMode.onWindowClose();\n                event.consume();\n            }\n        });\n\n        if (OsType.ofLocal() == OsType.LINUX || OsType.ofLocal() == OsType.MACOS) {\n            stage.getScene().addEventHandler(KeyEvent.KEY_PRESSED, event -> {\n                if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(event)) {\n                    AppOperationMode.onWindowClose();\n                    event.consume();\n                }\n            });\n        }\n\n        stage.getScene().addEventHandler(KeyEvent.KEY_PRESSED, event -> {\n            if (AppProperties.get().isShowcase() && event.getCode().equals(KeyCode.F12)) {\n                var image = stage.getScene().snapshot(null);\n                var awt = AppImages.toAwtImage(image);\n                var file = AppSystemInfo.ofCurrent()\n                        .getUserHome()\n                        .resolve(\"Desktop\", AppNames.ofCurrent().getKebapName() + \"-screenshot.png\");\n                try {\n                    ImageIO.write(awt, \"png\", file.toFile());\n                } catch (IOException e) {\n                    ErrorEventFactory.fromThrowable(e).handle();\n                }\n                TrackEvent.debug(\"Screenshot taken\");\n                event.consume();\n            }\n        });\n\n        TrackEvent.debug(\"Window listeners added\");\n    }\n\n    private void applyState(WindowState state) {\n        if (state != null) {\n            if (state.maximized) {\n                stage.setMaximized(true);\n                stage.setWidth(1280);\n                stage.setHeight(780);\n            } else {\n                stage.setX(state.windowX);\n                stage.setY(state.windowY);\n                stage.setWidth(state.windowWidth);\n                stage.setHeight(state.windowHeight);\n            }\n            TrackEvent.debug(\"Window loaded saved bounds\");\n        } else {\n            setDefaultSize();\n        }\n    }\n\n    private void setDefaultSize() {\n        if (AppProperties.get().isShowcase()) {\n            stage.setX(312);\n            stage.setY(149);\n            stage.setWidth(1296);\n            stage.setHeight(759);\n            return;\n        }\n\n        if (AppDistributionType.get() == AppDistributionType.WEBTOP) {\n            stage.setWidth(1000);\n            stage.setHeight(600);\n        }\n\n        var screens = Screen.getScreens();\n        if (screens.size() > 1) {\n            stage.setWidth(1280);\n            stage.setHeight(780);\n            return;\n        }\n\n        var screen = screens.getFirst();\n        var w = Math.min(1280, screen.getBounds().getWidth() - 10);\n        var h = Math.min(780, screen.getBounds().getHeight() - 10);\n        stage.setWidth(w);\n        stage.setHeight(h);\n    }\n\n    private void saveState() {\n        if (AppPrefs.get() == null || !AppPrefs.get().saveWindowLocation().get()) {\n            return;\n        }\n\n        if (AppProperties.get().isShowcase()) {\n            return;\n        }\n\n        var newState = WindowState.builder()\n                .maximized(stage.isMaximized())\n                .windowX((int) stage.getX())\n                .windowY((int) stage.getY())\n                .windowWidth((int) stage.getWidth())\n                .windowHeight((int) stage.getHeight())\n                .build();\n        AppCache.update(\"windowState\", newState);\n    }\n\n    private WindowState loadState() {\n        if (AppPrefs.get() == null) {\n            return null;\n        }\n\n        if (!AppPrefs.get().saveWindowLocation().get()) {\n            return null;\n        }\n\n        if (AppProperties.get().isShowcase()) {\n            return null;\n        }\n\n        WindowState state = AppCache.getNonNull(\"windowState\", WindowState.class, () -> null);\n        if (state == null) {\n            return null;\n        }\n\n        boolean inBounds = false;\n        for (Screen screen : Screen.getScreens()) {\n            Rectangle2D visualBounds = screen.getVisualBounds();\n            // Check whether the bounds intersect where the intersection is larger than 20 pixels!\n            if (state.windowWidth > 40\n                    && state.windowHeight > 40\n                    && visualBounds.intersects(new Rectangle2D(\n                            state.windowX + 20, state.windowY + 20, state.windowWidth - 40, state.windowHeight - 40))) {\n                inBounds = true;\n                break;\n            }\n        }\n        return inBounds ? state : null;\n    }\n\n    @Builder\n    @Jacksonized\n    @Value\n    private static class WindowState {\n        boolean maximized;\n        int windowX;\n        int windowY;\n        int windowWidth;\n        int windowHeight;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/window/AppModifiedStage.java",
    "content": "package io.xpipe.app.core.window;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.NativeMacOsWindowControl;\nimport io.xpipe.app.platform.NativeWinWindowControl;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.core.OsType;\n\nimport javafx.animation.PauseTransition;\nimport javafx.application.Platform;\nimport javafx.collections.ListChangeListener;\nimport javafx.collections.ObservableList;\nimport javafx.css.PseudoClass;\nimport javafx.stage.Stage;\nimport javafx.stage.StageStyle;\nimport javafx.stage.Window;\nimport javafx.util.Duration;\n\nimport lombok.SneakyThrows;\nimport org.apache.commons.lang3.SystemUtils;\n\nimport java.lang.ref.WeakReference;\n\npublic class AppModifiedStage extends Stage {\n\n    public static boolean mergeFrame() {\n        return SystemUtils.IS_OS_WINDOWS_10 || SystemUtils.IS_OS_WINDOWS_11 || SystemUtils.IS_OS_MAC;\n    }\n\n    public static void init() {\n        ObservableList<Window> list = Window.getWindows();\n        list.addListener((ListChangeListener<Window>) c -> {\n            if (c.next() && c.wasAdded()) {\n                var added = c.getAddedSubList();\n                for (Window window : added) {\n                    if (window instanceof Stage stage) {\n                        hookUpStage(stage);\n                    }\n                }\n            }\n        });\n    }\n\n    public static void prepareStage(Stage stage) {\n        if (mergeFrame()) {\n            stage.initStyle(StageStyle.UNIFIED);\n        }\n    }\n\n    private static void hookUpStage(Stage stage) {\n        applyModes(stage);\n\n        // Fix GC not working when the stage is no longer needed\n        var ref = new WeakReference<>(stage);\n\n        if (AppPrefs.get() != null) {\n            AppPrefs.get().theme().addListener((observable, oldValue, newValue) -> {\n                var val = ref.get();\n                if (val != null) {\n                    updateStage(val);\n                }\n            });\n            AppPrefs.get().performanceMode().addListener((observable, oldValue, newValue) -> {\n                var val = ref.get();\n                if (val != null) {\n                    updateStage(val);\n                }\n            });\n        }\n        if (stage.getScene() != null) {\n            stage.getScene().rootProperty().addListener((observable, oldValue, newValue) -> {\n                var val = ref.get();\n                if (val != null) {\n                    applyModes(val);\n                }\n            });\n        }\n    }\n\n    @SneakyThrows\n    private static void applyModes(Stage stage) {\n        if (stage.getScene() == null) {\n            return;\n        }\n\n        if (!stage.isShowing()) {\n            return;\n        }\n\n        var applyToStage = (OsType.ofLocal() == OsType.WINDOWS) || (OsType.ofLocal() == OsType.MACOS);\n        if (!applyToStage || AppPrefs.get() == null || AppPrefs.get().theme().getValue() == null) {\n            stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass(\"seamless-frame\"), false);\n            stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass(\"separate-frame\"), true);\n            return;\n        }\n\n        try {\n            switch (OsType.ofLocal()) {\n                case OsType.Linux ignored -> {}\n                case OsType.MacOs ignored -> {\n                    var ctrl = new NativeMacOsWindowControl(stage);\n                    var seamlessFrame = AppMainWindow.get() != null\n                            && AppMainWindow.get().getStage() == stage\n                            && !AppPrefs.get().performanceMode().get()\n                            && mergeFrame();\n                    var seamlessFrameApplied = ctrl.setAppearance(\n                                    seamlessFrame,\n                                    AppPrefs.get().theme().getValue().isDark())\n                            && seamlessFrame;\n                    stage.getScene()\n                            .getRoot()\n                            .pseudoClassStateChanged(\n                                    PseudoClass.getPseudoClass(\"seamless-frame\"), seamlessFrameApplied);\n                    stage.getScene()\n                            .getRoot()\n                            .pseudoClassStateChanged(\n                                    PseudoClass.getPseudoClass(\"separate-frame\"), !seamlessFrameApplied);\n                }\n                case OsType.Windows ignored -> {\n                    var ctrl = new NativeWinWindowControl(stage);\n                    ctrl.setWindowAttribute(\n                            NativeWinWindowControl.DmwaWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE.get(),\n                            AppPrefs.get().theme().getValue().isDark());\n                    boolean seamlessFrame;\n                    if (AppPrefs.get().performanceMode().get()\n                            || !mergeFrame()\n                            || AppMainWindow.get() == null\n                            || stage != AppMainWindow.get().getStage()) {\n                        seamlessFrame = false;\n                    } else {\n                        // This is not available on Windows 10\n                        seamlessFrame = ctrl.setWindowBackdrop(NativeWinWindowControl.DwmSystemBackDropType.MICA_ALT)\n                                || SystemUtils.IS_OS_WINDOWS_10;\n                    }\n                    stage.getScene()\n                            .getRoot()\n                            .pseudoClassStateChanged(PseudoClass.getPseudoClass(\"seamless-frame\"), seamlessFrame);\n                    stage.getScene()\n                            .getRoot()\n                            .pseudoClassStateChanged(PseudoClass.getPseudoClass(\"separate-frame\"), !seamlessFrame);\n                }\n            }\n        } catch (Throwable t) {\n            ErrorEventFactory.fromThrowable(t).omit().handle();\n        }\n    }\n\n    private static void updateStage(Stage stage) {\n        if (!stage.isShowing()) {\n            return;\n        }\n\n        PlatformThread.runLaterIfNeeded(() -> {\n            var transition = new PauseTransition(Duration.millis(300));\n            transition.setOnFinished(e -> {\n                applyModes(stage);\n                // We only need to update the frame by resizing on Windows\n                if (OsType.ofLocal() == OsType.WINDOWS) {\n                    stage.setWidth(stage.getWidth() - 1);\n                    Platform.runLater(() -> {\n                        stage.setWidth(stage.getWidth() + 1);\n                    });\n                }\n            });\n            transition.play();\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/window/AppSideWindow.java",
    "content": "package io.xpipe.app.core.window;\n\nimport io.xpipe.app.platform.PlatformInit;\n\nimport javafx.application.Platform;\nimport javafx.scene.control.Alert;\nimport javafx.scene.control.ButtonType;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\nimport javafx.scene.input.KeyEvent;\nimport javafx.scene.paint.Color;\nimport javafx.stage.Stage;\n\nimport java.util.Optional;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\n\npublic class AppSideWindow {\n\n    public static Optional<ButtonType> showBlockingAlert(Consumer<Alert> c) {\n        PlatformInit.init(true);\n\n        Supplier<Alert> supplier = () -> {\n            Alert a = createEmptyAlert();\n            var s = (Stage) a.getDialogPane().getScene().getWindow();\n            s.setOnShown(event -> {\n                Platform.runLater(() -> {\n                    AppWindowBounds.clampWindow(s).ifPresent(rectangle2D -> {\n                        s.setX(rectangle2D.getMinX());\n                        s.setY(rectangle2D.getMinY());\n                        // Somehow we have to set max size as setting the normal size does not work?\n                        s.setMaxWidth(rectangle2D.getWidth());\n                        s.setMaxHeight(rectangle2D.getHeight());\n                    });\n                });\n                event.consume();\n            });\n            AppWindowBounds.fixInvalidStagePosition(s);\n            AppWindowStyle.addFontSize(s);\n            a.getDialogPane().getScene().addEventHandler(KeyEvent.KEY_PRESSED, event -> {\n                if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(event)) {\n                    s.close();\n                    event.consume();\n                    return;\n                }\n\n                if (event.getCode().equals(KeyCode.ESCAPE)) {\n                    s.close();\n                    event.consume();\n                }\n            });\n            return a;\n        };\n\n        AtomicReference<Optional<ButtonType>> result = new AtomicReference<>();\n        if (!Platform.isFxApplicationThread()) {\n            CountDownLatch latch = new CountDownLatch(1);\n            Platform.runLater(() -> {\n                try {\n                    Alert a = supplier.get();\n                    c.accept(a);\n                    result.set(a.showAndWait());\n                } catch (Throwable t) {\n                    result.set(Optional.empty());\n                } finally {\n                    latch.countDown();\n                }\n            });\n            try {\n                latch.await();\n            } catch (InterruptedException ignored) {\n            }\n        } else {\n            Alert a = supplier.get();\n            c.accept(a);\n            result.set(a.showAndWait());\n        }\n        return result.get();\n    }\n\n    public static Alert createEmptyAlert() {\n        Alert alert = new Alert(Alert.AlertType.NONE);\n        if (AppMainWindow.get() != null) {\n            alert.initOwner(AppMainWindow.get().getStage());\n        }\n        alert.getDialogPane().getScene().setFill(Color.TRANSPARENT);\n        var stage = (Stage) alert.getDialogPane().getScene().getWindow();\n        AppModifiedStage.prepareStage(stage);\n        AppWindowStyle.addIcons(stage);\n        AppWindowStyle.addStylesheets(alert.getDialogPane().getScene());\n        AppWindowStyle.addNavigationPseudoClasses(alert.getDialogPane().getScene());\n        return alert;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/window/AppWindowBounds.java",
    "content": "package io.xpipe.app.core.window;\n\nimport io.xpipe.core.OsType;\n\nimport javafx.geometry.Rectangle2D;\nimport javafx.stage.Screen;\nimport javafx.stage.Stage;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\npublic class AppWindowBounds {\n\n    public static void fixInvalidStagePosition(Stage stage) {\n        if (OsType.ofLocal() != OsType.LINUX) {\n            return;\n        }\n\n        var xSet = new AtomicBoolean();\n        stage.xProperty().addListener((observable, oldValue, newValue) -> {\n            var n = newValue.doubleValue();\n            var o = oldValue.doubleValue();\n            if (stage.isShowing() && areNumbersValid(o, n)) {\n                // Ignore rounding events\n                if (Math.abs(n - o) < 0.5) {\n                    return;\n                }\n\n                if (!xSet.getAndSet(true) && !stage.isMaximized() && n <= 0.0 && o > 0.0 && Math.abs(n - o) > 100) {\n                    stage.setX(o);\n                }\n            }\n        });\n\n        var ySet = new AtomicBoolean();\n        stage.yProperty().addListener((observable, oldValue, newValue) -> {\n            var n = newValue.doubleValue();\n            var o = oldValue.doubleValue();\n            if (stage.isShowing() && areNumbersValid(o, n)) {\n                // Ignore rounding events\n                if (Math.abs(n - o) < 0.5) {\n                    return;\n                }\n\n                if (!ySet.getAndSet(true) && !stage.isMaximized() && n <= 0.0 && o > 0.0 && Math.abs(n - o) > 20) {\n                    stage.setY(o);\n                }\n            }\n        });\n    }\n\n    public static Optional<Rectangle2D> clampWindow(Stage stage) {\n        if (!areNumbersValid(stage.getWidth(), stage.getHeight())) {\n            return Optional.empty();\n        }\n\n        var allScreenBounds = computeWindowScreenBounds(stage);\n        if (!areNumbersValid(\n                allScreenBounds.getMinX(),\n                allScreenBounds.getMinY(),\n                allScreenBounds.getMaxX(),\n                allScreenBounds.getMaxY())) {\n            return Optional.empty();\n        }\n\n        if (allScreenBounds.getWidth() == 0 || allScreenBounds.getHeight() == 0) {\n            return Optional.empty();\n        }\n\n        // Alerts do not have a custom x/y set, but we are able to handle that\n\n        boolean changed = false;\n\n        double x = 0;\n        if (areNumbersValid(stage.getX())) {\n            x = stage.getX();\n            if (x < allScreenBounds.getMinX()) {\n                x = allScreenBounds.getMinX();\n                changed = true;\n            }\n        }\n\n        double y = 0;\n        if (areNumbersValid(stage.getY())) {\n            y = stage.getY();\n            if (y < allScreenBounds.getMinY()) {\n                y = allScreenBounds.getMinY();\n                changed = true;\n            }\n        }\n\n        double w = stage.getWidth();\n        double h = stage.getHeight();\n        if (x + w > allScreenBounds.getMaxX()) {\n            w = allScreenBounds.getMaxX() - x;\n            changed = true;\n        }\n        if (y + h > allScreenBounds.getMaxY()) {\n            h = allScreenBounds.getMaxY() - y;\n            changed = true;\n        }\n\n        // This should not happen but on weird Linux systems nothing is impossible\n        if (w < 0 || h < 0) {\n            return Optional.empty();\n        }\n\n        return changed ? Optional.of(new Rectangle2D(x, y, w, h)) : Optional.empty();\n    }\n\n    private static boolean areNumbersValid(double... args) {\n        return Arrays.stream(args).allMatch(Double::isFinite);\n    }\n\n    private static List<Screen> getWindowScreens(Stage stage) {\n        if (!areNumbersValid(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight())) {\n            return stage.getOwner() != null && stage.getOwner() instanceof Stage ownerStage\n                    ? getWindowScreens(ownerStage)\n                    : List.of(Screen.getPrimary());\n        }\n\n        return Screen.getScreensForRectangle(\n                new Rectangle2D(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight()));\n    }\n\n    private static Rectangle2D computeWindowScreenBounds(Stage stage) {\n        double minX = Double.POSITIVE_INFINITY;\n        double minY = Double.POSITIVE_INFINITY;\n        double maxX = Double.NEGATIVE_INFINITY;\n        double maxY = Double.NEGATIVE_INFINITY;\n        for (Screen screen : getWindowScreens(stage)) {\n            Rectangle2D screenBounds = screen.getBounds();\n            if (screenBounds.getMinX() < minX) {\n                minX = screenBounds.getMinX();\n            }\n            if (screenBounds.getMinY() < minY) {\n                minY = screenBounds.getMinY();\n            }\n            if (screenBounds.getMaxX() > maxX) {\n                maxX = screenBounds.getMaxX();\n            }\n            if (screenBounds.getMaxY() > maxY) {\n                maxY = screenBounds.getMaxY();\n            }\n        }\n        // Taskbar adjustment\n        maxY -= 50;\n\n        var w = maxX - minX;\n        var h = maxY - minY;\n\n        // This should not happen but on weird Linux systems nothing is impossible\n        if (w < 0 || h < 0) {\n            return new Rectangle2D(0, 0, 800, 600);\n        }\n\n        return new Rectangle2D(minX, minY, w, h);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/window/AppWindowStyle.java",
    "content": "package io.xpipe.app.core.window;\n\nimport io.xpipe.app.core.*;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.util.GlobalTimer;\nimport io.xpipe.core.OsType;\n\nimport javafx.application.Platform;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.css.PseudoClass;\nimport javafx.scene.Scene;\nimport javafx.scene.input.*;\nimport javafx.stage.Stage;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.List;\n\npublic class AppWindowStyle {\n\n    public static void addMaximizedPseudoClass(Stage stage) {\n        stage.getScene().rootProperty().subscribe(root -> {\n            stage.maximizedProperty().subscribe(v -> {\n                root.pseudoClassStateChanged(PseudoClass.getPseudoClass(\"maximized\"), v);\n            });\n        });\n    }\n\n    public static void addFontSize(Stage stage) {\n        stage.getScene().rootProperty().subscribe(root -> {\n            AppFontSizes.base(root);\n        });\n    }\n\n    public static void addNavigationPseudoClasses(Scene scene) {\n        var keyInput = new SimpleBooleanProperty();\n        var changed = new SimpleBooleanProperty();\n        keyInput.addListener((observable, oldValue, newValue) -> {\n            changed.set(true);\n        });\n\n        GlobalTimer.scheduleUntil(Duration.ofSeconds(1), false, () -> {\n            if (changed.get()) {\n                Platform.runLater(() -> {\n                    var r = scene.getRoot();\n                    var kb = keyInput.get();\n                    if (r != null) {\n                        // This property is broken on some systems\n                        var acc = Platform.isAccessibilityActive();\n                        r.pseudoClassStateChanged(PseudoClass.getPseudoClass(\"key-navigation\"), kb);\n                        r.pseudoClassStateChanged(PseudoClass.getPseudoClass(\"normal-navigation\"), !kb);\n                        r.pseudoClassStateChanged(PseudoClass.getPseudoClass(\"accessibility-navigation\"), acc);\n                    }\n                    changed.set(false);\n                });\n            }\n\n            return false;\n        });\n\n        scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> {\n            var c = event.getCode();\n            var list = List.of(KeyCode.SPACE, KeyCode.ENTER, KeyCode.SHIFT, KeyCode.TAB);\n            keyInput.set(list.stream().anyMatch(keyCode -> keyCode == c)\n                    || event.getCode().isNavigationKey());\n        });\n        scene.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {\n            keyInput.set(false);\n        });\n    }\n\n    public static void addIcons(Stage stage) {\n        stage.getIcons().clear();\n\n        // This allows for assigning logos even if AppImages has not been initialized yet\n        var dir = OsType.ofLocal() == OsType.MACOS ? \"logo/padded\" : \"logo/full\";\n        AppResources.with(AppResources.MAIN_MODULE, dir, path -> {\n            var size =\n                    switch (OsType.ofLocal()) {\n                        case OsType.Linux ignored -> 128;\n                        case OsType.MacOs ignored -> 128;\n                        case OsType.Windows ignored -> 32;\n                    };\n            stage.getIcons().add(AppImages.loadImage(path.resolve(\"logo_\" + size + \"x\" + size + \".png\")));\n        });\n    }\n\n    public static void addStylesheets(Scene scene) {\n        AppStyle.addStylesheets(scene);\n\n        scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> {\n            if (AppProperties.get().isDevelopmentEnvironment()\n                    && event.getCode().equals(KeyCode.F3)) {\n                AppStyle.reloadStylesheets(scene);\n                TrackEvent.debug(\"Reloaded stylesheets\");\n                event.consume();\n            }\n        });\n        TrackEvent.debug(\"Set stylesheet reload listener\");\n    }\n\n    public static void addClickShield(Stage stage) {\n        if (OsType.ofLocal() != OsType.MACOS) {\n            return;\n        }\n\n        var focusInstant = new SimpleObjectProperty<>(Instant.EPOCH);\n        stage.focusedProperty().subscribe((newValue) -> {\n            if (newValue) {\n                focusInstant.set(Instant.now());\n            }\n        });\n        var blockNextPress = new SimpleBooleanProperty();\n        stage.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {\n            var elapsed = Duration.between(focusInstant.get(), Instant.now());\n            if (elapsed.toMillis() < 50) {\n                blockNextPress.set(true);\n                event.consume();\n            } else {\n                blockNextPress.set(false);\n            }\n        });\n        stage.addEventFilter(MouseEvent.MOUSE_RELEASED, event -> {\n            var elapsed = Duration.between(focusInstant.get(), Instant.now());\n            if (elapsed.toMillis() < 100 && blockNextPress.get()) {\n                event.consume();\n            }\n        });\n        stage.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {\n            var elapsed = Duration.between(focusInstant.get(), Instant.now());\n            if (elapsed.toMillis() < 1000 && blockNextPress.get()) {\n                blockNextPress.set(false);\n                event.consume();\n            }\n        });\n        stage.addEventFilter(MouseEvent.ANY, event -> {\n            if (!stage.isFocused()) {\n                event.consume();\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/core/window/AppWindowTitle.java",
    "content": "package io.xpipe.app.core.window;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppNames;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.update.AppDistributionType;\nimport io.xpipe.app.util.LicenseProvider;\n\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.property.StringProperty;\n\nimport lombok.Getter;\n\npublic class AppWindowTitle {\n\n    @Getter\n    private static final StringProperty title = new SimpleStringProperty(createTitle());\n\n    public static void init() {\n        if (LicenseProvider.get() != null) {\n            var t = LicenseProvider.get().licenseTitle();\n            t.subscribe(ignored -> {\n                title.setValue(createTitle());\n            });\n        }\n\n        var l = AppI18n.activeLanguage();\n        l.subscribe(ignored -> {\n            title.setValue(createTitle());\n        });\n\n        if (AppDistributionType.get() != AppDistributionType.UNKNOWN) {\n            var u = AppDistributionType.get().getUpdateHandler().getPreparedUpdate();\n            u.subscribe(ignored -> {\n                title.setValue(createTitle());\n            });\n        }\n    }\n\n    private static String createTitle() {\n        var t = LicenseProvider.get() != null\n                ? \" \" + LicenseProvider.get().licenseTitle().getValue()\n                : \"\";\n        var base = String.format(\n                AppNames.ofMain().getName() + \"%s (%s)\", t, AppProperties.get().getVersion());\n        var prefix = AppProperties.get().isStaging() ? \"[Public Test Build, Not a proper release] \" : \"\";\n        var dist = AppDistributionType.get();\n        if (dist != AppDistributionType.UNKNOWN) {\n            var u = dist.getUpdateHandler().getPreparedUpdate();\n            var suffix = u.getValue() != null\n                    ? \" \" + AppI18n.get(\"updateReadyTitle\", u.getValue().getVersion())\n                    : \"\";\n            return prefix + base + suffix;\n        } else {\n            return prefix + base;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/CloudSetupProvider.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.core.ModuleLayerLoader;\n\nimport java.util.*;\n\npublic interface CloudSetupProvider {\n\n    List<CloudSetupProvider> ALL = new ArrayList<>();\n\n    static Optional<CloudSetupProvider> byId(String id) {\n        return ALL.stream().filter(d -> d.getId().equalsIgnoreCase(id)).findAny();\n    }\n\n    String getId();\n\n    String getNameKey();\n\n    LabelGraphic getGraphic();\n\n    ScanProvider getScan();\n\n    default void handleUnsupported() {}\n\n    class Loader implements ModuleLayerLoader {\n\n        @Override\n        public void init(ModuleLayer layer) {\n            ALL.addAll(ServiceLoader.load(layer, CloudSetupProvider.class).stream()\n                    .sorted(Comparator.comparing(p -> p.type().getModule().getName()))\n                    .map(p -> p.get())\n                    .toList());\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/ConnectionFileSystem.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.OsFileSystem;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.ShellDialects;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport lombok.Getter;\n\nimport java.io.BufferedInputStream;\nimport java.io.BufferedOutputStream;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.stream.Stream;\n\n@Getter\npublic class ConnectionFileSystem implements FileSystem {\n\n    @JsonIgnore\n    protected final ShellControl shellControl;\n\n    public ConnectionFileSystem(ShellControl shellControl) {\n        this.shellControl = shellControl;\n    }\n\n    @Override\n    public boolean writeInstantIfPossible(FileSystem sourceFs, FilePath sourceFile, FilePath targetFile)\n            throws Exception {\n        return false;\n    }\n\n    @Override\n    public boolean readInstantIfPossible(FilePath sourceFile, FileSystem targetFs, FilePath targetFile)\n            throws Exception {\n        return false;\n    }\n\n    @Override\n    public String getSuffix() {\n        return null;\n    }\n\n    @Override\n    public boolean isRunning() {\n        return shellControl.isRunning(true);\n    }\n\n    @Override\n    public boolean supportsLinkCreation() {\n        return shellControl.getOsType() != OsType.WINDOWS;\n    }\n\n    @Override\n    public boolean supportsOwnerColumn() {\n        return shellControl.getOsType() != OsType.WINDOWS && shellControl.getOsType() != OsType.MACOS;\n    }\n\n    @Override\n    public boolean supportsModeColumn() {\n        return shellControl.getOsType() != OsType.WINDOWS;\n    }\n\n    @Override\n    public boolean supportsDirectorySizes() {\n        return true;\n    }\n\n    @Override\n    public boolean supportsChmod() {\n        return shellControl.getOsType() != OsType.WINDOWS;\n    }\n\n    @Override\n    public boolean supportsChown() {\n        return shellControl.getOsType() != OsType.WINDOWS && shellControl.getOsType() != OsType.MACOS;\n    }\n\n    @Override\n    public boolean supportsChgrp() {\n        return shellControl.getOsType() != OsType.WINDOWS && shellControl.getOsType() != OsType.MACOS;\n    }\n\n    @Override\n    public boolean supportsTerminalOpen() {\n        return true;\n    }\n\n    @Override\n    public boolean supportsTerminalWorkingDirectory() {\n        return true;\n    }\n\n    @Override\n    public Optional<ShellControl> getRawShellControl() {\n        return Optional.of(shellControl);\n    }\n\n    @Override\n    public ShellControl getTerminalShellControl() {\n        return shellControl;\n    }\n\n    @Override\n    public void chmod(FilePath path, String mode, boolean recursive) throws Exception {\n        shellControl\n                .command(CommandBuilder.of()\n                        .add(\"chmod\")\n                        .addIf(recursive, \"-R\")\n                        .addLiteral(mode)\n                        .addFile(path))\n                .execute();\n    }\n\n    @Override\n    public void chown(FilePath path, String uid, boolean recursive) throws Exception {\n        shellControl\n                .command(CommandBuilder.of()\n                        .add(\"chown\")\n                        .addIf(recursive, \"-R\")\n                        .addLiteral(uid)\n                        .addFile(path))\n                .execute();\n    }\n\n    @Override\n    public void chgrp(FilePath path, String gid, boolean recursive) throws Exception {\n        shellControl\n                .command(CommandBuilder.of()\n                        .add(\"chgrp\")\n                        .addIf(recursive, \"-R\")\n                        .addLiteral(gid)\n                        .addFile(path))\n                .execute();\n    }\n\n    @Override\n    public void kill() {\n        shellControl.killExternal();\n    }\n\n    @Override\n    public void cd(FilePath dir) throws Exception {\n        shellControl.view().cd(dir);\n    }\n\n    @Override\n    public boolean requiresReinit() {\n        return !shellControl.isRunning(true) || shellControl.isAnyStreamClosed();\n    }\n\n    @Override\n    public void reinitIfNeeded() throws Exception {\n        shellControl.start();\n        if (shellControl.isAnyStreamClosed()) {\n            shellControl.restart();\n        }\n    }\n\n    @Override\n    public String getFileSeparator() {\n        return OsFileSystem.of(shellControl.getOsType()).getFileSystemSeparator();\n    }\n\n    @Override\n    public FilePath makeFileSystemCompatible(FilePath filePath) {\n        return OsFileSystem.of(shellControl.getOsType()).makeFileSystemCompatible(filePath);\n    }\n\n    @Override\n    public Optional<FilePath> pwd() throws Exception {\n        return Optional.ofNullable(shellControl.view().pwd());\n    }\n\n    @Override\n    public FileSystem createTransferOptimizedFileSystem() throws Exception {\n        // For local, we have our optimized streams regardless\n        if (!shellControl.isLocal() && shellControl.getShellDialect() == ShellDialects.CMD) {\n            var pwsh = shellControl\n                    .view()\n                    .findProgram(ShellDialects.POWERSHELL_CORE.getExecutableName())\n                    .isPresent();\n            if (pwsh) {\n                return new ConnectionFileSystem(\n                        shellControl.subShell(ShellDialects.POWERSHELL_CORE).start());\n            }\n\n            var powershell = shellControl\n                    .view()\n                    .findProgram(ShellDialects.POWERSHELL.getExecutableName())\n                    .isPresent();\n            if (powershell) {\n                return new ConnectionFileSystem(\n                        shellControl.subShell(ShellDialects.POWERSHELL).start());\n            }\n        }\n\n        return this;\n    }\n\n    @Override\n    public long getFileSize(FilePath file) throws Exception {\n        return Long.parseLong(shellControl\n                .getShellDialect()\n                .queryFileSize(shellControl, file.toString())\n                .readStdoutOrThrow());\n    }\n\n    @Override\n    public long getDirectorySize(FilePath file) throws Exception {\n        return shellControl.getShellDialect().queryDirectorySize(shellControl, file.toString());\n    }\n\n    @Override\n    public Optional<ShellControl> getShell() {\n        return Optional.of(shellControl);\n    }\n\n    @Override\n    public FileSystem open() throws Exception {\n        shellControl.start();\n\n        var d = shellControl.getShellDialect().getDumbMode();\n        if (!d.supportsAnyPossibleInteraction()) {\n            shellControl.close();\n            try {\n                d.throwIfUnsupported();\n            } catch (Exception e) {\n                throw ErrorEventFactory.expected(e);\n            }\n        }\n\n        if (!shellControl.getTtyState().isPreservesOutput()\n                || !shellControl.getTtyState().isSupportsInput()) {\n            var ex = new UnsupportedOperationException(\n                    \"Shell has a PTY allocated and as a result does not support file system operations.\");\n            ErrorEventFactory.preconfigure(\n                    ErrorEventFactory.fromThrowable(ex).documentationLink(DocumentationLink.TTY));\n            throw ex;\n        }\n\n        shellControl.checkLicenseOrThrow();\n\n        return this;\n    }\n\n    @Override\n    public InputStream openInput(FilePath file) throws Exception {\n        if (shellControl.isLocal()) {\n            return new BufferedInputStream(Files.newInputStream(file.asLocalPath()));\n        }\n\n        return shellControl\n                .getShellDialect()\n                .getFileReadCommand(shellControl, file.toString())\n                .startExternalStdout();\n    }\n\n    @Override\n    public OutputStream openOutput(FilePath file, long totalBytes) throws Exception {\n        if (shellControl.isLocal()) {\n            return new BufferedOutputStream(Files.newOutputStream(file.asLocalPath()));\n        }\n\n        var cmd =\n                shellControl.getShellDialect().createStreamFileWriteCommand(shellControl, file.toString(), totalBytes);\n        return cmd.startExternalStdin();\n    }\n\n    @Override\n    public boolean fileExists(FilePath file) throws Exception {\n        try (var pc = shellControl\n                .getShellDialect()\n                .createFileExistsCommand(shellControl, file.toString())\n                .start()) {\n            return pc.discardAndCheckExit();\n        }\n    }\n\n    @Override\n    public void delete(FilePath file) throws Exception {\n        try (var pc = shellControl\n                .getShellDialect()\n                .deleteFileOrDirectory(shellControl, file.toString())\n                .start()) {\n            pc.discardOrThrow();\n        }\n    }\n\n    @Override\n    public void copy(FilePath file, FilePath newFile) throws Exception {\n        try (var pc = shellControl\n                .getShellDialect()\n                .getFileCopyCommand(shellControl, file.toString(), newFile.toString())\n                .start()) {\n            pc.discardOrThrow();\n        }\n    }\n\n    @Override\n    public void move(FilePath file, FilePath newFile) throws Exception {\n        try (var pc = shellControl\n                .getShellDialect()\n                .getFileMoveCommand(shellControl, file.toString(), newFile.toString())\n                .start()) {\n            pc.discardOrThrow();\n        }\n    }\n\n    @Override\n    public void mkdirs(FilePath file) throws Exception {\n        try (var pc = shellControl\n                .command(\n                        CommandBuilder.ofFunction(proc -> proc.getShellDialect().getMkdirsCommand(file.toString())))\n                .start()) {\n            pc.discardOrThrow();\n        }\n    }\n\n    @Override\n    public void touch(FilePath file) throws Exception {\n        try (var pc = shellControl\n                .getShellDialect()\n                .getFileTouchCommand(shellControl, file.toString())\n                .start()) {\n            pc.discardOrThrow();\n        }\n    }\n\n    @Override\n    public void symbolicLink(FilePath linkFile, FilePath targetFile) throws Exception {\n        try (var pc = shellControl\n                .getShellDialect()\n                .symbolicLink(shellControl, linkFile.toString(), targetFile.toString())\n                .start()) {\n            pc.discardOrThrow();\n        }\n    }\n\n    @Override\n    public boolean directoryExists(FilePath file) throws Exception {\n        return shellControl\n                .getShellDialect()\n                .directoryExists(shellControl, file.toString())\n                .executeAndCheck();\n    }\n\n    @Override\n    public void directoryAccessible(FilePath file) throws Exception {\n        var current = shellControl.executeSimpleStringCommand(\n                shellControl.getShellDialect().getPrintWorkingDirectoryCommand());\n        shellControl\n                .command(shellControl.getShellDialect().getCdCommand(file.toString()))\n                .execute();\n        shellControl\n                .command(shellControl.getShellDialect().getCdCommand(current))\n                .execute();\n    }\n\n    @Override\n    public Optional<FileEntry> getFileInfo(FilePath file) throws Exception {\n        try (var stream = shellControl.getShellDialect().listFiles(this, shellControl, file.toString(), false)) {\n            var l = stream.toList();\n            return l.stream().findFirst();\n        }\n    }\n\n    @Override\n    public Stream<FileEntry> listFiles(FileSystem system, FilePath file) throws Exception {\n        return shellControl.getShellDialect().listFiles(system, shellControl, file.toString(), true);\n    }\n\n    @Override\n    public List<FilePath> listRoots() throws Exception {\n        return shellControl\n                .getShellDialect()\n                .listRoots(shellControl)\n                .map(s -> FilePath.of(s))\n                .toList();\n    }\n\n    @Override\n    public List<FilePath> listCommonDirectories() throws Exception {\n        var home = shellControl.view().userHome();\n        if (shellControl.getOsType() == OsType.WINDOWS) {\n            return List.of(home, home.join(\"Documents\"), home.join(\"Downloads\"), home.join(\"Desktop\"));\n        } else if (shellControl.getOsType() == OsType.MACOS) {\n            var list = List.of(\n                    home,\n                    home.join(\"Downloads\"),\n                    home.join(\"Documents\"),\n                    home.join(\"Desktop\"),\n                    FilePath.of(\"/Applications\"),\n                    FilePath.of(\"/Library\"),\n                    FilePath.of(\"/System\"),\n                    FilePath.of(\"/etc\"),\n                    FilePath.of(\"/tmp\"));\n            return list;\n        } else {\n            var list = new ArrayList<>(List.of(\n                    home,\n                    home.join(\"Downloads\"),\n                    home.join(\"Documents\"),\n                    FilePath.of(\"/etc\"),\n                    shellControl.getSystemTemporaryDirectory(),\n                    FilePath.of(\"/var\")));\n            var parentHome = home.getParent();\n            if (parentHome != null && !parentHome.toString().equals(\"/\")) {\n                list.add(3, parentHome);\n            }\n            return list;\n        }\n    }\n\n    @Override\n    public void close() {\n        // In case the shell control is already in an invalid state, this operation might fail\n        // Since we are only closing, just swallow all exceptions\n        try {\n            shellControl.close();\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).omit().expected().handle();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/ContainerImageStore.java",
    "content": "package io.xpipe.app.ext;\n\npublic interface ContainerImageStore {\n\n    @SuppressWarnings(\"unused\")\n    String getImageName();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/ContainerStoreState.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.process.ShellStoreState;\n\nimport lombok.AccessLevel;\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.experimental.FieldDefaults;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\n@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)\n@Getter\n@EqualsAndHashCode(callSuper = true)\n@SuperBuilder(toBuilder = true)\n@Jacksonized\npublic class ContainerStoreState extends ShellStoreState {\n\n    String imageName;\n    String containerState;\n    Boolean shellMissing;\n\n    @Override\n    public DataStoreState mergeCopy(DataStoreState newer) {\n        var n = (ContainerStoreState) newer;\n        var b = toBuilder();\n        mergeBuilder(n, b);\n        return b.build();\n    }\n\n    protected void mergeBuilder(ContainerStoreState css, ContainerStoreStateBuilder<?, ?> b) {\n        super.mergeBuilder(css, b);\n        b.containerState(useNewer(containerState, css.getContainerState()));\n        b.imageName(useNewer(imageName, css.getImageName()));\n        b.shellMissing(useNewer(shellMissing, css.getShellMissing()));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/CountGroupStoreProvider.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.comp.StoreSection;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.value.ObservableValue;\n\npublic interface CountGroupStoreProvider extends DataStoreProvider {\n\n    @Override\n    default boolean includeInConnectionCount() {\n        return false;\n    }\n\n    @Override\n    default ObservableValue<String> informationString(StoreSection section) {\n        return Bindings.createStringBinding(\n                () -> {\n                    var all = section.getAllChildren().getList();\n                    var shown = section.getShownChildren().getList();\n                    if (shown.size() == 0) {\n                        return AppI18n.get(\"noConnections\");\n                    }\n\n                    var string = all.size() == shown.size() ? all.size() : shown.size() + \"/\" + all.size();\n                    return all.size() > 0\n                            ? (all.size() == 1\n                                    ? AppI18n.get(\"hasConnection\", string)\n                                    : AppI18n.get(\"hasConnections\", string))\n                            : AppI18n.get(\"noConnections\");\n                },\n                section.getShownChildren().getList(),\n                section.getAllChildren().getList(),\n                AppI18n.activeLanguage());\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/DataStorageExtensionProvider.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.core.ModuleLayerLoader;\n\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.ServiceLoader;\nimport java.util.stream.Collectors;\n\npublic abstract class DataStorageExtensionProvider {\n\n    private static List<DataStorageExtensionProvider> ALL;\n\n    public static List<DataStorageExtensionProvider> getAll() {\n        return ALL;\n    }\n\n    public void storageInit() {}\n\n    public static class Loader implements ModuleLayerLoader {\n\n        @Override\n        public void init(ModuleLayer layer) {\n            ALL = ServiceLoader.load(layer, DataStorageExtensionProvider.class).stream()\n                    .map(ServiceLoader.Provider::get)\n                    .sorted(Comparator.comparing(\n                            scanProvider -> scanProvider.getClass().getName()))\n                    .collect(Collectors.toList());\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/DataStore.java",
    "content": "package io.xpipe.app.ext;\n\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\npublic interface DataStore {\n\n    default boolean isComplete() {\n        try {\n            checkComplete();\n            return true;\n        } catch (Throwable ignored) {\n            return false;\n        }\n    }\n\n    default void checkComplete() throws Throwable {}\n\n    /**\n     * Casts this instance to the required type without checking whether a cast is possible.\n     */\n    @SuppressWarnings(\"unchecked\")\n    default <DS extends DataStore> DS asNeeded() {\n        return (DS) this;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/DataStoreCreationCategory.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.storage.DataStorage;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Getter;\n\nimport java.util.UUID;\n\n@AllArgsConstructor\n@Getter\npublic enum DataStoreCreationCategory {\n    HOST(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID),\n    SHELL(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID),\n    COMMAND(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID),\n    TUNNEL(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID),\n    SERVICE(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID),\n    SCRIPT(DataStorage.ALL_SCRIPTS_CATEGORY_UUID),\n    CLUSTER(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID),\n    DESKTOP(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID),\n    SERIAL(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID),\n    MACRO(DataStorage.ALL_MACROS_CATEGORY_UUID),\n    FILE_SYSTEM(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID),\n    IDENTITY(DataStorage.ALL_IDENTITIES_CATEGORY_UUID);\n\n    private final UUID category;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppImages;\nimport io.xpipe.app.hub.comp.StoreEntryComp;\nimport io.xpipe.app.hub.comp.StoreEntryWrapper;\nimport io.xpipe.app.hub.comp.StoreSection;\nimport io.xpipe.app.storage.DataStoreCategory;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.core.FailableRunnable;\n\nimport javafx.beans.property.BooleanProperty;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableBooleanValue;\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.Arrays;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.UUID;\n\npublic interface DataStoreProvider {\n\n    default boolean showIncompleteInfo() {\n        return false;\n    }\n\n    default boolean includeInConnectionCount() {\n        return getUsageCategory() != DataStoreUsageCategory.GROUP;\n    }\n\n    default boolean canConfigure() {\n        var m = getClass().getDeclaredMethods();\n        return Arrays.stream(m).anyMatch(method -> method.getName().equals(\"guiDialog\"));\n    }\n\n    default DocumentationLink getHelpLink() {\n        return null;\n    }\n\n    default boolean canMoveCategories() {\n        return true;\n    }\n\n    default UUID getTargetCategory(DataStore store, UUID target) {\n        return target;\n    }\n\n    default int getOrderPriority() {\n        return 0;\n    }\n\n    default boolean showProviderChoice() {\n        return true;\n    }\n\n    default boolean shouldShow(StoreEntryWrapper w) {\n        return true;\n    }\n\n    default Comparator<StoreSection> getComparator() {\n        return null;\n    }\n\n    default void onParentRefresh(DataStoreEntry entry) {}\n\n    default void onChildrenRefresh(DataStoreEntry entry) {}\n\n    default ObservableBooleanValue busy(StoreEntryWrapper wrapper) {\n        return new SimpleBooleanProperty(false);\n    }\n\n    default void validate() {\n        if (getUsageCategory() == null) {\n            throw ExtensionException.corrupt(\"Provider %s does not have the usage category\".formatted(getId()));\n        }\n    }\n\n    default FailableRunnable<Exception> activateAction(DataStoreEntry store) {\n        return null;\n    }\n\n    default FailableRunnable<Exception> launch(DataStoreEntry store) {\n        return null;\n    }\n\n    default FailableRunnable<Exception> launchBrowser(\n            BrowserFullSessionModel sessionModel, DataStoreEntry store, BooleanProperty busy) {\n        return null;\n    }\n\n    default String displayName(DataStoreEntry entry) {\n        return entry.getName();\n    }\n\n    default List<String> getSearchableTerms(DataStore store) {\n        return List.of();\n    }\n\n    default StoreEntryComp customEntryComp(StoreSection s, boolean preferLarge) {\n        return StoreEntryComp.create(s, null, preferLarge);\n    }\n\n    default boolean shouldShowScan() {\n        return true;\n    }\n\n    default BaseRegionBuilder<?, ?> stateDisplay(StoreEntryWrapper w) {\n        return RegionBuilder.empty();\n    }\n\n    default boolean canConnectDuringCreation() {\n        return false;\n    }\n\n    default DataStoreCreationCategory getCreationCategory() {\n        return null;\n    }\n\n    default DataStoreUsageCategory getUsageCategory() {\n        var cc = getCreationCategory();\n        if (cc == DataStoreCreationCategory.SHELL || cc == DataStoreCreationCategory.HOST) {\n            return DataStoreUsageCategory.SHELL;\n        }\n\n        if (cc == DataStoreCreationCategory.COMMAND) {\n            return DataStoreUsageCategory.COMMAND;\n        }\n\n        if (cc == DataStoreCreationCategory.SCRIPT) {\n            return DataStoreUsageCategory.SCRIPT;\n        }\n\n        if (cc == DataStoreCreationCategory.SERIAL) {\n            return DataStoreUsageCategory.SERIAL;\n        }\n\n        return null;\n    }\n\n    default boolean canClone() {\n        return getCreationCategory() != null;\n    }\n\n    default DataStoreEntry getDisplayParent(DataStoreEntry store) {\n        return getSyntheticParent(store);\n    }\n\n    default DataStoreEntry getSyntheticParent(DataStoreEntry store) {\n        return null;\n    }\n\n    default GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {\n        return null;\n    }\n\n    default void init() {}\n\n    default void reset() {}\n\n    default boolean isSyncableFromLocalMachine() {\n        return false;\n    }\n\n    default boolean isSyncable(DataStoreEntry entry) {\n        return true;\n    }\n\n    default String summaryString(StoreEntryWrapper wrapper) {\n        return null;\n    }\n\n    default ObservableValue<String> informationString(StoreSection section) {\n        return new SimpleStringProperty(null);\n    }\n\n    default ObservableValue<String> i18n(String key) {\n        return AppI18n.observable(getId() + \".\" + key);\n    }\n\n    default ObservableValue<String> displayName() {\n        return i18n(\"displayName\");\n    }\n\n    default ObservableValue<String> displayDescription() {\n        return i18n(\"displayDescription\");\n    }\n\n    default String getModuleName() {\n        var n = getClass().getModule().getName();\n        var i = n.lastIndexOf('.');\n        return i != -1 ? n.substring(i + 1) : n;\n    }\n\n    default String getDisplayIconFileName(DataStore store) {\n        var png = getModuleName() + \":\" + getId() + \"_icon.png\";\n        if (AppImages.hasImage(png)) {\n            return png;\n        }\n\n        return getModuleName() + \":\" + getId() + \"_icon.svg\";\n    }\n\n    default DataStore defaultStore(DataStoreCategory category) {\n        return null;\n    }\n\n    String getId();\n\n    List<Class<?>> getStoreClasses();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/DataStoreProviders.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.core.JacksonMapper;\nimport io.xpipe.core.ModuleLayerLoader;\n\nimport com.fasterxml.jackson.databind.jsontype.NamedType;\n\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.ServiceLoader;\nimport java.util.stream.Collectors;\n\npublic class DataStoreProviders {\n\n    private static List<DataStoreProvider> ALL;\n\n    public static void init() {\n        DataStoreProviders.getAll().forEach(dataStoreProvider -> {\n            try {\n                dataStoreProvider.init();\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).omit().handle();\n            }\n        });\n    }\n\n    public static void reset() {\n        DataStoreProviders.getAll().forEach(dataStoreProvider -> {\n            try {\n                dataStoreProvider.reset();\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).omit().handle();\n            }\n        });\n    }\n\n    public static Optional<DataStoreProvider> byId(String id) {\n        if (ALL == null) {\n            throw new IllegalStateException(\"Not initialized\");\n        }\n\n        return ALL.stream().filter(d -> d.getId().equalsIgnoreCase(id)).findAny();\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <T extends DataStoreProvider> Optional<T> byStoreIfPresent(DataStore store) {\n        if (ALL == null) {\n            throw new IllegalStateException(\"Not initialized\");\n        }\n\n        return (Optional<T>) ALL.stream()\n                .filter(d -> d.getStoreClasses().contains(store.getClass()))\n                .findAny();\n    }\n\n    public static <T extends DataStoreProvider> T byStore(DataStore store) {\n        return DataStoreProviders.<T>byStoreIfPresent(store)\n                .orElseThrow(() -> new IllegalArgumentException(\"Unknown store class\"));\n    }\n\n    public static List<DataStoreProvider> getAll() {\n        return ALL;\n    }\n\n    public static class Loader implements ModuleLayerLoader {\n\n        @Override\n        public void init(ModuleLayer layer) {\n            TrackEvent.info(\"Loading extension providers ...\");\n            ALL = ServiceLoader.load(layer, DataStoreProvider.class).stream()\n                    .map(ServiceLoader.Provider::get)\n                    .collect(Collectors.toList());\n            ALL.removeIf(p -> {\n                try {\n                    p.validate();\n                    return false;\n                } catch (Throwable e) {\n                    ErrorEventFactory.fromThrowable(e).handle();\n                    return true;\n                }\n            });\n\n            for (DataStoreProvider p : getAll()) {\n                JacksonMapper.configure(objectMapper -> {\n                    for (Class<?> storeClass : p.getStoreClasses()) {\n                        objectMapper.registerSubtypes(new NamedType(storeClass));\n                    }\n                });\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/DataStoreState.java",
    "content": "package io.xpipe.app.ext;\n\nimport lombok.EqualsAndHashCode;\nimport lombok.experimental.SuperBuilder;\n\n@SuperBuilder(toBuilder = true)\n@EqualsAndHashCode\npublic abstract class DataStoreState {\n\n    public DataStoreState() {}\n\n    protected static <T> T useNewer(T older, T newer) {\n        return newer != null ? newer : older;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public <DS extends DataStoreState> DS asNeeded() {\n        return (DS) this;\n    }\n\n    public DataStoreState mergeCopy(DataStoreState newer) {\n        return this;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/DataStoreUsageCategory.java",
    "content": "package io.xpipe.app.ext;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\npublic enum DataStoreUsageCategory {\n    @JsonProperty(\"shell\")\n    SHELL,\n    @JsonProperty(\"tunnel\")\n    TUNNEL,\n    @JsonProperty(\"script\")\n    SCRIPT,\n    @JsonProperty(\"command\")\n    COMMAND,\n    @JsonProperty(\"desktop\")\n    DESKTOP,\n    @JsonProperty(\"group\")\n    GROUP,\n    @JsonProperty(\"serial\")\n    SERIAL,\n    @JsonProperty(\"identity\")\n    IDENTITY,\n    @SuppressWarnings(\"unused\")\n    @JsonProperty(\"macro\")\n    MACRO,\n    @JsonProperty(\"fileSystem\")\n    FILE_SYSTEM\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/EnabledStoreState.java",
    "content": "package io.xpipe.app.ext;\n\nimport lombok.AccessLevel;\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.experimental.FieldDefaults;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\n@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)\n@Getter\n@EqualsAndHashCode(callSuper = true)\n@SuperBuilder(toBuilder = true)\n@Jacksonized\npublic class EnabledStoreState extends DataStoreState {\n\n    boolean enabled;\n\n    @Override\n    public DataStoreState mergeCopy(DataStoreState newer) {\n        var n = (EnabledStoreState) newer;\n        return EnabledStoreState.builder().enabled(enabled || n.enabled).build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/ExpandedLifecycleStore.java",
    "content": "package io.xpipe.app.ext;\n\npublic interface ExpandedLifecycleStore extends DataStore {\n\n    default void finalizeStore() throws Exception {}\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/ExtensionException.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.core.AppInstallation;\nimport io.xpipe.app.core.AppNames;\n\npublic class ExtensionException extends RuntimeException {\n\n    public ExtensionException() {}\n\n    private ExtensionException(String message, Throwable cause) {\n        super(message, cause);\n    }\n\n    public static ExtensionException corrupt(String message, Throwable cause) {\n        try {\n            var loc = AppInstallation.ofCurrent().getBaseInstallationPath();\n            var full = message + \".\\n\\n\" + \"Please check whether the \"\n                    + AppNames.ofCurrent().getName() + \" installation data at \" + loc + \" is corrupted.\";\n            return new ExtensionException(full, cause);\n        } catch (Throwable t) {\n            var full = message + \".\\n\\n\" + \"Please check whether the \"\n                    + AppNames.ofCurrent().getName() + \" installation data is corrupted.\";\n            return new ExtensionException(full, cause);\n        }\n    }\n\n    public static ExtensionException corrupt(String message) {\n        return corrupt(message, null);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/FileEntry.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.core.FilePath;\n\nimport lombok.NonNull;\nimport lombok.Setter;\nimport lombok.Value;\nimport lombok.experimental.NonFinal;\n\nimport java.time.Instant;\nimport java.util.OptionalLong;\n\n@Value\n@NonFinal\npublic class FileEntry {\n    FileSystem fileSystem;\n    Instant date;\n    FileInfo info;\n\n    @NonNull\n    FileKind kind;\n\n    @NonFinal\n    @Setter\n    String size;\n\n    @NonNull\n    @NonFinal\n    @Setter\n    FilePath path;\n\n    public FileEntry(\n            FileSystem fileSystem,\n            @NonNull FilePath path,\n            Instant date,\n            String size,\n            FileInfo info,\n            @NonNull FileKind kind) {\n        this.fileSystem = fileSystem;\n        this.kind = kind;\n        this.path = kind == FileKind.DIRECTORY ? FilePath.of(path.toDirectory().toString()) : path;\n        this.date = date;\n        this.info = info;\n        this.size = size;\n    }\n\n    public static FileEntry ofDirectory(FileSystem fileSystem, FilePath path) {\n        return new FileEntry(fileSystem, path, Instant.now(), null, null, FileKind.DIRECTORY);\n    }\n\n    public OptionalLong getFileSizeLong() {\n        if (size == null) {\n            return OptionalLong.empty();\n        }\n\n        try {\n            var l = Long.parseLong(size);\n            return OptionalLong.of(l);\n        } catch (NumberFormatException e) {\n            return OptionalLong.empty();\n        }\n    }\n\n    public FileEntry resolved() {\n        return this;\n    }\n\n    public String getName() {\n        return path.getFileName();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/FileInfo.java",
    "content": "package io.xpipe.app.ext;\n\nimport lombok.Value;\n\npublic sealed interface FileInfo permits FileInfo.Windows, FileInfo.Unix {\n\n    boolean explicitlyHidden();\n\n    boolean possiblyExecutable();\n\n    @Value\n    class Windows implements FileInfo {\n\n        String attributes;\n\n        @Override\n        public boolean explicitlyHidden() {\n            return attributes.contains(\"h\");\n        }\n\n        @Override\n        public boolean possiblyExecutable() {\n            return true;\n        }\n    }\n\n    @Value\n    class Unix implements FileInfo {\n\n        String permissions;\n        Integer uid;\n        String user;\n        Integer gid;\n        String group;\n\n        @Override\n        public boolean explicitlyHidden() {\n            return false;\n        }\n\n        @Override\n        public boolean possiblyExecutable() {\n            return permissions == null || permissions.contains(\"x\");\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/FileKind.java",
    "content": "package io.xpipe.app.ext;\n\npublic enum FileKind {\n    FILE,\n    DIRECTORY,\n    LINK,\n    OTHER\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/FileSystem.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.core.FilePath;\n\nimport java.io.Closeable;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.function.Predicate;\nimport java.util.stream.Stream;\n\npublic interface FileSystem extends Closeable, AutoCloseable {\n\n    boolean writeInstantIfPossible(FileSystem sourceFs, FilePath sourceFile, FilePath targetFile) throws Exception;\n\n    boolean readInstantIfPossible(FilePath sourceFile, FileSystem targetFs, FilePath targetFile) throws Exception;\n\n    String getSuffix();\n\n    boolean isRunning();\n\n    boolean supportsLinkCreation();\n\n    boolean supportsOwnerColumn();\n\n    boolean supportsModeColumn();\n\n    boolean supportsDirectorySizes();\n\n    boolean supportsChmod();\n\n    boolean supportsChown();\n\n    boolean supportsChgrp();\n\n    boolean supportsTerminalOpen();\n\n    boolean supportsTerminalWorkingDirectory();\n\n    Optional<ShellControl> getRawShellControl();\n\n    ShellControl getTerminalShellControl();\n\n    void chmod(FilePath path, String mode, boolean recursive) throws Exception;\n\n    void chown(FilePath path, String uid, boolean recursive) throws Exception;\n\n    void chgrp(FilePath path, String gid, boolean recursive) throws Exception;\n\n    void kill();\n\n    void cd(FilePath dir) throws Exception;\n\n    boolean requiresReinit();\n\n    void reinitIfNeeded() throws Exception;\n\n    String getFileSeparator();\n\n    FilePath makeFileSystemCompatible(FilePath filePath);\n\n    Optional<FilePath> pwd() throws Exception;\n\n    FileSystem createTransferOptimizedFileSystem() throws Exception;\n\n    long getFileSize(FilePath file) throws Exception;\n\n    long getDirectorySize(FilePath file) throws Exception;\n\n    Optional<ShellControl> getShell();\n\n    FileSystem open() throws Exception;\n\n    InputStream openInput(FilePath file) throws Exception;\n\n    OutputStream openOutput(FilePath file, long totalBytes) throws Exception;\n\n    boolean fileExists(FilePath file) throws Exception;\n\n    void delete(FilePath file) throws Exception;\n\n    void copy(FilePath file, FilePath newFile) throws Exception;\n\n    void move(FilePath file, FilePath newFile) throws Exception;\n\n    void mkdirs(FilePath file) throws Exception;\n\n    void touch(FilePath file) throws Exception;\n\n    void symbolicLink(FilePath linkFile, FilePath targetFile) throws Exception;\n\n    boolean directoryExists(FilePath file) throws Exception;\n\n    void directoryAccessible(FilePath file) throws Exception;\n\n    Optional<FileEntry> getFileInfo(FilePath file) throws Exception;\n\n    Stream<FileEntry> listFiles(FileSystem system, FilePath file) throws Exception;\n\n    default List<FileEntry> listFilesRecursively(FileSystem system, FilePath file) throws Exception {\n        var all = new ArrayList<FileEntry>();\n        traverseFilesRecursively(system, file, all::add);\n        return all;\n    }\n\n    default void traverseFilesRecursively(FileSystem system, FilePath file, Predicate<FileEntry> visitor)\n            throws Exception {\n        List<FileEntry> base;\n        try (var filesStream = listFiles(system, file)) {\n            base = filesStream.toList();\n        }\n\n        if (base.size() != 1) {\n            for (FileEntry fileEntry : base) {\n                // False will cancel the whole traversal\n                if (!visitor.test(fileEntry)) {\n                    return;\n                }\n\n                if (fileEntry.getKind() != FileKind.DIRECTORY) {\n                    continue;\n                }\n\n                traverseFilesRecursively(system, fileEntry.getPath(), visitor);\n            }\n            return;\n        }\n\n        // Optimize this call to use tail recursion if possible\n\n        if (!visitor.test(base.getFirst()) || base.getFirst().getKind() != FileKind.DIRECTORY) {\n            return;\n        }\n\n        traverseFilesRecursively(system, base.getFirst().getPath(), visitor);\n    }\n\n    List<FilePath> listRoots() throws Exception;\n\n    List<FilePath> listCommonDirectories() throws Exception;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/FileSystemStore.java",
    "content": "package io.xpipe.app.ext;\n\npublic interface FileSystemStore extends DataStore {\n\n    FileSystem createFileSystem() throws Exception;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/FixedChildStore.java",
    "content": "package io.xpipe.app.ext;\n\nimport java.util.OptionalInt;\n\npublic interface FixedChildStore extends DataStore {\n\n    OptionalInt getFixedId();\n\n    default FixedChildStore merge(FixedChildStore other) {\n        return this;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/FixedHierarchyStore.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport java.util.List;\n\npublic interface FixedHierarchyStore extends DataStore {\n\n    default boolean canManuallyRefresh() {\n        return true;\n    }\n\n    default boolean removeLeftovers() {\n        return true;\n    }\n\n    List<? extends DataStoreEntryRef<? extends FixedChildStore>> listChildren() throws Exception;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/GroupStore.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\npublic interface GroupStore<T extends DataStore> extends DataStore {\n\n    DataStoreEntryRef<? extends T> getParent();\n\n    @Override\n    default void checkComplete() throws Throwable {\n        var p = getParent();\n        if (p != null) {\n            p.checkComplete();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/GuiDialog.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.platform.OptionsBuilder;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Value;\n\n@Value\n@AllArgsConstructor\npublic class GuiDialog {\n\n    OptionsBuilder options;\n    Runnable onFinish;\n\n    public GuiDialog(OptionsBuilder options) {\n        this.options = options;\n        this.onFinish = null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/HostAddress.java",
    "content": "package io.xpipe.app.ext;\n\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.NonNull;\n\nimport java.util.List;\n\n@EqualsAndHashCode\npublic class HostAddress {\n\n    private final String value;\n\n    @Getter\n    private final List<String> available;\n\n    private HostAddress(String value, List<String> available) {\n        if (value == null || value.isEmpty()) {\n            throw new IllegalArgumentException(\"Host address cannot be null or empty\");\n        }\n\n        if (available.stream().anyMatch(s -> s == null || s.isEmpty())) {\n            throw new IllegalArgumentException(\"Host address cannot be null or empty\");\n        }\n\n        this.value = value;\n        this.available = available;\n    }\n\n    public static HostAddress empty() {\n        return new HostAddress(\"unknown\", List.of(\"unknown\"));\n    }\n\n    public static HostAddress of(String host) {\n        if (host == null) {\n            return null;\n        }\n\n        return new HostAddress(host.strip(), List.of(host));\n    }\n\n    public static HostAddress of(@NonNull List<String> addresses) {\n        return new HostAddress(\n                addresses.getFirst().strip(),\n                addresses.stream().map(s -> s.strip()).toList());\n    }\n\n    public static HostAddress of(String host, @NonNull List<String> addresses) {\n        if (host == null) {\n            return null;\n        }\n\n        return new HostAddress(\n                host.strip(), addresses.stream().map(s -> s.strip()).toList());\n    }\n\n    public HostAddress withValue(String value) {\n        if (value == null || !available.contains(value)) {\n            return this;\n        }\n\n        return new HostAddress(value, this.available);\n    }\n\n    public boolean isSingle() {\n        return available.size() == 1;\n    }\n\n    @Override\n    public String toString() {\n        return value;\n    }\n\n    public String get() {\n        return value;\n    }\n\n    public int getSelectedIndex() {\n        return available.indexOf(value);\n    }\n\n    public HostAddress mergeWithIndex(HostAddress newer) {\n        var index = getSelectedIndex();\n        if (index < newer.getAvailable().size()) {\n            return new HostAddress(newer.getAvailable().get(index), newer.getAvailable());\n        } else {\n            return newer;\n        }\n    }\n\n    public boolean isEmpty() {\n        return available.isEmpty() || available.getFirst().equals(\"unknown\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/InternalCacheDataStore.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.storage.DataStateHandler;\n\npublic interface InternalCacheDataStore extends DataStore {\n\n    default <T> T getCache(String key, Class<T> c, T def) {\n        return DataStateHandler.get().getCache(this, key, c, () -> def);\n    }\n\n    default void setCache(String key, Object val) {\n        DataStateHandler.get().putCache(this, key, val);\n    }\n\n    default boolean canCacheToStorage() {\n        return DataStateHandler.get().canCacheToStorage(this);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/LinkFileEntry.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.core.FilePath;\n\nimport lombok.EqualsAndHashCode;\nimport lombok.NonNull;\nimport lombok.Value;\n\nimport java.time.Instant;\n\n@Value\n@EqualsAndHashCode(callSuper = true)\npublic class LinkFileEntry extends FileEntry {\n\n    @NonNull\n    FileEntry target;\n\n    public LinkFileEntry(\n            FileSystem fileSystem,\n            @NonNull FilePath path,\n            Instant date,\n            String size,\n            @NonNull FileInfo info,\n            @NonNull FileEntry target) {\n        super(fileSystem, path, date, size, info, FileKind.LINK);\n        this.target = target;\n    }\n\n    public FileEntry resolved() {\n        return target;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/LocalStore.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.ShellStoreState;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Value;\n\n@JsonTypeName(\"local\")\n@Value\npublic class LocalStore implements NetworkTunnelStore, ShellStore, StatefulDataStore<ShellStoreState> {\n\n    @Override\n    public Class<ShellStoreState> getStateClass() {\n        return ShellStoreState.class;\n    }\n\n    @Override\n    public ShellControlFunction shellFunction() {\n        return new ShellControlFunction() {\n            @Override\n            public ShellControl control() {\n                var pc = ProcessControlProvider.get().createLocalProcessControl(true);\n                pc.withSourceStore(LocalStore.this);\n                pc.withShellStateInit(LocalStore.this);\n                pc.withShellStateFail(LocalStore.this);\n                return pc;\n            }\n        };\n    }\n\n    @Override\n    public DataStoreEntryRef<?> getNetworkParent() {\n        return null;\n    }\n\n    @Override\n    public NetworkTunnelSession createTunnelSession(int localPort, int remotePort, String address) {\n        return null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/NameableStore.java",
    "content": "package io.xpipe.app.ext;\n\npublic interface NameableStore extends DataStore {\n\n    String getName();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/NetworkTunnelSession.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.process.ShellControl;\n\npublic abstract class NetworkTunnelSession extends Session {\n\n    public abstract int getLocalPort();\n\n    public abstract int getRemotePort();\n\n    public abstract ShellControl getShellControl();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/NetworkTunnelStore.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport java.util.Optional;\n\npublic interface NetworkTunnelStore extends DataStore, SelfReferentialStore {\n\n    static void checkTunnelable(DataStoreEntryRef<?> ref) throws ValidationException {\n        if (!(ref.getStore() instanceof NetworkTunnelStore t)) {\n            throw new ValidationException(\n                    AppI18n.get(\"parentHostDoesNotSupportTunneling\", ref.get().getName()));\n        }\n\n        var unsupported = t.getUnsupportedParent();\n        if (unsupported.isPresent()) {\n            throw new ValidationException(AppI18n.get(\n                    \"parentHostDoesNotSupportTunneling\", unsupported.get().get().getName()));\n        }\n    }\n\n    DataStoreEntryRef<?> getNetworkParent();\n\n    default boolean requiresTunnel() {\n        return getNetworkParent() != null;\n    }\n\n    default HostAddress getTunnelHostName() {\n        return HostAddress.empty();\n    }\n\n    default Optional<DataStoreEntryRef<NetworkTunnelStore>> getUnsupportedParent() {\n        DataStoreEntryRef<NetworkTunnelStore> current = getSelfEntry().ref();\n        while (true) {\n            var p = current.getStore().getNetworkParent();\n            if (p == null) {\n                return Optional.empty();\n            }\n\n            if (p.getStore() instanceof NetworkTunnelStore) {\n                current = p.asNeeded();\n            } else {\n                return Optional.of(current);\n            }\n        }\n    }\n\n    default boolean isLocallyTunnelable() {\n        return getUnsupportedParent().isEmpty();\n    }\n\n    NetworkTunnelSession createTunnelSession(int localPort, int remotePort, String address);\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/PrefsChoiceValue.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.util.Translatable;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.SneakyThrows;\n\nimport java.util.Arrays;\nimport java.util.List;\n\npublic interface PrefsChoiceValue extends PrefsValue, Translatable {\n\n    @SuppressWarnings(\"unchecked\")\n    @SneakyThrows\n    static <T> List<T> getAll(Class<T> type) {\n        if (Enum.class.isAssignableFrom(type)) {\n            return Arrays.asList(type.getEnumConstants());\n        }\n\n        try {\n            type.getDeclaredField(\"ALL\");\n        } catch (NoSuchFieldException e) {\n            return null;\n        }\n\n        try {\n            return (List<T>) type.getDeclaredField(\"ALL\").get(null);\n        } catch (IllegalAccessException | NoSuchFieldException e) {\n            return List.of(type.getEnumConstants());\n        }\n    }\n\n    static <T> List<T> getSupported(Class<T> type) {\n        var all = getAll(type);\n        if (all == null) {\n            throw new AssertionError();\n        }\n\n        return all.stream().filter(t -> ((PrefsChoiceValue) t).isSelectable()).toList();\n    }\n\n    @Override\n    default ObservableValue<String> toTranslatedString() {\n        return AppI18n.observable(getId());\n    }\n\n    @SuppressWarnings(\"unused\")\n    String getId();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/PrefsHandler.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.platform.OptionsBuilder;\n\nimport javafx.beans.property.Property;\n\nimport com.fasterxml.jackson.databind.JavaType;\n\npublic interface PrefsHandler {\n\n    <T> void addSetting(\n            String id, JavaType t, Property<T> property, OptionsBuilder builder, boolean requiresRestart, boolean log);\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/PrefsProvider.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.core.ModuleLayerLoader;\n\nimport java.util.List;\nimport java.util.ServiceLoader;\nimport java.util.stream.Collectors;\n\npublic abstract class PrefsProvider {\n\n    private static List<PrefsProvider> ALL;\n\n    public static List<PrefsProvider> getAll() {\n        return ALL;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <T extends PrefsProvider> T get(Class<T> c) {\n        return (T) ALL.stream()\n                .filter(prefsProvider -> prefsProvider.getClass().equals(c))\n                .findAny()\n                .orElseThrow();\n    }\n\n    public abstract void addPrefs(PrefsHandler handler);\n\n    public abstract void fixLocalValues();\n\n    public abstract void initDefaultValues();\n\n    public static class Loader implements ModuleLayerLoader {\n\n        @Override\n        public void init(ModuleLayer layer) {\n            ALL = ServiceLoader.load(layer, PrefsProvider.class).stream()\n                    .map(ServiceLoader.Provider::get)\n                    .collect(Collectors.toList());\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/PrefsValue.java",
    "content": "package io.xpipe.app.ext;\n\npublic interface PrefsValue {\n\n    default boolean isAvailable() {\n        return true;\n    }\n\n    default boolean isSelectable() {\n        return true;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/ProcessControlProvider.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.browser.BrowserStoreSessionTab;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.CommandControl;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.ShellDialect;\nimport io.xpipe.app.secret.SecretRetrievalStrategy;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.vnc.VncBaseStore;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.SecretValue;\n\nimport javafx.beans.property.Property;\n\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.ServiceLoader;\n\npublic abstract class ProcessControlProvider {\n\n    private static ProcessControlProvider INSTANCE;\n\n    public static void init(ModuleLayer layer) {\n        INSTANCE = ServiceLoader.load(layer, ProcessControlProvider.class).stream()\n                .map(p -> p.get())\n                .findFirst()\n                .orElseThrow();\n    }\n\n    public static ProcessControlProvider get() {\n        return INSTANCE;\n    }\n\n    public abstract String generatePublicSshKey(SecretValue privateKey, SecretRetrievalStrategy passphrase);\n\n    public abstract void showSshKeygenDialog(String commentDefault, Property<?> identityProperty);\n\n    public abstract ShellStore subShellEnvironment(DataStoreEntryRef<ShellStore> s, ShellDialect dialect);\n\n    public abstract BrowserStoreSessionTab<?> createVncSession(\n            BrowserFullSessionModel model, DataStoreEntryRef<VncBaseStore> ref);\n\n    public abstract DataStoreEntryRef<ShellStore> elevated(DataStoreEntryRef<ShellStore> e);\n\n    public abstract void reset();\n\n    public abstract ShellControl withDefaultScripts(ShellControl pc);\n\n    public abstract CommandControl command(ShellControl parent, CommandBuilder command, CommandBuilder terminalCommand);\n\n    public abstract ShellControl createLocalProcessControl(boolean stoppable);\n\n    public abstract Object getStorageSyncHandler();\n\n    public abstract Object getStorageUserHandler();\n\n    public abstract ShellDialect getEffectiveLocalDialect();\n\n    public abstract String executeMcpCommand(ShellControl sc, String command) throws Exception;\n\n    public ShellDialect getNextFallbackDialect() {\n        var av = getAvailableLocalDialects();\n        var index = av.indexOf(getEffectiveLocalDialect());\n        var next = (index + 1) % av.size();\n        return av.get(next);\n    }\n\n    public abstract void toggleFallbackShell();\n\n    public abstract List<ShellDialect> getAvailableLocalDialects();\n\n    public abstract <T extends DataStore> DataStoreEntryRef<T> replace(DataStoreEntryRef<T> ref);\n\n    public abstract ModalOverlay createNetworkScanModal();\n\n    public abstract void cloneRepository(String url, Path target) throws Exception;\n\n    public abstract void pullRepository(Path target) throws Exception;\n\n    public abstract void checkSshAgent(ShellControl sc, FilePath agent) throws Exception;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/ScanProvider.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.core.ModuleLayerLoader;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Value;\n\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.ServiceLoader;\nimport java.util.stream.Collectors;\n\npublic abstract class ScanProvider {\n\n    private static List<ScanProvider> ALL;\n\n    public static List<ScanProvider> getAll() {\n        return ALL;\n    }\n\n    public ScanOpportunity create(DataStoreEntry entry, ShellControl sc) throws Exception {\n        return null;\n    }\n\n    public abstract void scan(DataStoreEntry entry, ShellControl sc) throws Throwable;\n\n    public boolean requiresFullShell() {\n        return true;\n    }\n\n    public static class Loader implements ModuleLayerLoader {\n\n        @Override\n        public void init(ModuleLayer layer) {\n            ALL = ServiceLoader.load(layer, ScanProvider.class).stream()\n                    .map(ServiceLoader.Provider::get)\n                    .sorted(Comparator.comparing(\n                            scanProvider -> scanProvider.getClass().getName()))\n                    .collect(Collectors.toList());\n        }\n    }\n\n    @Value\n    @AllArgsConstructor\n    public class ScanOpportunity {\n        ObservableValue<String> name;\n        boolean disabled;\n        String licenseFeatureId;\n\n        public ScanOpportunity(String nameKey, boolean disabled) {\n            this.name = AppI18n.observable(nameKey);\n            this.disabled = disabled;\n            this.licenseFeatureId = null;\n        }\n\n        public ScanOpportunity(String nameKey, boolean disabled, String licenseFeatureId) {\n            this.name = AppI18n.observable(nameKey);\n            this.disabled = disabled;\n            this.licenseFeatureId = licenseFeatureId;\n        }\n\n        public ScanOpportunity(ObservableValue<String> name, boolean disabled) {\n            this.name = name;\n            this.disabled = disabled;\n            this.licenseFeatureId = null;\n        }\n\n        public String getLicensedFeatureId() {\n            return licenseFeatureId;\n        }\n\n        public ScanProvider getProvider() {\n            return ScanProvider.this;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/SelfReferentialStore.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic interface SelfReferentialStore extends DataStore {\n\n    Map<DataStore, DataStoreEntry> FALLBACK = new HashMap<>();\n\n    default boolean hasSelfEntry() {\n        if (DataStorage.get() == null) {\n            return false;\n        }\n\n        return DataStorage.get()\n                .getStoreEntryIfPresent(this, true)\n                .or(() -> {\n                    return DataStorage.get().getStoreEntryInProgressIfPresent(this);\n                })\n                .isPresent();\n    }\n\n    default DataStoreEntry getSelfEntry() {\n        if (DataStorage.get() == null) {\n            return DataStoreEntry.createTempWrapper(this);\n        }\n\n        return DataStorage.get()\n                .getStoreEntryIfPresent(this, true)\n                .or(() -> {\n                    return DataStorage.get().getStoreEntryInProgressIfPresent(this);\n                })\n                .orElseGet(() -> {\n                    synchronized (FALLBACK) {\n                        var ex = FALLBACK.get(this);\n                        if (ex != null) {\n                            return ex;\n                        }\n\n                        var e = DataStoreEntry.createNew(\n                                UUID.randomUUID(), DataStorage.DEFAULT_CATEGORY_UUID, \"Invalid\", this);\n                        FALLBACK.put(this, e);\n                        return e;\n                    }\n                });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/Session.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.util.GlobalTimer;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport java.time.Duration;\n\npublic abstract class Session implements AutoCloseable {\n\n    protected SessionListener listener = running -> {};\n\n    public synchronized void addListener(SessionListener n) {\n        var current = this.listener;\n        this.listener = running -> {\n            current.onStateChange(running);\n            n.onStateChange(running);\n        };\n    }\n\n    protected void startAliveListener() {\n        GlobalTimer.scheduleUntil(Duration.ofMillis(10000), false, () -> {\n            if (!isRunning()) {\n                return true;\n            }\n\n            ThreadHelper.runAsync(() -> {\n                if (!isRunning()) {\n                    return;\n                }\n\n                if (!checkAliveQuiet()) {\n                    handleSessionDeath();\n                }\n            });\n            return false;\n        });\n    }\n\n    protected void handleSessionDeath() {\n        try {\n            stop();\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).omit().handle();\n        }\n    }\n\n    protected abstract boolean isRunning();\n\n    public abstract void start() throws Exception;\n\n    public abstract void stop() throws Exception;\n\n    public boolean checkAliveQuiet() {\n        try {\n            return checkAlive();\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).omit().handle();\n            return false;\n        }\n    }\n\n    public abstract boolean checkAlive() throws Exception;\n\n    @Override\n    public void close() throws Exception {\n        stop();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/SessionListener.java",
    "content": "package io.xpipe.app.ext;\n\npublic interface SessionListener {\n\n    void onStateChange(boolean running);\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/SetupToolActionProvider.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.storage.DataStorage;\n\nimport lombok.SneakyThrows;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\npublic class SetupToolActionProvider implements ActionProvider {\n\n    @Override\n    public String getId() {\n        return \"setupTool\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends AbstractAction {\n\n        private final String type;\n\n        @Override\n        @SneakyThrows\n        public void executeImpl() {\n            var provider = CloudSetupProvider.byId(type);\n            if (provider.isEmpty()) {\n                throw ErrorEventFactory.expected(new IllegalArgumentException(\"Setup action not found: \" + type));\n            }\n\n            var local = DataStorage.get().local();\n            var sc = ((ShellStore) local.getStore()).getOrStartSession();\n            var scan = provider.get().getScan();\n            var op = scan.create(local, sc);\n\n            if (op == null) {\n                return;\n            }\n\n            if (op.isDisabled()) {\n                provider.get().handleUnsupported();\n                return;\n            }\n\n            scan.scan(local, sc);\n        }\n\n        @Override\n        public Map<String, String> toDisplayMap() {\n            var map = new LinkedHashMap<String, String>();\n            map.put(\"Action\", getDisplayName());\n            map.put(\"Type\", type);\n            return map;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/ShellControlFunction.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.process.ShellControl;\n\npublic interface ShellControlFunction {\n\n    ShellControl control() throws Exception;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/ShellControlParentStoreFunction.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.process.ShellControl;\n\npublic interface ShellControlParentStoreFunction extends ShellControlFunction {\n\n    default ShellControl control() throws Exception {\n        return control(getParentStore().standaloneControl());\n    }\n\n    ShellControl control(ShellControl parent) throws Exception;\n\n    ShellStore getParentStore();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/ShellDialectChoiceComp.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.PrettyImageHelper;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.MenuHelper;\nimport io.xpipe.app.process.ShellDialect;\n\nimport javafx.beans.property.Property;\nimport javafx.scene.control.ComboBox;\nimport javafx.scene.control.ListCell;\nimport javafx.scene.control.skin.ComboBoxListViewSkin;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.layout.Region;\n\nimport lombok.AllArgsConstructor;\n\nimport java.util.List;\nimport java.util.function.Supplier;\n\n@AllArgsConstructor\npublic class ShellDialectChoiceComp extends SimpleRegionBuilder {\n\n    public enum NullHandling {\n        NULL_IS_DEFAULT,\n        NULL_IS_ALL,\n        NULL_DISABLED\n    }\n\n    private final List<ShellDialect> available;\n    private final Property<ShellDialect> selected;\n    private final NullHandling nullHandling;\n\n    @Override\n    protected Region createSimple() {\n        Supplier<ListCell<ShellDialect>> supplier = () -> new ListCell<>() {\n            @Override\n            protected void updateItem(ShellDialect item, boolean empty) {\n                super.updateItem(item, empty);\n                setText(\n                        item != null\n                                ? item.getDisplayName()\n                                : nullHandling == NullHandling.NULL_IS_ALL\n                                        ? AppI18n.get(\"all\")\n                                        : AppI18n.get(\"default\"));\n                setGraphic(PrettyImageHelper.ofFixedSizeSquare(ShellDialectIcons.getImageName(item), 16)\n                        .build());\n            }\n        };\n        var cb = new ComboBox<ShellDialect>();\n        cb.setCellFactory(param -> supplier.get());\n        cb.setButtonCell(supplier.get());\n        cb.setValue(selected.getValue());\n        selected.bind(cb.valueProperty());\n\n        cb.setOnKeyPressed(event -> {\n            if (!event.getCode().equals(KeyCode.ENTER)) {\n                return;\n            }\n\n            cb.show();\n            event.consume();\n        });\n\n        if (nullHandling != NullHandling.NULL_DISABLED) {\n            cb.getItems().add(null);\n        }\n        cb.getItems().addAll(available);\n        cb.setVisibleRowCount(available.size() + 1);\n        cb.getStyleClass().add(\"choice-comp\");\n        cb.setMaxWidth(20000);\n        var skin = new ComboBoxListViewSkin<>(cb);\n        cb.setSkin(skin);\n        MenuHelper.fixComboBoxSkin(skin);\n        return cb;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/ShellDialectIcons.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.process.ShellDialect;\nimport io.xpipe.app.process.ShellDialects;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\npublic class ShellDialectIcons {\n\n    private static final Map<ShellDialect, String> ICONS = new LinkedHashMap<>();\n\n    static {\n        ICONS.put(ShellDialects.CMD, \"cmd_icon.svg\");\n        ICONS.put(ShellDialects.POWERSHELL, \"powershell_icon.svg\");\n        ICONS.put(ShellDialects.POWERSHELL_CORE, \"pwsh_icon.svg\");\n        ICONS.put(ShellDialects.SH, \"sh_icon.svg\");\n        ICONS.put(ShellDialects.ASH, \"sh_icon.svg\");\n        ICONS.put(ShellDialects.DASH, \"sh_icon.svg\");\n        ICONS.put(ShellDialects.BASH, \"bash_icon.svg\");\n        ICONS.put(ShellDialects.FISH, \"fish_icon.svg\");\n        ICONS.put(ShellDialects.ZSH, \"zsh_icon.svg\");\n        ICONS.put(ShellDialects.NUSHELL, \"nushell_icon.svg\");\n        ICONS.put(ShellDialects.XONSH, \"xonsh_icon.svg\");\n    }\n\n    public static String getImageName(ShellDialect t) {\n        if (t == null) {\n            return \"proc:defaultShell_icon.svg\";\n        }\n\n        return \"proc:\" + ICONS.get(t);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/ShellSession.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.core.FailableSupplier;\n\nimport lombok.Getter;\n\n@Getter\npublic class ShellSession extends Session {\n\n    private final FailableSupplier<ShellControl> supplier;\n    private ShellControl shellControl;\n\n    public ShellSession(FailableSupplier<ShellControl> supplier) {\n        this.supplier = supplier;\n    }\n\n    private ShellControl createControl() throws Exception {\n        var pc = supplier.get();\n        pc.onStartupFail(ignored -> {\n            listener.onStateChange(false);\n        });\n        pc.onInit(ignored -> {\n            listener.onStateChange(true);\n        });\n        pc.onKill(() -> {\n            listener.onStateChange(false);\n        });\n        // Listen for parent exit as onExit is called before exit is completed\n        // In case it is stuck, we would not get the right status otherwise\n        pc.getParentControl().ifPresent(p -> {\n            p.onExit(ignored -> {\n                listener.onStateChange(false);\n            });\n        });\n        pc.onExit(ignored -> {\n            listener.onStateChange(false);\n        });\n        return pc;\n    }\n\n    public boolean isRunning() {\n        if (shellControl == null) {\n            return false;\n        }\n\n        return shellControl.isRunning(true);\n    }\n\n    public void start() throws Exception {\n        if (shellControl != null && shellControl.isRunning(true)) {\n            return;\n        } else {\n            stop();\n        }\n\n        try {\n            shellControl = createControl();\n            shellControl.start();\n\n            var shouldAliveCheck = !shellControl.isLocal();\n            var supportsAliveCheck =\n                    shellControl.getShellDialect().getDumbMode().supportsAnyPossibleInteraction();\n            if (shouldAliveCheck && supportsAliveCheck) {\n                startAliveListener();\n            }\n        } catch (Exception ex) {\n            try {\n                stop();\n            } catch (Exception stopEx) {\n                ex.addSuppressed(stopEx);\n            }\n            throw ex;\n        }\n    }\n\n    public void stop() throws Exception {\n        if (shellControl == null) {\n            return;\n        }\n\n        shellControl.shutdown();\n    }\n\n    @Override\n    public boolean checkAlive() throws Exception {\n        if (shellControl == null) {\n            return false;\n        }\n\n        if (!shellControl.isRunning(true)) {\n            return false;\n        }\n\n        // If a subshell is active, then we are alive\n        if (shellControl.isSubShellActive()) {\n            return true;\n        }\n\n        // Don't run commands while in startup / exit\n        if (shellControl.isInitializing() || shellControl.isExiting()) {\n            return true;\n        }\n\n        try {\n            // Don't print it constantly\n            return shellControl\n                    .command(CommandBuilder.of().add(\"echo\", \"xpipetest\"))\n                    .sensitive()\n                    .executeAndCheck();\n        } catch (Exception ex) {\n            throw ErrorEventFactory.expected(ex);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/ShellStore.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.StubShellControl;\n\npublic interface ShellStore extends DataStore, FileSystemStore, ValidatableStore, SingletonSessionStore<ShellSession> {\n\n    default ShellControl getOrStartSession() throws Exception {\n        // Check if the cache is not available\n        if (!canCacheToStorage()) {\n            return standaloneControl().start();\n        }\n\n        var existingSession = getSession();\n        if (existingSession != null) {\n            existingSession.getShellControl().refreshRunningState();\n            if (!existingSession.checkAliveQuiet()) {\n                stopSessionIfNeeded();\n            } else {\n                existingSession.getShellControl().waitForSubShellExit();\n                return new StubShellControl(existingSession.getShellControl());\n            }\n        }\n\n        var session = startSessionIfNeeded();\n\n        // This might be null if this store has been removed from this storage since the session was started\n        // Then, the cache returns null\n        // getSession()\n\n        return new StubShellControl(session.getShellControl());\n    }\n\n    @Override\n    default ShellSession newSession() {\n        var func = shellFunction();\n        var session = new ShellSession(() -> func.control());\n        session.addListener(this);\n        return session;\n    }\n\n    @Override\n    default Class<?> getSessionClass() {\n        return ShellSession.class;\n    }\n\n    @Override\n    default FileSystem createFileSystem() throws Exception {\n        var func = shellFunction();\n        return new ConnectionFileSystem(func.control());\n    }\n\n    ShellControlFunction shellFunction();\n\n    @Override\n    default void validate() throws Exception {\n        try (var ignored = tempControl().start()) {}\n    }\n\n    default ShellControl standaloneControl() throws Exception {\n        return shellFunction().control();\n    }\n\n    default ShellControl tempControl() throws Exception {\n        if (isSessionRunning()) {\n            return getOrStartSession();\n        }\n\n        var func = shellFunction();\n        if (!(func instanceof ShellControlParentStoreFunction p)) {\n            return func.control();\n        }\n\n        // Don't reuse local shell\n        var parentSc = p.getParentStore() instanceof LocalStore l\n                ? l.standaloneControl()\n                : p.getParentStore().getOrStartSession();\n        return p.control(parentSc);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/SingletonSessionStore.java",
    "content": "package io.xpipe.app.ext;\n\npublic interface SingletonSessionStore<T extends Session>\n        extends ExpandedLifecycleStore, InternalCacheDataStore, SessionListener {\n\n    @Override\n    default void finalizeStore() throws Exception {\n        stopSessionIfNeeded();\n    }\n\n    default boolean isSessionRunning() {\n        return getCache(\"sessionRunning\", Boolean.class, false);\n    }\n\n    default boolean isSessionEnabled() {\n        return getCache(\"sessionEnabled\", Boolean.class, false);\n    }\n\n    default void setSessionEnabled(boolean value) {\n        setCache(\"sessionEnabled\", value);\n    }\n\n    @Override\n    default void onStateChange(boolean running) {\n        setSessionEnabled(running);\n        setCache(\"sessionRunning\", running);\n    }\n\n    T newSession();\n\n    Class<?> getSessionClass();\n\n    @SuppressWarnings(\"unchecked\")\n    default T getSession() {\n        return (T) getCache(\"session\", getSessionClass(), null);\n    }\n\n    default T startSessionIfNeeded() throws Exception {\n        synchronized (this) {\n            var s = getSession();\n            if (s != null) {\n                if (s.checkAliveQuiet()) {\n                    return s;\n                }\n\n                s.start();\n                return s;\n            }\n\n            try {\n                s = newSession();\n                if (s != null) {\n                    setSessionEnabled(true);\n                    s.start();\n                    setCache(\"session\", s);\n                    onStateChange(true);\n                    s.addListener(running -> {\n                        onStateChange(running);\n                    });\n                    return s;\n                } else {\n                    setSessionEnabled(false);\n                    return null;\n                }\n            } catch (Exception ex) {\n                setSessionEnabled(false);\n                onStateChange(false);\n                throw ex;\n            }\n        }\n    }\n\n    default void stopSessionIfNeeded() throws Exception {\n        synchronized (this) {\n            var ex = getSession();\n            setSessionEnabled(false);\n            if (ex != null) {\n                try {\n                    ex.stop();\n                } finally {\n                    onStateChange(false);\n                    setCache(\"session\", null);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/SingletonSessionStoreProvider.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.hub.action.impl.ToggleActionProvider;\nimport io.xpipe.app.hub.comp.*;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.value.ObservableBooleanValue;\nimport javafx.beans.value.ObservableValue;\n\npublic interface SingletonSessionStoreProvider extends DataStoreProvider {\n\n    @Override\n    default ObservableBooleanValue busy(StoreEntryWrapper wrapper) {\n        return Bindings.createBooleanBinding(\n                () -> {\n                    SingletonSessionStore<?> s = wrapper.getEntry().getStore().asNeeded();\n                    return s.isSessionEnabled() != s.isSessionRunning();\n                },\n                wrapper.getCache());\n    }\n\n    @Override\n    default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) {\n        var t = createToggleComp(sec);\n        return StoreEntryComp.create(sec, t, preferLarge);\n    }\n\n    default BaseRegionBuilder<?, ?> stateDisplay(StoreEntryWrapper w) {\n        return new SystemStateComp(Bindings.createObjectBinding(\n                () -> {\n                    SingletonSessionStore<?> s = w.getEntry().getStore().asNeeded();\n                    if (!supportsSession(s)) {\n                        return SystemStateComp.State.SUCCESS;\n                    }\n\n                    if (!s.isSessionEnabled() || (s.isSessionEnabled() && !s.isSessionRunning())) {\n                        return SystemStateComp.State.OTHER;\n                    }\n\n                    return s.isSessionRunning() ? SystemStateComp.State.SUCCESS : SystemStateComp.State.FAILURE;\n                },\n                w.getCache()));\n    }\n\n    default StoreToggleComp createToggleComp(StoreSection sec) {\n        var enabled = new SimpleBooleanProperty();\n        sec.getWrapper().getCache().subscribe((ignored) -> {\n            var entry = sec.getWrapper().getEntry();\n            if (entry.getStore() == null) {\n                return;\n            }\n\n            SingletonSessionStore<?> s = entry.getStore().asNeeded();\n            enabled.set(s.isSessionEnabled());\n        });\n\n        ObservableValue<LabelGraphic> g = enabled.map(aBoolean -> aBoolean\n                ? new LabelGraphic.IconGraphic(\"mdi2c-circle-slice-8\")\n                : new LabelGraphic.IconGraphic(\"mdi2p-power\"));\n        var t = new StoreToggleComp(null, g, sec, enabled, newState -> {\n            var entry = sec.getWrapper().getEntry();\n            if (entry.getStore() == null) {\n                return;\n            }\n\n            SingletonSessionStore<?> s = entry.getStore().asNeeded();\n            if (s.isSessionEnabled() != newState) {\n                var action = ToggleActionProvider.Action.builder()\n                        .ref(sec.getWrapper().getEntry().ref())\n                        .enabled(newState)\n                        .build();\n                action.executeAsync();\n            }\n        });\n\n        t.setCustomVisibility(Bindings.createBooleanBinding(\n                () -> {\n                    SingletonSessionStore<?> s =\n                            sec.getWrapper().getEntry().getStore().asNeeded();\n                    return supportsSession(s) && (showToggleWhenInactive(s) || s.isSessionEnabled());\n                },\n                sec.getWrapper().getCache(),\n                enabled));\n        return t;\n    }\n\n    default boolean showToggleWhenInactive(SingletonSessionStore<?> store) {\n        return true;\n    }\n\n    default boolean supportsSession(SingletonSessionStore<?> store) {\n        return true;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/StartOnInitStore.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.type.TypeFactory;\n\nimport java.util.HashSet;\nimport java.util.Set;\n\npublic interface StartOnInitStore extends SelfReferentialStore, DataStore {\n\n    static void init() {\n        ThreadHelper.runFailableAsync(() -> {\n            var enabled = getEnabledStores();\n            for (DataStoreEntry e : DataStorage.get().getStoreEntries()) {\n                if (e.getStore() instanceof StartOnInitStore i\n                        && e.getValidity().isUsable()\n                        && enabled.contains(i.getSelfEntry().ref())\n                        && i.canAutomaticallyStart()) {\n                    try {\n                        i.startOnInit();\n                    } catch (Throwable ex) {\n                        ErrorEventFactory.fromThrowable(ex)\n                                .description(\"Unable to automatically start connection \"\n                                        + DataStorage.get().getStoreEntryDisplayName(i.getSelfEntry()))\n                                .handle();\n                    }\n                }\n            }\n        });\n    }\n\n    static Set<DataStoreEntryRef<?>> getEnabledStores() {\n        synchronized (StartOnInitStore.class) {\n            var type = TypeFactory.defaultInstance().constructType(new TypeReference<Set<DataStoreEntryRef<?>>>() {});\n            Set<DataStoreEntryRef<?>> cached = AppCache.getNonNull(\"startOnInitStores\", type, () -> Set.of());\n            return cached;\n        }\n    }\n\n    static void setEnabledStores(Set<DataStoreEntryRef<?>> stores) {\n        synchronized (StartOnInitStore.class) {\n            AppCache.update(\"startOnInitStores\", stores);\n        }\n    }\n\n    default boolean isEnabled() {\n        synchronized (StartOnInitStore.class) {\n            return getEnabledStores().contains(getSelfEntry().ref());\n        }\n    }\n\n    default void enable() {\n        synchronized (StartOnInitStore.class) {\n            var enabled = new HashSet<>(getEnabledStores());\n            enabled.add(getSelfEntry().ref());\n            setEnabledStores(enabled);\n        }\n    }\n\n    default void disable() {\n        synchronized (StartOnInitStore.class) {\n            var enabled = new HashSet<>(getEnabledStores());\n            enabled.remove(getSelfEntry().ref());\n            setEnabledStores(enabled);\n        }\n    }\n\n    default boolean canAutomaticallyStart() {\n        return true;\n    }\n\n    void startOnInit() throws Exception;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/StatefulDataStore.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.storage.DataStateHandler;\n\nimport lombok.SneakyThrows;\n\nimport java.util.Arrays;\n\npublic interface StatefulDataStore<T extends DataStoreState> extends DataStore {\n\n    @SneakyThrows\n    default T createDefaultState() {\n        var c = getStateClass().getDeclaredMethod(\"builder\");\n        c.setAccessible(true);\n        var b = c.invoke(null);\n        var m = b.getClass().getDeclaredMethod(\"build\");\n        m.setAccessible(true);\n        return getStateClass().cast(m.invoke(b));\n    }\n\n    default T getState() {\n        return DataStateHandler.get().getState(this, this::createDefaultState);\n    }\n\n    default void setState(T val) {\n        DataStateHandler.get().setState(this, val);\n    }\n\n    @SneakyThrows\n    @SuppressWarnings(\"unchecked\")\n    default Class<T> getStateClass() {\n        var found = Arrays.stream(getClass().getDeclaredClasses())\n                .filter(aClass -> DataStoreState.class.isAssignableFrom(aClass))\n                .findAny();\n        if (found.isEmpty()) {\n            throw new IllegalArgumentException(\n                    \"Store class \" + getClass().getSimpleName() + \" does not have a state class set\");\n        }\n\n        return (Class<T>) found.get();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/UserScopeStore.java",
    "content": "package io.xpipe.app.ext;\n\npublic interface UserScopeStore {\n\n    boolean isPerUser();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/ValidatableStore.java",
    "content": "package io.xpipe.app.ext;\n\npublic interface ValidatableStore extends DataStore {\n\n    /**\n     * Performs a validation of this data store.\n     * <p>\n     * This validation can include one of multiple things:\n     * - Sanity checks of individual properties\n     * - Existence checks\n     * - Connection checks\n     * <p>\n     * All in all, a successful execution of this method should almost guarantee\n     * that the data store can be successfully accessed in the near future.\n     * <p>\n     * Note that some checks may take a long time, for example if a connection has to be validated.\n     * The caller should therefore expect a runtime of multiple seconds when calling this method.\n     *\n     * @throws Exception if any part of the validation went wrong\n     */\n    void validate() throws Exception;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/ValidationException.java",
    "content": "package io.xpipe.app.ext;\n\nimport lombok.experimental.StandardException;\n\n@StandardException\npublic class ValidationException extends Exception {}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/ext/WrapperFileSystem.java",
    "content": "package io.xpipe.app.ext;\n\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.core.FailableConsumer;\nimport io.xpipe.core.FilePath;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.function.Supplier;\nimport java.util.stream.Stream;\n\npublic class WrapperFileSystem implements FileSystem {\n\n    private final FileSystem fs;\n    private final Supplier<Boolean> runningCheck;\n\n    public WrapperFileSystem(FileSystem fs) {\n        this.fs = fs;\n        this.runningCheck = () -> fs.isRunning();\n    }\n\n    public FileSystem getWrappedFileSystem() {\n        return fs;\n    }\n\n    public void withFileSystem(FailableConsumer<FileSystem, Exception> consumer) throws Exception {\n        if (!runningCheck.get()) {\n            return;\n        }\n\n        consumer.accept(fs);\n    }\n\n    @Override\n    public boolean writeInstantIfPossible(FileSystem sourceFs, FilePath sourceFile, FilePath targetFile)\n            throws Exception {\n        if (!runningCheck.get()) {\n            return false;\n        }\n\n        return fs.writeInstantIfPossible(sourceFs, sourceFile, targetFile);\n    }\n\n    @Override\n    public boolean readInstantIfPossible(FilePath sourceFile, FileSystem targetFs, FilePath targetFile)\n            throws Exception {\n        if (!runningCheck.get()) {\n            return false;\n        }\n\n        return fs.readInstantIfPossible(sourceFile, targetFs, targetFile);\n    }\n\n    @Override\n    public String getSuffix() {\n        return fs.getSuffix();\n    }\n\n    @Override\n    public boolean isRunning() {\n        return fs.isRunning();\n    }\n\n    @Override\n    public boolean supportsLinkCreation() {\n        return fs.supportsLinkCreation();\n    }\n\n    @Override\n    public boolean supportsOwnerColumn() {\n        return fs.supportsOwnerColumn();\n    }\n\n    @Override\n    public boolean supportsModeColumn() {\n        return fs.supportsModeColumn();\n    }\n\n    @Override\n    public boolean supportsDirectorySizes() {\n        return fs.supportsDirectorySizes();\n    }\n\n    @Override\n    public boolean supportsChmod() {\n        return fs.supportsChmod();\n    }\n\n    @Override\n    public boolean supportsChown() {\n        return fs.supportsChown();\n    }\n\n    @Override\n    public boolean supportsChgrp() {\n        return fs.supportsChgrp();\n    }\n\n    @Override\n    public boolean supportsTerminalOpen() {\n        return fs.supportsTerminalOpen();\n    }\n\n    @Override\n    public boolean supportsTerminalWorkingDirectory() {\n        return fs.supportsTerminalWorkingDirectory();\n    }\n\n    @Override\n    public Optional<ShellControl> getRawShellControl() {\n        return fs.getRawShellControl();\n    }\n\n    @Override\n    public ShellControl getTerminalShellControl() {\n        return fs.getTerminalShellControl();\n    }\n\n    @Override\n    public void chmod(FilePath path, String mode, boolean recursive) throws Exception {\n        if (!runningCheck.get()) {\n            return;\n        }\n\n        fs.chmod(path, mode, recursive);\n    }\n\n    @Override\n    public void chown(FilePath path, String uid, boolean recursive) throws Exception {\n        if (!runningCheck.get()) {\n            return;\n        }\n\n        fs.chown(path, uid, recursive);\n    }\n\n    @Override\n    public void chgrp(FilePath path, String gid, boolean recursive) throws Exception {\n        if (!runningCheck.get()) {\n            return;\n        }\n\n        fs.chgrp(path, gid, recursive);\n    }\n\n    @Override\n    public void kill() {\n        if (!runningCheck.get()) {\n            return;\n        }\n\n        fs.kill();\n    }\n\n    @Override\n    public void cd(FilePath dir) throws Exception {\n        if (!runningCheck.get()) {\n            return;\n        }\n\n        fs.cd(dir);\n    }\n\n    @Override\n    public boolean requiresReinit() {\n        if (!runningCheck.get()) {\n            return false;\n        }\n\n        return fs.requiresReinit();\n    }\n\n    @Override\n    public void reinitIfNeeded() throws Exception {\n        fs.reinitIfNeeded();\n    }\n\n    @Override\n    public String getFileSeparator() {\n        if (!runningCheck.get()) {\n            return \"/\";\n        }\n\n        return fs.getFileSeparator();\n    }\n\n    @Override\n    public FilePath makeFileSystemCompatible(FilePath filePath) {\n        if (!runningCheck.get()) {\n            return filePath;\n        }\n\n        return fs.makeFileSystemCompatible(filePath);\n    }\n\n    @Override\n    public Optional<FilePath> pwd() throws Exception {\n        if (!runningCheck.get()) {\n            return Optional.empty();\n        }\n\n        return fs.pwd();\n    }\n\n    @Override\n    public FileSystem createTransferOptimizedFileSystem() throws Exception {\n        var optimized = fs.createTransferOptimizedFileSystem();\n        if (optimized == fs) {\n            return this;\n        }\n\n        return new WrapperFileSystem(optimized);\n    }\n\n    @Override\n    public long getFileSize(FilePath file) throws Exception {\n        if (!runningCheck.get()) {\n            return 0;\n        }\n\n        return fs.getFileSize(file);\n    }\n\n    @Override\n    public long getDirectorySize(FilePath file) throws Exception {\n        if (!runningCheck.get()) {\n            return 0;\n        }\n\n        return fs.getDirectorySize(file);\n    }\n\n    @Override\n    public Optional<ShellControl> getShell() {\n        return fs.getShell();\n    }\n\n    @Override\n    public FileSystem open() throws Exception {\n        return fs.open();\n    }\n\n    @Override\n    public InputStream openInput(FilePath file) throws Exception {\n        if (!runningCheck.get()) {\n            return InputStream.nullInputStream();\n        }\n\n        return fs.openInput(file);\n    }\n\n    @Override\n    public OutputStream openOutput(FilePath file, long totalBytes) throws Exception {\n        if (!runningCheck.get()) {\n            return OutputStream.nullOutputStream();\n        }\n\n        return fs.openOutput(file, totalBytes);\n    }\n\n    @Override\n    public boolean fileExists(FilePath file) throws Exception {\n        if (!runningCheck.get()) {\n            return false;\n        }\n\n        return fs.fileExists(file);\n    }\n\n    @Override\n    public void delete(FilePath file) throws Exception {\n        if (!runningCheck.get()) {\n            return;\n        }\n\n        fs.delete(file);\n    }\n\n    @Override\n    public void copy(FilePath file, FilePath newFile) throws Exception {\n        if (!runningCheck.get()) {\n            return;\n        }\n\n        fs.copy(file, newFile);\n    }\n\n    @Override\n    public void move(FilePath file, FilePath newFile) throws Exception {\n        if (!runningCheck.get()) {\n            return;\n        }\n\n        fs.move(file, newFile);\n    }\n\n    @Override\n    public void mkdirs(FilePath file) throws Exception {\n        if (!runningCheck.get()) {\n            return;\n        }\n\n        fs.mkdirs(file);\n    }\n\n    @Override\n    public void touch(FilePath file) throws Exception {\n        if (!runningCheck.get()) {\n            return;\n        }\n\n        fs.touch(file);\n    }\n\n    @Override\n    public void symbolicLink(FilePath linkFile, FilePath targetFile) throws Exception {\n        if (!runningCheck.get()) {\n            return;\n        }\n\n        fs.symbolicLink(linkFile, targetFile);\n    }\n\n    @Override\n    public boolean directoryExists(FilePath file) throws Exception {\n        if (!runningCheck.get()) {\n            return false;\n        }\n\n        return fs.directoryExists(file);\n    }\n\n    @Override\n    public void directoryAccessible(FilePath file) throws Exception {\n        if (!runningCheck.get()) {\n            return;\n        }\n\n        fs.directoryAccessible(file);\n    }\n\n    @Override\n    public Optional<FileEntry> getFileInfo(FilePath file) throws Exception {\n        if (!runningCheck.get()) {\n            return Optional.empty();\n        }\n\n        return fs.getFileInfo(file);\n    }\n\n    @Override\n    public Stream<FileEntry> listFiles(FileSystem system, FilePath file) throws Exception {\n        if (!runningCheck.get()) {\n            return Stream.empty();\n        }\n\n        return fs.listFiles(system, file);\n    }\n\n    @Override\n    public List<FilePath> listRoots() throws Exception {\n        if (!runningCheck.get()) {\n            return List.of();\n        }\n\n        return fs.listRoots();\n    }\n\n    @Override\n    public List<FilePath> listCommonDirectories() throws Exception {\n        if (!runningCheck.get()) {\n            return List.of();\n        }\n\n        return fs.listCommonDirectories();\n    }\n\n    @Override\n    public void close() throws IOException {\n        fs.close();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/BatchHubProvider.java",
    "content": "package io.xpipe.app.hub.action;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.SneakyThrows;\n\nimport java.util.List;\n\npublic interface BatchHubProvider<T extends DataStore> extends ActionProvider {\n\n    ObservableValue<String> getName();\n\n    LabelGraphic getIcon();\n\n    Class<?> getApplicableClass();\n\n    default boolean requiresValidStore() {\n        return true;\n    }\n\n    default boolean isApplicable(DataStoreEntryRef<T> o) {\n        return true;\n    }\n\n    default boolean isActive(DataStoreEntryRef<T> o) {\n        return true;\n    }\n\n    default void execute(List<DataStoreEntryRef<T>> refs) {\n        createBatchAction(refs).executeAsync();\n    }\n\n    default AbstractAction createBatchAction(List<DataStoreEntryRef<T>> refs) {\n        var individual = refs.stream()\n                .map(ref -> {\n                    return createBatchAction(ref);\n                })\n                .filter(action -> action != null)\n                .toList();\n        return BatchStoreAction.<T>builder()\n                .actions(individual)\n                .parallel(runParallel())\n                .build();\n    }\n\n    default boolean runParallel() {\n        return false;\n    }\n\n    @SneakyThrows\n    @SuppressWarnings(\"unchecked\")\n    default StoreAction<T> createBatchAction(DataStoreEntryRef<T> ref) {\n        var c = getActionClass().orElseThrow();\n        var bm = c.getDeclaredMethod(\"builder\");\n        bm.setAccessible(true);\n        var b = bm.invoke(null);\n\n        if (StoreAction.class.isAssignableFrom(c)) {\n            var refMethod = b.getClass().getMethod(\"ref\", DataStoreEntryRef.class);\n            refMethod.setAccessible(true);\n            refMethod.invoke(b, ref);\n        }\n\n        var m = b.getClass().getDeclaredMethod(\"build\");\n        m.setAccessible(true);\n        var defValue = c.cast(m.invoke(b));\n        return (StoreAction<T>) defValue;\n    }\n\n    default List<? extends ActionProvider> getChildren(List<DataStoreEntryRef<T>> batch) {\n        return List.of();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/BatchStoreAction.java",
    "content": "package io.xpipe.app.hub.action;\n\nimport io.xpipe.app.action.*;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.JacksonMapper;\n\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport lombok.Getter;\nimport lombok.SneakyThrows;\nimport lombok.experimental.SuperBuilder;\n\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.stream.Collectors;\n\n@SuperBuilder\n@Getter\npublic final class BatchStoreAction<T extends DataStore> extends SerializableAction implements StoreContextAction {\n\n    private final List<StoreAction<T>> actions;\n    private boolean parallel;\n\n    @Override\n    public ActionProvider getProvider() {\n        return actions.getFirst().getProvider();\n    }\n\n    @Override\n    public String getShortcutName() {\n        var names = actions.size() > 3\n                ? actions.size() + \" connections\"\n                : actions.stream()\n                        .map(a -> DataStorage.get()\n                                .getStoreEntryDisplayName(a.getRef().get()))\n                        .collect(Collectors.joining(\", \"));\n        return names + \" (\" + getDisplayName() + \")\";\n    }\n\n    @Override\n    public void executeImpl() {\n        if (!parallel) {\n            for (AbstractAction action : actions) {\n                // Don't confirm twice\n                if (!action.executeSyncImpl(false)) {\n                    break;\n                }\n            }\n        } else {\n            var latch = new CountDownLatch(actions.size());\n            for (AbstractAction action : actions) {\n                ThreadHelper.runAsync(() -> {\n                    // Don't confirm twice\n                    action.executeSyncImpl(false);\n                    latch.countDown();\n                });\n            }\n\n            try {\n                latch.await();\n            } catch (InterruptedException ignored) {\n            }\n        }\n    }\n\n    @Override\n    public boolean isMutation() {\n        return actions.stream().anyMatch(StoreAction::isMutation);\n    }\n\n    @Override\n    public boolean forceConfirmation() {\n        return actions.stream().anyMatch(StoreAction::forceConfirmation);\n    }\n\n    @SneakyThrows\n    public BatchStoreAction<T> withRefs(List<DataStoreEntryRef<T>> refs) {\n        var node = toNode();\n        node.set(\"ref\", JacksonMapper.getDefault().valueToTree(refs));\n        BatchStoreAction<T> action = ActionJacksonMapper.parse(node);\n        return action;\n    }\n\n    public List<DataStoreEntryRef<T>> getRefs() {\n        return actions.stream().map(action -> action.getRef()).collect(Collectors.toList());\n    }\n\n    public Optional<BatchStoreAction<?>> withConfigString(String configString) {\n        try {\n            var tree = (ObjectNode) JacksonMapper.getDefault().readTree(configString);\n            tree.set(\"ref\", JacksonMapper.getDefault().valueToTree(getRefs()));\n            tree.put(\"id\", getId());\n            BatchStoreAction<?> action = ActionJacksonMapper.parse(tree);\n            return Optional.ofNullable(action);\n        } catch (Exception ex) {\n            return Optional.empty();\n        }\n    }\n\n    @Override\n    public List<DataStoreEntry> getStoreEntryContext() {\n        return getRefs().stream().map(DataStoreEntryRef::get).toList();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/HubBranchProvider.java",
    "content": "package io.xpipe.app.hub.action;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport java.util.List;\n\npublic interface HubBranchProvider<T extends DataStore> extends HubMenuItemProvider<T> {\n\n    List<HubMenuItemProvider<?>> getChildren(DataStoreEntryRef<T> store);\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/HubLeafProvider.java",
    "content": "package io.xpipe.app.hub.action;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport lombok.SneakyThrows;\n\npublic interface HubLeafProvider<T extends DataStore> extends HubMenuItemProvider<T> {\n\n    default boolean isDefault() {\n        return false;\n    }\n\n    default void execute(DataStoreEntryRef<T> ref) {\n        createAction(ref).executeAsync();\n    }\n\n    @SneakyThrows\n    default AbstractAction createAction(DataStoreEntryRef<T> ref) {\n        var c = getActionClass().orElseThrow();\n        var bm = c.getDeclaredMethod(\"builder\");\n        bm.setAccessible(true);\n        var b = bm.invoke(null);\n\n        if (StoreAction.class.isAssignableFrom(c)) {\n            var refMethod = b.getClass().getMethod(\"ref\", DataStoreEntryRef.class);\n            refMethod.setAccessible(true);\n            refMethod.invoke(b, ref);\n        }\n\n        var m = b.getClass().getDeclaredMethod(\"build\");\n        m.setAccessible(true);\n        var defValue = c.cast(m.invoke(b));\n        return defValue;\n    }\n\n    default boolean requiresValidStore() {\n        return true;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/HubMenuItemProvider.java",
    "content": "package io.xpipe.app.hub.action;\n\nimport io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.value.ObservableValue;\n\npublic interface HubMenuItemProvider<T extends DataStore> extends ActionProvider {\n\n    default StoreActionCategory getCategory() {\n        return null;\n    }\n\n    default boolean isMajor() {\n        return false;\n    }\n\n    default boolean isApplicable(DataStoreEntryRef<T> o) {\n        return true;\n    }\n\n    ObservableValue<String> getName(DataStoreEntryRef<T> store);\n\n    LabelGraphic getIcon(DataStoreEntryRef<T> store);\n\n    Class<?> getApplicableClass();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/MultiStoreAction.java",
    "content": "package io.xpipe.app.hub.action;\n\nimport io.xpipe.app.action.ActionJacksonMapper;\nimport io.xpipe.app.action.SerializableAction;\nimport io.xpipe.app.action.StoreContextAction;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.core.JacksonMapper;\n\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport lombok.Getter;\nimport lombok.SneakyThrows;\nimport lombok.experimental.SuperBuilder;\n\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\n\n@SuperBuilder\npublic abstract class MultiStoreAction<T extends DataStore> extends SerializableAction implements StoreContextAction {\n\n    @Getter\n    protected final List<DataStoreEntryRef<T>> refs;\n\n    @Override\n    public String getShortcutName() {\n        var names = refs.size() > 3\n                ? refs.size() + \" connections\"\n                : refs.stream()\n                        .map(ref -> DataStorage.get().getStoreEntryDisplayName(ref.get()))\n                        .collect(Collectors.joining(\", \"));\n        return names + \" (\" + getDisplayName() + \")\";\n    }\n\n    @Override\n    protected void beforeExecute() {\n        for (DataStoreEntryRef<T> ref : refs) {\n            ref.get().incrementBusyCounter();\n        }\n    }\n\n    @Override\n    protected void afterExecute() {\n        for (DataStoreEntryRef<T> ref : refs) {\n            ref.get().decrementBusyCounter();\n        }\n    }\n\n    @SneakyThrows\n    public MultiStoreAction<T> withRefs(List<DataStoreEntryRef<T>> refs) {\n        var node = toNode();\n        node.set(\"ref\", JacksonMapper.getDefault().valueToTree(refs));\n        MultiStoreAction<T> action = ActionJacksonMapper.parse(node);\n        return action;\n    }\n\n    public Optional<MultiStoreAction<T>> withConfigString(String configString) {\n        try {\n            var tree = (ObjectNode) JacksonMapper.getDefault().readTree(configString);\n            tree.set(\"ref\", JacksonMapper.getDefault().valueToTree(refs));\n            tree.put(\"id\", getId());\n            MultiStoreAction<T> action = ActionJacksonMapper.parse(tree);\n            return Optional.ofNullable(action);\n        } catch (Exception ex) {\n            return Optional.empty();\n        }\n    }\n\n    @Override\n    public List<DataStoreEntry> getStoreEntryContext() {\n        return refs.stream().map(ref -> ref.get()).toList();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/StoreAction.java",
    "content": "package io.xpipe.app.hub.action;\n\nimport io.xpipe.app.action.ActionJacksonMapper;\nimport io.xpipe.app.action.SerializableAction;\nimport io.xpipe.app.action.StoreContextAction;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.core.JacksonMapper;\n\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport lombok.Getter;\nimport lombok.SneakyThrows;\nimport lombok.experimental.SuperBuilder;\n\nimport java.util.List;\nimport java.util.Optional;\n\n@SuperBuilder\npublic abstract class StoreAction<T extends DataStore> extends SerializableAction implements StoreContextAction {\n\n    @Getter\n    protected final DataStoreEntryRef<T> ref;\n\n    @Override\n    public String getShortcutName() {\n        var name = DataStorage.get().getStoreEntryDisplayName(ref.get());\n        return name + \" (\" + getDisplayName() + \")\";\n    }\n\n    @Override\n    protected void beforeExecute() throws Exception {\n        ref.get().notifyUpdate(true, false);\n        ref.get().incrementBusyCounter();\n    }\n\n    @Override\n    protected void afterExecute() {\n        ref.get().decrementBusyCounter();\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public <V extends DataStore> StoreAction<V> asNeeded() {\n        return (StoreAction<V>) this;\n    }\n\n    @SneakyThrows\n    public StoreAction<T> withRef(DataStoreEntryRef<T> ref) {\n        var node = toNode();\n        node.set(\"ref\", JacksonMapper.getDefault().valueToTree(ref));\n        StoreAction<T> action = ActionJacksonMapper.parse(node);\n        return action;\n    }\n\n    public Optional<StoreAction<T>> withConfigString(String configString) {\n        try {\n            var tree = (ObjectNode) JacksonMapper.getDefault().readTree(configString);\n            tree.set(\"ref\", JacksonMapper.getDefault().valueToTree(ref));\n            tree.put(\"id\", getId());\n            StoreAction<T> action = ActionJacksonMapper.parse(tree);\n            return Optional.ofNullable(action);\n        } catch (Exception ex) {\n            return Optional.empty();\n        }\n    }\n\n    @Override\n    public List<DataStoreEntry> getStoreEntryContext() {\n        return List.of(ref.get());\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/StoreActionCategory.java",
    "content": "package io.xpipe.app.hub.action;\n\npublic enum StoreActionCategory {\n    CUSTOM,\n    OPEN,\n    CONFIGURATION,\n    APPEARANCE,\n    DEVELOPER,\n    DELETION\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/impl/BrowseHubLeafProvider.java",
    "content": "package io.xpipe.app.hub.action.impl;\n\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.ext.FileSystemStore;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.core.FilePath;\n\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class BrowseHubLeafProvider implements HubLeafProvider<FileSystemStore> {\n\n    @Override\n    public Action createAction(DataStoreEntryRef<FileSystemStore> ref) {\n        return Action.builder().ref(ref).build();\n    }\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.OPEN;\n    }\n\n    @Override\n    public boolean isMajor() {\n        return true;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<FileSystemStore> store) {\n        return AppI18n.observable(\"browseFiles\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<FileSystemStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2f-folder-open-outline\");\n    }\n\n    @Override\n    public Class<FileSystemStore> getApplicableClass() {\n        return FileSystemStore.class;\n    }\n\n    @Override\n    public String getId() {\n        return \"browseStore\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<FileSystemStore> {\n\n        FilePath path;\n\n        @Override\n        public void executeImpl() throws Exception {\n            DataStoreEntryRef<FileSystemStore> replacement =\n                    ProcessControlProvider.get().replace(ref);\n            BrowserFullSessionModel.DEFAULT.openFileSystemSync(\n                    replacement, null, (m) -> path, new SimpleBooleanProperty(), true);\n            AppLayoutModel.get().selectBrowser();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/impl/CloneHubLeafProvider.java",
    "content": "package io.xpipe.app.hub.action.impl;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.hub.comp.StoreCreationDialog;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.time.Duration;\n\npublic class CloneHubLeafProvider implements HubLeafProvider<DataStore> {\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.CONFIGURATION;\n    }\n\n    @Override\n    public boolean isApplicable(DataStoreEntryRef<DataStore> o) {\n        return o.get().getProvider().canClone() && o.get().getBreakOutCategory() == null;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<DataStore> store) {\n        return AppI18n.observable(\"clone\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<DataStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2c-content-copy\");\n    }\n\n    @Override\n    public Class<DataStore> getApplicableClass() {\n        return DataStore.class;\n    }\n\n    @Override\n    public String getId() {\n        return \"cloneStore\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<DataStore> {\n\n        @Override\n        public void executeImpl() {\n            var entry = DataStoreEntry.createNew(\n                    ref.get().getName() + \" (\" + AppI18n.get(\"connectionCopy\") + \")\", ref.getStore());\n\n            entry.setIcon(ref.get().getIcon(), true);\n            entry.setColor(ref.get().getColor());\n            entry.setExpanded(ref.get().isExpanded());\n            entry.setFreeze(ref.get().isFreeze());\n            entry.setCategoryUuid(ref.get().getCategoryUuid());\n            entry.setPinToTop(ref.get().isPinToTop());\n            entry.setOrderIndex(ref.get().getOrderIndex());\n            entry.setNotes(ref.get().getNotes());\n\n            var instant = ref.get().getLastAccess().plus(Duration.ofSeconds(1));\n            entry.setLastModified(instant);\n            entry.setLastUsed(instant);\n\n            DataStorage.get().addStoreEntryIfNotPresent(entry);\n            StoreCreationDialog.showEdit(entry);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/impl/EditHubLeafProvider.java",
    "content": "package io.xpipe.app.hub.action.impl;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.hub.comp.StoreCreationDialog;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class EditHubLeafProvider implements HubLeafProvider<DataStore> {\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.CONFIGURATION;\n    }\n\n    @Override\n    public boolean isApplicable(DataStoreEntryRef<DataStore> o) {\n        return o.get().getProvider().canConfigure();\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<DataStore> store) {\n        return AppI18n.observable(\"configure\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<DataStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2w-wrench-outline\");\n    }\n\n    @Override\n    public Class<DataStore> getApplicableClass() {\n        return DataStore.class;\n    }\n\n    @Override\n    public boolean requiresValidStore() {\n        return false;\n    }\n\n    @Override\n    public String getId() {\n        return \"editStore\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<DataStore> {\n\n        @Override\n        public void executeImpl() {\n            StoreCreationDialog.showEdit(ref.get());\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/impl/InitHubLeafProvider.java",
    "content": "package io.xpipe.app.hub.action.impl;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.ThreadHelper;\n\npublic abstract class InitHubLeafProvider<T extends DataStore, O> implements HubLeafProvider<T> {\n\n    protected O available;\n\n    @Override\n    public void init() {\n        ThreadHelper.runFailableAsync(() -> {\n            available = check();\n\n            // Update entries to potentially show item\n            if (available != null) {\n                StoreViewState.get().getAllEntries().getList().stream()\n                        .filter(w -> w.getValidity().getValue().isUsable())\n                        .forEach(w -> {\n                            if (getApplicableClass()\n                                    .isAssignableFrom(w.getStore().getValue().getClass())) {\n                                w.update();\n                            }\n                        });\n            }\n        });\n    }\n\n    @Override\n    public boolean isApplicable(DataStoreEntryRef<T> o) {\n        return available != null;\n    }\n\n    protected abstract O check() throws Exception;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/impl/OpenHubMenuLeafProvider.java",
    "content": "package io.xpipe.app.hub.action.impl;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.hub.action.BatchHubProvider;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class OpenHubMenuLeafProvider implements HubLeafProvider<DataStore>, BatchHubProvider<DataStore> {\n\n    @Override\n    public boolean requiresValidStore() {\n        return true;\n    }\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.OPEN;\n    }\n\n    @Override\n    public boolean isApplicable(DataStoreEntryRef<DataStore> o) {\n        return o.get().getValidity().isUsable() && (o.get().getProvider().launch(o.get()) != null);\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<DataStore> store) {\n        return AppI18n.observable(\"open\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<DataStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2c-console\");\n    }\n\n    @Override\n    public Class<DataStore> getApplicableClass() {\n        return DataStore.class;\n    }\n\n    @Override\n    public String getId() {\n        return \"open\";\n    }\n\n    @Override\n    public boolean isDefault() {\n        return true;\n    }\n\n    @Override\n    public ObservableValue<String> getName() {\n        return AppI18n.observable(\"open\");\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2c-console\");\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<DataStore> {\n\n        @Override\n        public void executeImpl() throws Exception {\n            var r = ref.get().getProvider().launch(ref.get());\n            r.run();\n\n            // Terminal launching is done async, so to show the busy marker, just wait here\n            ThreadHelper.sleep(1000);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/impl/OpenSplitHubBatchProvider.java",
    "content": "package io.xpipe.app.hub.action.impl;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.hub.action.BatchHubProvider;\nimport io.xpipe.app.hub.action.MultiStoreAction;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.terminal.*;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\n\npublic class OpenSplitHubBatchProvider implements BatchHubProvider<ShellStore> {\n\n    @Override\n    public boolean isActive(DataStoreEntryRef<ShellStore> o) {\n        return TerminalSplitStrategy.getEffectiveSplitStrategyObservable().getValue() != null;\n    }\n\n    @Override\n    public Class<ShellStore> getApplicableClass() {\n        return ShellStore.class;\n    }\n\n    @Override\n    public String getId() {\n        return \"openSplit\";\n    }\n\n    @Override\n    public ObservableValue<String> getName() {\n        return AppI18n.observable(\"openSplit\");\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2c-console-network-outline\");\n    }\n\n    @Override\n    public AbstractAction createBatchAction(List<DataStoreEntryRef<ShellStore>> dataStoreEntryRefs) {\n        return Action.builder().refs(dataStoreEntryRefs).build();\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends MultiStoreAction<ShellStore> {\n\n        @Override\n        public void executeImpl() throws Exception {\n            var type = AppPrefs.get().terminalType().getValue();\n            if (type == null) {\n                throw ErrorEventFactory.expected(new IllegalStateException(AppI18n.get(\"noTerminalSet\")));\n            }\n\n            var panes = new ArrayList<TerminalLauncher.Config>();\n            for (DataStoreEntryRef<ShellStore> ref : getRefs()) {\n                var replacement = ProcessControlProvider.get().replace(ref);\n                ShellStore store = replacement.getStore().asNeeded();\n                var control = store.standaloneControl();\n                // These prepend scripts, not append\n                TerminalPromptManager.configurePromptScript(control);\n                ProcessControlProvider.get().withDefaultScripts(control);\n\n                var title = DataStorage.get().getStoreEntryDisplayName(ref.get());\n                var config =\n                        new TerminalLauncher.Config(ref.get(), title, null, UUID.randomUUID(), true, true, control);\n                panes.add(config);\n            }\n            TerminalLauncher.open(panes, true, type);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/impl/RefreshActionProvider.java",
    "content": "package io.xpipe.app.hub.action.impl;\n\nimport io.xpipe.app.action.*;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.hub.action.StoreAction;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class RefreshActionProvider implements ActionProvider {\n\n    @Override\n    public String getId() {\n        return \"refreshStore\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<DataStore> {\n        @Override\n        public void executeImpl() {\n            ref.get().validate();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/impl/RefreshChildrenHubLeafProvider.java",
    "content": "package io.xpipe.app.hub.action.impl;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.FixedHierarchyStore;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class RefreshChildrenHubLeafProvider implements HubLeafProvider<FixedHierarchyStore> {\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.OPEN;\n    }\n\n    @Override\n    public boolean isMajor() {\n        return true;\n    }\n\n    @Override\n    public boolean isApplicable(DataStoreEntryRef<FixedHierarchyStore> o) {\n        return o.getStore().canManuallyRefresh();\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<FixedHierarchyStore> store) {\n        return AppI18n.observable(\"refresh\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<FixedHierarchyStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2r-refresh\");\n    }\n\n    @Override\n    public Class<FixedHierarchyStore> getApplicableClass() {\n        return FixedHierarchyStore.class;\n    }\n\n    @Override\n    public boolean isDefault() {\n        return true;\n    }\n\n    @Override\n    public String getId() {\n        return \"refreshStoreChildren\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<FixedHierarchyStore> {\n\n        @Override\n        public void executeImpl() {\n            DataStorage.get().refreshChildren(ref.get());\n            ref.get().setExpanded(true);\n            StoreViewState.get().triggerStoreListVisibilityUpdate();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/impl/RefreshHubLeafProvider.java",
    "content": "package io.xpipe.app.hub.action.impl;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.value.ObservableValue;\n\npublic class RefreshHubLeafProvider implements HubLeafProvider<ShellStore> {\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.CONFIGURATION;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<ShellStore> store) {\n        return AppI18n.observable(\"refresh\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<ShellStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2r-refresh\");\n    }\n\n    @Override\n    public Class<?> getApplicableClass() {\n        return ShellStore.class;\n    }\n\n    @Override\n    public AbstractAction createAction(DataStoreEntryRef<ShellStore> ref) {\n        return RefreshActionProvider.Action.builder().ref(ref.asNeeded()).build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/impl/SampleHubLeafProvider.java",
    "content": "package io.xpipe.app.hub.action.impl;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.process.CommandControl;\nimport io.xpipe.app.process.ElevationFunction;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.ShellDialects;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.core.FilePath;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.io.BufferedReader;\nimport java.io.StringReader;\n\npublic class SampleHubLeafProvider implements HubLeafProvider<ShellStore> {\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.OPEN;\n    }\n\n    @Override\n    public boolean isApplicable(DataStoreEntryRef<ShellStore> o) {\n        // Allows you to individually check whether this action should be available for the specific store.\n        return true;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<ShellStore> store) {\n        // The displayed name of the action, allows you to use translation keys.\n        return AppI18n.observable(\"installConnector\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<ShellStore> store) {\n        // The ikonli icon of the button.\n        return new LabelGraphic.IconGraphic(\"mdi2c-code-greater-than\");\n    }\n\n    @Override\n    public Class<ShellStore> getApplicableClass() {\n        // For which general type of connection store to make this action available.\n        return ShellStore.class;\n    }\n\n    @Override\n    public String getId() {\n        return \"sample\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    @SuppressWarnings(\"unused\")\n    public static class Action extends StoreAction<ShellStore> {\n\n        @Override\n        public void executeImpl() throws Exception {\n            // Start a shell control\n            try (ShellControl sc = ref.getStore().standaloneControl().start()) {\n                // Once we are here, the shell connection is initialized and we can query all kinds of information\n\n                // Query the detected shell dialect, e.g. cmd, powershell, sh, bash, etc.\n                System.out.println(sc.getShellDialect());\n\n                // Query the os type\n                System.out.println(sc.getOsType());\n\n                // Simple commands can be executed in one line\n                // The shell dialects also provide the appropriate commands for common operations like echo for all\n                // supported shells\n                String echoOut =\n                        sc.executeSimpleStringCommand(sc.getShellDialect().getEchoCommand(\"hello!\", false));\n\n                // You can also implement custom handling for more complex commands\n                var lsOut = sc.command(\"ls\").readStdoutOrThrow();\n                // Read the stdout lines as a stream\n                BufferedReader reader = new BufferedReader(new StringReader(lsOut));\n                // We don't have to close this stream here, that will be automatically done by the command control\n                // after the try-with block\n                reader.lines().filter(s -> !s.isBlank()).forEach(s -> {\n                    System.out.println(s);\n                });\n\n                // Commands can also be more complex and span multiple lines.\n                // In this case, XPipe will internally write a command to a script file and then execute the script\n                try (CommandControl cc = sc.command(\"\"\"\n                                                    VAR=\"value\"\n                                                    echo \"$VAR\"\n                                                    \"\"\").start()) {\n                    // Reads stdout, stashes stderr. If the exit code is not 0, it will throw an exception with the\n                    // stderr contents.\n                    var output = cc.readStdoutOrThrow();\n                }\n\n                // More customization options\n                // If the command should be run as root, the command will be executed with\n                // sudo and the optional sudo password automatically provided by XPipe\n                // by using the information from the connection store.\n                // You can also set a custom working directory.\n                try (CommandControl cc = sc.command(\"kill <pid>\")\n                        .elevated(ElevationFunction.elevated(\"kill\"))\n                        .withWorkingDirectory(FilePath.of(\"/\"))\n                        .start()) {\n                    // Discard any output but throw an exception with the stderr contents if the exit code is not 0\n                    cc.discardOrThrow();\n                }\n\n                // Start a bash sub shell. Useful if the login shell is different\n                try (ShellControl bash = sc.subShell(ShellDialects.BASH).start()) {\n                    // Let's write to a file\n                    try (CommandControl cc = bash.command(\"cat > myfile.txt\").start()) {\n                        // Writing into stdin can also easily be done\n                        cc.getStdin().write(\"my file content\".getBytes(cc.getCharset()));\n                        // Close stdin to send EOF. It will be reopened by the shell control after the command is done\n                        cc.closeStdin();\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/impl/ScanHubBatchProvider.java",
    "content": "package io.xpipe.app.hub.action.impl;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.hub.action.BatchHubProvider;\nimport io.xpipe.app.hub.action.MultiStoreAction;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.process.ShellTtyState;\nimport io.xpipe.app.process.SystemState;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.ScanDialog;\nimport io.xpipe.app.util.ScanDialogAction;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\n\npublic class ScanHubBatchProvider implements BatchHubProvider<ShellStore> {\n\n    @Override\n    public ObservableValue<String> getName() {\n        return AppI18n.observable(\"addConnections\");\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2l-layers-plus\");\n    }\n\n    @Override\n    public Class<?> getApplicableClass() {\n        return ShellStore.class;\n    }\n\n    @Override\n    public boolean isApplicable(DataStoreEntryRef<ShellStore> o) {\n        var state = o.get().getStorePersistentState();\n        if (state instanceof SystemState systemState) {\n            return (systemState.getShellDialect() == null\n                            || systemState.getShellDialect().getDumbMode().supportsAnyPossibleInteraction())\n                    && (systemState.getTtyState() == null || systemState.getTtyState() == ShellTtyState.NONE);\n        } else {\n            return true;\n        }\n    }\n\n    @Override\n    public AbstractAction createBatchAction(List<DataStoreEntryRef<ShellStore>> dataStoreEntryRefs) {\n        return Action.builder().refs(dataStoreEntryRefs).build();\n    }\n\n    @Override\n    public String getId() {\n        return \"scanStoreBatch\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends MultiStoreAction<ShellStore> {\n\n        @Override\n        public void executeImpl() {\n            ScanDialog.showMulti(refs, ScanDialogAction.shellScanAction());\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/impl/ScanHubLeafProvider.java",
    "content": "package io.xpipe.app.hub.action.impl;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.ScanDialog;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class ScanHubLeafProvider implements HubLeafProvider<ShellStore> {\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.OPEN;\n    }\n\n    @Override\n    public boolean isMajor() {\n        return true;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<ShellStore> store) {\n        return AppI18n.observable(\"scanConnections\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<ShellStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2l-layers-plus\");\n    }\n\n    @Override\n    public Class<?> getApplicableClass() {\n        return ShellStore.class;\n    }\n\n    @Override\n    public AbstractAction createAction(DataStoreEntryRef<ShellStore> ref) {\n        return Action.builder().ref(ref).build();\n    }\n\n    @Override\n    public String getId() {\n        return \"scanStore\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<ShellStore> {\n\n        @Override\n        public void executeImpl() {\n            ScanDialog.showSingleAsync(ref.get());\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/impl/StartOnInitHubLeafProvider.java",
    "content": "package io.xpipe.app.hub.action.impl;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.StartOnInitStore;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class StartOnInitHubLeafProvider implements HubLeafProvider<StartOnInitStore> {\n\n    @Override\n    public Action createAction(DataStoreEntryRef<StartOnInitStore> ref) {\n        return Action.builder().ref(ref).build();\n    }\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.CUSTOM;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<StartOnInitStore> store) {\n        return AppI18n.observable(store.getStore().isEnabled() ? \"disableStartOnInit\" : \"enableStartOnInit\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<StartOnInitStore> store) {\n        return new LabelGraphic.IconGraphic(\n                store.getStore().isEnabled() ? \"mdi2t-toggle-switch-off-outline\" : \"mdi2t-toggle-switch-outline\");\n    }\n\n    @Override\n    public boolean isApplicable(DataStoreEntryRef<StartOnInitStore> o) {\n        return o.getStore().isEnabled() || o.getStore().canAutomaticallyStart();\n    }\n\n    @Override\n    public Class<StartOnInitStore> getApplicableClass() {\n        return StartOnInitStore.class;\n    }\n\n    @Override\n    public String getId() {\n        return \"toggleStartOnInit\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<StartOnInitStore> {\n\n        @Override\n        public void executeImpl() throws Exception {\n            if (ref.getStore().isEnabled()) {\n                ref.getStore().disable();\n            } else {\n                ref.getStore().enable();\n                ref.getStore().startOnInit();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/action/impl/ToggleActionProvider.java",
    "content": "package io.xpipe.app.hub.action.impl;\n\nimport io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.ext.SingletonSessionStore;\nimport io.xpipe.app.hub.action.StoreAction;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class ToggleActionProvider implements ActionProvider {\n\n    @Override\n    public String getId() {\n        return \"toggleStore\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<SingletonSessionStore<?>> {\n\n        private final boolean enabled;\n\n        @Override\n        public void executeImpl() throws Exception {\n            if (enabled) {\n                ref.getStore().startSessionIfNeeded();\n            } else {\n                ref.getStore().stopSessionIfNeeded();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/DataStoreCategoryChoiceComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.geometry.Insets;\nimport javafx.scene.control.ComboBox;\nimport javafx.scene.control.ListCell;\nimport javafx.scene.layout.Region;\n\nimport lombok.EqualsAndHashCode;\nimport lombok.Value;\n\nimport java.util.function.Predicate;\n\npublic class DataStoreCategoryChoiceComp extends SimpleRegionBuilder {\n\n    private final StoreCategoryWrapper root;\n    private final Property<StoreCategoryWrapper> external;\n    private final Property<StoreCategoryWrapper> value;\n    private final boolean applyExternalInitially;\n    private final Predicate<StoreCategoryWrapper> filter;\n\n    public DataStoreCategoryChoiceComp(\n            StoreCategoryWrapper root,\n            Property<StoreCategoryWrapper> external,\n            Property<StoreCategoryWrapper> value,\n            boolean applyExternalInitially, Predicate<StoreCategoryWrapper> filter\n    ) {\n        this.root = root;\n        this.external = external;\n        this.value = value;\n        this.applyExternalInitially = applyExternalInitially;\n        this.filter = filter;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var initialized = new SimpleBooleanProperty();\n        var last = value.getValue();\n        external.subscribe(newValue -> {\n            if (newValue == null) {\n                value.setValue(root);\n            } else if (root == null) {\n                value.setValue(newValue);\n            } else if (!newValue.getRoot().equals(root)) {\n                if (!initialized.get()) {\n                    value.setValue(root);\n                }\n            } else {\n                value.setValue(newValue);\n            }\n            initialized.set(true);\n        });\n        if (!applyExternalInitially) {\n            value.setValue(last);\n        }\n        var box = new ComboBox<>(StoreViewState.get().getSortedCategories(root).filtered(filter).getList());\n        box.setValue(value.getValue());\n        box.valueProperty().addListener((observable, oldValue, newValue) -> {\n            value.setValue(newValue);\n        });\n        value.addListener((observable, oldValue, newValue) -> {\n            PlatformThread.runLaterIfNeeded(() -> box.setValue(newValue));\n        });\n        box.setCellFactory(param -> {\n            return new Cell(true);\n        });\n        box.setButtonCell(new Cell(false));\n        return box;\n    }\n\n    @EqualsAndHashCode(callSuper = true)\n    @Value\n    private static class Cell extends ListCell<StoreCategoryWrapper> {\n\n        boolean indent;\n\n        @Override\n        protected void updateItem(StoreCategoryWrapper w, boolean empty) {\n            super.updateItem(w, empty);\n            textProperty().unbind();\n            if (w != null) {\n                textProperty().bind(w.getShownName());\n                setPadding(new Insets(6, 6, 6, 8 + (indent ? w.getDepth() * 8 : 0)));\n            } else {\n                setText(\"None\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/DenseStoreEntryComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.core.AppFontSizes;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.geometry.HPos;\nimport javafx.geometry.Insets;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.Label;\nimport javafx.scene.layout.*;\n\npublic class DenseStoreEntryComp extends StoreEntryComp {\n\n    public DenseStoreEntryComp(StoreSection section, BaseRegionBuilder<?, ?> content) {\n        super(section, content);\n    }\n\n    private Label createInformation(GridPane grid) {\n        var information = new Label();\n        information.setGraphicTextGap(7);\n        information.getStyleClass().add(\"information\");\n\n        var state = getWrapper().getEntry().getProvider() != null\n                ? getWrapper().getEntry().getProvider().stateDisplay(getWrapper())\n                : RegionBuilder.empty();\n        information.setGraphic(state.build());\n\n        var summary = getWrapper().getShownSummary();\n        if (getWrapper().getEntry().getProvider() != null) {\n            var info = getWrapper().getShownInformation();\n            information\n                    .textProperty()\n                    .bind(Bindings.createStringBinding(\n                            () -> {\n                                var summaryValue = summary.getValue();\n                                var infoValue = info.getValue();\n                                if (summaryValue != null && infoValue != null && grid.isHover()) {\n                                    return summaryValue;\n                                } else if (summaryValue != null && infoValue != null) {\n                                    return infoValue;\n                                } else if (infoValue == null && summaryValue != null) {\n                                    return summaryValue;\n                                } else if (summaryValue == null && infoValue != null) {\n                                    return infoValue;\n                                } else {\n                                    return null;\n                                }\n                            },\n                            grid.hoverProperty(),\n                            info,\n                            summary));\n        }\n\n        return information;\n    }\n\n    @Override\n    public boolean isFullSize() {\n        return false;\n    }\n\n    @Override\n    public int getHeight() {\n        return 37;\n    }\n\n    protected Region createContent() {\n        var grid = new GridPane();\n        grid.setHgap(8);\n\n        var tags = createTags().build();\n        var index = createOrderIndex().build();\n        var name = createName().build();\n        name.maxWidthProperty()\n                .bind(Bindings.createDoubleBinding(\n                        () -> {\n                            return grid.getWidth() / 2.5;\n                        },\n                        grid.widthProperty()));\n        var notes = new StoreNotesComp(getWrapper()).build();\n        var userIcon = createUserIcon().build();\n        var pinIcon = createPinIcon().build();\n\n        var selection = createBatchSelection().build();\n        grid.add(selection, 0, 0, 1, 2);\n        grid.getColumnConstraints().add(new ColumnConstraints(25));\n        StoreViewState.get().getBatchMode().subscribe(batch -> {\n            if (batch) {\n                grid.getColumnConstraints().set(0, new ColumnConstraints(25));\n            } else {\n                grid.getColumnConstraints().set(0, new ColumnConstraints(-8));\n            }\n        });\n\n        var storeIcon = createIcon(28, 24, AppFontSizes::xxxl).build();\n        GridPane.setHalignment(storeIcon, HPos.CENTER);\n        grid.add(storeIcon, 1, 0);\n        grid.getColumnConstraints().add(new ColumnConstraints(34));\n\n        var controlsSize = content != null ? 140 : 70;\n        var custom = new ColumnConstraints(0, controlsSize, controlsSize);\n        custom.setHalignment(HPos.RIGHT);\n\n        var infoCC = new ColumnConstraints();\n        infoCC.prefWidthProperty().bind(content != null ? INFO_WITH_CONTENT_WIDTH : INFO_NO_CONTENT_WIDTH);\n        infoCC.setHalignment(HPos.LEFT);\n\n        var nameCC = new ColumnConstraints();\n        nameCC.setMinWidth(100);\n        nameCC.setHgrow(Priority.ALWAYS);\n        grid.getColumnConstraints().addAll(nameCC);\n\n        var active = new StoreActiveComp(getWrapper()).build();\n        var nameBox = new HBox(name, tags, index, userIcon, pinIcon, notes);\n        getWrapper().getSessionActive().subscribe(aBoolean -> {\n            if (!aBoolean) {\n                nameBox.getChildren().remove(active);\n            } else {\n                nameBox.getChildren().add(3, active);\n            }\n        });\n        nameBox.setSpacing(4);\n        nameBox.setAlignment(Pos.CENTER_LEFT);\n        grid.addRow(0, nameBox);\n\n        var info = createInformation(grid);\n        grid.addRow(0, info);\n        grid.getColumnConstraints().addAll(infoCC, custom);\n\n        var cr = content != null ? content.build() : new Region();\n        cr.getStyleClass().add(\"custom-content\");\n        var bb = createButtonBar(name);\n        var controls = new HBox(cr, bb);\n        controls.setFillHeight(true);\n        controls.setAlignment(Pos.CENTER_RIGHT);\n        controls.setSpacing(10);\n        controls.setPadding(new Insets(0, 0, 0, 10));\n        HBox.setHgrow(cr, Priority.ALWAYS);\n        grid.addRow(0, controls);\n\n        grid.getStyleClass().add(\"store-entry-grid\");\n        grid.getStyleClass().add(\"dense\");\n\n        applyState(grid);\n        return grid;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/OsLogoComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.PrettyImageHelper;\nimport io.xpipe.app.comp.base.StackComp;\nimport io.xpipe.app.core.AppResources;\nimport io.xpipe.app.process.SystemState;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.layout.Region;\n\nimport java.nio.file.Files;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic class OsLogoComp extends SimpleRegionBuilder {\n\n    private static final Map<String, String> ICONS = new HashMap<>();\n    private static final String LINUX_DEFAULT_24 = \"linux-24.png\";\n    private final StoreEntryWrapper wrapper;\n    private final ObservableValue<SystemStateComp.State> state;\n\n    public OsLogoComp(StoreEntryWrapper wrapper, ObservableValue<SystemStateComp.State> state) {\n        this.wrapper = wrapper;\n        this.state = state;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var img = Bindings.createObjectBinding(\n                () -> {\n                    if (state.getValue() != SystemStateComp.State.SUCCESS) {\n                        return null;\n                    }\n\n                    var ps = wrapper.getPersistentState().getValue();\n                    if (!(ps instanceof SystemState ons)) {\n                        return null;\n                    }\n\n                    return getImage(ons.getOsName(), ons.getOsType());\n                },\n                wrapper.getPersistentState(),\n                state);\n        var hide = Bindings.createBooleanBinding(\n                () -> {\n                    return img.get() != null;\n                },\n                img);\n        return new StackComp(List.of(\n                        new SystemStateComp(state).hide(hide),\n                        PrettyImageHelper.ofFixedSize(img, 24, 24).visible(hide)))\n                .build();\n    }\n\n    private String getImage(String name, OsType.Any type) {\n        if (name == null) {\n            return null;\n        }\n\n        if (name.contains(\"Cisco\")) {\n            return null;\n        }\n\n        if (ICONS.isEmpty()) {\n            AppResources.with(AppResources.MAIN_MODULE, \"os\", file -> {\n                try (var list = Files.list(file)) {\n                    list.filter(path -> path.toString().endsWith(\".png\")\n                                    && !path.toString().contains(\"-dark\")\n                                    && !path.toString().endsWith(LINUX_DEFAULT_24)\n                                    && !path.toString().endsWith(\"-40.png\"))\n                            .map(path -> path.getFileName().toString())\n                            .forEach(path -> {\n                                var base = path.replace(\"-dark\", \"\").replace(\"-24.png\", \".svg\");\n                                ICONS.put(\n                                        FilePath.of(base)\n                                                .getBaseName()\n                                                .toString()\n                                                .split(\"-\")[0],\n                                        \"os/\" + base);\n                            });\n                }\n            });\n        }\n\n        var found = ICONS.entrySet().stream()\n                .filter(e -> name.toLowerCase().contains(e.getKey())\n                        || name.toLowerCase().replaceAll(\"\\\\s+\", \"\").contains(e.getKey()))\n                .findAny()\n                .map(e -> e.getValue());\n        if (found.isPresent()) {\n            return found.get();\n        }\n\n        if (type == OsType.SOLARIS) {\n            return \"os/illumos.svg\";\n        }\n\n        if (type == OsType.UNIX) {\n            return \"os/unix.svg\";\n        }\n\n        return \"os/linux.svg\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StandardStoreEntryComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.core.OsType;\n\nimport javafx.geometry.HPos;\nimport javafx.geometry.Insets;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.Label;\nimport javafx.scene.layout.*;\n\npublic class StandardStoreEntryComp extends StoreEntryComp {\n\n    public StandardStoreEntryComp(StoreSection section, BaseRegionBuilder<?, ?> content) {\n        super(section, content);\n    }\n\n    @Override\n    public boolean isFullSize() {\n        return true;\n    }\n\n    @Override\n    public int getHeight() {\n        return 57;\n    }\n\n    protected Region createContent() {\n        var name = createName().build();\n        var tags = createTags().build();\n        var index = createOrderIndex().build();\n        var notes = new StoreNotesComp(getWrapper()).build();\n        var userIcon = createUserIcon().build();\n        var pinIcon = createPinIcon().build();\n\n        var grid = new GridPane();\n        grid.setHgap(6);\n        grid.setVgap(OsType.ofLocal() == OsType.MACOS ? 2 : 0);\n\n        var selection = createBatchSelection();\n        grid.add(selection.build(), 0, 0, 1, 2);\n        grid.getColumnConstraints().add(new ColumnConstraints(25));\n        StoreViewState.get().getBatchMode().subscribe(batch -> {\n            if (batch) {\n                grid.getColumnConstraints().set(0, new ColumnConstraints(25));\n            } else {\n                grid.getColumnConstraints().set(0, new ColumnConstraints(-6));\n            }\n        });\n\n        var storeIcon = createIcon(46, 40, AppFontSizes::title);\n        grid.add(storeIcon.build(), 1, 0, 1, 2);\n        grid.getColumnConstraints().add(new ColumnConstraints(52));\n\n        var active = new StoreActiveComp(getWrapper()).build();\n        var nameBox = new HBox(name, tags, index, userIcon, pinIcon, notes);\n        nameBox.setSpacing(4);\n        nameBox.setAlignment(Pos.CENTER_LEFT);\n        grid.add(nameBox, 2, 0);\n        GridPane.setVgrow(nameBox, Priority.ALWAYS);\n        getWrapper().getSessionActive().subscribe(aBoolean -> {\n            if (!aBoolean) {\n                nameBox.getChildren().remove(active);\n            } else {\n                nameBox.getChildren().add(3, active);\n            }\n        });\n\n        var summaryBox = new HBox(createSummary());\n        summaryBox.setAlignment(Pos.TOP_LEFT);\n        GridPane.setVgrow(summaryBox, Priority.ALWAYS);\n        grid.add(summaryBox, 2, 1);\n\n        var nameCC = new ColumnConstraints();\n        nameCC.setMinWidth(100);\n        nameCC.setHgrow(Priority.ALWAYS);\n        nameCC.setPrefWidth(100);\n        grid.getColumnConstraints().addAll(nameCC);\n\n        grid.add(createInformation(), 3, 0, 1, 2);\n        var info = new ColumnConstraints();\n        info.prefWidthProperty().bind(content != null ? INFO_WITH_CONTENT_WIDTH : INFO_NO_CONTENT_WIDTH);\n        info.setHalignment(HPos.LEFT);\n        grid.getColumnConstraints().add(info);\n\n        var controlsSize = content != null ? 140 : 70;\n        var custom = new ColumnConstraints(0, controlsSize, controlsSize);\n        custom.setHalignment(HPos.RIGHT);\n        var cr = content != null ? content.build() : new Region();\n        cr.getStyleClass().add(\"custom-content\");\n        var bb = createButtonBar(name);\n        var controls = new HBox(cr, bb);\n        controls.setFillHeight(true);\n        HBox.setHgrow(cr, Priority.ALWAYS);\n        controls.setAlignment(Pos.CENTER_RIGHT);\n        controls.setSpacing(10);\n        controls.setPadding(new Insets(0, 0, 0, 10));\n        grid.add(controls, 4, 0, 1, 2);\n        grid.getColumnConstraints().add(custom);\n\n        grid.getStyleClass().add(\"store-entry-grid\");\n\n        applyState(grid);\n\n        return grid;\n    }\n\n    private Label createSummary() {\n        var summary = new Label();\n        summary.textProperty().bind(getWrapper().getShownDescription());\n        summary.getStyleClass().add(\"summary\");\n        AppFontSizes.xs(summary);\n        return summary;\n    }\n\n    private Label createInformation() {\n        var information = new Label();\n        information.setGraphicTextGap(7);\n        information.textProperty().bind(getWrapper().getShownInformation());\n        information.getStyleClass().add(\"information\");\n\n        var state = getWrapper().getEntry().getProvider() != null\n                ? getWrapper().getEntry().getProvider().stateDisplay(getWrapper())\n                : RegionBuilder.empty();\n        information.setGraphic(state.build());\n\n        return information;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreActiveComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.TooltipHelper;\nimport io.xpipe.app.core.AppI18n;\n\nimport javafx.geometry.Pos;\nimport javafx.scene.control.Tooltip;\nimport javafx.scene.input.*;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\nimport javafx.scene.shape.Circle;\n\npublic class StoreActiveComp extends SimpleRegionBuilder {\n\n    private final StoreEntryWrapper wrapper;\n\n    public StoreActiveComp(StoreEntryWrapper wrapper) {\n        this.wrapper = wrapper;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var c = new Circle(6);\n        c.getStyleClass().add(\"dot\");\n        c.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {\n            if (event.getButton() == MouseButton.PRIMARY) {\n                wrapper.stopSession();\n                event.consume();\n            }\n        });\n        var pane = new StackPane(c);\n        pane.setAlignment(Pos.CENTER);\n        pane.visibleProperty().bind(wrapper.getSessionActive());\n        pane.getStyleClass().add(\"store-active-comp\");\n        Tooltip.install(pane, TooltipHelper.create(AppI18n.observable(\"sessionActive\")));\n        return pane;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreCategoryComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.augment.ContextMenuAugment;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.ClipboardHelper;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.MenuHelper;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreColor;\nimport io.xpipe.app.util.DesktopHelper;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.css.PseudoClass;\nimport javafx.geometry.Insets;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.ContextMenu;\nimport javafx.scene.control.Menu;\nimport javafx.scene.control.MenuItem;\nimport javafx.scene.control.SeparatorMenuItem;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyEvent;\nimport javafx.scene.input.MouseButton;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Region;\n\nimport lombok.EqualsAndHashCode;\nimport lombok.Value;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Locale;\n\n@EqualsAndHashCode(callSuper = true)\n@Value\npublic class StoreCategoryComp extends SimpleRegionBuilder {\n\n    private static final PseudoClass SELECTED = PseudoClass.getPseudoClass(\"selected\");\n\n    StoreCategoryWrapper category;\n\n    @Override\n    protected Region createSimple() {\n        var prop = new SimpleStringProperty();\n        category.getName().subscribe(prop::setValue);\n        AppPrefs.get().censorMode().subscribe(aBoolean -> {\n            var n = category.getName().getValue();\n            prop.setValue(aBoolean ? \"*\".repeat(n.length()) : n);\n        });\n        prop.addListener((observable, oldValue, newValue) -> {\n            if (!AppPrefs.get().censorMode().get()) {\n                category.getName().setValue(newValue);\n            }\n        });\n        var name = new LazyTextFieldComp(prop)\n                .style(\"name\")\n                .applyStructure(struc -> {\n                    category.getRenameTrigger().onFire(() -> {\n                        struc.get().requestFocus();\n                        struc.getTextField().selectAll();\n                    });\n                })\n                .build();\n        var showing = new SimpleBooleanProperty();\n\n        var expandIcon = Bindings.createObjectBinding(\n                () -> {\n                    if (category.getChildren().getList().size() == 0) {\n                        return new LabelGraphic.IconGraphic(\"mdal-keyboard_arrow_right\");\n                    }\n\n                    var exp = category.getExpanded().get();\n                    return new LabelGraphic.IconGraphic(\n                            exp ? \"mdal-keyboard_arrow_down\" : \"mdi2c-chevron-double-right\");\n                },\n                category.getExpanded(),\n                category.getChildren().getList());\n        var expandButton = new IconButtonComp(expandIcon, () -> {\n                    category.toggleExpanded();\n                })\n                .apply(struc -> {\n                    struc.setAlignment(Pos.CENTER);\n                    if (OsType.ofLocal() == OsType.WINDOWS) {\n                        HBox.setMargin(struc, new Insets(0, 0, 2.3, 0));\n                    } else if (OsType.ofLocal() == OsType.MACOS) {\n                        HBox.setMargin(struc, new Insets(0, 0, 1.8, 0));\n                    }\n                })\n                .disable(Bindings.isEmpty(category.getChildren().getList()))\n                .style(\"expand-button\")\n                .describe(d -> d.nameKey(\"expand\").shortcut(new KeyCodeCombination(KeyCode.SPACE)));\n\n        var focus = new SimpleBooleanProperty();\n        var hover = new SimpleBooleanProperty();\n        var statusIcon = Bindings.createObjectBinding(\n                () -> {\n                    if (hover.get() || focus.get()) {\n                        return new LabelGraphic.IconGraphic(\"mdomz-settings\");\n                    }\n\n                    if (!DataStorage.get().supportsSync()\n                            || (!category.getCategory().canShare())) {\n                        return new LabelGraphic.IconGraphic(\"mdi2g-git\");\n                    }\n\n                    return new LabelGraphic.IconGraphic(category.getSync().getValue() ? \"mdi2g-git\" : \"mdi2c-cancel\");\n                },\n                category.getSync(),\n                hover,\n                focus);\n        var statusButton = new IconButtonComp(statusIcon)\n                .apply(struc -> AppFontSizes.xs(struc))\n                .apply(struc -> {\n                    struc.setAlignment(Pos.CENTER);\n                    struc.setPadding(new Insets(0, 0, 0, 0));\n                })\n                .apply(new ContextMenuAugment<>(\n                        mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, null, () -> {\n                            var cm = createContextMenu();\n                            showing.bind(cm.showingProperty());\n                            return cm;\n                        }))\n                .describe(d -> d.nameKey(\"configuration\"))\n                .style(\"status-button\");\n\n        var count = new CountComp(\n                category.getShownContainedEntriesCount(),\n                category.getAllContainedEntriesCount(),\n                string -> \"(\" + string + \")\");\n        count.hide(Bindings.equal(0, category.getShownContainedEntriesCount()));\n        count.minWidth(Region.USE_PREF_SIZE);\n\n        var showStatus = hover.or(new SimpleBooleanProperty(DataStorage.get().supportsSync()))\n                .or(showing)\n                .or(focus);\n        var h = new HorizontalComp(List.of(\n                expandButton,\n                RegionBuilder.hspacer(category.getCategory().getParentCategory() == null ? 3 : 0),\n                RegionBuilder.of(() -> name).hgrow(),\n                RegionBuilder.hspacer(2),\n                count,\n                RegionBuilder.hspacer(7),\n                statusButton.hide(showStatus.not())));\n        h.padding(new Insets(0, 10, 0, (category.getDepth() * 10)));\n\n        var categoryButton = new ButtonComp(\n                        null, new SimpleObjectProperty<>(new LabelGraphic.CompGraphic(h)), category::select)\n                .describe(d -> d.name(prop).shortcut(new KeyCodeCombination(KeyCode.SPACE)))\n                .style(\"category-button\")\n                .apply(struc -> hover.bind(struc.hoverProperty()))\n                .apply(struc -> focus.bind(struc.focusWithinProperty()))\n                .maxWidth(2000);\n        categoryButton.apply(new ContextMenuAugment<>(\n                mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY,\n                keyEvent -> keyEvent.getCode() == KeyCode.SPACE,\n                () -> createContextMenu()));\n        categoryButton.apply(struc -> {\n            struc.addEventFilter(KeyEvent.KEY_PRESSED, event -> {\n                if (event.getCode() == KeyCode.SPACE) {\n                    category.toggleExpanded();\n                    event.consume();\n                }\n            });\n        });\n\n        var l = category.getChildren()\n                .getList()\n                .sorted(Comparator.comparing(storeCategoryWrapper ->\n                        storeCategoryWrapper.nameProperty().getValue().toLowerCase(Locale.ROOT)));\n        var children =\n                new ListBoxViewComp<>(l, l, storeCategoryWrapper -> new StoreCategoryComp(storeCategoryWrapper), false);\n        children.style(\"children\");\n        children.minHeight(0);\n        children.setVisibilityControl(true);\n\n        var hide = Bindings.createBooleanBinding(\n                () -> {\n                    return !category.getExpanded().get()\n                            || category.getChildren().getList().isEmpty();\n                },\n                category.getChildren().getList(),\n                category.getExpanded());\n        var v = new VerticalComp(List.of(categoryButton, children.hide(hide)));\n        v.style(\"category\");\n        v.apply(struc -> {\n            StoreViewState.get().getActiveCategory().subscribe(val -> {\n                struc.pseudoClassStateChanged(SELECTED, val.equals(category));\n            });\n\n            category.getColor().subscribe((c) -> {\n                DataStoreColor.applyStyleClasses(c, struc);\n            });\n        });\n\n        return v.build();\n    }\n\n    private ContextMenu createContextMenu() {\n        var contextMenu = MenuHelper.createContextMenu();\n\n        if (AppPrefs.get().enableHttpApi().get()) {\n            var copyId = new MenuItem(AppI18n.get(\"copyId\"), new FontIcon(\"mdi2c-content-copy\"));\n            copyId.setOnAction(event ->\n                    ClipboardHelper.copyText(category.getCategory().getUuid().toString()));\n            contextMenu.getItems().add(copyId);\n        }\n\n        if (AppPrefs.get().developerMode().getValue()) {\n            var browse = new MenuItem(AppI18n.get(\"browseInternalStorage\"), new FontIcon(\"mdi2f-folder-open-outline\"));\n            browse.setOnAction(\n                    event -> DesktopHelper.browseFile(category.getCategory().getDirectory()));\n            contextMenu.getItems().add(browse);\n        }\n\n        var newCategory = new MenuItem(AppI18n.get(\"createNewCategory\"), new FontIcon(\"mdi2p-plus-thick\"));\n        newCategory.setOnAction(event -> {\n            StoreViewState.get().createNewCategory(category);\n        });\n        contextMenu.getItems().add(newCategory);\n\n        contextMenu.getItems().add(new SeparatorMenuItem());\n\n        var configure = new MenuItem(AppI18n.get(\"configure\"), new FontIcon(\"mdi2w-wrench-outline\"));\n        configure.setOnAction(event -> {\n            StoreCategoryConfigComp.show(category);\n        });\n        contextMenu.getItems().add(configure);\n\n        var rename = new MenuItem(AppI18n.get(\"rename\"), new FontIcon(\"mdal-edit\"));\n        rename.setOnAction(event -> {\n            category.getRenameTrigger().fire(null);\n            event.consume();\n        });\n        contextMenu.getItems().add(rename);\n\n        contextMenu.getItems().add(new SeparatorMenuItem());\n\n        if (category.canMove()) {\n            var move = new Menu(AppI18n.get(\"moveTo\"), new FontIcon(\"mdi2f-folder-move-outline\"));\n            StoreViewState.get()\n                    .getSortedCategories(getCategory().getRoot())\n                    .getList()\n                    .forEach(storeCategoryWrapper -> {\n                        MenuItem m = new MenuItem();\n                        m.textProperty()\n                                .setValue(\"  \".repeat(storeCategoryWrapper.getDepth())\n                                        + storeCategoryWrapper.getName().getValue());\n                        m.setOnAction(event -> {\n                            category.moveToParent(storeCategoryWrapper.getCategory());\n                            event.consume();\n                        });\n                        if (storeCategoryWrapper.getParent() == null\n                                || storeCategoryWrapper.equals(category)\n                                || storeCategoryWrapper.equals(category.getParent())) {\n                            m.setDisable(true);\n                        }\n\n                        move.getItems().add(m);\n                    });\n            contextMenu.getItems().add(move);\n        }\n\n        var del = new MenuItem(AppI18n.get(\"remove\"), new FontIcon(\"mdal-delete_outline\"));\n        del.setOnAction(event -> {\n            category.delete();\n        });\n        del.setDisable(!DataStorage.get().canDeleteStoreCategory(category.getCategory()));\n        contextMenu.getItems().add(del);\n\n        return contextMenu;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreCategoryConfigComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.ChoiceComp;\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreCategoryConfig;\nimport io.xpipe.app.storage.DataStoreColor;\nimport io.xpipe.app.storage.DataStoreEntry;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.control.ListCell;\nimport javafx.scene.control.ScrollPane;\nimport javafx.scene.layout.Region;\n\nimport lombok.AllArgsConstructor;\n\nimport java.util.LinkedHashMap;\nimport java.util.function.Supplier;\n\n@AllArgsConstructor\npublic class StoreCategoryConfigComp extends SimpleRegionBuilder {\n\n    private final StoreCategoryWrapper wrapper;\n    private final Property<DataStoreCategoryConfig> config;\n\n    public static void show(StoreCategoryWrapper wrapper) {\n        var config = new SimpleObjectProperty<>(wrapper.getCategory().getConfig());\n        var comp = new StoreCategoryConfigComp(wrapper, config);\n        comp.prefWidth(600);\n        var modal = ModalOverlay.of(\n                AppI18n.observable(\"categoryConfigTitle\", wrapper.getName().getValue()), comp, null);\n        modal.addButton(ModalButton.cancel());\n        modal.addButton(ModalButton.ok(() -> {\n            DataStorage.get().updateCategoryConfig(wrapper.getCategory(), config.getValue());\n        }));\n        modal.show();\n    }\n\n    @Override\n    protected Region createSimple() {\n        var colors = new LinkedHashMap<DataStoreColor, ObservableValue<String>>();\n        colors.put(null, AppI18n.observable(\"none\"));\n        for (DataStoreColor value : DataStoreColor.values()) {\n            colors.put(value, AppI18n.observable(value.getId()));\n        }\n\n        var c = config.getValue();\n        var color = new SimpleObjectProperty<>(c.getColor());\n        var scripts = new SimpleObjectProperty<>(c.getDontAllowScripts());\n        var confirm = new SimpleObjectProperty<>(c.getConfirmAllModifications());\n        var sync = new SimpleObjectProperty<>(c.getSync());\n        var freeze = new SimpleObjectProperty<>(c.getFreezeConfigurations());\n        var ref = new SimpleObjectProperty<>(\n                c.getDefaultIdentityStore() != null\n                        ? DataStorage.get()\n                                .getStoreEntryIfPresent(c.getDefaultIdentityStore())\n                                .map(DataStoreEntry::ref)\n                                .orElse(null)\n                        : null);\n        var connectionsCategory = wrapper.getRoot().equals(StoreViewState.get().getAllConnectionsCategory());\n\n        var colorChoice = new ChoiceComp<>(color, colors, false);\n        colorChoice.apply(struc -> {\n            Supplier<ListCell<DataStoreColor>> cell = () -> new ListCell<>() {\n                @Override\n                protected void updateItem(DataStoreColor color, boolean empty) {\n                    super.updateItem(color, empty);\n                    if (color == null) {\n                        setText(AppI18n.get(\"none\"));\n                        setGraphic(DataStoreColor.createDisplayGraphic(null));\n                        return;\n                    }\n\n                    setText(AppI18n.get(color.getId()));\n                    setGraphic(DataStoreColor.createDisplayGraphic(color));\n                }\n            };\n            struc.setButtonCell(cell.get());\n            struc.setCellFactory(ignored -> {\n                return cell.get();\n            });\n        });\n\n        var options = new OptionsBuilder();\n\n        var specialCategorySync = !wrapper.getCategory().canShare();\n        var syncDisable = !DataStorage.get().supportsSync()\n                || ((sync.getValue() == null || !sync.getValue())\n                        && !wrapper.getCategory().canShare());\n        var syncHide = !DataStorage.get().supportsSync();\n        options.name(\n                        specialCategorySync\n                                ? AppI18n.observable(\n                                        \"categorySyncSpecial\", wrapper.getName().getValue())\n                                : AppI18n.observable(\"categorySync\"))\n                .description(\"categorySyncDescription\")\n                .addYesNoToggle(sync)\n                .disable(syncDisable)\n                .hide(syncHide)\n                .nameAndDescription(\"categoryDontAllowScripts\")\n                .addYesNoToggle(scripts)\n                .hide(!connectionsCategory)\n                .nameAndDescription(\"categoryConfirmAllModifications\")\n                .addYesNoToggle(confirm)\n                .hide(!connectionsCategory)\n                .nameAndDescription(\"categoryFreeze\")\n                .addYesNoToggle(freeze)\n                .hide(!connectionsCategory)\n                .nameAndDescription(\"categoryDefaultIdentity\")\n                .addComp(\n                        new StoreChoiceComp<>(\n                                null,\n                                ref,\n                                DataStore.class,\n                                null,\n                                StoreViewState.get().getAllIdentitiesCategory()),\n                        ref)\n                .hide(!connectionsCategory)\n                .nameAndDescription(\"categoryColor\")\n                .addComp(colorChoice, color)\n                .bind(\n                        () -> {\n                            return new DataStoreCategoryConfig(\n                                    color.get(),\n                                    scripts.get(),\n                                    confirm.get(),\n                                    sync.get(),\n                                    freeze.get(),\n                                    ref.get() != null ? ref.get().get().getUuid() : null);\n                        },\n                        config);\n        var r = options.build();\n        var sp = new ScrollPane(r);\n        sp.setFitToWidth(true);\n        sp.prefHeightProperty().bind(r.heightProperty());\n        return sp;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreCategoryListComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.ScrollComp;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.scene.layout.Region;\n\npublic class StoreCategoryListComp extends SimpleRegionBuilder {\n\n    private final StoreCategoryWrapper root;\n\n    public StoreCategoryListComp(StoreCategoryWrapper root) {\n        this.root = root;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var sp = new ScrollComp(new StoreCategoryComp(root));\n        sp.style(\"store-category-bar\");\n        sp.apply(struc -> {\n            Region content = (Region) struc.getContent();\n            struc.setFitToWidth(true);\n            if (OsType.ofLocal() == OsType.MACOS) {\n                AppFontSizes.lg(struc);\n            }\n            struc.minHeightProperty()\n                    .bind(Bindings.createDoubleBinding(\n                            () -> {\n                                var h = content.getHeight();\n                                return Math.min(150, h + 2);\n                            },\n                            content.heightProperty()));\n        });\n        return sp.build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreCategoryWrapper.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.DerivedObservableList;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreCategory;\nimport io.xpipe.app.storage.DataStoreColor;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.*;\nimport javafx.beans.value.ObservableStringValue;\n\nimport lombok.Getter;\nimport org.int4.fx.values.util.Trigger;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.Optional;\n\n@Getter\npublic class StoreCategoryWrapper {\n\n    private final DataStoreCategory root;\n    private final int depth;\n    private final Property<String> name;\n    private final DataStoreCategory category;\n    private final Property<Instant> lastAccess;\n    private final BooleanProperty sync;\n    private final DerivedObservableList<StoreCategoryWrapper> children;\n    private final DerivedObservableList<StoreEntryWrapper> directContainedEntries;\n    private final IntegerProperty shownContainedEntriesCount = new SimpleIntegerProperty();\n    private final IntegerProperty allContainedEntriesCount = new SimpleIntegerProperty();\n    private final BooleanProperty expanded = new SimpleBooleanProperty();\n    private final Property<DataStoreColor> color = new SimpleObjectProperty<>();\n    private final BooleanProperty largeCategoryOptimizations = new SimpleBooleanProperty();\n    private final Trigger<Void> renameTrigger = Trigger.of();\n    private StoreCategoryWrapper cachedParent;\n\n    public StoreCategoryWrapper(DataStoreCategory category) {\n        var d = 0;\n        DataStoreCategory last = category;\n        DataStoreCategory p = category;\n        while ((p = DataStorage.get()\n                        .getStoreCategoryIfPresent(p.getParentCategory())\n                        .orElse(null))\n                != null) {\n            d++;\n            last = p;\n        }\n        depth = d;\n\n        this.root = last;\n        this.category = category;\n        this.name = new SimpleStringProperty(category.getName());\n        this.lastAccess = new SimpleObjectProperty<>(category.getLastAccess());\n        this.sync = new SimpleBooleanProperty(Boolean.TRUE.equals(\n                DataStorage.get().getEffectiveCategoryConfig(category).getSync()));\n        this.children = DerivedObservableList.arrayList(true);\n        this.directContainedEntries = DerivedObservableList.arrayList(true);\n        this.color.setValue(\n                DataStorage.get().getEffectiveCategoryConfig(category).getColor());\n        setupListeners();\n    }\n\n    public ObservableStringValue getShownName() {\n        return Bindings.createStringBinding(\n                () -> {\n                    var n = nameProperty().getValue();\n                    return AppPrefs.get().censorMode().get() ? \"*\".repeat(n.length()) : n;\n                },\n                AppPrefs.get().censorMode(),\n                nameProperty());\n    }\n\n    public boolean canMove() {\n        return DataStorage.get().canMoveStoreCategory(category);\n    }\n\n    public StoreCategoryWrapper getRoot() {\n        return StoreViewState.get().getCategoryWrapper(root);\n    }\n\n    public StoreCategoryWrapper getParent() {\n        if (category.getParentCategory() == null) {\n            return null;\n        }\n\n        if (cachedParent == null) {\n            cachedParent = StoreViewState.get().getCategories().getList().stream()\n                    .filter(storeCategoryWrapper ->\n                            storeCategoryWrapper.getCategory().getUuid().equals(category.getParentCategory()))\n                    .findAny()\n                    .orElse(null);\n        }\n\n        return cachedParent;\n    }\n\n    public void select() {\n        PlatformThread.runLaterIfNeeded(() -> {\n            StoreViewState.get().getActiveCategory().setValue(this);\n        });\n    }\n\n    public void moveToParent(DataStoreCategory newParent) {\n        DataStorage.get().moveCategoryToParent(category, newParent);\n    }\n\n    public void delete() {\n        for (var c : children.getList()) {\n            c.delete();\n        }\n        DataStorage.get().deleteStoreCategory(category, false, false);\n    }\n\n    private void setupListeners() {\n        name.addListener((c, o, n) -> {\n            if (n.equals(translatedName(category.getName()))) {\n                return;\n            }\n\n            category.setName(n);\n            if (!category.getName().equals(name.getValue())) {\n                Platform.runLater(() -> {\n                    name.setValue(category.getName());\n                });\n            }\n        });\n\n        expanded.addListener((c, o, n) -> {\n            category.setExpanded(n);\n        });\n\n        category.addListener(() -> PlatformThread.runLaterIfNeeded(() -> {\n            update();\n        }));\n\n        AppPrefs.get().showChildCategoriesInParentCategory().addListener((observable, oldValue, newValue) -> {\n            update();\n        });\n\n        AppI18n.activeLanguage().addListener((observable, oldValue, newValue) -> {\n            update();\n        });\n    }\n\n    public void toggleExpanded() {\n        this.expanded.set(!expanded.getValue());\n    }\n\n    public void update() {\n        // We are probably in shutdown then\n        if (StoreViewState.get() == null) {\n            return;\n        }\n\n        // Avoid reupdating name when changed from the name property!\n        var catName = translatedName(category.getName());\n        if (!catName.equals(name.getValue())) {\n            name.setValue(catName);\n        }\n\n        lastAccess.setValue(category.getLastAccess().minus(Duration.ofMillis(500)));\n        sync.setValue(Boolean.TRUE.equals(\n                DataStorage.get().getEffectiveCategoryConfig(category).getSync()));\n        expanded.setValue(category.isExpanded());\n        color.setValue(DataStorage.get().getEffectiveCategoryConfig(category).getColor());\n\n        var allEntries = new ArrayList<>(StoreViewState.get().getAllEntries().getList());\n        directContainedEntries.setContent(allEntries.stream()\n                .filter(entry -> {\n                    return entry.getEntry().getCategoryUuid().equals(category.getUuid());\n                })\n                .toList());\n\n        children.setContent(StoreViewState.get().getCategories().getList().stream()\n                .filter(storeCategoryWrapper -> getCategory()\n                        .getUuid()\n                        .equals(storeCategoryWrapper.getCategory().getParentCategory()))\n                .toList());\n        var direct = directContainedEntries\n                .getList()\n                .filtered(storeEntryWrapper -> storeEntryWrapper.includeInConnectionCount())\n                .size();\n        var sub = children.getList().stream()\n                .mapToInt(value -> value.allContainedEntriesCount.get())\n                .sum();\n        allContainedEntriesCount.setValue(direct + sub);\n\n        var performanceCount =\n                AppPrefs.get().showChildCategoriesInParentCategory().get() ? allContainedEntriesCount.get() : direct;\n        if (performanceCount > 500) {\n            largeCategoryOptimizations.setValue(true);\n        }\n\n        var directFiltered = directContainedEntries.getList().stream()\n                .filter(storeEntryWrapper -> {\n                    var filter = StoreViewState.get().getFilterString().getValue();\n                    if (filter != null) {\n                        var matches = storeEntryWrapper.matchesFilter(filter);\n                        return matches;\n                    }\n\n                    return storeEntryWrapper.includeInConnectionCount();\n                })\n                .count();\n        // Due to always including filtered entries, there is the possibility of exceeding the direct count\n        directFiltered = Math.min(directFiltered, direct);\n        var subFiltered = children.getList().stream()\n                .mapToInt(value -> value.shownContainedEntriesCount.get())\n                .sum();\n        shownContainedEntriesCount.setValue(directFiltered + subFiltered);\n        Optional.ofNullable(getParent()).ifPresent(storeCategoryWrapper -> {\n            storeCategoryWrapper.update();\n        });\n    }\n\n    private String translatedName(String original) {\n        if (original.equals(\"All connections\")) {\n            return AppI18n.get(\"allConnections\");\n        }\n        if (original.equals(\"All scripts\")) {\n            return AppI18n.get(\"allScripts\");\n        }\n        if (original.equals(\"All identities\")) {\n            return AppI18n.get(\"allIdentities\");\n        }\n        if (original.equals(\"All macros\")) {\n            return AppI18n.get(\"allMacros\");\n        }\n        if (original.equals(\"Local\")) {\n            return AppI18n.get(\"local\");\n        }\n        if (original.equals(\"Synced\")) {\n            return AppI18n.get(\"synced\");\n        }\n        if (original.equals(\"Predefined\") || original.equals(\"Samples\")) {\n            return AppI18n.get(\"samples\");\n        }\n        if (original.equals(\"Custom\")) {\n            return AppI18n.get(\"custom\");\n        }\n        if (original.equals(\"Default\")) {\n            return AppI18n.get(\"default\");\n        }\n\n        return original;\n    }\n\n    public Property<String> nameProperty() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreChoiceComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.core.OsType;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.*;\nimport javafx.geometry.Pos;\nimport javafx.scene.input.MouseButton;\nimport javafx.scene.layout.AnchorPane;\nimport javafx.scene.layout.Region;\n\nimport atlantafx.base.theme.Styles;\nimport lombok.RequiredArgsConstructor;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.function.Predicate;\n\n@RequiredArgsConstructor\npublic class StoreChoiceComp<T extends DataStore> extends SimpleRegionBuilder {\n\n    private final ObjectProperty<DataStoreEntryRef<T>> selected;\n\n    private StoreChoicePopover<T> popover;\n\n    public StoreChoiceComp(\n            DataStoreEntry self,\n            ObjectProperty<DataStoreEntryRef<T>> selected,\n            Class<?> storeClass,\n            Predicate<DataStoreEntryRef<T>> applicableCheck,\n            StoreCategoryWrapper categoryRoot,\n            boolean requireComplete) {\n        this(self, selected, storeClass, applicableCheck, categoryRoot, null, requireComplete);\n    }\n\n    public StoreChoiceComp(\n            DataStoreEntry self,\n            ObjectProperty<DataStoreEntryRef<T>> selected,\n            Class<?> storeClass,\n            Predicate<DataStoreEntryRef<T>> applicableCheck,\n            StoreCategoryWrapper categoryRoot,\n            StoreCategoryWrapper explicitCategory,\n            boolean requireComplete) {\n        this.selected = selected;\n        this.popover = new StoreChoicePopover<>(\n                self,\n                selected,\n                storeClass,\n                applicableCheck,\n                categoryRoot,\n                explicitCategory,\n                requireComplete,\n                StoreViewState.get().getAllConnectionsCategory().equals(categoryRoot)\n                        ? \"selectConnection\"\n                        : \"selectEntry\",\n                \"noCompatibleConnection\");\n    }\n\n    public StoreChoiceComp(\n            DataStoreEntry self,\n            ObjectProperty<DataStoreEntryRef<T>> selected,\n            Class<?> storeClass,\n            Predicate<DataStoreEntryRef<T>> applicableCheck,\n            StoreCategoryWrapper initialCategory) {\n        this(self, selected, storeClass, applicableCheck, initialCategory, true);\n    }\n\n    protected String toName(DataStoreEntry entry) {\n        if (entry == null) {\n            return null;\n        }\n\n        return DataStorage.get().getStoreEntryDisplayName(entry);\n    }\n\n    protected String toGraphic(DataStoreEntry entry) {\n        if (entry == null) {\n            return null;\n        }\n\n        return entry.getEffectiveIconFile();\n    }\n\n    @Override\n    protected Region createSimple() {\n        var button = new ButtonComp(\n                Bindings.createStringBinding(\n                        () -> {\n                            var val = selected.getValue();\n                            return toName(val != null ? val.get() : null);\n                        },\n                        selected),\n                () -> {});\n        button.describe(d -> d.name(Bindings.createStringBinding(\n                () -> {\n                    return selected.getValue() != null\n                            ? toName(selected.getValue().get())\n                            : AppI18n.get(\"selectConnection\");\n                },\n                selected,\n                AppI18n.activeLanguage())));\n        button.apply(struc -> {\n                    struc.setMaxWidth(20000);\n                    struc.setAlignment(Pos.CENTER_LEFT);\n                    BaseRegionBuilder<?, ?> graphic = PrettyImageHelper.ofFixedSize(\n                            Bindings.createStringBinding(\n                                    () -> {\n                                        return toGraphic(\n                                                selected.getValue() != null\n                                                        ? selected.getValue().get()\n                                                        : null);\n                                    },\n                                    selected),\n                            16,\n                            16);\n                    struc.setGraphic(graphic.build());\n                    struc.setOnAction(event -> {\n                        popover.show(struc);\n                        event.consume();\n                    });\n                    struc.setOnMouseClicked(event -> {\n                        if (event.getButton() != MouseButton.SECONDARY) {\n                            return;\n                        }\n\n                        selected.setValue(null);\n                        event.consume();\n                    });\n                })\n                .style(\"choice-comp\");\n\n        var r = button.build();\n\n        var dropdownIcon = new FontIcon(\"mdal-keyboard_arrow_down\");\n        dropdownIcon.setDisable(true);\n        dropdownIcon.setPickOnBounds(false);\n        dropdownIcon.visibleProperty().bind(r.disabledProperty().not());\n        AppFontSizes.xl(dropdownIcon);\n\n        var pane = new AnchorPane(r, dropdownIcon);\n        r.prefHeightProperty().bind(pane.heightProperty());\n        pane.focusedProperty().addListener((observable, oldValue, newValue) -> {\n            if (newValue) {\n                r.requestFocus();\n            }\n        });\n        AnchorPane.setTopAnchor(dropdownIcon, OsType.ofLocal() == OsType.MACOS ? 8.5 : 11.0);\n        AnchorPane.setRightAnchor(dropdownIcon, 7.0);\n        AnchorPane.setRightAnchor(r, 0.0);\n        AnchorPane.setLeftAnchor(r, 0.0);\n        pane.setPickOnBounds(false);\n        pane.setMaxWidth(20000);\n\n        var clearButton = new IconButtonComp(\"mdi2c-close\", () -> {\n            selected.setValue(null);\n            Platform.runLater(() -> {\n                pane.requestFocus();\n            });\n        });\n        clearButton.describe(d -> d.nameKey(\"clear\"));\n        clearButton.style(Styles.FLAT);\n        clearButton.hide(selected.isNull().or(pane.disabledProperty()));\n        clearButton.apply(struc -> {\n            struc.setOpacity(0.7);\n            struc.getStyleClass().add(\"clear-button\");\n            AppFontSizes.xs(struc);\n            AnchorPane.setRightAnchor(struc, 30.0);\n            AnchorPane.setTopAnchor(struc, 3.0);\n            AnchorPane.setBottomAnchor(struc, 3.0);\n        });\n        pane.getChildren().add(clearButton.build());\n        pane.getStyleClass().add(\"store-choice-comp\");\n\n        return pane;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreChoicePopover.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.MenuHelper;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.collections.ListChangeListener;\nimport javafx.geometry.Insets;\nimport javafx.scene.Node;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyEvent;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.VBox;\n\nimport atlantafx.base.controls.Popover;\nimport atlantafx.base.theme.Styles;\nimport lombok.RequiredArgsConstructor;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.List;\nimport java.util.Set;\nimport java.util.function.Consumer;\nimport java.util.function.Predicate;\n\n@RequiredArgsConstructor\npublic class StoreChoicePopover<T extends DataStore> {\n\n    private final DataStoreEntry self;\n    private final Property<DataStoreEntryRef<T>> selected;\n    private final Class<?> storeClass;\n    private final Predicate<DataStoreEntryRef<T>> applicableCheck;\n    private final StoreCategoryWrapper rootCategory;\n    private final StoreCategoryWrapper explicitCategory;\n    private final boolean requireComplete;\n    private final String titleKey;\n    private final String noMatchKey;\n    private Consumer<Popover> consumer;\n    private Popover popover;\n\n    public void withPopover(Consumer<Popover> consumer) {\n        this.consumer = consumer;\n    }\n\n    public void show(Node node) {\n        var p = getPopover();\n        if (!p.isShowing()) {\n            p.show(node);\n        } else {\n            p.hide();\n        }\n    }\n\n    public void hide() {\n        if (popover != null) {\n            popover.hide();\n        }\n    }\n\n    private Popover getPopover() {\n        // Rebuild popover if we have a non-null condition to allow for the content to be updated in case the condition\n        // changed\n        if (popover == null || applicableCheck != null) {\n            var cur = StoreViewState.get().getActiveCategory().getValue();\n            var selectedCategory = new SimpleObjectProperty<>(\n                    explicitCategory != null\n                            ? explicitCategory\n                            : (rootCategory != null\n                                    ? (rootCategory.getRoot().equals(cur.getRoot()) ? cur : rootCategory)\n                                    : cur));\n            var filterText = new SimpleStringProperty();\n            popover = new Popover();\n            Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {\n                var e = storeEntryWrapper.getEntry();\n\n                if (self != null\n                        && (e.equals(self)\n                                || DataStorage.get().getStoreParentHierarchy(e).contains(self))) {\n                    return false;\n                }\n\n                // Check if load failed\n                if (e.getStore() == null) {\n                    return false;\n                }\n\n                return storeClass.isAssignableFrom(e.getStore().getClass())\n                        && (!requireComplete || e.getValidity().isUsable())\n                        && (applicableCheck == null || applicableCheck.test(e.ref()));\n            };\n\n            var applicableMatch =\n                    StoreViewState.get().getCurrentTopLevelSection().anyMatches(applicable);\n            if (!applicableMatch) {\n                selectedCategory.set(rootCategory);\n            }\n\n            var applicableCount = StoreViewState.get().getAllEntries().getList().stream()\n                    .filter(applicable)\n                    .count();\n            var initialExpanded = applicableCount < 20;\n\n            var enabled = popover.showingProperty();\n            var section = new StoreSectionMiniComp(\n                    StoreSection.createTopLevel(\n                            StoreViewState.get().getAllEntries(),\n                            Set.of(),\n                            applicable,\n                            filterText,\n                            selectedCategory,\n                            StoreViewState.get().getEntriesListVisibilityObservable(),\n                            StoreViewState.get().getEntriesListUpdateObservable(),\n                            enabled),\n                    (s, comp) -> {\n                        if (!applicable.test(s.getWrapper())) {\n                            comp.disable(new SimpleBooleanProperty(true));\n                        }\n                    },\n                    sec -> {\n                        if (applicable.test(sec.getWrapper())) {\n                            this.selected.setValue(sec.getWrapper().getEntry().ref());\n                            popover.hide();\n                        }\n                    },\n                    initialExpanded);\n\n            var category = new DataStoreCategoryChoiceComp(\n                            rootCategory != null ? rootCategory.getRoot() : null,\n                            StoreViewState.get().getActiveCategory(),\n                            selectedCategory,\n                            explicitCategory == null,\n                    ignored -> true)\n                    .style(Styles.LEFT_PILL);\n            var filter = new FilterComp(filterText).style(Styles.CENTER_PILL).hgrow();\n\n            var addButton = RegionBuilder.of(() -> {\n                        var m = MenuHelper.createMenuButton();\n                        m.setGraphic(new FontIcon(\"mdi2p-plus-box-outline\"));\n                        m.setMaxHeight(100);\n                        m.setMinHeight(0);\n                        StoreCreationMenu.addButtons(m, false);\n                        return m;\n                    })\n                    .describe(d -> d.nameKey(\"addConnection\"))\n                    .padding(new Insets(-5))\n                    .style(Styles.RIGHT_PILL);\n\n            var top = new HorizontalComp(List.of(category, filter, addButton))\n                    .style(\"top\")\n                    .apply(struc -> struc.setFillHeight(true))\n                    .apply(struc -> {\n                        var first = ((Region) struc.getChildren().get(0));\n                        var second = ((Region) struc.getChildren().get(1));\n                        var third = ((Region) struc.getChildren().get(1));\n                        second.prefHeightProperty().bind(first.heightProperty());\n                        second.minHeightProperty().bind(first.heightProperty());\n                        second.maxHeightProperty().bind(first.heightProperty());\n                        third.prefHeightProperty().bind(first.heightProperty());\n                    })\n                    .apply(struc -> {\n                        // Ugly solution to focus the text field\n                        // Somehow this does not work through the normal on shown listeners\n                        struc.getChildren().get(0).focusedProperty().addListener((observable, oldValue, newValue) -> {\n                            if (newValue) {\n                                struc.getChildren().get(1).requestFocus();\n                            }\n                        });\n                    })\n                    .build();\n\n            var emptyText = Bindings.createStringBinding(\n                    () -> {\n                        var count = StoreViewState.get().getAllEntries().getList().stream()\n                                .filter(applicable)\n                                .count();\n                        return count == 0 ? AppI18n.get(noMatchKey) : null;\n                    },\n                    StoreViewState.get().getAllEntries().getList());\n            var emptyLabel =\n                    new LabelComp(emptyText, new SimpleObjectProperty<>(new LabelGraphic.IconGraphic(\"mdi2f-filter\")));\n            emptyLabel.apply(struc -> AppFontSizes.sm(struc));\n            emptyLabel.hide(BindingsHelper.map(emptyText, s -> s == null));\n            emptyLabel.minHeight(80);\n\n            var listStack = new StackComp(List.of(emptyLabel, section));\n            listStack.vgrow();\n\n            var r = listStack.build();\n            var content = new VBox(top, r);\n            content.setFillWidth(true);\n            content.getStyleClass().add(\"choice-comp-content\");\n            content.setPrefWidth(480);\n            content.setMaxHeight(550);\n\n            popover.setContentNode(content);\n            popover.setCloseButtonEnabled(true);\n            popover.setArrowLocation(Popover.ArrowLocation.TOP_CENTER);\n            popover.setHeaderAlwaysVisible(true);\n            popover.setDetachable(true);\n            popover.setTitle(AppI18n.get(titleKey));\n            popover.setAutoHide(!AppPrefs.get().limitedTouchscreenMode().get());\n            AppFontSizes.xs(popover.getContentNode());\n\n            popover.addEventFilter(KeyEvent.KEY_PRESSED, event -> {\n                if (event.getCode() == KeyCode.ESCAPE) {\n                    popover.hide();\n                    event.consume();\n                }\n            });\n\n            // Hide on connection creation dialog\n            AppDialog.getModalOverlays().addListener((ListChangeListener<? super ModalOverlay>) c -> {\n                popover.hide();\n            });\n\n            if (consumer != null) {\n                consumer.accept(popover);\n            }\n        }\n\n        return popover;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreComboChoiceComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.platform.MenuHelper;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.application.Platform;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.scene.control.ComboBox;\nimport javafx.scene.control.skin.ComboBoxListViewSkin;\nimport javafx.scene.layout.Region;\n\nimport lombok.RequiredArgsConstructor;\nimport lombok.Value;\n\nimport java.util.function.Function;\nimport java.util.function.Predicate;\n\n@RequiredArgsConstructor\npublic class StoreComboChoiceComp<T extends DataStore> extends SimpleRegionBuilder {\n\n    @Value\n    public static class ComboValue<T extends DataStore> {\n\n        public static <T extends DataStore> ComboValue<T> of(String manualHost, DataStoreEntryRef<T> ref) {\n            var manualNull = manualHost == null || manualHost.isEmpty();\n            if (manualNull && ref == null) {\n                return null;\n            }\n\n            return manualNull ? new ComboValue<>(null, ref) : new ComboValue<>(manualHost, null);\n        }\n\n        String manualHost;\n        DataStoreEntryRef<T> ref;\n    }\n\n    private final Property<ComboValue<T>> selected;\n    private final Function<T, String> stringConverter;\n    private final StoreChoicePopover<T> popover;\n    private final boolean requireComplete;\n\n    public StoreComboChoiceComp(\n            Function<T, String> stringConverter,\n            DataStoreEntry self,\n            Property<ComboValue<T>> selected,\n            Class<?> storeClass,\n            Predicate<DataStoreEntryRef<T>> applicableCheck,\n            StoreCategoryWrapper initialCategory,\n            boolean requireComplete) {\n        this.stringConverter = stringConverter;\n        this.selected = selected;\n        this.requireComplete = requireComplete;\n\n        var popoverProp = new SimpleObjectProperty<>(\n                selected.getValue() != null ? selected.getValue().getRef() : null);\n        popoverProp.subscribe(tDataStoreEntryRef -> {\n            if (tDataStoreEntryRef != null) {\n                selected.setValue(new ComboValue<>(null, tDataStoreEntryRef));\n            }\n        });\n        selected.subscribe(cv -> {\n            if (cv == null || cv.getRef() == null) {\n                popoverProp.setValue(null);\n            }\n        });\n\n        this.popover = new StoreChoicePopover<>(\n                self,\n                popoverProp,\n                storeClass,\n                applicableCheck,\n                initialCategory,\n                null,\n                requireComplete,\n                \"selectConnection\",\n                \"noCompatibleConnection\");\n\n        this.selected.addListener((v, o, n) -> {\n            TrackEvent.withTrace(\"Store combo choice value changed\")\n                    .tag(\"value\", n)\n                    .handle();\n        });\n    }\n\n    private String toName(DataStoreEntry entry) {\n        if (entry == null) {\n            return null;\n        }\n\n        var converted =\n                stringConverter != null ? stringConverter.apply(entry.getStore().asNeeded()) : null;\n        var convertedString = converted != null ? \" [\" + converted + \"]\" : \"\";\n        return entry.getName() + convertedString;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var combo = new ComboBox<String>();\n\n        popover.withPopover(po -> {\n            ((Region) po.getContentNode()).setMaxHeight(350);\n            po.showingProperty().addListener((o, oldValue, newValue) -> {\n                if (!newValue) {\n                    combo.hide();\n                }\n            });\n        });\n\n        var skin = new ComboBoxListViewSkin<>(combo) {\n            @Override\n            public void show() {\n                popover.show(combo);\n            }\n\n            @Override\n            public void hide() {\n                popover.hide();\n            }\n        };\n        combo.setSkin(skin);\n        MenuHelper.fixComboBoxSkin(skin);\n        combo.setMaxWidth(20000);\n        combo.setEditable(true);\n\n        combo.getEditor().focusedProperty().subscribe(f -> {\n            if (f) {\n                Platform.runLater(() -> {\n                    combo.getEditor().selectAll();\n                });\n            }\n        });\n\n        combo.setValue(selected.getValue() != null ? selected.getValue().getManualHost() : null);\n\n        var internalUpdate = new SimpleBooleanProperty();\n        combo.getEditor().textProperty().addListener((v, o, n) -> {\n            if (internalUpdate.get()) {\n                return;\n            }\n\n            selected.setValue(n != null && !n.isEmpty() ? new ComboValue<>(n, null) : null);\n        });\n\n        selected.subscribe((v) -> {\n            if (v != null && v.getRef() != null) {\n                PlatformThread.runLaterIfNeeded(() -> {\n                    internalUpdate.set(true);\n                    combo.setValue(toName(v.getRef().get()));\n                    if (combo.getValue() != null) {\n                        combo.getItems().setAll(combo.getValue());\n                    } else {\n                        combo.getItems().clear();\n                    }\n                    internalUpdate.set(false);\n                });\n            }\n        });\n\n        return combo;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreCreationComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.GuiDialog;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.platform.Validator;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.application.Platform;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.geometry.Insets;\nimport javafx.scene.control.*;\nimport javafx.scene.control.skin.ScrollPaneSkin;\nimport javafx.scene.layout.Priority;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.VBox;\n\nimport net.synedra.validatorfx.GraphicDecorationStackPane;\n\npublic class StoreCreationComp extends ModalOverlayContentComp {\n\n    private final StoreCreationModel model;\n\n    public StoreCreationComp(StoreCreationModel model) {\n        this.model = model;\n    }\n\n    @Override\n    protected void setModalOverlay(ModalOverlay modalOverlay) {\n        super.setModalOverlay(modalOverlay);\n        model.getShowing().set(modalOverlay != null);\n    }\n\n    private OptionsBuilder createStoreProperties() {\n        var nameKey = model.storeTypeNameKey();\n        var built = new OptionsBuilder()\n                .name(nameKey + \"Name\")\n                .description(nameKey + \"NameDescription\")\n                .addString(model.getName())\n                .nonNull()\n                .check(val -> Validator.create(val, AppI18n.observable(\"readOnlyStoreError\"), model.getName(), s -> {\n                    var same = s != null\n                            && model.getExistingEntry() != null\n                            && DataStorage.get().getEffectiveReadOnlyState(model.getExistingEntry())\n                            && s.equals(model.getExistingEntry().getName());\n                    return !same;\n                }));\n        return built;\n    }\n\n    private Region createLayout() {\n        var layout = new VBox();\n        layout.getStyleClass().add(\"store-creator\");\n        var providerChoice = new StoreProviderChoiceComp(model.getFilter(), model.getProvider());\n        providerChoice.maxWidth(2000);\n        var provider = model.getProvider().getValue() != null\n                ? model.getProvider().getValue()\n                : providerChoice.getProviders().getFirst();\n        var showProviders = (!model.isStaticDisplay() && provider.showProviderChoice())\n                || (model.isStaticDisplay() && provider.showProviderChoice());\n        if (model.isStaticDisplay()) {\n            providerChoice.apply(struc -> struc.setDisable(true));\n        }\n\n        if (showProviders) {\n            layout.getChildren().addFirst(providerChoice.build());\n        }\n        layout.getChildren().add(new Region());\n\n        var activeDialog = new SimpleObjectProperty<GuiDialog>();\n        model.getProvider().subscribe(n -> {\n            if (n != null) {\n                var d = n.guiDialog(model.getExistingEntry(), model.getStore());\n                activeDialog.set(d);\n                if (d == null) {\n                    return;\n                }\n\n                if (d.getOnFinish() != null) {\n                    model.getFinished().addListener((observable, oldValue, newValue) -> {\n                        if (newValue && d.equals(activeDialog.get())) {\n                            ThreadHelper.runAsync(() -> {\n                                d.getOnFinish().run();\n                            });\n                        }\n                    });\n                }\n\n                var propOptions = createStoreProperties();\n                model.getInitialStore().setValue(model.getStore().getValue());\n\n                var valSp = new GraphicDecorationStackPane();\n                valSp.setFocusTraversable(false);\n\n                var full = new OptionsBuilder();\n\n                // Start focus on top for newly created stores\n                if (model.getExistingEntry() == null) {\n                    d.getOptions().disableFirstIncompleteFocus();\n                    full.disableFirstIncompleteFocus();\n                }\n\n                full.sub(d.getOptions());\n                full.sub(propOptions);\n\n                var comp = full.buildComp();\n                var region = comp.style(\"store-creator-options\").build();\n                valSp.getChildren().add(region);\n\n                var sp = new ScrollPane(valSp);\n                sp.setFocusTraversable(false);\n                sp.setSkin(new ScrollPaneSkin(sp));\n                sp.setFitToWidth(true);\n                var vbar = (ScrollBar) sp.lookup(\".scroll-bar:vertical\");\n\n                var topSep = new Separator();\n                topSep.setPadding(new Insets(10, 0, 0, 0));\n                topSep.visibleProperty().bind(vbar.visibleProperty());\n\n                var bottomSep = new Separator();\n                bottomSep.setPadding(new Insets(0, 0, 0, 0));\n                bottomSep.visibleProperty().bind(vbar.visibleProperty());\n\n                var vbox = new VBox(topSep, sp, bottomSep);\n                VBox.setVgrow(sp, Priority.ALWAYS);\n\n                layout.getChildren().set(showProviders ? 1 : 0, vbox);\n\n                model.getValidator().setValue(full.buildEffectiveValidator());\n\n                Platform.runLater(() -> {\n                    region.requestFocus();\n                });\n            }\n        });\n\n        return layout;\n    }\n\n    @Override\n    protected Region createSimple() {\n        return createLayout();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreCreationConsumer.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.storage.DataStoreEntry;\n\npublic interface StoreCreationConsumer {\n\n    void consume(DataStoreEntry entry, boolean validated);\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreCreationDialog.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.DataStoreCreationCategory;\nimport io.xpipe.app.ext.DataStoreProvider;\nimport io.xpipe.app.ext.DataStoreProviders;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.util.*;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyEvent;\n\nimport java.util.function.Consumer;\nimport java.util.function.Predicate;\n\npublic class StoreCreationDialog {\n\n    public static void showEdit(DataStoreEntry e) {\n        showEdit(e, dataStoreEntry -> {});\n    }\n\n    public static void showEdit(DataStoreEntry e, Consumer<DataStoreEntry> c) {\n        showEdit(e, e.getStore(), c);\n    }\n\n    public static void showEdit(DataStoreEntry e, DataStore base, Consumer<DataStoreEntry> c) {\n        StoreCreationConsumer consumer = (newE, validated) -> {\n            ThreadHelper.runAsync(() -> {\n                if (!DataStorage.get().getStoreEntries().contains(e)\n                        || DataStorage.get().getEffectiveReadOnlyState(e)) {\n                    DataStorage.get().addStoreEntryIfNotPresent(newE);\n                } else {\n                    // We didn't change anything\n                    if (e.getStore().equals(newE.getStore())) {\n                        e.setName(newE.getName());\n                    } else {\n                        var madeValid = !e.getValidity().isUsable()\n                                && newE.getValidity().isUsable();\n                        DataStorage.get().updateEntry(e, newE);\n                        if (madeValid) {\n                            if (validated\n                                    && e.getProvider().shouldShowScan()\n                                    && AppPrefs.get()\n                                            .openConnectionSearchWindowOnConnectionCreation()\n                                            .get()) {\n                                ScanDialog.showSingleAsync(e);\n                            }\n                        }\n                    }\n                }\n\n                // Select new category if needed\n                var cat = DataStorage.get()\n                        .getStoreCategoryIfPresent(e.getCategoryUuid())\n                        .orElseThrow();\n                PlatformThread.runLaterIfNeeded(() -> {\n                    StoreViewState.get()\n                            .selectCategoryIntoViewIfNeeded(StoreViewState.get().getCategoryWrapper(cat));\n                });\n\n                c.accept(e);\n            });\n        };\n        show(e.getName(), DataStoreProviders.byStore(base), base, v -> true, consumer, true, e);\n    }\n\n    public static void showCreation(DataStoreProvider selected, DataStoreCreationCategory category) {\n        showCreation(\n                null,\n                selected != null ? selected.defaultStore(DataStorage.get().getSelectedCategory()) : null,\n                category,\n                dataStoreEntry -> {},\n                true);\n    }\n\n    public static void showCreation(\n            String name,\n            DataStore base,\n            DataStoreCreationCategory category,\n            Consumer<DataStoreEntry> listener,\n            boolean selectCategory) {\n        var prov = base != null ? DataStoreProviders.byStore(base) : null;\n        StoreCreationConsumer consumer = (e, validated) -> {\n            try {\n                var returned = DataStorage.get().addStoreEntryIfNotPresent(e);\n                listener.accept(returned);\n                if (validated\n                        && e.getProvider().shouldShowScan()\n                        && AppPrefs.get()\n                                .openConnectionSearchWindowOnConnectionCreation()\n                                .get()) {\n                    ScanDialog.showSingleAsync(e);\n                }\n\n                if (selectCategory) {\n                    // Select new category if needed\n                    var cat = DataStorage.get()\n                            .getStoreCategoryIfPresent(e.getCategoryUuid())\n                            .orElseThrow();\n                    PlatformThread.runLaterIfNeeded(() -> {\n                        StoreViewState.get()\n                                .selectCategoryIntoViewIfNeeded(\n                                        StoreViewState.get().getCategoryWrapper(cat));\n                    });\n                }\n            } catch (Exception ex) {\n                ErrorEventFactory.fromThrowable(ex).handle();\n            }\n        };\n        show(\n                name,\n                prov,\n                base,\n                dataStoreProvider -> (category != null && category.equals(dataStoreProvider.getCreationCategory()))\n                        || dataStoreProvider.equals(prov),\n                consumer,\n                false,\n                null);\n    }\n\n    private static void show(\n            String initialName,\n            DataStoreProvider provider,\n            DataStore s,\n            Predicate<DataStoreProvider> filter,\n            StoreCreationConsumer con,\n            boolean staticDisplay,\n            DataStoreEntry existingEntry) {\n        var ex = StoreCreationQueueEntry.findExisting(existingEntry);\n        if (ex.isPresent()) {\n            ex.get().execute();\n            return;\n        }\n\n        var prop = new SimpleObjectProperty<>(provider);\n        var store = new SimpleObjectProperty<>(s);\n        var model = new StoreCreationModel(prop, store, filter, initialName, existingEntry, staticDisplay, con);\n        var modal = createModalOverlay(model);\n        modal.show();\n    }\n\n    private static ModalOverlay createModalOverlay(StoreCreationModel model) {\n        var comp = new StoreCreationComp(model);\n        comp.prefWidth(650);\n        var nameKey = model.storeTypeNameKey() + \"Add\";\n        var modal = ModalOverlay.of(nameKey, comp);\n        var queueEntry = StoreCreationQueueEntry.of(model, modal);\n        comp.apply(struc -> {\n            struc.addEventHandler(KeyEvent.KEY_PRESSED, e -> {\n                if (e.getCode() == KeyCode.ESCAPE) {\n                    var changed = model.hasBeenModified();\n                    if (!changed) {\n                        modal.close();\n                        e.consume();\n                    }\n                }\n            });\n        });\n        modal.hideable(queueEntry);\n        AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> {\n            if (model.getFinished().get() || !modal.isShowing()) {\n                return;\n            }\n\n            modal.hide();\n            AppLayoutModel.get().getQueueEntries().add(queueEntry);\n        });\n        modal.setRequireCloseButtonForClose(true);\n        var loadingLabel = new LabelComp(Bindings.createStringBinding(\n                () -> {\n                    return model.getBusy().get() ? AppI18n.get(\"testingConnection\") : null;\n                },\n                model.getBusy(),\n                AppI18n.activeLanguage()));\n        modal.addButtonBarComp(loadingLabel);\n        modal.addButtonBarComp(RegionBuilder.hspacer());\n        modal.addButton(new ModalButton(\n                        \"docs\",\n                        () -> {\n                            model.showDocs();\n                        },\n                        false,\n                        false)\n                .augment(button -> {\n                    button.visibleProperty().bind(Bindings.not(model.canShowDocs()));\n                }));\n        modal.addButton(new ModalButton(\n                        \"connect\",\n                        () -> {\n                            model.connect();\n                        },\n                        false,\n                        false)\n                .augment(button -> {\n                    button.visibleProperty().bind(Bindings.not(model.canConnect()));\n                }));\n        modal.addButton(new ModalButton(\n                        \"skip\",\n                        () -> {\n                            model.commit(false);\n                            modal.close();\n                        },\n                        false,\n                        false))\n                .augment(button -> {\n                    button.visibleProperty().bind(model.getSkippable());\n                    button.disableProperty().bind(model.getBusy());\n                });\n\n        modal.addButton(new ModalButton(\n                        \"finish\",\n                        () -> {\n                            model.finish();\n                        },\n                        false,\n                        true))\n                .augment(button -> {\n                    button.graphicProperty()\n                            .bind(Bindings.createObjectBinding(\n                                    () -> {\n                                        return model.getBusy().get()\n                                                ? new LoadingIconComp(model.getBusy(), AppFontSizes::base)\n                                                        .style(\"store-creator-busy\")\n                                                        .build()\n                                                : null;\n                                    },\n                                    PlatformThread.sync(model.getBusy())));\n                    button.textProperty()\n                            .bind(Bindings.createStringBinding(\n                                    () -> {\n                                        return !model.getBusy().get() ? AppI18n.get(\"finish\") : null;\n                                    },\n                                    PlatformThread.sync(model.getBusy()),\n                                    AppI18n.activeLanguage()));\n                });\n        model.getFinished().addListener((obs, oldValue, newValue) -> {\n            modal.close();\n        });\n        return modal;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreCreationMenu.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.comp.base.PrettyImageHelper;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.util.ScanDialog;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.scene.control.Menu;\nimport javafx.scene.control.MenuButton;\nimport javafx.scene.control.MenuItem;\nimport javafx.scene.control.SeparatorMenuItem;\n\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.Comparator;\n\npublic class StoreCreationMenu {\n\n    public static void addButtons(MenuButton menu, boolean allowSearch) {\n        if (allowSearch) {\n            var automatically = new MenuItem();\n            automatically.setGraphic(new FontIcon(\"mdi2e-eye-plus-outline\"));\n            automatically.textProperty().bind(AppI18n.observable(\"addAutomatically\"));\n            automatically.setOnAction(event -> {\n                ScanDialog.showSingleAsync(null);\n                event.consume();\n            });\n            menu.getItems().add(automatically);\n            menu.getItems().add(networkScanMenu());\n            menu.getItems().add(new SeparatorMenuItem());\n\n            var disableSearch = Bindings.createBooleanBinding(\n                    () -> {\n                        var allCat = StoreViewState.get().getAllConnectionsCategory();\n                        var connections = StoreViewState.get().getAllEntries().getList().stream()\n                                .filter(wrapper -> allCat.equals(\n                                        wrapper.getCategory().getValue().getRoot()))\n                                .toList();\n                        return 1 == connections.size()\n                                && StoreViewState.get()\n                                        .getActiveCategory()\n                                        .getValue()\n                                        .getRoot()\n                                        .equals(allCat);\n                    },\n                    StoreViewState.get().getAllEntries().getList());\n            automatically.disableProperty().bind(disableSearch);\n        }\n\n        menu.getItems().add(categoryMenu(\"addHost\", \"mdi2h-home-plus\", DataStoreCreationCategory.HOST, \"ssh\"));\n\n        menu.getItems().add(categoryMenu(\"addDesktop\", \"mdi2c-camera-plus\", DataStoreCreationCategory.DESKTOP, null));\n\n        menu.getItems()\n                .add(categoryMenu(\n                        \"addIdentity\",\n                        \"mdi2a-account-multiple-plus\",\n                        DataStoreCreationCategory.IDENTITY,\n                        \"localIdentity\"));\n\n        menu.getItems().add(cloudMenu());\n\n        menu.getItems().add(new SeparatorMenuItem());\n\n        menu.getItems()\n                .add(categoryMenu(\"addService\", \"mdi2l-link-plus\", DataStoreCreationCategory.SERVICE, \"customService\"));\n\n        menu.getItems()\n                .add(categoryMenu(\n                        \"addTunnel\", \"mdi2v-vector-polyline-plus\", DataStoreCreationCategory.TUNNEL, \"sshLocalTunnel\"));\n\n        menu.getItems()\n                .add(categoryMenu(\n                        \"addFileSystem\",\n                        \"mdi2f-folder-plus-outline\",\n                        DataStoreCreationCategory.FILE_SYSTEM,\n                        \"genericS3Bucket\"));\n\n        menu.getItems().add(new SeparatorMenuItem());\n\n        menu.getItems()\n                .add(categoryMenu(\"addCommand\", \"mdi2c-code-greater-than\", DataStoreCreationCategory.COMMAND, null));\n\n        menu.getItems()\n                .add(categoryMenu(\n                        \"addScript\", \"mdi2s-script-text-outline\", DataStoreCreationCategory.SCRIPT, \"script\"));\n\n        menu.getItems().add(new SeparatorMenuItem());\n\n        var actionMenu = categoryMenu(\"addMacro\", \"mdmz-miscellaneous_services\", DataStoreCreationCategory.MACRO, null);\n        var item = new MenuItem();\n        item.setGraphic(PrettyImageHelper.ofFixedSize(\"action.png\", 16, 16).build());\n        item.textProperty().bind(AppI18n.observable(\"actionShortcut\"));\n        item.setOnAction(event -> {\n            Platform.runLater(() -> {\n                AbstractAction.expectPick();\n            });\n\n            // Fix weird JavaFX NPE\n            actionMenu.getParentPopup().hide();\n        });\n        actionMenu.getItems().addFirst(item);\n\n        menu.getItems().add(categoryMenu(\"addSerial\", \"mdi2s-serial-port\", DataStoreCreationCategory.SERIAL, \"serial\"));\n\n        menu.getItems().add(new SeparatorMenuItem());\n\n        menu.getItems().add(actionMenu);\n    }\n\n    private static Menu categoryMenu(\n            String name, String graphic, DataStoreCreationCategory category, String defaultProvider) {\n        var providers = DataStoreProviders.getAll().stream()\n                .filter(dataStoreProvider -> category.equals(dataStoreProvider.getCreationCategory()))\n                .sorted(Comparator.comparingInt(dataStoreProvider -> dataStoreProvider.getOrderPriority()))\n                .toList();\n\n        var menu = new Menu();\n        menu.setGraphic(new FontIcon(graphic));\n        menu.textProperty().bind(AppI18n.observable(name));\n\n        if (providers.isEmpty()) {\n            return menu;\n        }\n\n        menu.setOnAction(event -> {\n            if (event.getTarget() != menu) {\n                return;\n            }\n\n            Platform.runLater(() -> {\n                if (defaultProvider != null) {\n                    providers.stream()\n                            .filter(dataStoreProvider ->\n                                    dataStoreProvider.getId().equals(defaultProvider))\n                            .findFirst()\n                            .ifPresent(dataStoreProvider -> {\n                                var index = providers.indexOf(dataStoreProvider);\n                                menu.getItems().get(index).fire();\n                            });\n                    return;\n                }\n\n                var onlyItem = menu.getItems().getFirst();\n                onlyItem.fire();\n            });\n\n            // Fix weird JavaFX NPE\n            menu.getParentPopup().hide();\n        });\n\n        int lastOrder = providers.getFirst().getOrderPriority();\n        for (io.xpipe.app.ext.DataStoreProvider dataStoreProvider : providers) {\n            if (dataStoreProvider.getOrderPriority() != lastOrder) {\n                menu.getItems().add(new SeparatorMenuItem());\n                lastOrder = dataStoreProvider.getOrderPriority();\n            }\n\n            var item = new MenuItem();\n            item.textProperty().bind(dataStoreProvider.displayName());\n            item.setGraphic(PrettyImageHelper.ofFixedSizeSquare(dataStoreProvider.getDisplayIconFileName(null), 16)\n                    .build());\n            item.setOnAction(event -> {\n                StoreCreationDialog.showCreation(dataStoreProvider, category);\n                event.consume();\n            });\n            menu.getItems().add(item);\n        }\n        return menu;\n    }\n\n    private static Menu cloudMenu() {\n        var menu = new Menu();\n        menu.setGraphic(new FontIcon(\"mdi2t-toy-brick-plus-outline\"));\n        menu.textProperty().bind(AppI18n.observable(\"addCloud\"));\n\n        for (var p : CloudSetupProvider.ALL) {\n            var item = new MenuItem();\n            item.textProperty().bind(AppI18n.observable(p.getNameKey()));\n            item.setGraphic(p.getGraphic().createGraphicNode());\n            item.setOnAction(event -> {\n                var action =\n                        SetupToolActionProvider.Action.builder().type(p.getId()).build();\n                action.executeAsync();\n                event.consume();\n            });\n            menu.getItems().add(item);\n        }\n        return menu;\n    }\n\n    private static MenuItem networkScanMenu() {\n        var menu = new MenuItem();\n        menu.setGraphic(new FontIcon(\"mdi2a-access-point-plus\"));\n        menu.textProperty().bind(AppI18n.observable(\"addNetwork\"));\n        menu.setOnAction(event -> {\n            ProcessControlProvider.get().createNetworkScanModal().show();\n            event.consume();\n        });\n        return menu;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreCreationModel.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.DataStoreProvider;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.ext.ValidatableStore;\nimport io.xpipe.app.ext.ValidationException;\nimport io.xpipe.app.hub.action.impl.OpenHubMenuLeafProvider;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.SimpleValidator;\nimport io.xpipe.app.platform.Validator;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreCategory;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.util.*;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.*;\nimport javafx.beans.value.ObservableBooleanValue;\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.AccessLevel;\nimport lombok.Getter;\nimport lombok.experimental.FieldDefaults;\n\nimport java.util.UUID;\nimport java.util.function.Predicate;\n\n@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)\n@Getter\npublic class StoreCreationModel {\n\n    ObjectProperty<DataStore> initialStore = new SimpleObjectProperty<>();\n    Property<DataStoreProvider> provider;\n    ObjectProperty<DataStore> store;\n    Predicate<DataStoreProvider> filter;\n    BooleanProperty busy = new SimpleBooleanProperty();\n    Property<Validator> validator = new SimpleObjectProperty<>(new SimpleValidator());\n    BooleanProperty finished = new SimpleBooleanProperty();\n    BooleanProperty showing = new SimpleBooleanProperty();\n    ObservableValue<DataStoreEntry> entry;\n    BooleanProperty skippable = new SimpleBooleanProperty();\n    BooleanProperty connectable = new SimpleBooleanProperty();\n    StringProperty name;\n    DataStoreEntry existingEntry;\n    boolean staticDisplay;\n    StoreCreationConsumer consumer;\n\n    public StoreCreationModel(\n            Property<DataStoreProvider> provider,\n            ObjectProperty<DataStore> store,\n            Predicate<DataStoreProvider> filter,\n            String initialName,\n            DataStoreEntry existingEntry,\n            boolean staticDisplay,\n            StoreCreationConsumer consumer) {\n        this.provider = provider;\n        this.store = store;\n        this.filter = filter;\n        this.name = new SimpleStringProperty(initialName != null && !initialName.isEmpty() ? initialName : null);\n        this.existingEntry = existingEntry;\n        this.staticDisplay = staticDisplay;\n        this.consumer = consumer;\n\n        this.provider.addListener((c, o, n) -> {\n            store.unbind();\n            store.setValue(null);\n            if (n != null) {\n                store.setValue(n.defaultStore(getTargetCategory(existingEntry)));\n            }\n        });\n\n        this.provider.subscribe((n) -> {\n            if (n != null) {\n                connectable.setValue(n.canConnectDuringCreation());\n            }\n        });\n\n        this.validator.addListener((observable, oldValue, newValue) -> {\n            Platform.runLater(() -> {\n                newValue.validate();\n            });\n        });\n        this.entry = Bindings.createObjectBinding(\n                () -> {\n                    if (name.getValue() == null\n                            || store.getValue() == null\n                            || name.getValue().isBlank()) {\n                        return null;\n                    }\n\n                    var initial = DataStoreEntry.createNew(\n                            UUID.randomUUID(),\n                            DataStorage.get().getSelectedCategory().getUuid(),\n                            name.getValue(),\n                            store.getValue());\n                    var entryRef = existingEntry != null ? existingEntry : DataStorage.get().getDefaultDisplayParent(initial).orElse(initial);\n                    var targetCategory = getTargetCategory(entryRef);\n                    return DataStoreEntry.createNew(\n                            UUID.randomUUID(), targetCategory.getUuid(), name.getValue(), store.getValue());\n                },\n                name,\n                store);\n\n        skippable.bind(Bindings.createBooleanBinding(\n                () -> {\n                    if (entry.getValue() != null\n                            && store.get().isComplete()\n                            && store.get() instanceof ValidatableStore) {\n                        if (existingEntry != null) {\n                            return !existingEntry.isFreeze()\n                                    || !existingEntry.getName().equals(name.getValue());\n                        } else {\n                            return true;\n                        }\n                    } else {\n                        return false;\n                    }\n                },\n                store,\n                name));\n    }\n\n    private DataStoreCategory getTargetCategory(DataStoreEntry base) {\n        var targetCategory = base != null\n                ? base.getCategoryUuid()\n                : DataStorage.get().getSelectedCategory().getUuid();\n        var rootCategory = DataStorage.get()\n                .getRootCategory(DataStorage.get()\n                        .getStoreCategoryIfPresent(targetCategory)\n                        .orElse(DataStorage.get().getAllConnectionsCategory()));\n\n        // Don't put it in the wrong root category\n        if ((provider.getValue().getCreationCategory() != null\n                && !provider.getValue().getCreationCategory().getCategory().equals(rootCategory.getUuid()))) {\n            targetCategory = provider.getValue().getCreationCategory().getCategory();\n        }\n\n        // Don't use the all connections category\n        if (targetCategory.equals(DataStorage.get().getAllConnectionsCategory().getUuid())) {\n            targetCategory = DataStorage.get().getDefaultConnectionsCategory().getUuid();\n        }\n\n        // Don't use the all scripts category\n        if (targetCategory.equals(DataStorage.get().getAllScriptsCategory().getUuid())) {\n            targetCategory = DataStorage.CUSTOM_SCRIPTS_CATEGORY_UUID;\n        }\n\n        // Don't use the all identities category\n        if (targetCategory.equals(DataStorage.get().getAllIdentitiesCategory().getUuid())) {\n            targetCategory = DataStorage.LOCAL_IDENTITIES_CATEGORY_UUID;\n        }\n\n        // Custom category stuff\n        targetCategory = provider.getValue().getTargetCategory(store.getValue(), targetCategory);\n        return DataStorage.get().getStoreCategoryIfPresent(targetCategory).orElseThrow();\n    }\n\n    ObservableBooleanValue canConnect() {\n        return connectable\n                .not()\n                .or(Bindings.createBooleanBinding(\n                        () -> {\n                            return store.getValue() == null || !store.getValue().isComplete();\n                        },\n                        store));\n    }\n\n    void connect() {\n        var temp = entry.getValue() != null ? entry.getValue() : DataStoreEntry.createTempWrapper(store.getValue());\n        var action = OpenHubMenuLeafProvider.Action.builder().ref(temp.ref()).build();\n        action.executeAsync();\n    }\n\n    boolean hasBeenModified() {\n        if (initialStore.getValue() == null) {\n            return true;\n        }\n\n        var eq = initialStore.getValue().equals(store.getValue());\n        return !eq;\n    }\n\n    boolean wasChanged() {\n        if (existingEntry != null && existingEntry.getStore().equals(store.getValue())) {\n            return false;\n        }\n\n        return true;\n    }\n\n    void finish() {\n        if (finished.get()) {\n            return;\n        }\n\n        if (store.getValue() == null) {\n            return;\n        }\n\n        if (!validator.getValue().validate()) {\n            var msg = validator\n                    .getValue()\n                    .getValidationResult()\n                    .getMessages()\n                    .getFirst()\n                    .getText();\n            ErrorEventFactory.fromMessage(msg).expected().handle();\n            return;\n        }\n\n        // We didn't change anything\n        if (store.getValue().isComplete() && !wasChanged()) {\n            commit(false);\n            return;\n        }\n\n        var currentEntry = entry.getValue();\n        if (currentEntry == null) {\n            return;\n        }\n\n        ThreadHelper.runAsync(() -> {\n            if (busy.get()) {\n                return;\n            }\n\n            try (var ignored = new BooleanScope(busy).exclusive().start()) {\n                DataStorage.get().addStoreEntryInProgress(currentEntry);\n                validate();\n\n                // If the validation happens while the dialog is minimized, remove queue entry\n                var queueEntry = StoreCreationQueueEntry.findExisting(existingEntry);\n                if (queueEntry.isPresent()) {\n                    queueEntry.get().hide();\n                }\n\n                // The dialog might be completely closed, then discard changes\n                if (showing.get() || queueEntry.isPresent()) {\n                    commit(true);\n                }\n            } catch (Throwable ex) {\n                if (ex instanceof ValidationException) {\n                    ErrorEventFactory.expected(ex);\n                } else if (ex instanceof StackOverflowError) {\n                    // Cycles in connection graphs can fail hard but are expected\n                    ErrorEventFactory.expected(ex);\n                }\n\n                var event = ErrorEventFactory.fromThrowable(ex);\n                var queueEntry = StoreCreationQueueEntry.findExisting(existingEntry);\n                if (queueEntry.isEmpty() && !showing.get()) {\n                    event.omit();\n                }\n                event.handle();\n            } finally {\n                if (DataStorage.get() != null) {\n                    DataStorage.get().removeStoreEntryInProgress(currentEntry);\n                }\n            }\n        });\n    }\n\n    private void validate() throws Throwable {\n        var s = entry.getValue().getStore();\n        if (s == null) {\n            return;\n        }\n\n        s.checkComplete();\n\n        if (s instanceof ShellStore ss) {\n            // Start session for later\n            ss.getOrStartSession();\n        } else if (s instanceof ValidatableStore vs) {\n            vs.validate();\n        }\n    }\n\n    void showDocs() {\n        Hyperlinks.open(provider.getValue().getHelpLink().getLink());\n    }\n\n    ObservableBooleanValue canShowDocs() {\n        var disable = Bindings.createBooleanBinding(\n                () -> {\n                    return provider.getValue() == null || provider.getValue().getHelpLink() == null;\n                },\n                provider);\n        return disable;\n    }\n\n    void commit(boolean validated) {\n        if (finished.get()) {\n            return;\n        }\n\n        finished.setValue(true);\n        consumer.consume(entry.getValue(), validated);\n    }\n\n    public String storeTypeNameKey() {\n        var p = provider.getValue();\n        var nameKey = p == null\n                        || p.getCreationCategory() == null\n                        || p.getCreationCategory().getCategory().equals(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID)\n                ? \"connection\"\n                : p.getCreationCategory().getCategory().equals(DataStorage.ALL_SCRIPTS_CATEGORY_UUID)\n                        ? (p.getId().equals(\"scriptGroup\") ? \"scriptGroup\" : \"script\")\n                        : p.getCreationCategory().getCategory().equals(DataStorage.ALL_IDENTITIES_CATEGORY_UUID)\n                                ? \"identity\"\n                                : \"macro\";\n        return nameKey;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreCreationQueueEntry.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntry;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.EqualsAndHashCode;\nimport lombok.Value;\n\nimport java.util.Optional;\n\n@Value\n@EqualsAndHashCode(callSuper = true)\npublic class StoreCreationQueueEntry extends AppLayoutModel.QueueEntry {\n\n    public static StoreCreationQueueEntry of(StoreCreationModel model, ModalOverlay modal) {\n        var provider = model.getProvider().getValue();\n        var graphic = provider != null\n                        && provider.getDisplayIconFileName(model.getStore().get()) != null\n                ? new LabelGraphic.ImageGraphic(\n                        provider.getDisplayIconFileName(model.getStore().get()), 20)\n                : new LabelGraphic.IconGraphic(\"mdi2b-beaker-plus-outline\");\n        return new StoreCreationQueueEntry(\n                AppI18n.observable(model.storeTypeNameKey() + \"Add\"),\n                graphic,\n                () -> {\n                    AppLayoutModel.get().selectConnections();\n                    modal.show();\n                },\n                model.getExistingEntry());\n    }\n\n    public static Optional<AppLayoutModel.QueueEntry> findExisting(DataStoreEntry entry) {\n        if (entry == null) {\n            return Optional.empty();\n        }\n\n        return AppLayoutModel.get().getQueueEntries().stream()\n                .filter(queueEntry -> queueEntry instanceof StoreCreationQueueEntry q && entry.equals(q.getEntry()))\n                .findFirst();\n    }\n\n    DataStoreEntry entry;\n\n    public StoreCreationQueueEntry(\n            ObservableValue<String> name, LabelGraphic icon, Runnable action, DataStoreEntry entry) {\n        super(name, icon, () -> {\n            action.run();\n            return true;\n        });\n        this.entry = entry;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreEntryBatchSelectComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.util.BooleanScope;\n\nimport javafx.application.Platform;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.collections.ListChangeListener;\nimport javafx.scene.control.CheckBox;\nimport javafx.scene.input.MouseButton;\nimport javafx.scene.input.MouseEvent;\nimport javafx.scene.layout.Region;\n\npublic class StoreEntryBatchSelectComp extends SimpleRegionBuilder {\n\n    private final StoreSection section;\n\n    public StoreEntryBatchSelectComp(StoreSection section) {\n        this.section = section;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var selfUpdate = new SimpleBooleanProperty(false);\n        var cb = new CheckBox();\n        externalUpdate(cb);\n        cb.setAllowIndeterminate(true);\n        cb.selectedProperty().addListener((observable, oldValue, newValue) -> {\n            BooleanScope.executeExclusive(selfUpdate, () -> {\n                if (newValue) {\n                    StoreViewState.get().selectBatchMode(section);\n                } else {\n                    StoreViewState.get().unselectBatchMode(section);\n                }\n            });\n        });\n\n        StoreViewState.get().getBatchModeSelection().getList().addListener((ListChangeListener<\n                        ? super StoreEntryWrapper>)\n                c -> {\n                    if (selfUpdate.get()) {\n                        return;\n                    }\n\n                    Platform.runLater(() -> {\n                        externalUpdate(cb);\n                    });\n                });\n        section.getShownChildren().getList().addListener((ListChangeListener<? super StoreSection>) c -> {\n            BooleanScope.executeExclusive(selfUpdate, () -> {\n                if (cb.isSelected()) {\n                    StoreViewState.get().selectBatchMode(section);\n                }\n            });\n        });\n\n        cb.getStyleClass().add(\"batch-mode-selector\");\n        cb.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {\n            if (event.getButton() == MouseButton.PRIMARY) {\n                cb.setSelected(!cb.isSelected());\n                event.consume();\n            }\n        });\n        return cb;\n    }\n\n    private void externalUpdate(CheckBox checkBox) {\n        if (section.getWrapper() != null && section.getWrapper().getEntry().getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {\n            checkBox.setSelected(false);\n            checkBox.setIndeterminate(false);\n            checkBox.setDisable(true);\n            return;\n        }\n\n        var isSelected = section.getWrapper() == null\n                ? checkBox.isSelected()\n                : StoreViewState.get().isBatchModeSelected(section.getWrapper());\n        checkBox.setSelected(isSelected);\n        if (section.getShownChildren().getList().size() == 0) {\n            checkBox.setIndeterminate(false);\n            return;\n        }\n\n        var count = section.getShownChildren().getList().stream()\n                .filter(c -> StoreViewState.get().isBatchModeSelected(c.getWrapper()))\n                .count();\n        checkBox.setIndeterminate(\n                count > 0 && count != section.getShownChildren().getList().size());\n        return;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreEntryComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.RegionDescriptor;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.augment.ContextMenuAugment;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.*;\nimport io.xpipe.app.hub.action.HubBranchProvider;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.HubMenuItemProvider;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.platform.*;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreColor;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.util.*;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.*;\nimport javafx.beans.value.ObservableDoubleValue;\nimport javafx.css.PseudoClass;\nimport javafx.event.ActionEvent;\nimport javafx.geometry.Insets;\nimport javafx.geometry.Pos;\nimport javafx.scene.Node;\nimport javafx.scene.control.*;\nimport javafx.scene.input.MouseButton;\nimport javafx.scene.input.MouseEvent;\nimport javafx.scene.layout.Region;\n\nimport atlantafx.base.layout.InputGroup;\nimport atlantafx.base.theme.Styles;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.lang.ref.WeakReference;\nimport java.nio.file.Files;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.function.Consumer;\nimport java.util.stream.Collectors;\n\npublic abstract class StoreEntryComp extends SimpleRegionBuilder {\n\n    public static final PseudoClass FAILED = PseudoClass.getPseudoClass(\"failed\");\n    public static final PseudoClass INCOMPLETE = PseudoClass.getPseudoClass(\"incomplete\");\n    public static final ObservableDoubleValue INFO_NO_CONTENT_WIDTH = Bindings.createDoubleBinding(\n            () -> {\n                var w = App.getApp().getStage().getWidth();\n                if (w > 2000) {\n                    return (w / 1.8) - 170;\n                } else if (w >= 1000) {\n                    return (w / 2.0) - 170;\n                } else {\n                    return (w / 1.7) - 120;\n                }\n            },\n            App.getApp().getStage().widthProperty());\n    public static final ObservableDoubleValue INFO_WITH_CONTENT_WIDTH = Bindings.createDoubleBinding(\n            () -> {\n                var w = App.getApp().getStage().getWidth();\n                if (w > 2000) {\n                    return (w / 1.8) - 240;\n                } else if (w >= 1000) {\n                    return (w / 2.0) - 240;\n                } else {\n                    return (w / 1.7) - 190;\n                }\n            },\n            App.getApp().getStage().widthProperty());\n    private static String DEFAULT_NOTES = null;\n    protected final StoreSection section;\n    protected final BaseRegionBuilder<?, ?> content;\n    protected final IntegerProperty contextMenuCount = new SimpleIntegerProperty();\n\n    public StoreEntryComp(StoreSection section, BaseRegionBuilder<?, ?> content) {\n        this.section = section;\n        this.content = content;\n    }\n\n    public static StoreEntryComp create(StoreSection section, BaseRegionBuilder<?, ?> content, boolean preferLarge) {\n        var forceCondensed = AppPrefs.get() != null\n                && AppPrefs.get().condenseConnectionDisplay().get();\n        if (!preferLarge || forceCondensed) {\n            return new DenseStoreEntryComp(section, content);\n        } else {\n            return new StandardStoreEntryComp(section, content);\n        }\n    }\n\n    public static StoreEntryComp customSection(StoreSection e) {\n        var prov = e.getWrapper().getEntry().getProvider();\n        if (prov != null) {\n            return prov.customEntryComp(e, e.getDepth() == 1);\n        } else {\n            var forceCondensed = AppPrefs.get() != null\n                    && AppPrefs.get().condenseConnectionDisplay().get();\n            return forceCondensed ? new DenseStoreEntryComp(e, null) : new StandardStoreEntryComp(e, null);\n        }\n    }\n\n    private static String getDefaultNotes() {\n        var prefs = AppPrefs.get().notesTemplate().getValue();\n        if (prefs != null) {\n            return prefs;\n        }\n\n        if (DEFAULT_NOTES == null) {\n            AppResources.with(AppResources.MAIN_MODULE, \"misc/notes_default.md\", f -> {\n                DEFAULT_NOTES = Files.readString(f);\n            });\n        }\n        return DEFAULT_NOTES;\n    }\n\n    public StoreEntryWrapper getWrapper() {\n        return section.getWrapper();\n    }\n\n    public abstract boolean isFullSize();\n\n    public abstract int getHeight();\n\n    @Override\n    protected final Region createSimple() {\n        var r = createContent();\n        var name = (Region) r.lookup(\".name\");\n        var buttonBar = r.lookup(\".button-bar\");\n        var iconChooser = r.lookup(\".icon\");\n        var batchMode = r.lookup(\".batch-mode-selector\");\n        var customContent = r.lookup(\".custom-content\");\n\n        var button = new Button();\n        button.setGraphic(r);\n        button.getStyleClass().add(\"store-entry-comp\");\n        button.setPadding(Insets.EMPTY);\n        button.setMaxWidth(10000);\n        button.setFocusTraversable(true);\n        RegionDescriptor.builder()\n                .name(getWrapper().getShownName())\n                .description(getWrapper().getShownDescription())\n                .showTooltips(false)\n                .build()\n                .apply(button);\n        button.setOnAction(event -> {\n            if (getWrapper().getRenaming().get()) {\n                return;\n            }\n\n            event.consume();\n            ThreadHelper.runFailableAsync(() -> {\n                getWrapper().executeDefaultAction();\n            });\n        });\n        button.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {\n            var notOnButton = NodeHelper.isParent(iconChooser, event.getTarget())\n                    || NodeHelper.isParent(batchMode, event.getTarget())\n                    || NodeHelper.isParent(buttonBar, event.getTarget())\n                    || NodeHelper.isParent(customContent, event.getTarget());\n            if (AppPrefs.get().requireDoubleClickForConnections().get() && !notOnButton) {\n                if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() != 2) {\n                    event.consume();\n                }\n            } else {\n                if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() > 1) {\n                    event.consume();\n                }\n            }\n        });\n        button.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {\n            var notOnButton = NodeHelper.isParent(iconChooser, event.getTarget())\n                    || NodeHelper.isParent(batchMode, event.getTarget())\n                    || NodeHelper.isParent(buttonBar, event.getTarget())\n                    || NodeHelper.isParent(customContent, event.getTarget());\n            if (AppPrefs.get().requireDoubleClickForConnections().get() && !notOnButton) {\n                if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() != 2) {\n                    event.consume();\n                }\n            } else {\n                if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() > 1) {\n                    event.consume();\n                }\n            }\n        });\n        new ContextMenuAugment<>(\n                        mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY,\n                        null,\n                        () -> this.createContextMenu(name))\n                .accept(button);\n\n        var loading = new LoadingOverlayComp(\n                RegionBuilder.of(() -> button), getWrapper().getEffectiveBusy(), false);\n        if (OsType.ofLocal() == OsType.MACOS) {\n            AppFontSizes.base(button);\n        } else if (OsType.ofLocal() == OsType.LINUX) {\n            AppFontSizes.xl(button);\n        } else {\n            AppFontSizes.apply(button, sizes -> {\n                if (sizes.getBase().equals(\"10.5\")) {\n                    return sizes.getXl();\n                } else {\n                    return sizes.getLg();\n                }\n            });\n        }\n        return loading.build();\n    }\n\n    protected abstract Region createContent();\n\n    protected void applyState(Node node) {\n        getWrapper().getValidity().subscribe(val -> {\n            switch (val) {\n                case LOAD_FAILED -> {\n                    node.pseudoClassStateChanged(FAILED, true);\n                    node.pseudoClassStateChanged(INCOMPLETE, false);\n                }\n                case INCOMPLETE -> {\n                    node.pseudoClassStateChanged(FAILED, false);\n                    node.pseudoClassStateChanged(INCOMPLETE, true);\n                }\n                default -> {\n                    node.pseudoClassStateChanged(FAILED, false);\n                    node.pseudoClassStateChanged(INCOMPLETE, false);\n                }\n            }\n        });\n    }\n\n    protected BaseRegionBuilder<?, ?> createName() {\n        var prop = new SimpleStringProperty();\n        getWrapper().getShownName().subscribe(prop::setValue);\n        prop.addListener((observable, oldValue, newValue) -> {\n            if (!AppPrefs.get().censorMode().get()) {\n                getWrapper().getName().setValue(newValue);\n            }\n        });\n        var name = new LazyTextFieldComp(prop);\n        name.style(\"name\");\n        name.applyStructure(struc -> {\n            getWrapper().getRenaming().bind(struc.getTextField().focusedProperty());\n        });\n        return name;\n    }\n\n    protected BaseRegionBuilder<?, ?> createTags() {\n        var tagsProp = Bindings.createStringBinding(\n                () -> {\n                    return getWrapper().getTags().stream()\n                            .map(s -> {\n                                return \"[\" + s + \"]\";\n                            })\n                            .collect(Collectors.joining(\" \"));\n                },\n                getWrapper().getTags());\n        var tagsLabel = new LabelComp(tagsProp);\n        tagsLabel.apply(struc -> struc.setOpacity(0.85));\n        return tagsLabel;\n    }\n\n    protected BaseRegionBuilder<?, ?> createOrderIndex() {\n        var prop = new SimpleStringProperty();\n        getWrapper().getOrderIndex().subscribe(number -> {\n            var i = number.intValue();\n            var displayed = i == Integer.MIN_VALUE ? \"-\" : i == Integer.MAX_VALUE ? \"+\" : i != 0 ? \"\" + i : null;\n            prop.set(displayed != null ? \"[\" + displayed + \"]\" : null);\n        });\n        var comp = new LabelComp(prop);\n        comp.style(\"orderIndex\");\n        comp.apply(struc -> struc.setOpacity(0.85));\n        return comp;\n    }\n\n    protected BaseRegionBuilder<?, ?> createUserIcon() {\n        var button = new IconButtonComp(\"mdi2a-account\");\n        button.style(\"user-icon\");\n        button.describe(d -> d.nameKey(\"personalConnection\"));\n        button.apply(struc -> {\n            AppFontSizes.base(struc);\n            struc.setDisable(true);\n            struc.setOpacity(1.0);\n        });\n        button.hide(Bindings.not(getWrapper().getPerUser()));\n        button.apply(struc -> struc.setOpacity(0.85));\n        return button;\n    }\n\n    protected BaseRegionBuilder<?, ?> createPinIcon() {\n        var button = new IconButtonComp(\"mdi2p-pin-outline\");\n        button.disable(new SimpleBooleanProperty(true));\n        button.describe(d -> d.nameKey(\"pinned\"));\n        button.apply(struc -> {\n            AppFontSizes.xs(struc);\n            struc.setOpacity(1.0);\n        });\n        button.hide(Bindings.not(getWrapper().getPinToTop()));\n        button.apply(struc -> struc.setOpacity(0.85));\n        return button;\n    }\n\n    protected BaseRegionBuilder<?, ?> createIcon(int w, int h, Consumer<Node> fontSize) {\n        var icon = new StoreIconComp(getWrapper(), w, h);\n        icon.apply(struc -> {\n            struc.opacityProperty()\n                    .bind(Bindings.createDoubleBinding(\n                            () -> {\n                                if (!getWrapper().getValidity().getValue().isUsable()) {\n                                    return 0.5;\n                                }\n\n                                return !getWrapper().getEffectiveBusy().get() ? 1.0 : 0.15;\n                            },\n                            getWrapper().getValidity(),\n                            getWrapper().getEffectiveBusy()));\n        });\n        var loading = new LoadingIconComp(getWrapper().getEffectiveBusy(), fontSize);\n        loading.prefWidth(w);\n        loading.prefHeight(h);\n        var stack = new StackComp(List.of(icon, loading));\n        return stack;\n    }\n\n    protected Region createButtonBar(Region name) {\n        var list = DerivedObservableList.wrap(getWrapper().getMajorActionProviders(), false);\n        var buttons = list.mapped(actionProvider -> {\n                    var button = buildButton(actionProvider);\n                    return button.build();\n                })\n                .filtered(region -> region != null)\n                .getList();\n\n        var ig = new InputGroup();\n        Runnable update = () -> {\n            var l = new ArrayList<Node>(buttons);\n            var settingsButton = createSettingsButton(name).build();\n            l.add(settingsButton);\n            l.forEach(o -> o.getStyleClass().remove(Styles.FLAT));\n            ig.getChildren().setAll(l);\n        };\n        buttons.subscribe(update);\n        update.run();\n        ig.setAlignment(Pos.CENTER_RIGHT);\n        ig.getStyleClass().add(\"button-bar\");\n        AppFontSizes.base(ig);\n        return ig;\n    }\n\n    private BaseRegionBuilder<?, ?> buildButton(HubMenuItemProvider<?> p) {\n        var leaf = p instanceof HubLeafProvider<?> l ? l : null;\n        var branch = p instanceof HubBranchProvider<?> b ? b : null;\n        var button = new IconButtonComp(\n                p.getIcon(getWrapper().getEntry().ref()),\n                leaf != null\n                        ? () -> {\n                            leaf.execute(getWrapper().getEntry().ref());\n                        }\n                        : null);\n        if (branch != null) {\n            button.apply(new ContextMenuAugment<>(\n                    mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, keyEvent -> false, () -> {\n                        var cm = MenuHelper.createContextMenu();\n                        var children = branch\n                                .getChildren(getWrapper().getEntry().ref())\n                                .stream()\n                                .filter(hubMenuItemProvider -> {\n                                    return hubMenuItemProvider.isApplicable(\n                                            getWrapper().getEntry().ref());\n                                })\n                                .toList();\n                        var cats = Arrays.stream(StoreActionCategory.values())\n                                .collect(Collectors.toCollection(ArrayList::new));\n                        cats.addFirst(null);\n                        for (var cat : cats) {\n                            var catChildren = children.stream()\n                                    .filter(actionProvider -> actionProvider.getCategory() == cat)\n                                    .toList();\n                            if (catChildren.isEmpty()) {\n                                continue;\n                            }\n\n                            catChildren.forEach(childProvider -> {\n                                var menu = buildMenuItemForAction(childProvider);\n                                if (menu != null) {\n                                    cm.getItems().add(menu);\n                                }\n                            });\n                            cm.getItems().add(new SeparatorMenuItem());\n                        }\n\n                        if (cm.getItems().getLast() instanceof SeparatorMenuItem) {\n                            cm.getItems().removeLast();\n                        }\n\n                        return cm;\n                    }));\n        }\n        button.describe(d -> d.name(p.getName(getWrapper().getEntry().ref())));\n        return button;\n    }\n\n    protected BaseRegionBuilder<?, ?> createSettingsButton(Region name) {\n        var settingsButton = new IconButtonComp(\"mdi2d-dots-horizontal-circle-outline\", null);\n        settingsButton.style(\"settings\");\n        settingsButton.describe(d -> d.nameKey(\"more\"));\n        settingsButton.apply(new ContextMenuAugment<>(\n                event -> event.getButton() == MouseButton.PRIMARY,\n                null,\n                () -> StoreEntryComp.this.createContextMenu(name)));\n        return settingsButton;\n    }\n\n    protected BaseRegionBuilder<?, ?> createBatchSelection() {\n        var c = new StoreEntryBatchSelectComp(section);\n        c.hide(StoreViewState.get().getBatchMode().not());\n        return c;\n    }\n\n    private void handleContextMenuCount(ContextMenu contextMenu) {\n        var ref = new WeakReference<>(contextMenu);\n        contextMenuCount.set(contextMenuCount.get() + 1);\n        contextMenuCount.addListener((observable, oldValue, newValue) -> {\n            var cm = ref.get();\n            if (cm != null) {\n                cm.hide();\n            }\n        });\n    }\n\n    protected ContextMenu createContextMenu(Region name) {\n        var contextMenu = MenuHelper.createContextMenu();\n        handleContextMenuCount(contextMenu);\n\n        var cats = Arrays.stream(StoreActionCategory.values()).collect(Collectors.toCollection(ArrayList::new));\n        cats.addFirst(null);\n        for (var cat : cats) {\n            var items = new ArrayList<MenuItem>();\n\n            for (var p : getWrapper().getMinorActionProviders()) {\n                var item = buildMenuItemForAction(p);\n                if (item == null || p.getCategory() != cat) {\n                    continue;\n                }\n\n                items.add(item);\n            }\n\n            if (cat == StoreActionCategory.CONFIGURATION\n                    && getWrapper().getEntry().getValidity() != DataStoreEntry.Validity.LOAD_FAILED) {\n                var rename = new MenuItem(AppI18n.get(\"rename\"), new FontIcon(\"mdal-edit\"));\n                rename.setOnAction(event -> {\n                    name.requestFocus();\n                });\n                items.add(items.size(), rename);\n\n                var notes = new MenuItem(AppI18n.get(\"addNotes\"), new FontIcon(\"mdi2c-comment-text-outline\"));\n                notes.setOnAction(event -> {\n                    StoreNotesComp.showDialog(getWrapper(), getDefaultNotes());\n                    event.consume();\n                });\n                notes.visibleProperty().bind(BindingsHelper.map(getWrapper().getNotes(), s -> s == null));\n                items.add(items.size(), notes);\n\n                var freeze = new MenuItem();\n                freeze.graphicProperty()\n                        .bind(Bindings.createObjectBinding(\n                                () -> {\n                                    var is = getWrapper().getReadOnly().get();\n                                    return is\n                                            ? new FontIcon(\"mdi2l-lock-open-variant-outline\")\n                                            : new FontIcon(\"mdi2l-lock-open-outline\");\n                                },\n                                getWrapper().getReadOnly()));\n                freeze.textProperty()\n                        .bind(Bindings.createStringBinding(\n                                () -> {\n                                    var is = getWrapper().getReadOnly().get();\n                                    return is\n                                            ? AppI18n.get(\"unfreezeConfiguration\")\n                                            : AppI18n.get(\"freezeConfiguration\");\n                                },\n                                AppI18n.activeLanguage(),\n                                getWrapper().getReadOnly()));\n                freeze.setOnAction(event -> getWrapper()\n                        .getEntry()\n                        .setFreeze(!getWrapper().getReadOnly().get()));\n                items.add(freeze);\n            }\n\n            if (cat == StoreActionCategory.DEVELOPER) {\n                if (AppPrefs.get().developerMode().getValue()) {\n                    var browse = new MenuItem(\n                            AppI18n.get(\"browseInternalStorage\"), new FontIcon(\"mdi2f-folder-open-outline\"));\n                    browse.setOnAction(event ->\n                            DesktopHelper.browseFile(getWrapper().getEntry().getDirectory()));\n                    items.add(browse);\n                }\n\n                if (AppPrefs.get().enableHttpApi().get()) {\n                    var copyId = new MenuItem(AppI18n.get(\"copyId\"), new FontIcon(\"mdi2c-content-copy\"));\n                    copyId.setOnAction(event -> ClipboardHelper.copyText(\n                            getWrapper().getEntry().getUuid().toString()));\n                    items.add(copyId);\n                }\n            }\n\n            if (cat == StoreActionCategory.APPEARANCE) {\n                if (section.getDepth() == 1) {\n                    var color = new Menu(AppI18n.get(\"color\"), new FontIcon(\"mdi2f-format-color-fill\"));\n                    var none = new MenuItem();\n                    none.textProperty().bind(AppI18n.observable(\"none\"));\n                    none.setOnAction(event -> {\n                        getWrapper().getEntry().setColor(null);\n                        event.consume();\n                    });\n                    none.setGraphic(DataStoreColor.createDisplayGraphic(null));\n                    color.getItems().add(none);\n                    Arrays.stream(DataStoreColor.values()).forEach(dataStoreColor -> {\n                        MenuItem m = new MenuItem();\n                        m.textProperty().bind(AppI18n.observable(dataStoreColor.getId()));\n                        m.setOnAction(event -> {\n                            getWrapper().getEntry().setColor(dataStoreColor);\n                            event.consume();\n                        });\n                        m.setGraphic(DataStoreColor.createDisplayGraphic(dataStoreColor));\n                        color.getItems().add(m);\n                    });\n                    items.add(color);\n                }\n\n                {\n                    var tags = new Menu(AppI18n.get(\"tags\"), new FontIcon(\"mdi2t-tag-text-outline\"));\n\n                    var allTags = StoreViewState.get().getAllAvailableTags();\n                    for (String tag : allTags) {\n                        var tagItem = new MenuItem(tag);\n                        if (getWrapper().getTags().contains(tag)) {\n                            tagItem.setGraphic(new FontIcon(\"mdi2c-check\"));\n                        }\n                        tagItem.addEventFilter(ActionEvent.ACTION, event -> {\n                            getWrapper().toggleTag(tag);\n                            event.consume();\n                        });\n                        tags.getItems().add(tagItem);\n                    }\n\n                    if (allTags.size() > 0) {\n                        tags.getItems().add(new SeparatorMenuItem());\n                    }\n\n                    var index = MenuHelper.createMenuItem(\n                            new LabelGraphic.IconGraphic(\"mdi2t-tag-plus-outline\"), \"createTag\");\n                    index.setOnAction(event -> {\n                        var tagName = new SimpleStringProperty();\n                        var modal = ModalOverlay.of(\"addNewTag\", new TextFieldComp(tagName).prefWidth(350));\n                        modal.withDefaultButtons(() -> {\n                            getWrapper().getEntry().addTag(tagName.getValue());\n                        });\n                        modal.show();\n                        event.consume();\n                    });\n                    tags.getItems().add(index);\n\n                    items.add(tags);\n                }\n\n                {\n                    var order = new Menu(AppI18n.get(\"order\"), new FontIcon(\"mdi2f-format-list-bulleted\"));\n\n                    var index = new MenuItem(AppI18n.get(\"index\"), new FontIcon(\"mdi2o-order-numeric-ascending\"));\n                    index.setOnAction(event -> {\n                        StoreOrderIndexDialog.show(getWrapper());\n                        event.consume();\n                    });\n                    order.getItems().add(index);\n\n                    order.getItems().add(new SeparatorMenuItem());\n\n                    var noOrder = new MenuItem(AppI18n.get(\"none\"), new FontIcon(\"mdi2r-reorder-horizontal\"));\n                    noOrder.setOnAction(event -> {\n                        DataStorage.get().setOrderIndex(getWrapper().getEntry(), 0);\n                        event.consume();\n                    });\n                    if (getWrapper().getEntry().getOrderIndex() == Integer.MIN_VALUE\n                            && getWrapper().getEntry().getOrderIndex() == Integer.MAX_VALUE) {\n                        order.getItems().add(noOrder);\n                    }\n\n                    var first = new MenuItem(AppI18n.get(\"moveToFirst\"), new FontIcon(\"mdi2o-order-bool-descending\"));\n                    first.setOnAction(event -> {\n                        getWrapper().orderFirst();\n                        event.consume();\n                    });\n                    order.getItems().add(first);\n\n                    var last = new MenuItem(AppI18n.get(\"moveToLast\"), new FontIcon(\"mdi2o-order-bool-ascending\"));\n                    last.setOnAction(event -> {\n                        getWrapper().orderLast();\n                        event.consume();\n                    });\n                    order.getItems().add(last);\n\n                    order.getItems().add(new SeparatorMenuItem());\n\n                    var top =\n                            new MenuItem(AppI18n.get(\"keepFirst\"), new FontIcon(\"mdi2o-order-bool-descending-variant\"));\n                    top.setOnAction(event -> {\n                        getWrapper().orderStickFirst();\n                        event.consume();\n                    });\n                    top.setDisable(getWrapper().getEntry().getOrderIndex() == Integer.MIN_VALUE);\n                    order.getItems().add(top);\n\n                    var bottom =\n                            new MenuItem(AppI18n.get(\"keepLast\"), new FontIcon(\"mdi2o-order-bool-ascending-variant\"));\n                    bottom.setOnAction(event -> {\n                        getWrapper().orderStickLast();\n                        event.consume();\n                    });\n                    bottom.setDisable(getWrapper().getEntry().getOrderIndex() == Integer.MAX_VALUE);\n                    order.getItems().add(bottom);\n\n                    order.getItems().add(new SeparatorMenuItem());\n\n                    var clear =\n                            new MenuItem(AppI18n.get(\"clearIndex\"), new FontIcon(\"mdi2o-order-bool-ascending-variant\"));\n                    clear.setOnAction(event -> {\n                        getWrapper().orderWithIndex(0);\n                        event.consume();\n                    });\n                    if (getWrapper().getOrderIndex().get() != 0) {\n                        order.getItems().add(clear);\n                    }\n\n                    items.add(order);\n                }\n\n                if (getWrapper().getEntry().getProvider() != null\n                        && getWrapper().getEntry().getProvider().canMoveCategories()) {\n                    var move = new Menu(AppI18n.get(\"category\"), new FontIcon(\"mdi2f-folder-move-outline\"));\n                    StoreViewState.get()\n                            .getSortedCategories(\n                                    getWrapper().getCategory().getValue().getRoot())\n                            .getList()\n                            .forEach(storeCategoryWrapper -> {\n                                MenuItem m = new MenuItem();\n                                m.textProperty()\n                                        .setValue(\"  \".repeat(storeCategoryWrapper.getDepth())\n                                                + storeCategoryWrapper.getName().getValue());\n                                m.setOnAction(event -> {\n                                    getWrapper().moveTo(storeCategoryWrapper.getCategory());\n                                    event.consume();\n                                });\n                                if (storeCategoryWrapper.getParent() == null) {\n                                    m.setDisable(true);\n                                }\n\n                                move.getItems().add(m);\n                            });\n                    items.add(move);\n                }\n\n                if (getWrapper().getPinToTop().getValue() || section.getDepth() > 1) {\n                    var pinToTop = new MenuItem();\n                    pinToTop.graphicProperty()\n                            .bind(Bindings.createObjectBinding(\n                                    () -> {\n                                        var is = getWrapper().getPinToTop().get();\n                                        return is\n                                                ? new FontIcon(\"mdi2p-pin-off-outline\")\n                                                : new FontIcon(\"mdi2p-pin-outline\");\n                                    },\n                                    getWrapper().getPinToTop()));\n                    pinToTop.textProperty()\n                            .bind(Bindings.createStringBinding(\n                                    () -> {\n                                        var is = getWrapper().getPinToTop().get();\n                                        return is ? AppI18n.get(\"unpinFromTop\") : AppI18n.get(\"pinToTop\");\n                                    },\n                                    AppI18n.activeLanguage(),\n                                    getWrapper().getPinToTop()));\n                    pinToTop.setOnAction(event -> getWrapper().togglePinToTop());\n                    items.add(pinToTop);\n                }\n\n                if (getWrapper().canBreakOutCategory()) {\n                    var breakOut = new MenuItem();\n                    var is = getWrapper().getBreakoutCategory().isPresent();\n                    if (is) {\n                        breakOut.textProperty().bind(AppI18n.observable(\"mergeCategory\"));\n                        breakOut.setGraphic(new FontIcon(\"mdi2c-collapse-all-outline\"));\n                    } else {\n                        breakOut.textProperty().bind(AppI18n.observable(\"breakOutCategory\"));\n                        breakOut.setGraphic(new FontIcon(\"mdi2e-expand-all-outline\"));\n                    }\n                    breakOut.setOnAction(event -> {\n                        if (is) {\n                            getWrapper().mergeBreakOutCategory();\n                        } else {\n                            getWrapper().breakOutCategory();\n                        }\n                        event.consume();\n                    });\n                    items.add(breakOut);\n                }\n            }\n\n            if (cat == StoreActionCategory.DELETION) {\n                var del = new MenuItem(AppI18n.get(\"remove\"), new FontIcon(\"mdal-delete_outline\"));\n                del.disableProperty()\n                        .bind(Bindings.createBooleanBinding(\n                                () -> {\n                                    return !getWrapper().getDeletable().get();\n                                },\n                                getWrapper().getDeletable()));\n                del.setOnAction(event -> getWrapper().delete());\n                contextMenu.getItems().add(del);\n            }\n\n            if (items.isEmpty()) {\n                continue;\n            }\n\n            contextMenu.getItems().addAll(items);\n            contextMenu.getItems().add(new SeparatorMenuItem());\n        }\n\n        return contextMenu;\n    }\n\n    private MenuItem buildMenuItemForAction(ActionProvider p) {\n        var leaf = p instanceof HubLeafProvider<?> l ? l : null;\n        var branch = p instanceof HubBranchProvider<?> b ? b : null;\n        var cs = leaf != null ? leaf : branch;\n\n        if (cs == null || cs.isMajor() || (leaf != null && leaf.isDefault())) {\n            return null;\n        }\n\n        var name = cs.getName(getWrapper().getEntry().ref());\n        var icon = cs.getIcon(getWrapper().getEntry().ref());\n        var item = branch != null\n                ? new Menu(null, icon.createGraphicNode())\n                : new MenuItem(null, icon.createGraphicNode());\n        item.textProperty().bind(name);\n\n        Menu menu = item instanceof Menu m ? m : null;\n\n        if (branch != null) {\n            var items = branch.getChildren(getWrapper().getEntry().ref()).stream()\n                    .filter(actionProvider -> getWrapper().showActionProvider(actionProvider, false))\n                    .map(c -> buildMenuItemForAction(c))\n                    .toList();\n            menu.getItems().addAll(items);\n            return menu;\n        }\n\n        item.setOnAction(event -> {\n            leaf.execute(getWrapper().getEntry().ref());\n            event.consume();\n            if (event.getTarget() instanceof Menu m) {\n                m.getParentPopup().hide();\n            }\n        });\n\n        return item;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreEntryListBatchBarComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.augment.ContextMenuAugment;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.hub.action.BatchHubProvider;\nimport io.xpipe.app.platform.DerivedObservableList;\nimport io.xpipe.app.platform.MenuHelper;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.collections.ListChangeListener;\nimport javafx.collections.ObservableList;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.Menu;\nimport javafx.scene.control.MenuItem;\nimport javafx.scene.input.MouseButton;\nimport javafx.scene.layout.Region;\n\nimport atlantafx.base.theme.Styles;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class StoreEntryListBatchBarComp extends SimpleRegionBuilder {\n\n    @Override\n    protected Region createSimple() {\n        var checkbox = new StoreEntryBatchSelectComp(StoreViewState.get().getCurrentTopLevelSection());\n        var l = new LabelComp(Bindings.createStringBinding(\n                () -> {\n                    return AppI18n.get(\n                            \"connectionsSelected\",\n                            StoreViewState.get()\n                                    .getEffectiveBatchModeSelection()\n                                    .getList()\n                                    .size());\n                },\n                StoreViewState.get().getEffectiveBatchModeSelection().getList(),\n                AppI18n.activeLanguage()));\n        l.minWidth(Region.USE_PREF_SIZE);\n        l.apply(struc -> {\n            struc.setAlignment(Pos.CENTER);\n        });\n        var actions = new HorizontalComp(createActions());\n        actions.spacing(2);\n        var close = new IconButtonComp(\"mdi2c-close\", () -> {\n            StoreViewState.get().getBatchMode().setValue(false);\n        });\n        close.describe(d -> d.nameKey(\"close\"));\n        close.apply(struc -> {\n            struc.getStyleClass().remove(Styles.FLAT);\n            struc.minWidthProperty().bind(struc.heightProperty());\n            struc.prefWidthProperty().bind(struc.heightProperty());\n            struc.maxWidthProperty().bind(struc.heightProperty());\n        });\n        var bar = new HorizontalComp(List.of(\n                checkbox,\n                RegionBuilder.hspacer(12),\n                l,\n                RegionBuilder.hspacer(20),\n                actions,\n                RegionBuilder.hspacer(),\n                RegionBuilder.hspacer(20),\n                close));\n        bar.apply(struc -> {\n            struc.setFillHeight(true);\n            struc.setAlignment(Pos.CENTER_LEFT);\n        });\n        bar.minHeight(40);\n        bar.prefHeight(40);\n        bar.style(\"bar\");\n        bar.style(\"store-entry-list-status-bar\");\n        return bar.build();\n    }\n\n    private ObservableList<BaseRegionBuilder<?, ?>> createActions() {\n        var l = DerivedObservableList.<ActionProvider>arrayList(true);\n        StoreViewState.get().getEffectiveBatchModeSelection().getList().addListener((ListChangeListener<\n                        ? super StoreEntryWrapper>)\n                c -> {\n                    l.setContent(getCompatibleActionProviders());\n                });\n        return l.<BaseRegionBuilder<?, ?>>mapped(actionProvider -> {\n                    return buildButton(actionProvider);\n                })\n                .getList();\n    }\n\n    private List<ActionProvider> getCompatibleActionProviders() {\n        var l = StoreViewState.get().getEffectiveBatchModeSelection().getList();\n        if (l.isEmpty()) {\n            return List.of();\n        }\n\n        var all = new ArrayList<>(ActionProvider.ALL);\n        for (StoreEntryWrapper w : l) {\n            var actions = ActionProvider.ALL.stream()\n                    .filter(actionProvider -> {\n                        var s = actionProvider instanceof BatchHubProvider<?> b ? b : null;\n                        if (s == null) {\n                            return false;\n                        }\n\n                        if (!s.getApplicableClass()\n                                .isAssignableFrom(w.getStore().getValue().getClass())) {\n                            return false;\n                        }\n\n                        if (!w.getEntry().getValidity().isUsable() && s.requiresValidStore()) {\n                            return false;\n                        }\n\n                        if (!s.isApplicable(w.getEntry().ref())) {\n                            return false;\n                        }\n\n                        return true;\n                    })\n                    .toList();\n            all.removeIf(actionProvider -> !actions.contains(actionProvider));\n        }\n        return all;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private <T extends DataStore> BaseRegionBuilder<?, ?> buildButton(ActionProvider p) {\n        BatchHubProvider<T> s = (BatchHubProvider<T>) p;\n        if (s == null) {\n            return RegionBuilder.empty();\n        }\n\n        var childrenRefs = StoreViewState.get()\n                .getEffectiveBatchModeSelection()\n                .mapped(storeEntryWrapper -> storeEntryWrapper.getEntry().<T>ref());\n        var batchActions = s.getChildren(childrenRefs.getList());\n        var button = new ButtonComp(s.getName(), new SimpleObjectProperty<>(s.getIcon()), () -> {\n            if (batchActions.size() > 0) {\n                return;\n            }\n\n            runActions(s);\n        });\n\n        button.disable(Bindings.createBooleanBinding(\n                () -> {\n                    return childrenRefs.getList().stream().anyMatch(ref -> !s.isActive(ref));\n                },\n                childrenRefs.getList()));\n\n        if (batchActions.size() > 0) {\n            button.apply(new ContextMenuAugment<>(\n                    mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, keyEvent -> false, () -> {\n                        var cm = MenuHelper.createContextMenu();\n                        s.getChildren(childrenRefs.getList()).forEach(childProvider -> {\n                            var menu = buildMenuItemForAction(childrenRefs.getList(), childProvider);\n                            cm.getItems().add(menu);\n                        });\n                        return cm;\n                    }));\n        }\n        return button;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private <T extends DataStore> MenuItem buildMenuItemForAction(List<DataStoreEntryRef<T>> batch, ActionProvider a) {\n        BatchHubProvider<T> s = (BatchHubProvider<T>) a;\n        var name = s.getName();\n        var icon = s.getIcon();\n        var children = s.getChildren(batch);\n        if (children.size() > 0) {\n            var menu = new Menu();\n            menu.textProperty().bind(name);\n            menu.setGraphic(icon.createGraphicNode());\n            var items = children.stream()\n                    .filter(actionProvider -> actionProvider instanceof BatchHubProvider<?>)\n                    .map(c -> buildMenuItemForAction(batch, c))\n                    .toList();\n            menu.getItems().addAll(items);\n            return menu;\n        } else {\n            var item = new MenuItem();\n            item.textProperty().bind(name);\n            item.setGraphic(icon.createGraphicNode());\n            item.setOnAction(event -> {\n                runActions(s);\n                event.consume();\n                if (event.getTarget() instanceof Menu m) {\n                    m.getParentPopup().hide();\n                }\n            });\n            return item;\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private <T extends DataStore> void runActions(BatchHubProvider<?> s) {\n        var l = new ArrayList<>(\n                StoreViewState.get().getEffectiveBatchModeSelection().getList());\n        var mapped = l.stream().map(w -> w.getEntry().<T>ref()).toList();\n        ((BatchHubProvider<T>) s).execute(mapped);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreEntryListComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.ListBoxViewComp;\nimport io.xpipe.app.comp.base.MultiContentComp;\nimport io.xpipe.app.comp.base.VerticalComp;\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.prefs.AppPrefs;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.css.PseudoClass;\nimport javafx.geometry.Insets;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.VBox;\n\nimport java.util.LinkedHashMap;\nimport java.util.List;\n\npublic class StoreEntryListComp extends SimpleRegionBuilder {\n\n    private BaseRegionBuilder<?, ?> createList() {\n        var shown = StoreViewState.get()\n                .getCurrentTopLevelSection()\n                .getShownChildren()\n                .getList();\n        var all = StoreViewState.get()\n                .getCurrentTopLevelSection()\n                .getAllChildren()\n                .getList();\n        var content = new ListBoxViewComp<>(\n                shown,\n                all,\n                (StoreSection e) -> {\n                    var custom = StoreSection.customSection(e).hgrow();\n                    return custom;\n                },\n                true);\n        content.setVisibilityControl(true);\n        content.apply(struc -> {\n            // Reset scroll\n            StoreViewState.get().getActiveCategory().addListener((observable, oldValue, newValue) -> {\n                struc.setVvalue(0);\n            });\n\n            // Reset scroll\n            AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> {\n                struc.setVvalue(0);\n            });\n\n            // Reset scroll\n            StoreViewState.get().getFilterString().addListener((observable, oldValue, newValue) -> {\n                struc.setVvalue(0);\n            });\n\n            AppPrefs.get().condenseConnectionDisplay().subscribe(dense -> {\n                struc.pseudoClassStateChanged(PseudoClass.getPseudoClass(\"dense\"), dense);\n            });\n        });\n        content.style(\"store-list-comp\");\n        content.vgrow();\n\n        var statusBar = new StoreEntryListBatchBarComp();\n        statusBar.apply(struc -> {\n            VBox.setMargin(struc, new Insets(3, 6, 4, 2));\n        });\n        statusBar.hide(StoreViewState.get().getBatchMode().not());\n        return new VerticalComp(List.of(content, statusBar));\n    }\n\n    @Override\n    protected Region createSimple() {\n        var scriptsIntroShowing = new SimpleBooleanProperty(!AppCache.getBoolean(\"scriptsIntroCompleted\", false));\n        var initialCount = 1;\n        var showIntro = Bindings.createBooleanBinding(\n                () -> {\n                    var allCat = StoreViewState.get().getAllConnectionsCategory();\n                    var connections = StoreViewState.get().getAllEntries().getList().stream()\n                            .filter(wrapper -> allCat.equals(\n                                    wrapper.getCategory().getValue().getRoot()))\n                            .toList();\n                    return initialCount == connections.size()\n                            && StoreViewState.get()\n                                    .getActiveCategory()\n                                    .getValue()\n                                    .getRoot()\n                                    .equals(allCat);\n                },\n                StoreViewState.get().getAllEntries().getList(),\n                StoreViewState.get().getActiveCategory());\n        var showIdentitiesIntro = Bindings.createBooleanBinding(\n                () -> {\n                    var allCat = StoreViewState.get().getAllIdentitiesCategory();\n                    var connections = StoreViewState.get().getAllEntries().getList().stream()\n                            .filter(wrapper -> allCat.equals(\n                                    wrapper.getCategory().getValue().getRoot()))\n                            .toList();\n                    return 0 == connections.size()\n                            && StoreViewState.get()\n                                    .getActiveCategory()\n                                    .getValue()\n                                    .getRoot()\n                                    .equals(allCat);\n                },\n                StoreViewState.get().getAllEntries().getList(),\n                StoreViewState.get().getActiveCategory());\n        var showScriptsIntro = Bindings.createBooleanBinding(\n                () -> {\n                    if (StoreViewState.get()\n                            .getActiveCategory()\n                            .getValue()\n                            .equals(StoreViewState.get().getScriptSourcesCategory())) {\n                        return false;\n                    }\n\n                    if (StoreViewState.get()\n                            .getActiveCategory()\n                            .getValue()\n                            .getRoot()\n                            .equals(StoreViewState.get().getAllScriptsCategory())) {\n                        return scriptsIntroShowing.get();\n                    }\n\n                    return false;\n                },\n                scriptsIntroShowing,\n                StoreViewState.get().getActiveCategory());\n        var showScriptSourcesIntro = Bindings.createBooleanBinding(\n                () -> {\n                    var cat = StoreViewState.get().getScriptSourcesCategory();\n                    if (StoreViewState.get().getActiveCategory().getValue().equals(cat)) {\n                        return cat.getAllContainedEntriesCount().get() == 0;\n                    }\n\n                    return false;\n                },\n                StoreViewState.get().getAllEntries().getList(),\n                StoreViewState.get().getActiveCategory());\n        var showList = Bindings.createBooleanBinding(\n                () -> {\n                    if (StoreViewState.get()\n                            .getActiveCategory()\n                            .getValue()\n                            .getRoot()\n                            .equals(StoreViewState.get().getAllScriptsCategory())) {\n                        return !scriptsIntroShowing.get();\n                    }\n\n                    if (StoreViewState.get()\n                            .getCurrentTopLevelSection()\n                            .getShownChildren()\n                            .getList()\n                            .isEmpty()) {\n                        return false;\n                    }\n\n                    return true;\n                },\n                StoreViewState.get().getActiveCategory(),\n                scriptsIntroShowing,\n                StoreViewState.get()\n                        .getCurrentTopLevelSection()\n                        .getShownChildren()\n                        .getList());\n        var map = new LinkedHashMap<BaseRegionBuilder<?, ?>, ObservableValue<Boolean>>();\n        map.put(\n                new StoreNotFoundComp(),\n                Bindings.and(\n                        Bindings.not(Bindings.isEmpty(\n                                StoreViewState.get().getAllEntries().getList())),\n                        Bindings.isEmpty(StoreViewState.get()\n                                .getCurrentTopLevelSection()\n                                .getShownChildren()\n                                .getList())));\n        map.put(createList(), showList);\n        map.put(new StoreIntroComp(), showIntro);\n        map.put(new StoreScriptsIntroComp(scriptsIntroShowing), showScriptsIntro);\n        map.put(new StoreScriptSourcesIntroComp(), showScriptSourcesIntro);\n        map.put(new StoreIdentitiesIntroComp(), showIdentitiesIntro);\n\n        return new MultiContentComp(false, map, false).build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreEntryListOverviewComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.CountComp;\nimport io.xpipe.app.comp.base.FilterComp;\nimport io.xpipe.app.comp.base.IconButtonComp;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.MenuHelper;\nimport io.xpipe.app.util.ObservableSubscriber;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.css.PseudoClass;\nimport javafx.geometry.Insets;\nimport javafx.geometry.Orientation;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.Separator;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Priority;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.VBox;\nimport javafx.scene.text.TextAlignment;\n\nimport atlantafx.base.theme.Styles;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.function.Function;\n\npublic class StoreEntryListOverviewComp extends SimpleRegionBuilder {\n\n    private final ObservableSubscriber filterTrigger;\n\n    public StoreEntryListOverviewComp(ObservableSubscriber filterTrigger) {\n        this.filterTrigger = filterTrigger;\n    }\n\n    private Region createGroupListHeader() {\n        var label = new Label();\n        var name = BindingsHelper.flatMap(\n                StoreViewState.get().getActiveCategory(),\n                categoryWrapper -> AppI18n.observable(\n                        categoryWrapper.getRoot().equals(StoreViewState.get().getAllConnectionsCategory())\n                                ? \"connections\"\n                                : categoryWrapper\n                                                .getRoot()\n                                                .equals(StoreViewState.get().getAllScriptsCategory())\n                                        ? \"scripts\"\n                                        : \"identities\"));\n        label.textProperty().bind(name);\n        label.getStyleClass().add(\"name\");\n\n        var allCount = StoreViewState.get()\n                .entriesCount(\n                        storeEntryWrapper -> {\n                            var rootCategory =\n                                    storeEntryWrapper.getCategory().getValue().getRoot();\n                            var inRootCategory = StoreViewState.get()\n                                    .getActiveCategory()\n                                    .getValue()\n                                    .getRoot()\n                                    .equals(rootCategory);\n                            return inRootCategory;\n                        },\n                        StoreViewState.get().getActiveCategory());\n        var count = new CountComp(allCount, allCount, Function.identity());\n\n        var c = count.build();\n        var sep = new Separator(Orientation.VERTICAL);\n        sep.setPadding(new Insets(6, 3, 4, 3));\n        var topBar = new HBox(\n                label,\n                c,\n                RegionBuilder.hspacer().build(),\n                createIndexSortButton().build(),\n                sep,\n                createDateSortButton().build(),\n                RegionBuilder.hspacer(2).build(),\n                createAlphabeticalSortButton().build());\n        if (OsType.ofLocal() == OsType.MACOS) {\n            AppFontSizes.xxxl(label);\n            AppFontSizes.xxxl(c);\n        } else {\n            AppFontSizes.xxl(label);\n            AppFontSizes.xxl(c);\n        }\n        topBar.setAlignment(Pos.CENTER);\n        topBar.getStyleClass().add(\"top\");\n        return topBar;\n    }\n\n    private Region createGroupListFilter() {\n        var filter = new FilterComp(StoreViewState.get().getFilterString()).build();\n        filterTrigger.subscribe(() -> {\n            filter.requestFocus();\n        });\n        var add = createAddButton();\n        var batchMode = createBatchModeButton().build();\n        var hbox = new HBox(add, filter, batchMode);\n        filter.minHeightProperty().bind(add.heightProperty());\n        filter.prefHeightProperty().bind(add.heightProperty());\n        filter.maxHeightProperty().bind(add.heightProperty());\n        batchMode.minHeightProperty().bind(add.heightProperty());\n        batchMode.prefHeightProperty().bind(add.heightProperty());\n        batchMode.maxHeightProperty().bind(add.heightProperty());\n        batchMode.minWidthProperty().bind(add.heightProperty());\n        batchMode.prefWidthProperty().bind(add.heightProperty());\n        batchMode.maxWidthProperty().bind(add.heightProperty());\n        hbox.setSpacing(8);\n        hbox.setAlignment(Pos.CENTER);\n        HBox.setHgrow(filter, Priority.ALWAYS);\n\n        filter.getStyleClass().add(\"filter-bar\");\n        return hbox;\n    }\n\n    private Region createAddButton() {\n        var menu = MenuHelper.createMenuButton();\n        menu.setGraphic(new FontIcon(\"mdi2p-plus-thick\"));\n        menu.setOnShowing(event -> {\n            menu.getItems().clear();\n            StoreCreationMenu.addButtons(menu, true);\n            event.consume();\n        });\n        menu.textProperty().bind(AppI18n.observable(\"new\"));\n        menu.setAlignment(Pos.CENTER);\n        menu.setTextAlignment(TextAlignment.CENTER);\n        menu.setOpacity(0.85);\n        menu.setMinWidth(Region.USE_PREF_SIZE);\n        menu.getStyleClass().add(\"creation-menu\");\n        return menu;\n    }\n\n    private BaseRegionBuilder<?, ?> createBatchModeButton() {\n        var batchMode = StoreViewState.get().getBatchMode();\n        var b = new IconButtonComp(\"mdi2l-layers\", () -> {\n            batchMode.setValue(!batchMode.getValue());\n        });\n        b.describe(d -> d.nameKey(\"batchMode\"));\n        b.style(\"batch-mode-button\");\n        b.apply(struc -> {\n            batchMode.subscribe(a -> {\n                struc.pseudoClassStateChanged(PseudoClass.getPseudoClass(\"active\"), a);\n            });\n            struc.getStyleClass().remove(Styles.FLAT);\n        });\n        return b;\n    }\n\n    private BaseRegionBuilder<?, ?> createIndexSortButton() {\n        var sortMode = StoreViewState.get().getGlobalSortMode();\n        var icon = Bindings.createObjectBinding(\n                () -> {\n                    if (sortMode.getValue() == StoreSectionSortMode.INDEX_ASC) {\n                        return new LabelGraphic.IconGraphic(\"mdi2o-order-numeric-ascending\");\n                    }\n                    if (sortMode.getValue() == StoreSectionSortMode.INDEX_DESC) {\n                        return new LabelGraphic.IconGraphic(\"mdi2o-order-numeric-descending\");\n                    }\n                    return new LabelGraphic.IconGraphic(\"mdi2o-order-numeric-ascending\");\n                },\n                sortMode);\n        var button = new IconButtonComp(icon, () -> {\n            if (sortMode.getValue() == StoreSectionSortMode.INDEX_ASC) {\n                sortMode.setValue(StoreSectionSortMode.INDEX_DESC);\n            } else {\n                sortMode.setValue(StoreSectionSortMode.INDEX_ASC);\n            }\n        });\n        button.apply(struc -> {\n            struc.opacityProperty()\n                    .bind(Bindings.createDoubleBinding(\n                            () -> {\n                                if (sortMode.getValue() == StoreSectionSortMode.INDEX_ASC\n                                        || sortMode.getValue() == StoreSectionSortMode.INDEX_DESC) {\n                                    return 1.0;\n                                }\n                                return 0.4;\n                            },\n                            sortMode));\n        });\n        button.describe(d -> d.nameKey(\"sortIndexed\"));\n        return button;\n    }\n\n    private BaseRegionBuilder<?, ?> createAlphabeticalSortButton() {\n        var sortMode = StoreViewState.get().getTieSortMode();\n        var icon = Bindings.createObjectBinding(\n                () -> {\n                    if (sortMode.getValue() == StoreSectionSortMode.ALPHABETICAL_ASC) {\n                        return new LabelGraphic.IconGraphic(\"mdi2o-order-alphabetical-descending\");\n                    }\n                    if (sortMode.getValue() == StoreSectionSortMode.ALPHABETICAL_DESC) {\n                        return new LabelGraphic.IconGraphic(\"mdi2o-order-alphabetical-ascending\");\n                    }\n                    return new LabelGraphic.IconGraphic(\"mdi2o-order-alphabetical-descending\");\n                },\n                sortMode);\n        var alphabetical = new IconButtonComp(icon, () -> {\n            if (sortMode.getValue() == StoreSectionSortMode.ALPHABETICAL_ASC) {\n                sortMode.setValue(StoreSectionSortMode.ALPHABETICAL_DESC);\n            } else if (sortMode.getValue() == StoreSectionSortMode.ALPHABETICAL_DESC) {\n                sortMode.setValue(StoreSectionSortMode.ALPHABETICAL_ASC);\n            } else {\n                sortMode.setValue(StoreSectionSortMode.ALPHABETICAL_ASC);\n            }\n        });\n        alphabetical.apply(alphabeticalR -> {\n            alphabeticalR\n                    .opacityProperty()\n                    .bind(Bindings.createDoubleBinding(\n                            () -> {\n                                if (sortMode.getValue() == StoreSectionSortMode.ALPHABETICAL_ASC\n                                        || sortMode.getValue() == StoreSectionSortMode.ALPHABETICAL_DESC) {\n                                    return 1.0;\n                                }\n                                return 0.4;\n                            },\n                            sortMode));\n        });\n        alphabetical.describe(d -> d.nameKey(\"sortAlphabetical\"));\n        return alphabetical;\n    }\n\n    private BaseRegionBuilder<?, ?> createDateSortButton() {\n        var sortMode = StoreViewState.get().getTieSortMode();\n        var icon = Bindings.createObjectBinding(\n                () -> {\n                    if (sortMode.getValue() == StoreSectionSortMode.DATE_ASC) {\n                        return new LabelGraphic.IconGraphic(\"mdi2s-sort-clock-ascending-outline\");\n                    }\n                    if (sortMode.getValue() == StoreSectionSortMode.DATE_DESC) {\n                        return new LabelGraphic.IconGraphic(\"mdi2s-sort-clock-descending-outline\");\n                    }\n                    return new LabelGraphic.IconGraphic(\"mdi2s-sort-clock-ascending-outline\");\n                },\n                sortMode);\n        var date = new IconButtonComp(icon, () -> {\n            if (sortMode.getValue() == StoreSectionSortMode.DATE_ASC) {\n                sortMode.setValue(StoreSectionSortMode.DATE_DESC);\n            } else if (sortMode.getValue() == StoreSectionSortMode.DATE_DESC) {\n                sortMode.setValue(StoreSectionSortMode.DATE_ASC);\n            } else {\n                sortMode.setValue(StoreSectionSortMode.DATE_ASC);\n            }\n        });\n        date.apply(dateR -> {\n            dateR.opacityProperty()\n                    .bind(Bindings.createDoubleBinding(\n                            () -> {\n                                if (sortMode.getValue() == StoreSectionSortMode.DATE_ASC\n                                        || sortMode.getValue() == StoreSectionSortMode.DATE_DESC) {\n                                    return 1.0;\n                                }\n                                return 0.4;\n                            },\n                            sortMode));\n        });\n        date.describe(d -> d.nameKey(\"sortLastUsed\"));\n        return date;\n    }\n\n    @Override\n    public Region createSimple() {\n        var bar = new VBox(createGroupListHeader(), createGroupListFilter());\n        bar.setFillWidth(true);\n        bar.getStyleClass().add(\"bar\");\n        bar.getStyleClass().add(\"store-header-bar\");\n        return bar;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreEntryWrapper.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.action.*;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.FixedHierarchyStore;\nimport io.xpipe.app.ext.GroupStore;\nimport io.xpipe.app.ext.LocalStore;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.ext.SingletonSessionStore;\nimport io.xpipe.app.hub.action.HubBranchProvider;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.HubMenuItemProvider;\nimport io.xpipe.app.hub.action.impl.EditHubLeafProvider;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.DerivedObservableList;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreCategory;\nimport io.xpipe.app.storage.DataStoreColor;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.*;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.FXCollections;\nimport javafx.collections.ObservableList;\n\nimport lombok.Getter;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\n@Getter\npublic class StoreEntryWrapper {\n\n    private final Property<String> name;\n    private final DataStoreEntry entry;\n    private final Property<Instant> lastAccess;\n    private final BooleanProperty disabled = new SimpleBooleanProperty();\n    private final BooleanProperty busy = new SimpleBooleanProperty();\n    private final Property<DataStoreEntry.Validity> validity = new SimpleObjectProperty<>();\n    private final ListProperty<HubMenuItemProvider<?>> majorActionProviders =\n            new SimpleListProperty<>(FXCollections.observableArrayList());\n    private final ListProperty<HubMenuItemProvider<?>> minorActionProviders =\n            new SimpleListProperty<>(FXCollections.observableArrayList());\n    private final Property<ActionProvider> defaultActionProvider = new SimpleObjectProperty<>();\n    private final BooleanProperty deletable = new SimpleBooleanProperty();\n    private final BooleanProperty expanded = new SimpleBooleanProperty();\n    private final Property<Object> persistentState = new SimpleObjectProperty<>();\n    private final Property<Map<String, Object>> cache = new SimpleObjectProperty<>(Map.of());\n    private final Property<DataStoreColor> color = new SimpleObjectProperty<>();\n    private final Property<StoreCategoryWrapper> category = new SimpleObjectProperty<>();\n    private final Property<String> summary = new SimpleObjectProperty<>();\n    private final ObjectProperty<String> notes;\n    private final Property<String> customIcon = new SimpleObjectProperty<>();\n    private final Property<String> iconFile = new SimpleObjectProperty<>();\n    private final BooleanProperty sessionActive = new SimpleBooleanProperty();\n    private final Property<DataStore> store = new SimpleObjectProperty<>();\n    private final Property<String> information = new SimpleStringProperty();\n    private final BooleanProperty perUser = new SimpleBooleanProperty();\n    private final ObservableValue<String> shownName;\n    private final ObservableValue<String> shownSummary;\n    private final ObservableValue<String> shownDescription;\n    private final Property<String> shownInformation;\n    private final BooleanProperty largeCategoryOptimizations = new SimpleBooleanProperty();\n    private final BooleanProperty readOnly = new SimpleBooleanProperty();\n    private final BooleanProperty renaming = new SimpleBooleanProperty();\n    private final BooleanProperty pinToTop = new SimpleBooleanProperty();\n    private final IntegerProperty orderIndex = new SimpleIntegerProperty();\n    private final BooleanProperty effectiveBusy = new SimpleBooleanProperty();\n    private final ObservableList<String> tags = FXCollections.observableArrayList();\n    private boolean effectiveBusyProviderBound = false;\n\n    public StoreEntryWrapper(DataStoreEntry entry) {\n        this.entry = entry;\n        this.name = new SimpleStringProperty(entry.getName());\n        this.lastAccess = new SimpleObjectProperty<>(entry.getLastAccess().minus(Duration.ofMillis(500)));\n        this.shownName = Bindings.createStringBinding(\n                () -> {\n                    var n = name.getValue();\n                    if (n == null) {\n                        n = \"?\";\n                    }\n\n                    return AppPrefs.get().censorMode().get() ? \"*\".repeat(n.length()) : n;\n                },\n                AppPrefs.get().censorMode(),\n                name);\n        this.shownSummary = Bindings.createStringBinding(\n                () -> {\n                    var n = summary.getValue();\n                    return n != null && AppPrefs.get().censorMode().get() ? \"*\".repeat(n.length()) : n;\n                },\n                AppPrefs.get().censorMode(),\n                summary);\n        this.shownDescription = Bindings.createStringBinding(\n                () -> {\n                    var summaryValue = shownSummary.getValue();\n                    if (summaryValue != null) {\n                        return summaryValue;\n                    } else {\n                        var provider = getEntry().getProvider();\n                        if (provider != null) {\n                            return AppI18n.get(provider.getId() + \".displayName\");\n                        } else {\n                            return null;\n                        }\n                    }\n                },\n                shownSummary,\n                AppI18n.activeLanguage());\n        this.shownInformation = new SimpleObjectProperty<>();\n        this.notes = new SimpleObjectProperty<>(entry.getNotes());\n\n        setupListeners();\n    }\n\n    public void moveTo(DataStoreCategory category) {\n        ThreadHelper.runAsync(() -> {\n            DataStorage.get().moveEntryToCategory(entry, category);\n        });\n    }\n\n    public boolean includeInConnectionCount() {\n        return getEntry().getProvider() != null && getEntry().getProvider().includeInConnectionCount();\n    }\n\n    public boolean isInStorage() {\n        return DataStorage.get() != null && DataStorage.get().getStoreEntries().contains(entry);\n    }\n\n    public void editDialog() {\n        StoreCreationDialog.showEdit(entry);\n    }\n\n    public void delete() {\n        ThreadHelper.runAsync(() -> {\n            DataStorage.get().deleteWithChildren(this.entry);\n        });\n    }\n\n    private void setupListeners() {\n        name.addListener((c, o, n) -> {\n            entry.setName(n);\n        });\n\n        expanded.addListener((c, o, n) -> {\n            entry.setExpanded(n);\n        });\n\n        entry.addListener(() -> PlatformThread.runLaterIfNeeded(() -> {\n            update();\n        }));\n    }\n\n    public void stopSession() {\n        ThreadHelper.runFailableAsync(() -> {\n            if (entry.getStore() instanceof SingletonSessionStore<?> singletonSessionStore) {\n                singletonSessionStore.stopSessionIfNeeded();\n            }\n        });\n    }\n\n    public synchronized void update() {\n        // We are probably in shutdown then\n        if (AppOperationMode.isInShutdown()) {\n            return;\n        }\n\n        // We received a delayed update after removal\n        if (!DataStorage.get().getStoreEntries().contains(entry)) {\n            return;\n        }\n\n        // Avoid reupdating name when changed from the name property!\n        if (!entry.getName().equals(name.getValue())) {\n            name.setValue(entry.getName());\n        }\n\n        if (effectiveBusyProviderBound && !getValidity().getValue().isUsable()) {\n            this.effectiveBusyProviderBound = false;\n            this.effectiveBusy.unbind();\n            this.effectiveBusy.bind(busy);\n        }\n\n        lastAccess.setValue(entry.getLastAccess());\n        disabled.setValue(entry.isDisabled());\n        validity.setValue(entry.getValidity());\n        expanded.setValue(entry.isExpanded());\n        persistentState.setValue(entry.getStorePersistentState());\n\n        // Use map copy to recognize update\n        // This is a synchronized map, so we synchronize the access\n        synchronized (entry.getStoreCache()) {\n            if (!entry.getStoreCache().equals(cache.getValue())) {\n                cache.setValue(new HashMap<>(entry.getStoreCache()));\n            }\n        }\n        orderIndex.setValue(entry.getOrderIndex());\n        color.setValue(entry.getColor());\n        notes.setValue(entry.getNotes());\n        customIcon.setValue(entry.getIcon());\n        readOnly.setValue(entry.isFreeze());\n        iconFile.setValue(entry.getEffectiveIconFile());\n        busy.setValue(entry.getBusyCounter().get() != 0);\n        deletable.setValue(\n                !(entry.getStore() instanceof LocalStore) && !DataStorage.get().getEffectiveReadOnlyState(entry));\n        sessionActive.setValue(entry.getStore() instanceof SingletonSessionStore<?> ss\n                && entry.getStore() instanceof ShellStore\n                && ss.isSessionRunning());\n        category.setValue(StoreViewState.get().getCategories().getList().stream()\n                .filter(storeCategoryWrapper ->\n                        storeCategoryWrapper.getCategory().getUuid().equals(entry.getCategoryUuid()))\n                .findFirst()\n                .orElse(StoreViewState.get().getAllConnectionsCategory()));\n        largeCategoryOptimizations.setValue(\n                category.getValue().getLargeCategoryOptimizations().getValue());\n        perUser.setValue(\n                !category.getValue().getRoot().equals(StoreViewState.get().getAllIdentitiesCategory())\n                        && entry.isPerUserStore());\n        pinToTop.setValue(entry.isPinToTop());\n\n        var orderedTags = entry.getTags().stream().sorted().toList();\n        DerivedObservableList.wrap(tags, true).setContent(orderedTags);\n\n        var storeChanged = store.getValue() != entry.getStore();\n        store.setValue(entry.getStore());\n        if (storeChanged || !information.isBound()) {\n            information.unbind();\n            shownInformation.unbind();\n            if (entry.getValidity().isUsable()\n                    || (entry.getValidity() != DataStoreEntry.Validity.LOAD_FAILED\n                            && entry.getProvider().showIncompleteInfo())) {\n                var section = StoreViewState.get().getSectionForWrapper(this);\n                if (section.isPresent()) {\n                    try {\n                        var is = entry.getProvider().informationString(section.get());\n                        information.bind(is);\n                        shownInformation.bind(Bindings.createStringBinding(\n                                () -> {\n                                    // Might have changed validity meanwhile\n                                    var showInfo = entry.getValidity().isUsable()\n                                            || (entry.getValidity() != DataStoreEntry.Validity.LOAD_FAILED\n                                                    && entry.getProvider().showIncompleteInfo());\n                                    if (!showInfo) {\n                                        return null;\n                                    }\n\n                                    var n = information.getValue();\n                                    return n != null\n                                                    && AppPrefs.get()\n                                                            .censorMode()\n                                                            .get()\n                                            ? \"*\".repeat(n.length())\n                                            : n;\n                                },\n                                AppPrefs.get().censorMode(),\n                                information));\n                    } catch (Exception e) {\n                        ErrorEventFactory.fromThrowable(e).omit().handle();\n                        information.bind(new SimpleStringProperty());\n                    }\n                }\n            }\n        }\n\n        if (!entry.getValidity().isUsable()) {\n            summary.setValue(null);\n        } else {\n            try {\n                summary.setValue(\n                        entry.getProvider() != null ? entry.getProvider().summaryString(this) : null);\n            } catch (Exception ex) {\n                // Summary creation might fail or have a bug\n                ErrorEventFactory.fromThrowable(ex).omit().handle();\n            }\n        }\n\n        if (!isInStorage()) {\n            majorActionProviders.clear();\n            defaultActionProvider.setValue(null);\n        } else {\n            try {\n                var defaultProvider = ActionProvider.ALL.stream()\n                        .filter(e -> entry.getStore() != null\n                                && e instanceof HubLeafProvider<?> def\n                                && (entry.getValidity().isUsable()\n                                        || (!def.requiresValidStore() && entry.getProvider() != null))\n                                && def.getApplicableClass()\n                                        .isAssignableFrom(entry.getStore().getClass())\n                                && def.isApplicable(entry.ref())\n                                && def.isDefault())\n                        .findFirst()\n                        .or(() -> {\n                            if (entry.getStore() instanceof GroupStore<?>) {\n                                return Optional.empty();\n                            } else if (entry.getProvider() != null\n                                    && entry.getProvider().canConfigure()) {\n                                return Optional.of(new EditHubLeafProvider());\n                            } else {\n                                return Optional.empty();\n                            }\n                        })\n                        .orElse(null);\n                this.defaultActionProvider.setValue(defaultProvider);\n\n                var newMajorProviders = ActionProvider.ALL.stream()\n                        .map(actionProvider -> actionProvider instanceof HubMenuItemProvider<?> sa ? sa : null)\n                        .filter(Objects::nonNull)\n                        .filter(dataStoreActionProvider -> {\n                            return showActionProvider(dataStoreActionProvider, true);\n                        })\n                        .toList();\n                if (!majorActionProviders.equals(newMajorProviders)) {\n                    majorActionProviders.setAll(newMajorProviders);\n                }\n\n                var newMinorProviders = ActionProvider.ALL.stream()\n                        .map(actionProvider -> actionProvider instanceof HubMenuItemProvider<?> sa ? sa : null)\n                        .filter(Objects::nonNull)\n                        .filter(dataStoreActionProvider -> {\n                            return showActionProvider(dataStoreActionProvider, false);\n                        })\n                        .collect(Collectors.toCollection(ArrayList::new));\n                newMinorProviders.removeIf(storeActionProvider -> {\n                    return newMajorProviders.stream().anyMatch(mj -> {\n                        return mj instanceof HubBranchProvider<?> branch\n                                && branch.getChildren(entry.ref()).stream()\n                                        .anyMatch(c -> c.getClass().equals(storeActionProvider.getClass()));\n                    });\n                });\n                if (!minorActionProviders.equals(newMinorProviders)) {\n                    minorActionProviders.setAll(newMinorProviders);\n                }\n            } catch (Exception ex) {\n                ErrorEventFactory.fromThrowable(ex).omit().handle();\n            }\n        }\n\n        if (!effectiveBusyProviderBound && getValidity().getValue().isUsable()) {\n            this.effectiveBusyProviderBound = true;\n            this.effectiveBusy.unbind();\n            this.effectiveBusy.bind(busy.or(getEntry().getProvider().busy(this)));\n        }\n\n        if (!this.effectiveBusy.isBound() && !getValidity().getValue().isUsable()) {\n            this.effectiveBusy.bind(busy);\n        }\n\n        // The property values are only registered as changed once they are queried\n        // If we use information bindings that depend on some of these properties\n        // but use the store methods to retrieve data instead of the wrapper properties,\n        // the bindings do not get updated as the change events are not fired.\n        // We can also fire them manually with this\n        persistentState.getValue();\n        store.getValue();\n        cache.getValue();\n    }\n\n    public boolean showActionProvider(ActionProvider p, boolean major) {\n        if (p instanceof HubLeafProvider<?> leaf) {\n            return (entry.getValidity().isUsable() || (!leaf.requiresValidStore() && entry.getProvider() != null))\n                    && leaf.getApplicableClass()\n                            .isAssignableFrom(entry.getStore().getClass())\n                    && leaf.isApplicable(entry.ref())\n                    && (!major || leaf.isMajor());\n        }\n\n        if (p instanceof HubBranchProvider<?> branch\n                && entry.getStore() != null\n                && branch.getApplicableClass().isAssignableFrom(entry.getStore().getClass())\n                && branch.isApplicable(entry.ref())\n                && (!major || branch.isMajor())) {\n            return branch.getChildren(entry.ref()).stream().anyMatch(child -> {\n                return showActionProvider(child, false);\n            });\n        }\n\n        return false;\n    }\n\n    public boolean canBreakOutCategory() {\n        return (getStore().getValue() instanceof FixedHierarchyStore\n                        || getStore().getValue() instanceof GroupStore<?>)\n                && StoreViewState.get().getParentSectionForWrapper(this).isPresent();\n    }\n\n    public void breakOutCategory() {\n        ThreadHelper.runAsync(() -> {\n            var cat = DataStorage.get().breakOutCategory(entry);\n            if (cat != null) {\n                Platform.runLater(() -> {\n                    StoreViewState.get()\n                            .getActiveCategory()\n                            .setValue(StoreViewState.get().getCategoryWrapper(cat));\n                });\n            }\n        });\n    }\n\n    public Optional<StoreCategoryWrapper> getBreakoutCategory() {\n        if (entry.getBreakOutCategory() == null) {\n            return Optional.empty();\n        }\n\n        var cat = DataStorage.get().getStoreCategoryIfPresent(entry.getBreakOutCategory());\n        if (cat.isEmpty()) {\n            return Optional.empty();\n        }\n\n        return Optional.of(StoreViewState.get().getCategoryWrapper(cat.get()));\n    }\n\n    public void toggleTag(String tag) {\n        if (tags.contains(tag)) {\n            entry.removeTag(tag);\n        } else {\n            entry.addTag(tag);\n        }\n    }\n\n    public void mergeBreakOutCategory() {\n        ThreadHelper.runAsync(() -> {\n            DataStorage.get().mergeBreakOutCategory(entry);\n            Platform.runLater(() -> {\n                StoreViewState.get()\n                        .getActiveCategory()\n                        .setValue(StoreViewState.get()\n                                .getCategoryWrapper(DataStorage.get().getStoreCategory(entry)));\n            });\n        });\n    }\n\n    public void executeDefaultAction() {\n        if (entry.getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {\n            return;\n        }\n\n        if (getEntry().getValidity() == DataStoreEntry.Validity.INCOMPLETE) {\n            editDialog();\n            return;\n        }\n\n        var found = getDefaultActionProvider().getValue();\n        if (found != null) {\n            if (found instanceof HubLeafProvider<?> def) {\n                def.execute(getEntry().ref());\n            }\n        } else {\n            entry.setExpanded(!entry.isExpanded());\n        }\n    }\n\n    public void orderWithIndex(int index) {\n        DataStorage.get().setOrderIndex(entry, index);\n    }\n\n    public void orderLast() {\n        var section = StoreViewState.get().getParentSectionForWrapper(this);\n        if (section.isEmpty()) {\n            return;\n        }\n\n        var isSingle = section.get().getAllChildren().getList().stream()\n                        .filter(sec -> sec.getWrapper().getOrderIndex().get() == orderIndex.getValue())\n                        .count()\n                == 1;\n        var max = section.get().getAllChildren().getList().stream()\n                .map(sec -> sec.getWrapper().getOrderIndex().getValue())\n                .filter(value -> value != null && value != Integer.MIN_VALUE && value != Integer.MAX_VALUE)\n                .mapToInt(value -> value)\n                .max()\n                .orElse(0);\n        if (isSingle && max == orderIndex.getValue()) {\n            return;\n        }\n\n        orderWithIndex(max + 1);\n    }\n\n    public void orderFirst() {\n        var section = StoreViewState.get().getParentSectionForWrapper(this);\n        if (section.isEmpty()) {\n            return;\n        }\n\n        var isSingle = section.get().getAllChildren().getList().stream()\n                        .filter(sec -> sec.getWrapper().getOrderIndex().get() == orderIndex.getValue())\n                        .count()\n                == 1;\n        var min = section.get().getAllChildren().getList().stream()\n                .map(sec -> sec.getWrapper().getOrderIndex().getValue())\n                .filter(value -> value != null && value != Integer.MIN_VALUE && value != Integer.MAX_VALUE)\n                .mapToInt(value -> value)\n                .min()\n                .orElse(0);\n        if (isSingle && min == orderIndex.getValue()) {\n            return;\n        }\n\n        orderWithIndex(min - 1);\n    }\n\n    public void orderStickFirst() {\n        orderWithIndex(Integer.MIN_VALUE);\n    }\n\n    public void orderStickLast() {\n        orderWithIndex(Integer.MAX_VALUE);\n    }\n\n    public void toggleExpanded() {\n        this.expanded.set(!expanded.getValue());\n    }\n\n    public void togglePinToTop() {\n        if (getEntry().isPinToTop()) {\n            getEntry().setPinToTop(false);\n            StoreViewState.get().triggerStoreListUpdate();\n        } else {\n            var root = StoreViewState.get().getCurrentTopLevelSection().getAllChildren().getList().stream()\n                    .filter(storeSection -> storeSection.anyMatches(storeEntryWrapper -> storeEntryWrapper == this))\n                    .findFirst();\n            var sortMode = StoreSectionSortMode.DATE_DESC;\n            var date = root.isPresent()\n                    ? sortMode.date(sortMode.getRepresentative(root.get())).plus(Duration.ofSeconds(1))\n                    : Instant.now();\n            getEntry().setPinToTop(!getEntry().isPinToTop());\n            StoreViewState.get().triggerStoreListUpdate();\n            getEntry().setLastUsed(date);\n            getEntry().setLastModified(date);\n            StoreViewState.get().triggerStoreListUpdate();\n        }\n    }\n\n    public boolean matchesFilter(String filter) {\n        if (filter == null || name.getValue().toLowerCase().contains(filter.toLowerCase())) {\n            return true;\n        }\n\n        if (getEntry().getUuid().toString().equalsIgnoreCase(filter)) {\n            return true;\n        }\n\n        if (entry.getValidity().isUsable()\n                && entry.getProvider().getSearchableTerms(entry.getStore()).stream()\n                        .anyMatch(s -> s.toLowerCase().contains(filter.toLowerCase()))) {\n            return true;\n        }\n\n        var is = information.getValue();\n        if (is != null && is.toLowerCase().contains(filter.toLowerCase())) {\n            return true;\n        }\n\n        var ss = summary.getValue();\n        if (ss != null && ss.toLowerCase().contains(filter.toLowerCase())) {\n            return true;\n        }\n\n        if (tags.stream().anyMatch(s -> s.toLowerCase().contains(filter.toLowerCase()))) {\n            return true;\n        }\n\n        return false;\n    }\n\n    public Property<String> nameProperty() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreIconChoiceComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppImages;\nimport io.xpipe.app.icon.SystemIcon;\nimport io.xpipe.app.icon.SystemIconManager;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.util.BooleanScope;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.*;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.*;\nimport javafx.scene.input.MouseButton;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\nimport javafx.scene.text.TextAlignment;\n\nimport atlantafx.base.theme.Styles;\nimport atlantafx.base.theme.Tweaks;\nimport lombok.Getter;\n\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport static atlantafx.base.theme.Styles.TEXT_SMALL;\n\npublic class StoreIconChoiceComp extends ModalOverlayContentComp {\n\n    private final Property<SystemIcon> selected;\n    private final Set<SystemIcon> icons;\n    private final int columns;\n    private final SimpleStringProperty filter;\n    private final Runnable doubleClick;\n    private final DataStoreEntry entry;\n\n    @Getter\n    private final BooleanProperty busy = new SimpleBooleanProperty();\n\n    public StoreIconChoiceComp(\n            Property<SystemIcon> selected,\n            Set<SystemIcon> icons,\n            int columns,\n            SimpleStringProperty filter,\n            Runnable doubleClick, DataStoreEntry entry\n    ) {\n        this.selected = selected;\n        this.icons = icons;\n        this.columns = columns;\n        this.filter = filter;\n        this.doubleClick = doubleClick;\n        this.entry = entry;\n    }\n\n    @Override\n    protected void setModalOverlay(ModalOverlay modalOverlay) {\n        super.setModalOverlay(modalOverlay);\n        if (modalOverlay != null) {\n            ThreadHelper.runFailableAsync(() -> {\n                BooleanScope.executeExclusive(busy, () -> {\n                    SystemIconManager.loadAllAvailableIconImages();\n                });\n            });\n        }\n    }\n\n    @Override\n    protected Region createSimple() {\n        var table = new TableView<List<SystemIcon>>();\n        table.visibleProperty().bind(PlatformThread.sync(busy.not()));\n        initTable(table);\n        filter.addListener((observable, oldValue, newValue) -> updateData(table, newValue));\n        busy.addListener((observable, oldValue, newValue) -> {\n            if (oldValue && !newValue) {\n                updateData(table, filter.getValue());\n            }\n        });\n\n        var loading = createLoadingPane();\n        var stack = new StackPane();\n        stack.getChildren().addAll(table, loading);\n\n        return stack;\n    }\n\n    public void refresh() {\n        ThreadHelper.runFailableAsync(() -> {\n            BooleanScope.executeExclusive(busy, () -> {\n                SystemIconManager.rebuild();\n            });\n        });\n    }\n\n    private void initTable(TableView<List<SystemIcon>> table) {\n        for (int i = 0; i < columns; i++) {\n            var col = new TableColumn<List<SystemIcon>, SystemIcon>(\"col\" + i);\n            final int colIndex = i;\n            col.setCellValueFactory(cb -> {\n                var row = cb.getValue();\n                var item = row.size() > colIndex ? row.get(colIndex) : null;\n                return new SimpleObjectProperty<>(item);\n            });\n            col.setCellFactory(cb -> new IconCell());\n            col.getStyleClass().add(Tweaks.ALIGN_CENTER);\n            table.getColumns().add(col);\n        }\n\n        table.setPlaceholder(new Region());\n        table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS);\n        table.getSelectionModel().setCellSelectionEnabled(true);\n        table.getStyleClass().add(\"icon-browser\");\n        table.disableProperty().bind(PlatformThread.sync(busy));\n    }\n\n    private Region createLoadingPane() {\n        var refreshButton = new ButtonComp(\n                AppI18n.observable(\"refreshIcons\"),\n                new SimpleObjectProperty<>(new LabelGraphic.IconGraphic(\"mdi2r-refresh\")),\n                () -> {\n                    ThreadHelper.runFailableAsync(() -> {\n                        BooleanScope.executeExclusive(busy, () -> {\n                            SystemIconManager.rebuild();\n                        });\n                    });\n                });\n        refreshButton.hide(Bindings.createBooleanBinding(\n                () -> {\n                    return SystemIconManager.hasLoadedAnyImages();\n                },\n                busy));\n        refreshButton.disable(busy);\n\n        var text = new LabelComp(AppI18n.observable(\"refreshIconsDescription\"));\n        text.apply(struc -> {\n            struc.setWrapText(true);\n            struc.setTextAlignment(TextAlignment.CENTER);\n            struc.setPrefWidth(300);\n        });\n        text.style(Styles.TEXT_SUBTLE);\n        text.visible(busy);\n\n        var loading = new LoadingIconComp(busy, AppFontSizes::title);\n        loading.prefWidth(50);\n        loading.prefHeight(50);\n\n        var vbox = new VerticalComp(List.of(text, loading, refreshButton)).spacing(25);\n        vbox.apply(struc -> {\n            struc.setAlignment(Pos.CENTER);\n            struc.setPickOnBounds(false);\n        });\n        return vbox.build();\n    }\n\n    private void updateData(TableView<List<SystemIcon>> table, String filterString) {\n        if (SystemIconManager.isCacheOutdated()) {\n            table.getItems().clear();\n            return;\n        }\n\n        var available = icons.stream()\n                .filter(systemIcon -> AppImages.hasImage(\n                        \"icons/\" + systemIcon.getSource().getId() + \"/\" + systemIcon.getId() + \"-40.png\"))\n                .sorted(Comparator.comparing(systemIcon -> systemIcon.getId()))\n                .collect(Collectors.toCollection(ArrayList::new));\n        available.addFirst(new SystemIcon(null, \"default\"));\n\n        List<SystemIcon> shown;\n        if (filterString != null && !filterString.isBlank() && filterString.strip().length() >= 2) {\n            shown = available.stream()\n                    .filter(icon -> containsString(icon.getId(), filterString.strip()))\n                    .collect(Collectors.toCollection(ArrayList::new));\n        } else {\n            shown = new ArrayList<>(available);\n        }\n\n        var data = partitionList(shown, columns);\n        table.getItems().setAll(data);\n\n        var selectMatch = shown.size() == 1\n                || shown.stream().anyMatch(systemIcon -> systemIcon.getId().equals(filterString));\n        // Table updates seem to not always be instant, sometimes the column is not there yet\n        if (selectMatch && table.getColumns().size() > 0) {\n            table.getSelectionModel().select(0, table.getColumns().getFirst());\n            selected.setValue(shown.getFirst());\n        }\n    }\n\n    private <T> List<List<T>> partitionList(List<T> list, int size) {\n        List<List<T>> partitions = new ArrayList<>();\n        if (list.size() == 0) {\n            return partitions;\n        }\n\n        int length = list.size();\n        int numOfPartitions = length / size + ((length % size == 0) ? 0 : 1);\n\n        for (int i = 0; i < numOfPartitions; i++) {\n            int from = i * size;\n            int to = Math.min((i * size + size), length);\n            partitions.add(list.subList(from, to));\n        }\n        return partitions;\n    }\n\n    private boolean containsString(String s1, String s2) {\n        return s1.toLowerCase(Locale.ROOT).contains(s2.toLowerCase(Locale.ROOT));\n    }\n\n    public class IconCell extends TableCell<List<SystemIcon>, SystemIcon> {\n\n        private final Label root = new Label();\n        private final StringProperty image = new SimpleStringProperty();\n\n        public IconCell() {\n            super();\n\n            root.setContentDisplay(ContentDisplay.TOP);\n            Region imageView = PrettyImageHelper.ofFixedSize(image, 40, 40).build();\n            root.setGraphic(imageView);\n            root.setGraphicTextGap(10);\n            root.getStyleClass().addAll(\"icon-label\", TEXT_SMALL);\n\n            setOnMouseClicked(event -> {\n                if (event.getButton() == MouseButton.PRIMARY) {\n                    selected.setValue(getItem());\n                }\n\n                if (event.getClickCount() > 1) {\n                    doubleClick.run();\n                }\n            });\n        }\n\n        @Override\n        protected void updateItem(SystemIcon icon, boolean empty) {\n            super.updateItem(icon, empty);\n\n            if (icon == null) {\n                setGraphic(null);\n                return;\n            }\n\n            if (icon.getSource() == null) {\n                root.setText(AppI18n.get(\"default\"));\n                image.setValue(entry.getProvider().getDisplayIconFileName(entry.getStore()));\n                setGraphic(root);\n                return;\n            }\n\n            root.setText(icon.getId());\n            image.set(SystemIconManager.getAndLoadIconFile(icon));\n            setGraphic(root);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreIconChoiceDialog.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.icon.SystemIcon;\nimport io.xpipe.app.icon.SystemIconManager;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.storage.DataStoreEntry;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.ObjectProperty;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.property.SimpleStringProperty;\n\nimport lombok.Getter;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\npublic class StoreIconChoiceDialog {\n\n    private final ObjectProperty<SystemIcon> selected = new SimpleObjectProperty<>();\n    private final DataStoreEntry entry;\n\n    @Getter\n    private final ModalOverlay overlay;\n\n    public StoreIconChoiceDialog(DataStoreEntry entry) {\n        this.entry = entry;\n        this.overlay = createOverlay();\n    }\n\n    public static void show(DataStoreEntry entry) {\n        var dialog = new StoreIconChoiceDialog(entry);\n        dialog.getOverlay().show();\n    }\n\n    private ModalOverlay createOverlay() {\n        var filterText = new SimpleStringProperty();\n        var filter = new FilterComp(filterText).hgrow();\n        // Ugly solution to focus the filter on show\n        filter.apply(r -> {\n            r.sceneProperty().subscribe(s -> {\n                if (s != null) {\n                    Platform.runLater(() -> {\n                        Platform.runLater(() -> {\n                            r.requestFocus();\n                        });\n                    });\n                }\n            });\n        });\n\n        var comp = new StoreIconChoiceComp(\n                selected,\n                SystemIconManager.getIcons(),\n                5,\n                filterText,\n                () -> {\n                    finish();\n                },\n                entry);\n        comp.prefWidth(600);\n\n        var modal = ModalOverlay.of(\n                \"chooseCustomIcon\",\n                comp);\n        var refresh = new ButtonComp(null, new FontIcon(\"mdi2r-refresh\"), () -> {\n            comp.refresh();\n        }).maxHeight(100).disable(comp.getBusy());\n        var settings = new ButtonComp(null, new FontIcon(\"mdomz-settings\"), () -> {\n                    overlay.close();\n                    AppPrefs.get().selectCategory(\"icons\");\n                })\n                .disable(comp.getBusy())\n                .maxHeight(100);\n        modal.addButtonBarComp(settings);\n        modal.addButtonBarComp(refresh);\n        modal.addButtonBarComp(filter);\n        modal.addButton(ModalButton.ok(() -> {\n                    finish();\n                }))\n                .augment(button -> button.disableProperty().bind(Bindings.createBooleanBinding(() -> {\n                    return selected.get() == null || comp.getBusy().get();\n                }, selected, PlatformThread.sync(comp.getBusy()))));\n        return modal;\n    }\n\n    private void finish() {\n        entry.setIcon(\n                selected.get() != null && selected.getValue().getSource() != null\n                        ? selected.getValue().getSource().getId() + \"/\"\n                                + selected.getValue().getId()\n                        : null,\n                true);\n        overlay.close();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreIconComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.PrettyImageHelper;\nimport io.xpipe.app.comp.base.TooltipHelper;\nimport io.xpipe.app.storage.DataStoreEntry;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.Tooltip;\nimport javafx.scene.input.*;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\n\nimport lombok.AllArgsConstructor;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\n@AllArgsConstructor\npublic class StoreIconComp extends SimpleRegionBuilder {\n\n    private final StoreEntryWrapper wrapper;\n    private final int w;\n    private final int h;\n\n    @Override\n    protected Region createSimple() {\n        var imageComp = PrettyImageHelper.ofFixedSize(wrapper.getIconFile(), w, h);\n        var storeIcon = imageComp.build();\n        if (wrapper.getValidity().getValue().isUsable()) {\n            Tooltip.install(\n                    storeIcon,\n                    TooltipHelper.create(wrapper.getEntry().getProvider().displayName()));\n        }\n\n        var background = new Region();\n        background.getStyleClass().add(\"background\");\n\n        var dots = new FontIcon(\"mdi2d-dots-horizontal\");\n        dots.setIconSize((int) (h * 1.3));\n\n        var stack = new StackPane(background, storeIcon, dots);\n        stack.setMinHeight(w + 5);\n        stack.setMinWidth(w + 5);\n        stack.setMaxHeight(w + 5);\n        stack.setMaxWidth(w + 5);\n        stack.getStyleClass().add(\"icon\");\n        stack.setAlignment(Pos.CENTER);\n\n        dots.visibleProperty().bind(stack.hoverProperty());\n        storeIcon\n                .opacityProperty()\n                .bind(Bindings.createDoubleBinding(\n                        () -> {\n                            return stack.isHover() ? 0.5 : 1.0;\n                        },\n                        stack.hoverProperty()));\n\n        stack.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {\n            if (event.getButton() == MouseButton.PRIMARY) {\n                if (wrapper.getValidity().getValue() == DataStoreEntry.Validity.LOAD_FAILED) {\n                    return;\n                }\n\n                StoreIconChoiceDialog.show(wrapper.getEntry());\n                event.consume();\n            }\n        });\n\n        return stack;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreIdentitiesIntroComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.IntroComp;\nimport io.xpipe.app.comp.base.IntroListComp;\nimport io.xpipe.app.ext.DataStoreCreationCategory;\nimport io.xpipe.app.ext.DataStoreProviders;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.storage.DataStorage;\n\nimport javafx.scene.layout.Region;\n\nimport java.util.List;\n\npublic class StoreIdentitiesIntroComp extends SimpleRegionBuilder {\n\n    @Override\n    public Region createSimple() {\n        var top = new IntroComp(\"identitiesIntro\", new LabelGraphic.IconGraphic(\"mdi2a-account-group\"));\n        top.setButtonDefault(true);\n        top.setButtonGraphic(new LabelGraphic.IconGraphic(\"mdi2p-play-circle\"));\n        top.setButtonAction(() -> {\n            var canSync = DataStorage.get().supportsSync();\n            var prov = canSync\n                    ? DataStoreProviders.byId(\"syncedIdentity\").orElseThrow()\n                    : DataStoreProviders.byId(\"localIdentity\").orElseThrow();\n            StoreCreationDialog.showCreation(prov, DataStoreCreationCategory.IDENTITY);\n        });\n\n        var bottom = new IntroComp(\"identitiesIntroBottom\", new LabelGraphic.IconGraphic(\"mdi2g-git\"));\n        bottom.setButtonGraphic(new LabelGraphic.IconGraphic(\"mdi2p-play-circle\"));\n        bottom.setButtonAction(() -> {\n            AppPrefs.get().selectCategory(\"vaultSync\");\n        });\n\n        var list = new IntroListComp(List.of(top, bottom));\n        return list.build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreIntroComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.IntroComp;\nimport io.xpipe.app.comp.base.IntroListComp;\nimport io.xpipe.app.comp.base.PrettyImageHelper;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.util.ScanDialog;\n\nimport javafx.scene.layout.Region;\n\nimport java.util.List;\n\npublic class StoreIntroComp extends SimpleRegionBuilder {\n\n    @Override\n    public Region createSimple() {\n        var hub = new IntroComp(\"storeIntro\", new LabelGraphic.NodeGraphic(() -> PrettyImageHelper.ofSpecificFixedSize(\n                        \"welcome/wave.svg\", 80, 144)\n                .build()));\n        hub.setButtonAction(() -> {\n            ScanDialog.showSingleAsync(DataStorage.get().local());\n        });\n        hub.setButtonGraphic(new LabelGraphic.IconGraphic(\"mdi2m-magnify\"));\n        hub.setButtonDefault(true);\n\n        var sync = new IntroComp(\"storeIntroImport\", new LabelGraphic.IconGraphic(\"mdi2g-git\"));\n        sync.setButtonGraphic(new LabelGraphic.IconGraphic(\"mdi2g-git\"));\n        sync.setButtonAction(() -> {\n            AppPrefs.get().selectCategory(\"vaultSync\");\n        });\n\n        var list = new IntroListComp(List.of(hub, sync));\n        return list.build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreLayoutComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.DelayedInitComp;\nimport io.xpipe.app.comp.base.LeftSplitPaneComp;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.core.window.AppMainWindow;\nimport io.xpipe.app.platform.InputHelper;\nimport io.xpipe.app.terminal.TerminalDockHubComp;\nimport io.xpipe.app.terminal.TerminalDockHubManager;\nimport io.xpipe.app.util.ObservableSubscriber;\n\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyCombination;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\n\npublic class StoreLayoutComp extends SimpleRegionBuilder {\n\n    @Override\n    protected Region createSimple() {\n        var delayed = new DelayedInitComp(\n                RegionBuilder.of(() -> createContent()),\n                () -> StoreViewState.get() != null && StoreViewState.get().isInitialized());\n        return delayed.build();\n    }\n\n    private Region createContent() {\n        var filterTrigger = new ObservableSubscriber();\n        var left = new StoreSidebarComp(filterTrigger);\n        left.hide(AppMainWindow.get().getStage().widthProperty().lessThan(1000));\n        left.minWidth(270);\n        left.maxWidth(500);\n        left.minHeight(0);\n        var comp = new LeftSplitPaneComp(left, new StoreEntryListComp())\n                .withInitialWidth(AppLayoutModel.get().getSavedState().getSidebarWidth())\n                .withOnDividerChange(aDouble -> {\n                    if (aDouble == 0.0) {\n                        return;\n                    }\n\n                    AppLayoutModel.get().getSavedState().setSidebarWidth(aDouble);\n                });\n        comp.apply(struc -> {\n            InputHelper.onKeyCombination(\n                    struc, new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN), false, keyEvent -> {\n                        filterTrigger.trigger();\n                        keyEvent.consume();\n                    });\n        });\n\n        var stack = new StackPane(comp.build());\n        stack.getStyleClass().add(\"store-layout\");\n\n        if (TerminalDockHubManager.isAvailable()) {\n            var model = TerminalDockHubManager.get();\n            var dock = new TerminalDockHubComp(model.getDockModel());\n            stack.getChildren().add(dock.build());\n        }\n\n        return stack;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreListChoiceComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.ListProperty;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.geometry.Insets;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.VBox;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Predicate;\n\npublic class StoreListChoiceComp<T extends DataStore> extends SimpleRegionBuilder {\n\n    private final ListProperty<DataStoreEntryRef<T>> selectedList;\n    private final Class<T> storeClass;\n    private final Predicate<DataStoreEntryRef<T>> applicableCheck;\n    private final StoreCategoryWrapper initialCategory;\n    private boolean editable;\n\n    public StoreListChoiceComp(\n            ListProperty<DataStoreEntryRef<T>> selectedList,\n            Class<T> storeClass,\n            Predicate<DataStoreEntryRef<T>> applicableCheck,\n            StoreCategoryWrapper initialCategory) {\n        this.selectedList = selectedList;\n        this.storeClass = storeClass;\n        this.applicableCheck = applicableCheck;\n        this.initialCategory = initialCategory;\n        this.editable = true;\n    }\n\n    public StoreListChoiceComp<T> setEditable(boolean editable) {\n        this.editable = editable;\n        return this;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var listBox = new ListBoxViewComp<>(\n                        selectedList,\n                        selectedList,\n                        t -> {\n                            if (t == null) {\n                                return null;\n                            }\n\n                            var label = new LabelComp(t.get().getName()).apply(struc -> {\n                                struc.setGraphic(PrettyImageHelper.ofFixedSizeSquare(\n                                                t.get().getEffectiveIconFile(), 16)\n                                        .build());\n                                struc.setGraphicTextGap(8);\n                            });\n\n                            var up = new IconButtonComp(\"mdi2a-arrow-up\", () -> {\n                                var index = selectedList.get().indexOf(t);\n                                if (index != -1) {\n                                    var prior = Math.max(index - 1, 0);\n                                    selectedList.get().remove(index);\n                                    selectedList.get().add(prior, t);\n                                }\n                            });\n                            up.disable(Bindings.createBooleanBinding(\n                                    () -> {\n                                        return selectedList.get().indexOf(t) == 0;\n                                    },\n                                    selectedList));\n\n                            var down = new IconButtonComp(\"mdi2a-arrow-down\", () -> {\n                                var index = selectedList.get().indexOf(t);\n                                if (index != -1) {\n                                    var next = Math.min(index + 1, selectedList.size() - 1);\n                                    selectedList.get().remove(index);\n                                    selectedList.get().add(next, t);\n                                }\n                            });\n                            down.disable(Bindings.createBooleanBinding(\n                                    () -> {\n                                        return selectedList.get().indexOf(t) == selectedList.size() - 1;\n                                    },\n                                    selectedList));\n\n                            var delete = new IconButtonComp(\"mdal-delete_outline\", () -> {\n                                selectedList.remove(t);\n                            });\n                            var row = editable\n                                    ? new HorizontalComp(List.of(label, RegionBuilder.hspacer(), up, down, delete))\n                                            .spacing(5)\n                                    : new HorizontalComp(List.of(label, RegionBuilder.hspacer()));\n                            return row.style(\"entry\");\n                        },\n                        false)\n                .padding(new Insets(0))\n                .apply(struc -> struc.setMinHeight(0))\n                .apply(struc -> ((VBox) struc.getContent()).setSpacing(5));\n        var selected = new SimpleObjectProperty<DataStoreEntryRef<T>>();\n        var add = new StoreChoiceComp<>(null, selected, storeClass, applicableCheck, initialCategory);\n        selected.addListener((observable, oldValue, newValue) -> {\n            if (newValue != null) {\n                if (!selectedList.contains(newValue) && (applicableCheck == null || applicableCheck.test(newValue))) {\n                    selectedList.add(newValue);\n                }\n                selected.setValue(null);\n            }\n        });\n        var list = new ArrayList<BaseRegionBuilder<?, ?>>();\n        list.add(listBox);\n        if (editable) {\n            list.add(RegionBuilder.vspacer(5).hide(Bindings.isEmpty(selectedList)));\n            list.add(add);\n        }\n        var vbox = new VerticalComp(list).apply(struc -> struc.setFillWidth(true));\n        return vbox.style(\"data-store-list-choice-comp\").build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreNotFoundComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\n\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\n\npublic class StoreNotFoundComp extends SimpleRegionBuilder {\n\n    @Override\n    public Region createSimple() {\n        return new StackPane();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreNotesComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.*;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.storage.DataStorage;\n\nimport io.xpipe.app.util.FileOpener;\nimport javafx.application.Platform;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.ReadOnlyStringWrapper;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.scene.control.Button;\nimport javafx.scene.input.MouseButton;\n\nimport java.util.UUID;\n\npublic class StoreNotesComp extends RegionBuilder<Button> {\n\n    public static void showDialog(StoreEntryWrapper wrapper, String initial) {\n        var prop = new SimpleStringProperty(initial);\n        var md = new MarkdownEditorComp(prop, \"notes-\" + wrapper.getName().getValue())\n                .prefWidth(700)\n                .prefHeight(800);\n\n        var modal = ModalOverlay.of(new ReadOnlyStringWrapper(wrapper.getName().getValue()), md, null);\n        if (wrapper.getNotes().getValue() != null) {\n            modal.addButton(new ModalButton(\"delete\", () -> {\n                wrapper.getEntry().setNotes(null);\n                DataStorage.get().saveAsync();\n            }, true, false));\n        }\n        modal.addButton(new ModalButton(\"cancel\", () -> {}, true, false));\n        modal.addButton(new ModalButton(\"apply\", () -> {\n            wrapper.getEntry().setNotes(prop.getValue());\n            DataStorage.get().saveAsync();\n        }, true, true));\n        modal.show();\n    }\n\n    private final StoreEntryWrapper wrapper;\n\n    public StoreNotesComp(StoreEntryWrapper wrapper) {\n        this.wrapper = wrapper;\n    }\n\n    @Override\n    protected Button createSimple() {\n        var n = wrapper.getNotes();\n        var button = new IconButtonComp(\"mdi2n-note-text-outline\")\n                .apply(struc -> AppFontSizes.xs(struc))\n                .describe(d ->\n                        d.nameKey(\"notes\").focusTraversal(RegionDescriptor.FocusTraversal.ENABLED_FOR_ACCESSIBILITY))\n                .style(\"notes-button\")\n                .hide(n.isNull())\n                .build();\n        button.setOpacity(0.85);\n        button.prefWidthProperty().bind(button.heightProperty());\n\n        button.setOnAction(e -> {\n            showDialog(wrapper, wrapper.getNotes().getValue());\n            e.consume();\n        });\n\n        var editKey = UUID.randomUUID().toString();\n        button.setOnMouseClicked(e -> {\n            if (e.getButton() == MouseButton.PRIMARY && e.isShiftDown()) {\n                FileOpener.openString(\"notes.md\", editKey, wrapper.getNotes().getValue(), s -> wrapper.getEntry().setNotes(s));\n                e.consume();\n            }\n        });\n        return button;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreOrderIndexDialog.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.platform.OptionsBuilder;\n\nimport javafx.beans.property.SimpleObjectProperty;\n\npublic class StoreOrderIndexDialog {\n\n    public static void show(StoreEntryWrapper wrapper) {\n        var entry = wrapper.getEntry();\n        var prop = new SimpleObjectProperty<>(\n                entry.getOrderIndex() != 0\n                                && entry.getOrderIndex() != Integer.MIN_VALUE\n                                && entry.getOrderIndex() != Integer.MAX_VALUE\n                        ? entry.getOrderIndex()\n                        : null);\n        var options = new OptionsBuilder()\n                .nameAndDescription(\"orderIndex\")\n                .addComp(new IntFieldComp(prop, Integer.MIN_VALUE, Integer.MAX_VALUE))\n                .buildComp()\n                .prefWidth(400);\n        var modal = ModalOverlay.of(\"changeOrderIndexTitle\", options);\n        modal.withDefaultButtons(() -> {\n            if (prop.getValue() == null) {\n                return;\n            }\n\n            wrapper.orderWithIndex(prop.getValue());\n        });\n        modal.show();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreProviderChoiceComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.*;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.DataStoreProvider;\nimport io.xpipe.app.ext.DataStoreProviders;\nimport io.xpipe.app.platform.JfxHelper;\nimport io.xpipe.app.platform.MenuHelper;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\nimport javafx.scene.control.ComboBox;\nimport javafx.scene.control.ListCell;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.layout.Region;\n\nimport lombok.AccessLevel;\nimport lombok.AllArgsConstructor;\nimport lombok.experimental.FieldDefaults;\n\nimport java.util.List;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\n\n@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)\n@AllArgsConstructor\npublic class StoreProviderChoiceComp extends RegionBuilder<ComboBox<DataStoreProvider>> {\n\n    Predicate<DataStoreProvider> filter;\n    Property<DataStoreProvider> provider;\n\n    public List<DataStoreProvider> getProviders() {\n        return DataStoreProviders.getAll().stream()\n                .filter(val -> filter == null || filter.test(val))\n                .toList();\n    }\n\n    private Region createGraphic(DataStoreProvider provider) {\n        if (provider == null) {\n            return null;\n        }\n\n        var graphic = provider.getDisplayIconFileName(null);\n        return JfxHelper.createNamedEntry(provider.displayName(), provider.displayDescription(), graphic);\n    }\n\n    @Override\n    public ComboBox<DataStoreProvider> createSimple() {\n        Supplier<ListCell<DataStoreProvider>> cellFactory = () -> new ListCell<>() {\n            @Override\n            protected void updateItem(DataStoreProvider item, boolean empty) {\n                super.updateItem(item, empty);\n                setGraphic(createGraphic(item));\n                if (item != null) {\n                    accessibleTextProperty().bind(item.displayName());\n                    accessibleHelpProperty().bind(item.displayDescription());\n                } else {\n                    accessibleTextProperty().unbind();\n                    accessibleHelpProperty().unbind();\n                }\n            }\n        };\n        var cb = MenuHelper.<DataStoreProvider>createComboBox();\n        cb.setCellFactory(param -> {\n            return cellFactory.get();\n        });\n        cb.setButtonCell(cellFactory.get());\n        var l = getProviders();\n        l.forEach(dataStoreProvider -> cb.getItems().add(dataStoreProvider));\n        if (provider.getValue() == null) {\n            provider.setValue(l.getFirst());\n        }\n        cb.setValue(provider.getValue());\n        provider.bind(cb.valueProperty());\n        cb.getStyleClass().add(\"choice-comp\");\n        RegionDescriptor.builder()\n                .nameKey(\"chooseConnectionType\")\n                .description(Bindings.createStringBinding(\n                        () -> {\n                            return provider.getValue() != null\n                                    ? provider.getValue().displayName().getValue()\n                                    : null;\n                        },\n                        provider,\n                        AppI18n.activeLanguage()))\n                .build()\n                .apply(cb);\n        cb.setOnKeyPressed(event -> {\n            if (!event.getCode().equals(KeyCode.ENTER)) {\n                return;\n            }\n\n            cb.show();\n            event.consume();\n        });\n        return cb;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreQuickAccessButtonComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.IconButtonComp;\nimport io.xpipe.app.comp.base.PrettyImageHelper;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.MenuHelper;\nimport io.xpipe.app.prefs.AppPrefs;\n\nimport javafx.geometry.Side;\nimport javafx.scene.control.Button;\nimport javafx.scene.control.ContextMenu;\nimport javafx.scene.control.Menu;\nimport javafx.scene.control.MenuItem;\n\nimport java.util.ArrayList;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.function.Consumer;\n\npublic class StoreQuickAccessButtonComp extends RegionBuilder<Button> {\n\n    private final StoreSection section;\n    private final Consumer<StoreSection> action;\n\n    public StoreQuickAccessButtonComp(StoreSection section, Consumer<StoreSection> action) {\n        this.section = section;\n        this.action = action;\n    }\n\n    private ContextMenu createMenu() {\n        if (section.getShownChildren().getList().isEmpty()) {\n            return null;\n        }\n\n        var cm = MenuHelper.createContextMenu();\n        cm.getStyleClass().add(\"condensed\");\n        Menu menu = (Menu) recurse(cm, section);\n        cm.getItems().addAll(menu.getItems());\n        return cm;\n    }\n\n    private MenuItem recurse(ContextMenu contextMenu, StoreSection section) {\n        var c = section.getShownChildren();\n        var w = section.getWrapper();\n        var graphic = w.getEntry().getEffectiveIconFile();\n        if (c.getList().isEmpty()) {\n            var item = new MenuItem(\n                    w.getName().getValue(), new LabelGraphic.ImageGraphic(graphic, 16).createGraphicNode());\n            item.setOnAction(event -> {\n                action.accept(section);\n                contextMenu.hide();\n                event.consume();\n            });\n            return item;\n        }\n\n        var items = new ArrayList<MenuItem>();\n        for (StoreSection sub : c.getList()) {\n            if (!sub.getWrapper().getValidity().getValue().isUsable()) {\n                continue;\n            }\n\n            items.add(recurse(contextMenu, sub));\n        }\n        var m = new Menu(\n                w.getName().getValue(),\n                PrettyImageHelper.ofFixedSizeSquare(graphic, 16).build());\n        m.getItems().setAll(items);\n        if (!AppPrefs.get().limitedTouchscreenMode().get()) {\n            m.setOnAction(event -> {\n                if (event.getTarget() == m) {\n                    if (m.getItems().isEmpty()) {\n                        return;\n                    }\n\n                    action.accept(section);\n                    contextMenu.hide();\n                    event.consume();\n                }\n            });\n        }\n        return m;\n    }\n\n    @Override\n    public Button createSimple() {\n        var button = new IconButtonComp(\"mdi2c-chevron-double-right\");\n        button.apply(struc -> {\n            AtomicReference<ContextMenu> menu = new AtomicReference<>();\n            struc.setOnAction(event -> {\n                if (menu.get() == null) {\n                    menu.set(createMenu());\n                }\n\n                if (menu.get() == null) {\n                    return;\n                }\n\n                MenuHelper.toggleMenuShow(menu.get(), struc, Side.RIGHT);\n                event.consume();\n            });\n        });\n        return button.build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreScriptSourcesIntroComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.IntroComp;\nimport io.xpipe.app.comp.base.IntroListComp;\nimport io.xpipe.app.ext.DataStoreCreationCategory;\nimport io.xpipe.app.ext.DataStoreProviders;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport javafx.scene.layout.Region;\n\nimport java.util.List;\n\npublic class StoreScriptSourcesIntroComp extends SimpleRegionBuilder {\n\n    @Override\n    public Region createSimple() {\n        var intro = new IntroComp(\"scriptSourcesIntro\", new LabelGraphic.IconGraphic(\"mdi2d-download\"));\n        intro.setButtonGraphic(new LabelGraphic.IconGraphic(\"mdi2p-play-circle\"));\n        intro.setButtonDefault(true);\n        intro.setButtonAction(() -> {\n            StoreCreationDialog.showCreation(\n                    DataStoreProviders.byId(\"scriptCollectionSource\").orElseThrow(), DataStoreCreationCategory.SCRIPT);\n        });\n        var list = new IntroListComp(List.of(intro));\n        return list.build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreScriptsIntroComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.IntroComp;\nimport io.xpipe.app.comp.base.IntroListComp;\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.platform.LabelGraphic;\n\nimport javafx.beans.property.BooleanProperty;\nimport javafx.scene.layout.Region;\n\nimport java.util.List;\n\npublic class StoreScriptsIntroComp extends SimpleRegionBuilder {\n\n    private final BooleanProperty show;\n\n    public StoreScriptsIntroComp(BooleanProperty show) {\n        this.show = show;\n    }\n\n    @Override\n    public Region createSimple() {\n        var top = new IntroComp(\"scriptsIntro\", new LabelGraphic.IconGraphic(\"mdi2s-script-text\"));\n\n        var bottom = new IntroComp(\"scriptsIntroBottom\", new LabelGraphic.IconGraphic(\"mdi2t-tooltip-edit\"));\n        bottom.setButtonGraphic(new LabelGraphic.IconGraphic(\"mdi2p-play-circle\"));\n        bottom.setButtonDefault(true);\n        bottom.setButtonAction(() -> {\n            AppCache.update(\"scriptsIntroCompleted\", true);\n            show.set(false);\n        });\n\n        var list = new IntroListComp(List.of(top, bottom));\n        return list.build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreSection.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.platform.DerivedObservableList;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.value.ObservableBooleanValue;\nimport javafx.beans.value.ObservableIntegerValue;\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.Getter;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.function.Predicate;\n\n@Getter\npublic class StoreSection {\n\n    private final StoreEntryWrapper wrapper;\n    private final DerivedObservableList<StoreSection> allChildren;\n    private final DerivedObservableList<StoreSection> shownChildren;\n    private final int depth;\n    private final ObservableBooleanValue showDetails;\n\n    public StoreSection(\n            StoreEntryWrapper wrapper,\n            DerivedObservableList<StoreSection> allChildren,\n            DerivedObservableList<StoreSection> shownChildren,\n            int depth) {\n        this.wrapper = wrapper;\n        this.allChildren = allChildren;\n        this.shownChildren = shownChildren;\n        this.depth = depth;\n        if (wrapper != null) {\n            this.showDetails = Bindings.createBooleanBinding(\n                    () -> {\n                        return wrapper.getExpanded().get()\n                                || allChildren.getList().isEmpty();\n                    },\n                    wrapper.getExpanded(),\n                    allChildren.getList());\n        } else {\n            this.showDetails = new SimpleBooleanProperty(true);\n        }\n    }\n\n    public static BaseRegionBuilder<?, ?> customSection(StoreSection e) {\n        return new StoreSectionComp(e);\n    }\n\n    private static DerivedObservableList<StoreSection> sorted(\n            StoreEntryWrapper wrapper,\n            DerivedObservableList<StoreSection> list,\n            ObservableIntegerValue updateObservable) {\n        var sortMode = StoreViewState.get()\n                .createEffectiveSortMode(\n                        wrapper != null ? wrapper.getEntry().getProvider().getComparator() : null);\n        return list.sorted(\n                (o1, o2) -> {\n                    var r = sortMode.getValue().compare(o1, o2);\n                    if (r != 0) {\n                        return r;\n                    }\n\n                    var current = sortMode.getValue();\n                    if (current != null) {\n                        return current.compare(o1, o2);\n                    } else {\n                        return 0;\n                    }\n                },\n                sortMode,\n                updateObservable);\n    }\n\n    public static StoreSection createTopLevel(\n            DerivedObservableList<StoreEntryWrapper> all,\n            Set<StoreEntryWrapper> selected,\n            Predicate<StoreEntryWrapper> entryFilter,\n            ObservableValue<String> filterString,\n            ObservableValue<StoreCategoryWrapper> category,\n            ObservableIntegerValue visibilityObservable,\n            ObservableIntegerValue updateObservable,\n            ObservableBooleanValue enabled) {\n        var allEnabled = all.blockUpdatesIf(Bindings.not(enabled));\n        var topLevel = allEnabled.filtered(\n                section -> {\n                    if (!enabled.getValue()) {\n                        return false;\n                    }\n\n                    return DataStorage.get()\n                            .isRootEntry(section.getEntry(), category.getValue().getCategory());\n                },\n                enabled,\n                category,\n                updateObservable);\n        var cached = topLevel.mapped(storeEntryWrapper -> create(\n                List.of(),\n                storeEntryWrapper,\n                1,\n                allEnabled,\n                selected,\n                entryFilter,\n                filterString,\n                category,\n                visibilityObservable,\n                updateObservable,\n                enabled));\n        var ordered = sorted(null, cached, updateObservable);\n        var shown = ordered.filtered(\n                section -> {\n                    if (!enabled.getValue()) {\n                        return false;\n                    }\n\n                    // matches filter\n                    return (filterString == null || section.matchesFilter(filterString.getValue()))\n                            &&\n                            // matches selector\n                            (section.anyMatches(entryFilter))\n                            &&\n                            // same category\n                            (showInCategory(category.getValue(), section.getWrapper()));\n                },\n                enabled,\n                category,\n                filterString,\n                updateObservable);\n        return new StoreSection(null, ordered, shown, 0);\n    }\n\n    private static StoreSection create(\n            List<StoreEntryWrapper> parents,\n            StoreEntryWrapper e,\n            int depth,\n            DerivedObservableList<StoreEntryWrapper> all,\n            Set<StoreEntryWrapper> selected,\n            Predicate<StoreEntryWrapper> entryFilter,\n            ObservableValue<String> filterString,\n            ObservableValue<StoreCategoryWrapper> category,\n            ObservableIntegerValue visibilityObservable,\n            ObservableIntegerValue updateObservable,\n            ObservableBooleanValue enabled) {\n        if (e.getEntry().getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {\n            return new StoreSection(\n                    e, DerivedObservableList.arrayList(true), DerivedObservableList.arrayList(true), depth);\n        }\n\n        var allChildren = all.filtered(\n                other -> {\n                    // Legacy implementation that does not use children caches. Use for testing\n                    //                                if (true) return DataStorage.get()\n                    //                                        .getDefaultDisplayParent(other.getEntry())\n                    //                                        .map(found -> found.equals(e.getEntry()))\n                    //                                        .orElse(false);\n\n                    // is children. This check is fast as the children are cached in the storage\n                    if (DataStorage.get() == null\n                            || !DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry())) {\n                        return false;\n                    }\n\n                    return true;\n                },\n                enabled,\n                e.getPersistentState(),\n                e.getCache(),\n                updateObservable);\n        var l = new ArrayList<>(parents);\n        l.add(e);\n        var cached = allChildren.mapped(c -> create(\n                l,\n                c,\n                depth + 1,\n                all,\n                selected,\n                entryFilter,\n                filterString,\n                category,\n                visibilityObservable,\n                updateObservable,\n                enabled));\n        var ordered = sorted(e, cached, updateObservable);\n        var filtered = ordered.filtered(\n                section -> {\n                    if (!enabled.getValue()) {\n                        return false;\n                    }\n\n                    var isBatchSelected = selected.contains(section.getWrapper());\n\n                    var matchesFilter = filterString == null\n                            || section.matchesFilter(filterString.getValue())\n                            || l.stream().anyMatch(p -> p.matchesFilter(filterString.getValue()));\n                    if (!isBatchSelected && !matchesFilter) {\n                        return false;\n                    }\n\n                    var hasFilter = filterString != null\n                            && filterString.getValue() != null\n                            && filterString.getValue().length() > 0;\n                    if (!isBatchSelected && !hasFilter) {\n                        var showProvider = true;\n                        try {\n                            showProvider = section.getWrapper()\n                                    .getEntry()\n                                    .getProvider()\n                                    .shouldShow(section.getWrapper());\n                        } catch (Exception ignored) {\n                        }\n                        if (!showProvider) {\n                            return false;\n                        }\n                    }\n\n                    var matchesSelector = section.anyMatches(entryFilter);\n                    if (!isBatchSelected && !matchesSelector) {\n                        return false;\n                    }\n\n                    // Prevent updates for children on category switching by checking depth\n                    var showCategory = showInCategory(category.getValue(), section.getWrapper()) || depth > 0;\n                    if (!showCategory) {\n                        return false;\n                    }\n\n                    // If this entry is already shown as root due to a different category than parent, don't\n                    // show it\n                    // again here\n                    var notRoot = !DataStorage.get()\n                            .isRootEntry(\n                                    section.getWrapper().getEntry(),\n                                    category.getValue().getCategory());\n                    if (!notRoot) {\n                        return false;\n                    }\n\n                    return true;\n                },\n                enabled,\n                category,\n                filterString,\n                e.getPersistentState(),\n                e.getCache(),\n                visibilityObservable,\n                updateObservable);\n        return new StoreSection(e, cached, filtered, depth);\n    }\n\n    private static boolean showInCategory(StoreCategoryWrapper categoryWrapper, StoreEntryWrapper entryWrapper) {\n        var current = entryWrapper.getCategory().getValue();\n        while (current != null) {\n            if (categoryWrapper\n                    .getCategory()\n                    .getUuid()\n                    .equals(current.getCategory().getUuid())) {\n                return true;\n            }\n\n            if (!AppPrefs.get().showChildCategoriesInParentCategory().get()) {\n                break;\n            }\n\n            current = current.getParent();\n        }\n        return false;\n    }\n\n    public boolean matchesFilter(String filter) {\n        return anyMatches(storeEntryWrapper -> storeEntryWrapper.matchesFilter(filter));\n    }\n\n    public boolean anyMatches(Predicate<StoreEntryWrapper> c) {\n        return c == null\n                || (wrapper != null && c.test(wrapper))\n                || allChildren.getList().stream().anyMatch(storeEntrySection -> storeEntrySection.anyMatches(c));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreSectionBaseComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.IconButtonComp;\nimport io.xpipe.app.comp.base.ListBoxViewComp;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreColor;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.value.ObservableBooleanValue;\nimport javafx.css.PseudoClass;\nimport javafx.scene.control.Button;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Pane;\nimport javafx.scene.layout.VBox;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\n\npublic abstract class StoreSectionBaseComp extends RegionBuilder<VBox> {\n\n    private static final PseudoClass EXPANDED = PseudoClass.getPseudoClass(\"expanded\");\n    private static final PseudoClass ROOT = PseudoClass.getPseudoClass(\"root\");\n    private static final PseudoClass TOP = PseudoClass.getPseudoClass(\"top\");\n    private static final PseudoClass SUB = PseudoClass.getPseudoClass(\"sub\");\n    private static final PseudoClass ODD = PseudoClass.getPseudoClass(\"odd-depth\");\n    private static final PseudoClass EVEN = PseudoClass.getPseudoClass(\"even-depth\");\n\n    protected final StoreSection section;\n\n    public StoreSectionBaseComp(StoreSection section) {\n        this.section = section;\n    }\n\n    protected ObservableBooleanValue effectiveExpanded(ObservableBooleanValue expanded) {\n        return section.getWrapper() != null\n                ? Bindings.createBooleanBinding(\n                        () -> {\n                            return expanded.get()\n                                    && section.getShownChildren().getList().size() > 0;\n                        },\n                        expanded,\n                        section.getShownChildren().getList())\n                : new SimpleBooleanProperty(true);\n    }\n\n    protected void addPseudoClassListeners(VBox vbox, ObservableBooleanValue expanded) {\n        var observable = effectiveExpanded(expanded);\n        BindingsHelper.preserve(this, observable);\n        observable.subscribe(val -> {\n            vbox.pseudoClassStateChanged(EXPANDED, val);\n        });\n\n        vbox.pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0);\n        vbox.pseudoClassStateChanged(ODD, section.getDepth() % 2 != 0);\n        vbox.pseudoClassStateChanged(ROOT, section.getDepth() == 0);\n        vbox.pseudoClassStateChanged(SUB, section.getDepth() > 1);\n        vbox.pseudoClassStateChanged(TOP, section.getDepth() == 1);\n\n        if (section.getWrapper() != null) {\n            if (section.getDepth() == 1) {\n                BindingsHelper.attach(vbox, section.getWrapper().getColor(), val -> {\n                    var newList = new ArrayList<>(vbox.getStyleClass());\n                    newList.removeIf(s -> Arrays.stream(DataStoreColor.values())\n                            .anyMatch(dataStoreColor -> dataStoreColor.getId().equals(s)));\n                    newList.remove(\"gray\");\n                    newList.add(\"color-box\");\n                    if (val != null) {\n                        newList.add(val.getId());\n                    } else {\n                        newList.add(\"gray\");\n                    }\n                    vbox.getStyleClass().setAll(newList);\n                });\n            }\n\n            BindingsHelper.attach(vbox, section.getWrapper().getPerUser(), val -> {\n                vbox.pseudoClassStateChanged(PseudoClass.getPseudoClass(\"per-user\"), val);\n            });\n        }\n    }\n\n    protected void addVisibilityListeners(VBox root, Pane pane, Supplier<HBox> hbox) {\n        AtomicReference<HBox> built = new AtomicReference<>();\n        Consumer<Boolean> update = (visible) -> {\n            if (visible) {\n                // Ignore any changes before this was added to the scene\n                if (root.getScene() == null && built.get() == null) {\n                    return;\n                }\n\n                if (!root.isVisible()) {\n                    return;\n                }\n\n                if (built.get() == null) {\n                    built.set(hbox.get());\n                }\n\n                pane.getChildren().setAll(built.get());\n            } else {\n                if (root.isVisible()) {\n                    return;\n                }\n\n                pane.getChildren().clear();\n            }\n        };\n\n        root.visibleProperty().subscribe((newValue) -> {\n            if (root.getScene() == null) {\n                update.accept(newValue);\n            } else {\n                Platform.runLater(() -> {\n                    update.accept(newValue);\n                });\n            }\n        });\n    }\n\n    protected ListBoxViewComp<StoreSection> createChildrenList(\n            Function<StoreSection, BaseRegionBuilder<?, ?>> function, ObservableBooleanValue hide) {\n        var content = new ListBoxViewComp<>(\n                section.getShownChildren().getList(),\n                section.getAllChildren().getList(),\n                function,\n                section.getWrapper() == null);\n        content.setVisibilityControl(true);\n        content.minHeight(0);\n        content.hgrow();\n        content.style(\"children-content\");\n        content.hide(hide);\n        content.apply(struc -> struc.setFocusTraversable(false));\n        return content;\n    }\n\n    protected RegionBuilder<Button> createExpandButton(Runnable action, int width, ObservableBooleanValue expanded) {\n        var icon = Bindings.createObjectBinding(\n                () -> new LabelGraphic.IconGraphic(\n                        expanded.get() && section.getShownChildren().getList().size() > 0\n                                ? \"mdal-keyboard_arrow_down\"\n                                : \"mdal-keyboard_arrow_right\"),\n                expanded,\n                section.getShownChildren().getList());\n        var expandButton = new IconButtonComp(icon, action);\n        expandButton\n                .minWidth(width)\n                .prefWidth(width)\n                .describe(d -> d.nameKey(\"expand\"))\n                .disable(Bindings.size(section.getShownChildren().getList()).isEqualTo(0))\n                .style(\"expand-button\")\n                .maxHeight(100);\n        return expandButton;\n    }\n\n    protected RegionBuilder<Button> createQuickAccessButton(int width, Consumer<StoreSection> r) {\n        var quickAccessDisabled = Bindings.createBooleanBinding(\n                () -> {\n                    return section.getShownChildren().getList().isEmpty();\n                },\n                section.getShownChildren().getList());\n        var quickAccessButton = new StoreQuickAccessButtonComp(section, r)\n                .style(\"quick-access-button\")\n                .minWidth(width)\n                .prefWidth(width)\n                .maxHeight(100)\n                .describe(d -> d.nameKey(\"quickAccess\"))\n                .disable(quickAccessDisabled);\n        return quickAccessButton;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreSectionComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.RegionDescriptor;\nimport io.xpipe.app.comp.base.HorizontalComp;\nimport io.xpipe.app.comp.base.VerticalComp;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.Button;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyCodeCombination;\nimport javafx.scene.input.KeyEvent;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Pane;\nimport javafx.scene.layout.VBox;\n\nimport org.int4.fx.builders.pane.StackPaneBuilder;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class StoreSectionComp extends StoreSectionBaseComp {\n\n    public StoreSectionComp(StoreSection section) {\n        super(section);\n    }\n\n    @Override\n    public VBox createSimple() {\n        var entryButton = StoreEntryComp.customSection(section);\n\n        var paneComp = new StackPaneBuilder();\n        paneComp.minHeight(entryButton.getHeight());\n        paneComp.maxHeight(entryButton.getHeight());\n        paneComp.prefHeight(entryButton.getHeight());\n\n        var effectiveExpanded = effectiveExpanded(section.getWrapper().getExpanded());\n        var content = createChildrenList(c -> StoreSection.customSection(c), Bindings.not(effectiveExpanded));\n\n        var full = new VerticalComp(\n                List.of(paneComp, RegionBuilder.hseparator().hide(Bindings.not(effectiveExpanded)), content));\n        full.style(\"store-entry-section-comp\");\n        full.apply(struc -> {\n            struc.setFillWidth(true);\n            var pane = ((Pane) struc.getChildren().getFirst());\n            addPseudoClassListeners(struc, section.getWrapper().getExpanded());\n            addVisibilityListeners(struc, pane, () -> buildContent(entryButton).build());\n        });\n        return full.build();\n    }\n\n    private RegionBuilder<HBox> buildContent(StoreEntryComp entryButton) {\n        entryButton.hgrow();\n        entryButton.apply(struc -> {\n            struc.addEventFilter(KeyEvent.KEY_PRESSED, event -> {\n                if (section.getWrapper().getRenaming().get()) {\n                    return;\n                }\n\n                if (event.getCode() == KeyCode.SPACE) {\n                    section.getWrapper().toggleExpanded();\n                    event.consume();\n                }\n                if (event.getCode() == KeyCode.RIGHT) {\n                    var ref = (VBox) ((HBox) struc.getParent()).getChildren().getFirst();\n                    if (entryButton.isFullSize()) {\n                        var btn = (Button) ref.getChildren().getFirst();\n                        btn.fire();\n                    }\n                    event.consume();\n                }\n            });\n        });\n\n        var quickAccessButton = createQuickAccessButton(30, c -> {\n            ThreadHelper.runFailableAsync(() -> {\n                c.getWrapper().executeDefaultAction();\n            });\n        });\n        quickAccessButton.vgrow();\n        quickAccessButton.describe(d -> d.nameKey(\"quickAccess\")\n                .focusTraversal(RegionDescriptor.FocusTraversal.ENABLED_FOR_ACCESSIBILITY)\n                .shortcut(new KeyCodeCombination(KeyCode.RIGHT)));\n\n        var expandButton = createExpandButton(\n                () -> section.getWrapper().toggleExpanded(),\n                30,\n                section.getWrapper().getExpanded());\n        expandButton.vgrow();\n        expandButton.describe(d -> d.nameKey(\"expand\")\n                .focusTraversal(RegionDescriptor.FocusTraversal.ENABLED_FOR_ACCESSIBILITY)\n                .shortcut(new KeyCodeCombination(KeyCode.SPACE)));\n        var buttonList = new ArrayList<BaseRegionBuilder<?, ?>>();\n        if (entryButton.isFullSize()) {\n            buttonList.add(quickAccessButton);\n        }\n        buttonList.add(expandButton);\n        var buttons = new VerticalComp(buttonList);\n        var topEntryList = new HorizontalComp(List.of(buttons, entryButton));\n        topEntryList.apply(struc -> {\n            struc.setAlignment(Pos.CENTER_LEFT);\n        });\n\n        topEntryList.minHeight(entryButton.getHeight());\n        topEntryList.maxHeight(entryButton.getHeight());\n        topEntryList.prefHeight(entryButton.getHeight());\n\n        return topEntryList;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreSectionMiniComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.*;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.BooleanProperty;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.Button;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Pane;\nimport javafx.scene.layout.VBox;\n\nimport org.int4.fx.builders.common.AbstractRegionBuilder;\nimport org.int4.fx.builders.pane.StackPaneBuilder;\n\nimport java.util.ArrayList;\nimport java.util.function.BiConsumer;\nimport java.util.function.Consumer;\n\npublic class StoreSectionMiniComp extends StoreSectionBaseComp {\n\n    private final BooleanProperty expanded;\n    private final BiConsumer<StoreSection, RegionBuilder<Button>> augment;\n    private final Consumer<StoreSection> action;\n    private final boolean forceInitialExpand;\n\n    public StoreSectionMiniComp(\n            StoreSection section,\n            BiConsumer<StoreSection, RegionBuilder<Button>> augment,\n            Consumer<StoreSection> action,\n            boolean forceInitialExpand) {\n        super(section);\n        this.augment = augment;\n        this.action = action;\n        this.forceInitialExpand = forceInitialExpand;\n        this.expanded = new SimpleBooleanProperty(section.getWrapper() == null\n                || section.getWrapper().getExpanded().getValue()\n                || forceInitialExpand);\n    }\n\n    @Override\n    public VBox createSimple() {\n        var list = new ArrayList<AbstractRegionBuilder<?, ?>>();\n        if (section.getWrapper() != null) {\n            var paneComp = new StackPaneBuilder();\n            paneComp.minHeight(28);\n            paneComp.maxHeight(28);\n            paneComp.prefHeight(28);\n            list.add(paneComp);\n        }\n\n        var content = createChildrenList(\n                c -> new StoreSectionMiniComp(c, this.augment, this.action, this.forceInitialExpand),\n                Bindings.not(expanded));\n        list.add(content);\n\n        var full = new VerticalComp(list);\n        full.style(\"store-section-mini-comp\");\n        full.apply(struc -> {\n            struc.setFillWidth(true);\n            addPseudoClassListeners(struc, expanded);\n            if (section.getWrapper() != null) {\n                var pane = ((Pane) struc.getChildren().getFirst());\n                addVisibilityListeners(struc, pane, () -> buildContent().build());\n            }\n        });\n        return full.build();\n    }\n\n    private RegionBuilder<HBox> buildContent() {\n        var root = new ButtonComp(section.getWrapper().getShownName(), () -> {\n            action.accept(section);\n        });\n        root.hgrow();\n        root.maxWidth(10000);\n        root.style(\"item\");\n        root.apply(struc -> {\n            struc.setAlignment(Pos.CENTER_LEFT);\n            struc.setGraphic(PrettyImageHelper.ofFixedSize(section.getWrapper().getIconFile(), 16, 16)\n                    .build());\n            struc.setMnemonicParsing(false);\n        });\n        augment.accept(section, root);\n\n        var expandButton = createExpandButton(() -> expanded.set(!expanded.get()), 20, expanded);\n\n        var quickAccessButton = createQuickAccessButton(20, action);\n\n        var buttonList = new ArrayList<BaseRegionBuilder<?, ?>>();\n        buttonList.add(expandButton);\n        buttonList.add(root);\n        if (section.getDepth() == 1) {\n            buttonList.add(quickAccessButton);\n        }\n        var h = new HorizontalComp(buttonList);\n        h.apply(struc -> struc.setFillHeight(true));\n\n        h.minHeight(28);\n        h.prefHeight(28);\n        h.maxHeight(28);\n\n        return h;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreSectionSortMode.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.storage.DataStoreEntry;\n\nimport java.time.Instant;\nimport java.util.*;\nimport java.util.stream.Stream;\n\npublic interface StoreSectionSortMode {\n\n    StoreSectionSortMode INDEX_DESC = new StoreSectionSortMode() {\n\n        @Override\n        public String getId() {\n            return \"index-desc\";\n        }\n\n        @Override\n        public Comparator<StoreSection> comparator() {\n            return Comparator.<StoreSection>comparingInt(\n                            e -> e.getWrapper().getOrderIndex().getValue())\n                    .reversed();\n        }\n    };\n    StoreSectionSortMode INDEX_ASC = new StoreSectionSortMode() {\n        @Override\n        public String getId() {\n            return \"index-asc\";\n        }\n\n        @Override\n        public Comparator<StoreSection> comparator() {\n            return Comparator.comparingInt(e -> e.getWrapper().getOrderIndex().getValue());\n        }\n    };\n    StoreSectionSortMode ALPHABETICAL_DESC = new StoreSectionSortMode() {\n\n        @Override\n        public String getId() {\n            return \"alphabetical-desc\";\n        }\n\n        @Override\n        public Comparator<StoreSection> comparator() {\n            return Comparator.comparing(\n                    e -> e.getWrapper().nameProperty().getValue().toLowerCase(Locale.ROOT));\n        }\n    };\n    StoreSectionSortMode ALPHABETICAL_ASC = new StoreSectionSortMode() {\n        @Override\n        public String getId() {\n            return \"alphabetical-asc\";\n        }\n\n        @Override\n        public Comparator<StoreSection> comparator() {\n            return Comparator.<StoreSection, String>comparing(\n                            e -> e.getWrapper().nameProperty().getValue().toLowerCase(Locale.ROOT))\n                    .reversed();\n        }\n    };\n    StoreSectionSortMode.DateSortMode DATE_DESC = new StoreSectionSortMode.DateSortMode() {\n\n        public Instant date(StoreSection s) {\n            var la = s.getWrapper().getLastAccess().getValue();\n            if (la == null) {\n                return Instant.MAX;\n            }\n\n            return la;\n        }\n\n        @Override\n        protected int compare(Instant s1, Instant s2) {\n            return s1.compareTo(s2);\n        }\n\n        @Override\n        public String getId() {\n            return \"date-desc\";\n        }\n    };\n    StoreSectionSortMode.DateSortMode DATE_ASC = new StoreSectionSortMode.DateSortMode() {\n\n        public Instant date(StoreSection s) {\n            var la = s.getWrapper().getLastAccess().getValue();\n            if (la == null) {\n                return Instant.MIN;\n            }\n\n            return la;\n        }\n\n        @Override\n        protected int compare(Instant s1, Instant s2) {\n            return s2.compareTo(s1);\n        }\n\n        @Override\n        public String getId() {\n            return \"date-asc\";\n        }\n    };\n\n    List<StoreSectionSortMode> ALL = List.of(ALPHABETICAL_DESC, ALPHABETICAL_ASC, DATE_DESC, DATE_ASC);\n\n    static Optional<StoreSectionSortMode> fromId(String id) {\n        return ALL.stream()\n                .filter(storeSortMode -> storeSortMode.getId().equals(id))\n                .findFirst();\n    }\n\n    String getId();\n\n    Comparator<StoreSection> comparator();\n\n    abstract class DateSortMode implements StoreSectionSortMode {\n\n        private final Map<StoreSection, StoreSection> cachedRepresentatives = new IdentityHashMap<>();\n        private int entriesListObservableIndex = -1;\n\n        public StoreSection computeRepresentative(StoreSection s) {\n            return Stream.concat(\n                            s.getShownChildren().getList().stream()\n                                    .filter(section ->\n                                            section.getWrapper().getEntry().getValidity()\n                                                    != DataStoreEntry.Validity.LOAD_FAILED)\n                                    .map(this::getRepresentative),\n                            Stream.of(s))\n                    .max(Comparator.comparing(section -> date(section)))\n                    .orElseThrow();\n        }\n\n        public StoreSection getRepresentative(StoreSection s) {\n            if (StoreViewState.get().getEntriesListUpdateObservable().get() != entriesListObservableIndex) {\n                cachedRepresentatives.clear();\n                entriesListObservableIndex =\n                        StoreViewState.get().getEntriesListUpdateObservable().get();\n            }\n\n            var found = cachedRepresentatives.get(s);\n            if (found != null) {\n                return found;\n            }\n\n            var r = computeRepresentative(s);\n            cachedRepresentatives.put(s, r);\n            return r;\n        }\n\n        public abstract Instant date(StoreSection s);\n\n        protected abstract int compare(Instant s1, Instant s2);\n\n        @Override\n        public Comparator<StoreSection> comparator() {\n            return (o1, o2) -> {\n                var r1 = getRepresentative(o1);\n                var r2 = getRepresentative(o2);\n                return DateSortMode.this.compare(date(r1), date(r2));\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreSidebarComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.VerticalComp;\nimport io.xpipe.app.util.ObservableSubscriber;\n\nimport javafx.scene.layout.Region;\n\nimport java.util.List;\n\npublic class StoreSidebarComp extends SimpleRegionBuilder {\n\n    private final ObservableSubscriber filterTrigger;\n\n    public StoreSidebarComp(ObservableSubscriber filterTrigger) {\n        this.filterTrigger = filterTrigger;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var sideBar = new VerticalComp(List.of(\n                new StoreEntryListOverviewComp(filterTrigger)\n                        .style(\"color-box\")\n                        .style(\"gray\")\n                        .style(\"bar\"),\n                new StoreCategoryListComp(StoreViewState.get().getAllConnectionsCategory())\n                        .style(\"color-box\")\n                        .style(\"gray\")\n                        .style(\"bar\"),\n                new StoreCategoryListComp(StoreViewState.get().getAllIdentitiesCategory())\n                        .style(\"color-box\")\n                        .style(\"gray\")\n                        .style(\"bar\"),\n                new StoreCategoryListComp(StoreViewState.get().getAllScriptsCategory())\n                        .style(\"color-box\")\n                        .style(\"gray\")\n                        .style(\"bar\"),\n                RegionBuilder.of(() -> new Region())\n                        .style(\"color-box\")\n                        .style(\"gray\")\n                        .style(\"bar\")\n                        .style(\"filler-bar\")\n                        .minHeight(10)\n                        .vgrow()));\n        sideBar.apply(struc -> struc.setFillWidth(true));\n        sideBar.style(\"sidebar\");\n        sideBar.prefWidth(240);\n        return sideBar.build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreToggleComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.ToggleSwitchComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.BooleanProperty;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.layout.Region;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Setter;\n\nimport java.util.function.BiConsumer;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\n\n@AllArgsConstructor\npublic class StoreToggleComp extends SimpleRegionBuilder {\n\n    private final String nameKey;\n    private final ObservableValue<LabelGraphic> graphic;\n    private final StoreSection section;\n    private final BooleanProperty value;\n    private final Consumer<Boolean> onChange;\n\n    @Setter\n    private ObservableValue<Boolean> customVisibility = new SimpleBooleanProperty(true);\n\n    public StoreToggleComp(\n            String nameKey,\n            ObservableValue<LabelGraphic> graphic,\n            StoreSection section,\n            BooleanProperty initial,\n            Consumer<Boolean> onChange) {\n        this.nameKey = nameKey;\n        this.graphic = graphic;\n        this.section = section;\n        this.value = initial;\n        this.onChange = onChange;\n    }\n\n    public static <T extends DataStore> StoreToggleComp enableToggle(\n            String nameKey, StoreSection section, BooleanProperty value, BiConsumer<T, Boolean> setter) {\n        var val = new SimpleBooleanProperty();\n        ObservableValue<LabelGraphic> g = val.map(aBoolean -> aBoolean\n                ? new LabelGraphic.IconGraphic(\"mdi2c-circle-slice-8\")\n                : new LabelGraphic.IconGraphic(\"mdi2p-power\"));\n        var t = new StoreToggleComp(nameKey, g, section, value, v -> {\n            setter.accept(section.getWrapper().getEntry().getStore().asNeeded(), v);\n        });\n        t.value.subscribe((newValue) -> {\n            val.set(newValue);\n        });\n        return t;\n    }\n\n    public static <T extends DataStore> StoreToggleComp childrenToggle(\n            boolean graphic, StoreSection section, Function<T, Boolean> initial, BiConsumer<T, Boolean> setter) {\n        return childrenToggle(\"showNonRunningChildren\", graphic, section, initial, setter);\n    }\n\n    public static <T extends DataStore> StoreToggleComp childrenToggle(\n            String nameKey,\n            boolean graphic,\n            StoreSection section,\n            Function<T, Boolean> initial,\n            BiConsumer<T, Boolean> setter) {\n        var val = new SimpleBooleanProperty();\n        ObservableValue<LabelGraphic> g = graphic\n                ? val.map(aBoolean -> aBoolean\n                        ? new LabelGraphic.IconGraphic(\"mdi2e-eye-plus\")\n                        : new LabelGraphic.IconGraphic(\"mdi2e-eye-minus\"))\n                : null;\n        var t = new StoreToggleComp(\n                null,\n                g,\n                section,\n                new SimpleBooleanProperty(\n                        initial.apply(section.getWrapper().getEntry().getStore().asNeeded())),\n                v -> {\n                    Platform.runLater(() -> {\n                        setter.accept(section.getWrapper().getEntry().getStore().asNeeded(), v);\n                        StoreViewState.get().triggerStoreListVisibilityUpdate();\n                    });\n                });\n        t.describe(d -> d.nameKey(nameKey));\n        t.value.subscribe((newValue) -> {\n            val.set(newValue);\n        });\n        return t;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var disable = section.getWrapper().getValidity().map(state -> state != DataStoreEntry.Validity.COMPLETE);\n        var visible = Bindings.createBooleanBinding(\n                () -> {\n                    if (!this.customVisibility.getValue()) {\n                        return false;\n                    }\n\n                    return section.getWrapper().getValidity().getValue() == DataStoreEntry.Validity.COMPLETE;\n                },\n                section.getWrapper().getValidity(),\n                section.getShowDetails(),\n                this.customVisibility);\n        var t = new ToggleSwitchComp(value, AppI18n.observable(nameKey), graphic)\n                .visible(visible)\n                .disable(disable);\n        t.describe(d -> d.nameKey(\"toggleEnabled\"));\n        value.addListener((observable, oldValue, newValue) -> {\n            ThreadHelper.runAsync(() -> {\n                onChange.accept(newValue);\n            });\n        });\n        return t.build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/StoreViewState.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.ext.DataStoreUsageCategory;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.DerivedObservableList;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreCategory;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.StorageListener;\nimport io.xpipe.app.util.GlobalTimer;\n\nimport javafx.application.Platform;\nimport javafx.beans.Observable;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.*;\nimport javafx.beans.value.ObservableIntegerValue;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.FXCollections;\nimport javafx.collections.ListChangeListener;\n\nimport lombok.Getter;\n\nimport java.time.Duration;\nimport java.util.*;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\n\npublic class StoreViewState {\n\n    private static StoreViewState INSTANCE;\n    private final StringProperty filter = new SimpleStringProperty();\n\n    @Getter\n    private final DerivedObservableList<StoreEntryWrapper> allEntries =\n            DerivedObservableList.synchronizedArrayList(true);\n\n    @Getter\n    private final DerivedObservableList<StoreCategoryWrapper> categories =\n            DerivedObservableList.synchronizedArrayList(true);\n\n    @Getter\n    private final IntegerProperty entriesListVisibilityObservable = new SimpleIntegerProperty();\n\n    @Getter\n    private final IntegerProperty entriesListUpdateObservable = new SimpleIntegerProperty();\n\n    @Getter\n    private final Property<StoreCategoryWrapper> activeCategory = new SimpleObjectProperty<>();\n\n    @Getter\n    private final Property<StoreSectionSortMode> globalSortMode = new SimpleObjectProperty<>();\n\n    @Getter\n    private final Property<StoreSectionSortMode> tieSortMode = new SimpleObjectProperty<>();\n\n    @Getter\n    private final BooleanProperty batchMode = new SimpleBooleanProperty(false);\n\n    @Getter\n    private final DerivedObservableList<StoreEntryWrapper> batchModeSelection =\n            DerivedObservableList.synchronizedArrayList(true);\n\n    private final Set<StoreEntryWrapper> batchModeSelectionSet = new HashSet<>();\n\n    @Getter\n    private final DerivedObservableList<StoreEntryWrapper> effectiveBatchModeSelection = batchModeSelection.filtered(\n            storeEntryWrapper -> {\n                if (storeEntryWrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP) {\n                    return false;\n                }\n\n                return true;\n            },\n            entriesListVisibilityObservable,\n            entriesListUpdateObservable);\n\n    @Getter\n    private boolean initialized = false;\n\n    @Getter\n    private StoreSection currentTopLevelSection;\n\n    private StoreViewState() {\n        initContent();\n        addListeners();\n    }\n\n    public static void init() {\n        if (INSTANCE != null) {\n            return;\n        }\n\n        INSTANCE = new StoreViewState();\n        INSTANCE.initSortMode();\n        INSTANCE.updateContent();\n        INSTANCE.initSections();\n        INSTANCE.updateContent();\n        INSTANCE.initFilterListener();\n        INSTANCE.initBatchListeners();\n        INSTANCE.initialized = true;\n    }\n\n    public static void reset() {\n        if (INSTANCE == null) {\n            return;\n        }\n\n        var active = INSTANCE.activeCategory.getValue().getCategory();\n        if (active != null) {\n            AppCache.update(\"selectedCategory\", active.getUuid());\n        }\n\n        var globalMode = INSTANCE.globalSortMode.getValue();\n        if (globalMode != null) {\n            AppCache.update(\"globalSortMode\", globalMode.getId());\n        }\n\n        var tieMode = INSTANCE.tieSortMode.getValue();\n        if (tieMode != null) {\n            AppCache.update(\"tieSortMode\", tieMode.getId());\n        }\n\n        INSTANCE = null;\n    }\n\n    public static StoreViewState get() {\n        return INSTANCE;\n    }\n\n    public List<String> getAllAvailableTags() {\n        var l = new LinkedHashSet<String>();\n        for (StoreEntryWrapper storeEntryWrapper : getAllEntries().getList()) {\n            l.addAll(storeEntryWrapper.getTags());\n        }\n        return l.stream().sorted().toList();\n    }\n\n    public ObservableValue<Comparator<StoreSection>> createEffectiveSortMode(\n            Comparator<StoreSection> customComparator) {\n        return Bindings.createObjectBinding(\n                () -> {\n                    var global = globalSortMode.getValue() != null\n                            ? globalSortMode.getValue().comparator()\n                            : null;\n                    var tie = customComparator != null\n                            ? customComparator\n                            : tieSortMode.getValue() != null\n                                    ? tieSortMode.getValue().comparator()\n                                    : StoreSectionSortMode.DATE_DESC.comparator();\n                    var fallback = Comparator.<StoreSection, String>comparing(\n                            sec -> sec.getWrapper().getName().getValue());\n                    var failed = Comparator.<StoreSection>comparingInt(value -> {\n                        if (value.getWrapper().getValidity().getValue() == DataStoreEntry.Validity.LOAD_FAILED) {\n                            return 1;\n                        }\n\n                        return 0;\n                    });\n                    return global != null\n                            ? failed.thenComparing(global.thenComparing(tie)).thenComparing(fallback)\n                            : failed.thenComparing(tie).thenComparing(fallback);\n                },\n                globalSortMode,\n                tieSortMode);\n    }\n\n    public ObservableIntegerValue entriesCount(Predicate<StoreEntryWrapper> filter, Observable... observables) {\n        return Bindings.size(allEntries\n                .filtered(\n                        storeEntryWrapper -> {\n                            if (!storeEntryWrapper.includeInConnectionCount()) {\n                                return false;\n                            }\n\n                            return filter.test(storeEntryWrapper);\n                        },\n                        observables)\n                .getList());\n    }\n\n    public boolean isBatchModeSelected(StoreEntryWrapper entry) {\n        return batchModeSelectionSet.contains(entry);\n    }\n\n    public void selectBatchMode(StoreSection section) {\n        var wrapper = section.getWrapper();\n        if (wrapper != null && wrapper.getEntry().getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {\n            return;\n        }\n        if (wrapper != null && !batchModeSelectionSet.contains(wrapper)) {\n            batchModeSelection.getList().add(wrapper);\n        }\n        if (wrapper == null || wrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP) {\n            section.getShownChildren().getList().forEach(c -> selectBatchMode(c));\n        }\n    }\n\n    public void unselectBatchMode(StoreSection section) {\n        var wrapper = section.getWrapper();\n        if (wrapper != null && wrapper.getEntry().getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {\n            return;\n        }\n        if (wrapper != null) {\n            batchModeSelection.getList().remove(wrapper);\n        }\n        if (wrapper == null || wrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP) {\n            section.getShownChildren().getList().forEach(c -> unselectBatchMode(c));\n        }\n    }\n\n    private void initSortMode() {\n        String global = AppCache.getNonNull(\"globalSortMode\", String.class, () -> null);\n        var globalMode = global != null ? StoreSectionSortMode.fromId(global).orElse(null) : null;\n        globalSortMode.setValue(globalMode != null ? globalMode : StoreSectionSortMode.INDEX_ASC);\n\n        String tie = AppCache.getNonNull(\"tieSortMode\", String.class, () -> null);\n        var tieMode = global != null ? StoreSectionSortMode.fromId(tie).orElse(null) : null;\n        tieSortMode.setValue(tieMode != null ? tieMode : StoreSectionSortMode.DATE_ASC);\n    }\n\n    private void updateContent() {\n        categories.getList().forEach(c -> c.update());\n        allEntries.getList().forEach(e -> e.update());\n    }\n\n    private void initSections() {\n        try {\n            currentTopLevelSection = StoreSection.createTopLevel(\n                    allEntries,\n                    batchModeSelectionSet,\n                    storeEntryWrapper -> true,\n                    filter,\n                    activeCategory,\n                    entriesListVisibilityObservable,\n                    entriesListUpdateObservable,\n                    new ReadOnlyBooleanWrapper(true));\n        } catch (Exception exception) {\n            currentTopLevelSection = new StoreSection(\n                    null, DerivedObservableList.arrayList(true), DerivedObservableList.arrayList(true), 0);\n            ErrorEventFactory.fromThrowable(exception).handle();\n        }\n    }\n\n    private void initFilterListener() {\n        filter.addListener((observable, oldValue, newValue) -> {\n            onFilterUpdate(newValue);\n        });\n    }\n\n    private void onFilterUpdate(String newValue) {\n        var all = getAllConnectionsCategory();\n        categories.getList().forEach(e -> {\n            e.update();\n        });\n        var matchingCats = categories.getList().stream()\n                .filter(storeCategoryWrapper -> storeCategoryWrapper.getRoot().equals(all))\n                .filter(storeCategoryWrapper -> storeCategoryWrapper.getDirectContainedEntries().getList().stream()\n                        .anyMatch(wrapper -> wrapper.matchesFilter(newValue)))\n                .toList();\n        if (matchingCats.size() == 1) {\n            var onlyMatch = matchingCats.getFirst();\n            selectCategoryIntoViewIfNeeded(onlyMatch);\n        }\n    }\n\n    public void selectCategoryIntoViewIfNeeded(StoreCategoryWrapper category) {\n        StoreCategoryWrapper matchingParent = category;\n        while ((matchingParent = matchingParent.getParent()) != null) {\n            if (matchingParent.equals(activeCategory.getValue())) {\n                break;\n            }\n        }\n\n        if (matchingParent == null) {\n            PlatformThread.runLaterIfNeeded(() -> {\n                activeCategory.setValue(category);\n            });\n        }\n    }\n\n    private void initBatchListeners() {\n        batchModeSelection.getList().addListener((ListChangeListener<? super StoreEntryWrapper>) c -> {\n            if (c.getList().isEmpty()) {\n                batchModeSelectionSet.clear();\n                return;\n            }\n\n            while (c.next()) {\n                if (c.wasAdded()) {\n                    batchModeSelectionSet.addAll(c.getAddedSubList());\n                } else if (c.wasRemoved()) {\n                    c.getRemoved().forEach(batchModeSelectionSet::remove);\n                }\n            }\n        });\n\n        allEntries.getList().addListener((ListChangeListener<? super StoreEntryWrapper>) c -> {\n            batchModeSelection.getList().retainAll(c.getList());\n        });\n\n        batchMode.addListener((observable, oldValue, newValue) -> {\n            batchModeSelection.getList().clear();\n        });\n    }\n\n    private void initContent() {\n        allEntries\n                .getList()\n                .setAll(FXCollections.observableArrayList(DataStorage.get().getStoreEntries().stream()\n                        .map(StoreEntryWrapper::new)\n                        .toList()));\n        categories\n                .getList()\n                .setAll(FXCollections.observableArrayList(DataStorage.get().getStoreCategories().stream()\n                        .map(StoreCategoryWrapper::new)\n                        .toList()));\n\n        activeCategory.addListener((observable, oldValue, newValue) -> {\n            DataStorage.get().setSelectedCategory(newValue.getCategory());\n        });\n        var selected = AppCache.getNonNull(\"selectedCategory\", UUID.class, () -> DataStorage.DEFAULT_CATEGORY_UUID);\n        activeCategory.setValue(categories.getList().stream()\n                .filter(storeCategoryWrapper ->\n                        storeCategoryWrapper.getCategory().getUuid().equals(selected))\n                .findFirst()\n                .orElse(categories.getList().stream()\n                        .filter(storeCategoryWrapper ->\n                                storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.DEFAULT_CATEGORY_UUID))\n                        .findFirst()\n                        .orElseThrow()));\n    }\n\n    public void triggerStoreListVisibilityUpdate() {\n        if (AppOperationMode.isInStartup() || AppOperationMode.isInShutdown()) {\n            return;\n        }\n\n        PlatformThread.runLaterIfNeeded(() -> {\n            entriesListVisibilityObservable.set(entriesListVisibilityObservable.get() + 1);\n        });\n    }\n\n    public void triggerStoreListUpdate() {\n        if (AppOperationMode.isInStartup() || AppOperationMode.isInShutdown()) {\n            return;\n        }\n\n        PlatformThread.runLaterIfNeeded(() -> {\n            entriesListUpdateObservable.set(entriesListUpdateObservable.get() + 1);\n        });\n    }\n\n    public void createNewCategory(StoreCategoryWrapper parent) {\n        var cat = DataStoreCategory.createNew(parent.getCategory().getUuid(), AppI18n.get(\"newCategory\"));\n        DataStorage.get().addStoreCategory(cat);\n        // Ugly solution to ensure that the category is added to the scene\n        GlobalTimer.delay(\n                () -> {\n                    var wrapper = getCategoryWrapper(cat);\n                    Platform.runLater(() -> {\n                        wrapper.getRenameTrigger().fire(null);\n                    });\n                },\n                Duration.ofMillis(500));\n    }\n\n    private void addListeners() {\n        if (AppPrefs.get() != null) {\n            AppPrefs.get().condenseConnectionDisplay().addListener((observable, oldValue, newValue) -> {\n                Platform.runLater(() -> {\n                    synchronized (this) {\n                        var l = new ArrayList<>(allEntries.getList());\n                        allEntries.getList().clear();\n                        allEntries.getList().setAll(l);\n                    }\n                });\n            });\n        }\n\n        // Watch out for synchronizing all calls to the entries and categories list!\n        DataStorage.get().addListener(new StorageListener() {\n\n            @Override\n            public void onStoreListUpdate() {\n                Platform.runLater(() -> {\n                    triggerStoreListUpdate();\n                });\n            }\n\n            @Override\n            public void onStoreAdd(DataStoreEntry... entry) {\n                Platform.runLater(() -> {\n                    // Some entries might already be removed again\n                    var wrappers = Arrays.stream(entry)\n                            .map(StoreEntryWrapper::new)\n                            .filter(storeEntryWrapper ->\n                                    DataStorage.get().getStoreEntries().contains(storeEntryWrapper.getEntry()))\n                            .toList();\n                    wrappers.forEach(StoreEntryWrapper::update);\n\n                    // Don't update anything if we have already reset\n                    if (INSTANCE == null) {\n                        return;\n                    }\n\n                    synchronized (this) {\n                        allEntries.getList().addAll(wrappers);\n                    }\n                    synchronized (this) {\n                        categories.getList().stream()\n                                .filter(storeCategoryWrapper -> allEntries.getList().stream()\n                                        .anyMatch(storeEntryWrapper -> storeEntryWrapper\n                                                .getEntry()\n                                                .getCategoryUuid()\n                                                .equals(storeCategoryWrapper\n                                                        .getCategory()\n                                                        .getUuid())))\n                                .forEach(storeCategoryWrapper -> storeCategoryWrapper.update());\n                    }\n                    wrappers.forEach(storeEntryWrapper -> storeEntryWrapper.update());\n                });\n            }\n\n            @Override\n            public void onStoreRemove(DataStoreEntry... entry) {\n                var a = Arrays.stream(entry).collect(Collectors.toSet());\n                List<StoreEntryWrapper> l;\n                synchronized (this) {\n                    l = allEntries.getList().stream()\n                            .filter(storeEntryWrapper -> a.contains(storeEntryWrapper.getEntry()))\n                            .toList();\n                }\n                List<StoreCategoryWrapper> cats;\n                synchronized (this) {\n                    cats = categories.getList().stream()\n                            .filter(storeCategoryWrapper -> allEntries.getList().stream()\n                                    .anyMatch(storeEntryWrapper -> storeEntryWrapper\n                                            .getEntry()\n                                            .getCategoryUuid()\n                                            .equals(storeCategoryWrapper\n                                                    .getCategory()\n                                                    .getUuid())))\n                            .toList();\n                }\n                Platform.runLater(() -> {\n                    // Don't update anything if we have already reset\n                    if (INSTANCE == null) {\n                        return;\n                    }\n\n                    synchronized (this) {\n                        allEntries.getList().removeAll(l);\n                    }\n                    cats.forEach(storeCategoryWrapper -> storeCategoryWrapper.update());\n                });\n            }\n\n            @Override\n            public void onCategoryAdd(DataStoreCategory category) {\n                var l = new StoreCategoryWrapper(category);\n                Platform.runLater(() -> {\n                    // Don't update anything if we have already reset\n                    if (INSTANCE == null) {\n                        return;\n                    }\n\n                    l.update();\n                    synchronized (this) {\n                        categories.getList().add(l);\n                    }\n                    l.update();\n                });\n            }\n\n            @Override\n            public void onCategoryRemove(DataStoreCategory category) {\n                Optional<StoreCategoryWrapper> found;\n                synchronized (this) {\n                    found = categories.getList().stream()\n                            .filter(storeCategoryWrapper ->\n                                    storeCategoryWrapper.getCategory().equals(category))\n                            .findFirst();\n                }\n                if (found.isEmpty()) {\n                    return;\n                }\n\n                Platform.runLater(() -> {\n                    // Don't update anything if we have already reset\n                    if (INSTANCE == null) {\n                        return;\n                    }\n\n                    if (found.get().equals(activeCategory.getValue())) {\n                        activeCategory.setValue(found.get().getParent());\n                    }\n\n                    synchronized (this) {\n                        categories.getList().remove(found.get());\n                    }\n                    var p = found.get().getParent();\n                    if (p != null) {\n                        p.update();\n                    }\n                });\n            }\n\n            @Override\n            public void onEntryCategoryChange() {\n                Platform.runLater(() -> {\n                    synchronized (this) {\n                        categories.getList().forEach(storeCategoryWrapper -> storeCategoryWrapper.update());\n                    }\n                });\n            }\n        });\n    }\n\n    public Optional<StoreSection> getSectionForWrapper(StoreEntryWrapper wrapper) {\n        if (currentTopLevelSection == null) {\n            return Optional.empty();\n        }\n\n        StoreSection current = getCurrentTopLevelSection();\n        while (true) {\n            var child = current.getAllChildren().getList().stream()\n                    .filter(section -> section.getWrapper().equals(wrapper))\n                    .findFirst();\n            if (child.isPresent()) {\n                return child;\n            }\n\n            var traverse = current.getAllChildren().getList().stream()\n                    .filter(section -> section.anyMatches(w -> w.equals(wrapper)))\n                    .findFirst();\n            if (traverse.isPresent()) {\n                current = traverse.get();\n            } else {\n                return Optional.empty();\n            }\n        }\n    }\n\n    public Optional<StoreSection> getParentSectionForWrapper(StoreEntryWrapper wrapper) {\n        StoreSection current = getCurrentTopLevelSection();\n        while (true) {\n            var child = current.getAllChildren().getList().stream()\n                    .filter(section -> section.getWrapper().equals(wrapper))\n                    .findFirst();\n            if (child.isPresent()) {\n                return Optional.of(current);\n            }\n\n            var traverse = current.getAllChildren().getList().stream()\n                    .filter(section -> section.anyMatches(w -> w.equals(wrapper)))\n                    .findFirst();\n            if (traverse.isPresent()) {\n                current = traverse.get();\n            } else {\n                return Optional.empty();\n            }\n        }\n    }\n\n    public DerivedObservableList<StoreCategoryWrapper> getSortedCategories(StoreCategoryWrapper root) {\n        Comparator<StoreCategoryWrapper> comparator = new Comparator<>() {\n            @Override\n            public int compare(StoreCategoryWrapper o1, StoreCategoryWrapper o2) {\n                var o1Root = o1.getRoot();\n                var o2Root = o2.getRoot();\n                if (o1Root.equals(getAllConnectionsCategory()) && !o1Root.equals(o2Root)) {\n                    return -1;\n                }\n                if (o2Root.equals(getAllConnectionsCategory()) && !o1Root.equals(o2Root)) {\n                    return 1;\n                }\n\n                var p1 = o1.getParent();\n                var p2 = o2.getParent();\n                if (p1 == null && p2 == null) {\n                    return 0;\n                }\n\n                if (p1 == null) {\n                    return -1;\n                }\n\n                if (p2 == null) {\n                    return 1;\n                }\n\n                if (o1.getDepth() > o2.getDepth()) {\n                    if (p1 == o2) {\n                        return 1;\n                    }\n\n                    return compare(p1, o2);\n                }\n\n                if (o1.getDepth() < o2.getDepth()) {\n                    if (p2 == o1) {\n                        return -1;\n                    }\n\n                    return compare(o1, p2);\n                }\n\n                var parent = compare(p1, p2);\n                if (parent != 0) {\n                    return parent;\n                }\n\n                return o1.nameProperty()\n                        .getValue()\n                        .compareToIgnoreCase(o2.nameProperty().getValue());\n            }\n        };\n        return categories\n                .filtered(cat -> root == null || cat.getRoot().equals(root))\n                .sorted(comparator);\n    }\n\n    public StoreCategoryWrapper getAllConnectionsCategory() {\n        return categories.getList().stream()\n                .filter(storeCategoryWrapper ->\n                        storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID))\n                .findFirst()\n                .orElseThrow();\n    }\n\n    public StoreCategoryWrapper getAllScriptsCategory() {\n        return categories.getList().stream()\n                .filter(storeCategoryWrapper ->\n                        storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.ALL_SCRIPTS_CATEGORY_UUID))\n                .findFirst()\n                .orElseThrow();\n    }\n\n    public StoreCategoryWrapper getCustomScriptsCategory() {\n        return categories.getList().stream()\n                .filter(storeCategoryWrapper ->\n                        storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.CUSTOM_SCRIPTS_CATEGORY_UUID))\n                .findFirst()\n                .orElseThrow();\n    }\n\n    public StoreCategoryWrapper getScriptSourcesCategory() {\n        return categories.getList().stream()\n                .filter(storeCategoryWrapper ->\n                        storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.SCRIPT_SOURCES_CATEGORY_UUID))\n                .findFirst()\n                .orElseThrow();\n    }\n\n    public StoreCategoryWrapper getAllIdentitiesCategory() {\n        return categories.getList().stream()\n                .filter(storeCategoryWrapper ->\n                        storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.ALL_IDENTITIES_CATEGORY_UUID))\n                .findFirst()\n                .orElseThrow();\n    }\n\n    @SuppressWarnings(\"unused\")\n    public StoreCategoryWrapper getAllMacrosCategory() {\n        return categories.getList().stream()\n                .filter(storeCategoryWrapper ->\n                        storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.ALL_MACROS_CATEGORY_UUID))\n                .findFirst()\n                .orElseThrow();\n    }\n\n    public StoreEntryWrapper getEntryWrapper(DataStoreEntry entry) {\n        return allEntries.getList().stream()\n                .filter(storeCategoryWrapper -> storeCategoryWrapper.getEntry().equals(entry))\n                .findFirst()\n                .orElseThrow();\n    }\n\n    public StoreCategoryWrapper getCategoryWrapper(DataStoreCategory entry) {\n        return categories.getList().stream()\n                .filter(storeCategoryWrapper ->\n                        storeCategoryWrapper.getCategory().equals(entry))\n                .findFirst()\n                .orElseThrow();\n    }\n\n    public Property<String> getFilterString() {\n        return filter;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/hub/comp/SystemStateComp.java",
    "content": "package io.xpipe.app.hub.comp;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.process.ShellStoreState;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.ReadOnlyObjectWrapper;\nimport javafx.beans.value.ObservableValue;\nimport javafx.geometry.Pos;\nimport javafx.scene.layout.Region;\n\nimport atlantafx.base.theme.Styles;\nimport lombok.Getter;\nimport org.kordamp.ikonli.javafx.FontIcon;\nimport org.kordamp.ikonli.javafx.StackedFontIcon;\n\n@Getter\npublic class SystemStateComp extends SimpleRegionBuilder {\n\n    private final ObservableValue<State> state;\n\n    public SystemStateComp(State state) {\n        this.state = new ReadOnlyObjectWrapper<>(state);\n    }\n\n    public SystemStateComp(ObservableValue<State> state) {\n        this.state = state;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var fi = new FontIcon();\n        fi.getStyleClass().add(\"inner-icon\");\n        state.subscribe(s -> {\n            var i = s == State.FAILURE ? \"mdi2l-lightning-bolt\" : s == State.SUCCESS ? \"mdal-check\" : \"mdsmz-remove\";\n            PlatformThread.runLaterIfNeeded(() -> fi.setIconLiteral(i));\n        });\n\n        var border = new FontIcon(\"mdi2s-square-rounded-outline\");\n        border.getStyleClass().add(\"outer-icon\");\n        border.setOpacity(0.3);\n\n        var success = Styles.toDataURI(\"\"\"\n                                       .stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-success-emphasis; }\n                                       \"\"\");\n        var failure = Styles.toDataURI(\"\"\"\n                                       .stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-danger-emphasis; }\n                                       \"\"\");\n        var other = Styles.toDataURI(\"\"\"\n                                     .stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-accent-emphasis; }\n                                     \"\"\");\n\n        var pane = new StackedFontIcon();\n        pane.getChildren().addAll(fi, border);\n        pane.setAlignment(Pos.CENTER);\n\n        var dataClass1 = \"\"\"\n                         .stacked-ikonli-font-icon > .outer-icon {\n                             -fx-icon-size: 26px;\n                         }\n                         .stacked-ikonli-font-icon > .inner-icon {\n                             -fx-icon-size: 12px;\n                         }\n                         \"\"\";\n        pane.getStylesheets().add(Styles.toDataURI(dataClass1));\n\n        state.subscribe(val -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                pane.getStylesheets().removeAll(success, failure, other);\n                pane.getStylesheets().add(val == State.SUCCESS ? success : val == State.FAILURE ? failure : other);\n            });\n        });\n\n        return pane;\n    }\n\n    public enum State {\n        FAILURE,\n        SUCCESS,\n        OTHER;\n\n        public static ObservableValue<State> shellState(StoreEntryWrapper w) {\n            return Bindings.createObjectBinding(\n                    () -> {\n                        if (!w.getValidity().getValue().isUsable()) {\n                            return null;\n                        }\n\n                        if (w.getPersistentState().getValue() instanceof ShellStoreState s) {\n                            if (s.getShellDialect() != null\n                                    && !s.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) {\n                                return SUCCESS;\n                            }\n\n                            return s.getRunning() != null ? s.getRunning() ? SUCCESS : FAILURE : OTHER;\n                        }\n\n                        return OTHER;\n                    },\n                    w.getPersistentState(),\n                    w.getValidity());\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/icon/SystemIcon.java",
    "content": "package io.xpipe.app.icon;\n\nimport lombok.Value;\n\n@Value\npublic class SystemIcon {\n\n    SystemIconSource source;\n    String id;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/icon/SystemIconCache.java",
    "content": "package io.xpipe.app.icon;\n\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.prefs.AppPrefs;\n\nimport com.github.weisj.jsvg.SVGDocument;\nimport com.github.weisj.jsvg.SVGRenderingHints;\nimport com.github.weisj.jsvg.attributes.ViewBox;\nimport com.github.weisj.jsvg.parser.SVGLoader;\nimport org.apache.commons.io.FileUtils;\n\nimport java.awt.*;\nimport java.awt.image.BufferedImage;\nimport java.io.IOException;\nimport java.net.URL;\nimport java.nio.file.*;\nimport java.security.MessageDigest;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport javax.imageio.ImageIO;\n\npublic class SystemIconCache {\n\n    private static final Path DIRECTORY =\n            AppProperties.get().getDataDir().resolve(\"cache\").resolve(\"icons\").resolve(\"raster\");\n    private static final int[] sizes = new int[] {16, 24, 40, 80};\n    public static final int VERSION = 3;\n\n    public static Path getDirectory(SystemIconSource source) {\n        var target = DIRECTORY.resolve(source.getId());\n        return target;\n    }\n\n    public static int getCacheSourceHash() {\n        try {\n            var hashFile = DIRECTORY.resolve(\"sourcehash\");\n            var hash = Files.exists(hashFile) ? Files.readString(hashFile).strip() : null;\n            return hash != null ? Integer.parseInt(hash) : 0;\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n            return 0;\n        }\n    }\n\n    public static void rebuildCache(Map<SystemIconSource, SystemIconSourceData> all, int sourceHash) {\n        try {\n            var versionFile = DIRECTORY.resolve(\"version\");\n            var version =\n                    Files.exists(versionFile) ? Files.readString(versionFile).strip() : null;\n            if (!String.valueOf(VERSION).equals(version)) {\n                if (Files.isDirectory(DIRECTORY)) {\n                    FileUtils.cleanDirectory(DIRECTORY.toFile());\n                } else {\n                    Files.createDirectories(DIRECTORY);\n                }\n                Files.writeString(versionFile, String.valueOf(VERSION));\n            }\n\n            var hashFile = DIRECTORY.resolve(\"sourcehash\");\n            Files.writeString(hashFile, String.valueOf(sourceHash));\n\n            for (var e : all.entrySet()) {\n                var target = DIRECTORY.resolve(e.getKey().getId());\n                Files.createDirectories(target);\n\n                Map<String, ImageColorScheme> colorSchemeMap = new HashMap<>();\n\n                var baseIcons = e.getValue().getIcons().stream()\n                        .filter(f -> f.getColorSchemeData() == SystemIconSourceFile.ColorSchemeData.DEFAULT)\n                        .toList();\n                for (var icon : baseIcons) {\n                    var schemeFile = target.resolve(icon.getName() + \".scheme\");\n                    if (refreshChecksum(icon.getFile(), target, icon.getName(), false)) {\n                        if (Files.exists(schemeFile)) {\n                            var scheme = Files.readString(schemeFile);\n                            var schemeValue = ImageColorScheme.valueOf(scheme.toUpperCase());\n                            colorSchemeMap.put(icon.getName(), schemeValue);\n                            continue;\n                        }\n                    }\n\n                    var scheme = rasterizeSizes(icon.getFile(), target, icon.getName(), false);\n                    if (scheme == ImageColorScheme.TRANSPARENT) {\n                        var message = \"Failed to rasterize icon \"\n                                + icon.getFile().getFileName().toString() + \": Rasterized image is transparent\";\n                        ErrorEventFactory.fromMessage(message).omit().expected().handle();\n                        continue;\n                    }\n\n                    colorSchemeMap.put(icon.getName(), scheme);\n                    Files.writeString(schemeFile, scheme.name().toLowerCase(Locale.ROOT));\n                }\n\n                var darkIconNames = e.getValue().getIcons().stream()\n                        .filter(f -> f.getColorSchemeData() == SystemIconSourceFile.ColorSchemeData.DARK)\n                        .map(f -> f.getName())\n                        .collect(Collectors.toSet());\n                var darkAvailableIcons = e.getValue().getIcons().stream()\n                        .filter(f -> f.getColorSchemeData() == SystemIconSourceFile.ColorSchemeData.DARK)\n                        .toList();\n                for (var icon : darkAvailableIcons) {\n                    var existingBaseScheme = colorSchemeMap.get(icon.getName());\n                    var generateDarkIcon = existingBaseScheme == null\n                            || existingBaseScheme == ImageColorScheme.DARK\n                            || AppPrefs.get().preferMonochromeIcons().get();\n\n                    if (!generateDarkIcon\n                            && !AppPrefs.get().preferMonochromeIcons().get()) {\n                        delete(target, icon.getName(), true);\n                        continue;\n                    }\n\n                    if (generateDarkIcon) {\n                        if (refreshChecksum(icon.getFile(), target, icon.getName(), true)) {\n                            continue;\n                        }\n\n                        var scheme = rasterizeSizes(icon.getFile(), target, icon.getName(), true);\n                        if (scheme == ImageColorScheme.TRANSPARENT) {\n                            var message = \"Failed to rasterize icon \"\n                                    + icon.getFile().getFileName().toString() + \": Rasterized image is transparent\";\n                            ErrorEventFactory.fromMessage(message)\n                                    .omit()\n                                    .expected()\n                                    .handle();\n                        }\n\n                        continue;\n                    }\n                }\n\n                // Generate dark icons manually if there is none provided by inverting the colors\n                for (var icon : baseIcons) {\n                    var existingBaseScheme = colorSchemeMap.get(icon.getName());\n                    var generateDarkModeInverse =\n                            existingBaseScheme == ImageColorScheme.DARK && !darkIconNames.contains(icon.getName());\n                    if (generateDarkModeInverse) {\n                        if (refreshChecksum(icon.getFile(), target, icon.getName(), true)) {\n                            continue;\n                        }\n\n                        rasterizeSizesInverted(icon.getFile(), target, icon.getName(), true);\n                        continue;\n                    }\n                }\n\n                if (AppPrefs.get().preferMonochromeIcons().get()) {\n                    var lightAvailableIcons = e.getValue().getIcons().stream()\n                            .filter(f -> f.getColorSchemeData() == SystemIconSourceFile.ColorSchemeData.LIGHT)\n                            .toList();\n                    for (var icon : lightAvailableIcons) {\n                        if (refreshChecksum(icon.getFile(), target, icon.getName(), false)) {\n                            continue;\n                        }\n\n                        rasterizeSizes(icon.getFile(), target, icon.getName(), false);\n                    }\n                }\n            }\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n        }\n    }\n\n    private static boolean refreshChecksum(Path source, Path dir, String name, boolean dark) throws Exception {\n        // Might have been deleted at some point\n        if (!Files.exists(source)) {\n            return true;\n        }\n\n        var md5Name = name + (dark ? \"-dark\" : \"\") + \".md5\";\n        var bytes = Files.readAllBytes(source);\n        var md = MessageDigest.getInstance(\"MD5\");\n        md.update(bytes);\n        md.update(String.valueOf(AppPrefs.get().preferMonochromeIcons().get()).getBytes());\n        var digest = md.digest();\n        var md5File = dir.resolve(md5Name);\n        if (Files.exists(md5File) && Arrays.equals(Files.readAllBytes(md5File), digest)) {\n            return true;\n        } else {\n            Files.write(md5File, digest);\n            return false;\n        }\n    }\n\n    private static ImageColorScheme rasterizeSizes(Path path, Path dir, String name, boolean dark) {\n        TrackEvent.trace(\"Rasterizing image \" + path.getFileName().toString());\n        try {\n            ImageColorScheme c = null;\n            for (var size : sizes) {\n                var image = rasterize(path, size);\n                if (image == null) {\n                    continue;\n                }\n                if (c == null) {\n                    c = determineColorScheme(image);\n                    if (c == ImageColorScheme.TRANSPARENT) {\n                        return ImageColorScheme.TRANSPARENT;\n                    }\n                }\n                write(dir, name, dark, size, image);\n            }\n            return c != null ? c : ImageColorScheme.TRANSPARENT;\n        } catch (Exception ex) {\n            var message = \"Failed to rasterize icon icon \" + path.getFileName().toString() + \": \" + ex.getMessage();\n            ErrorEventFactory.fromThrowable(ex)\n                    .description(message)\n                    .omit()\n                    .expected()\n                    .handle();\n            return ImageColorScheme.TRANSPARENT;\n        }\n    }\n\n    private static void rasterizeSizesInverted(Path path, Path dir, String name, boolean dark) throws IOException {\n        try {\n            for (var size : sizes) {\n                var image = rasterize(path, size);\n                if (image == null) {\n                    continue;\n                }\n\n                var invert = invert(image);\n                write(dir, name, dark, size, invert);\n            }\n        } catch (Exception ex) {\n            if (ex instanceof IOException) {\n                throw ex;\n            }\n\n            ErrorEventFactory.fromThrowable(ex).omit().expected().handle();\n        }\n    }\n\n    private static BufferedImage rasterize(Path path, int px) throws IOException {\n        SVGLoader loader = new SVGLoader();\n        URL svgUrl = path.toUri().toURL();\n        SVGDocument svgDocument = loader.load(svgUrl);\n        if (svgDocument == null) {\n            return null;\n        }\n\n        BufferedImage image = new BufferedImage(px, px, BufferedImage.TYPE_INT_ARGB);\n        Graphics2D g = image.createGraphics();\n        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);\n        g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);\n        g.setRenderingHint(SVGRenderingHints.KEY_IMAGE_ANTIALIASING, SVGRenderingHints.VALUE_IMAGE_ANTIALIASING_ON);\n        g.setRenderingHint(SVGRenderingHints.KEY_SOFT_CLIPPING, SVGRenderingHints.VALUE_SOFT_CLIPPING_ON);\n        svgDocument.render((Component) null, g, new ViewBox(0, 0, px, px));\n        g.dispose();\n        return image;\n    }\n\n    private static BufferedImage write(Path dir, String name, boolean dark, int px, BufferedImage image)\n            throws IOException {\n        var out = dir.resolve(name + \"-\" + px + (dark ? \"-dark\" : \"\") + \".png\");\n        ImageIO.write(image, \"png\", out.toFile());\n        return image;\n    }\n\n    private static void delete(Path dir, String name, boolean dark) throws IOException {\n        for (var px : sizes) {\n            var out = dir.resolve(name + \"-\" + px + (dark ? \"-dark\" : \"\") + \".png\");\n            Files.deleteIfExists(out);\n        }\n    }\n\n    private static BufferedImage invert(BufferedImage image) {\n        var buffer = new BufferedImage(image.getWidth(), image.getHeight(), java.awt.image.BufferedImage.TYPE_INT_ARGB);\n        for (int y = 0; y < image.getHeight(); y++) {\n            for (int x = 0; x < image.getWidth(); x++) {\n                int clr = image.getRGB(x, y);\n                int alpha = (clr >> 24) & 0xff;\n                int red = (clr & 0x00ff0000) >> 16;\n                int green = (clr & 0x0000ff00) >> 8;\n                int blue = clr & 0x000000ff;\n                buffer.setRGB(x, y, new Color(255 - red, 255 - green, 255 - blue, alpha).getRGB());\n            }\n        }\n        return buffer;\n    }\n\n    private static ImageColorScheme determineColorScheme(BufferedImage image) {\n        var transparent = true;\n        var counter = 0;\n        var mean = 0.0;\n        for (int y = 0; y < image.getHeight(); y++) {\n            for (int x = 0; x < image.getWidth(); x++) {\n                int clr = image.getRGB(x, y);\n                int alpha = (clr >> 24) & 0xff;\n                int red = (clr & 0x00ff0000) >> 16;\n                int green = (clr & 0x0000ff00) >> 8;\n                int blue = clr & 0x000000ff;\n\n                if (alpha > 0) {\n                    transparent = false;\n                }\n\n                if (alpha < 100) {\n                    continue;\n                }\n\n                mean += (red + green + blue) * (alpha / 255.0);\n                counter++;\n            }\n        }\n\n        if (transparent) {\n            return ImageColorScheme.TRANSPARENT;\n        }\n\n        if (counter == 0) {\n            return ImageColorScheme.TRANSPARENT;\n        }\n\n        mean /= counter * 3;\n        if (mean < 50) {\n            return ImageColorScheme.DARK;\n        } else if (mean > 195) {\n            return ImageColorScheme.LIGHT;\n        } else {\n            return ImageColorScheme.MIXED;\n        }\n    }\n\n    private enum ImageColorScheme {\n        TRANSPARENT,\n        MIXED,\n        LIGHT,\n        DARK\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/icon/SystemIconManager.java",
    "content": "package io.xpipe.app.icon;\n\nimport io.xpipe.app.core.*;\nimport io.xpipe.app.ext.ValidationException;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.*;\n\npublic class SystemIconManager {\n\n    private static final Path DIRECTORY =\n            AppProperties.get().getDataDir().resolve(\"cache\").resolve(\"icons\").resolve(\"pool\");\n\n    private static final Set<SystemIcon> loadedIconImages = new HashSet<>();\n    private static final Map<SystemIconSource, SystemIconSourceData> LOADED_SOURCES = new HashMap<>();\n    private static final Set<SystemIcon> ICONS = new HashSet<>();\n    private static int cacheSourceHash;\n    private static int sourceHash;\n\n    public static boolean hasLoadedAnyImages() {\n        var available = getIcons().stream()\n                .anyMatch(systemIcon -> AppImages.hasImage(\n                        \"icons/\" + systemIcon.getSource().getId() + \"/\" + systemIcon.getId() + \"-40.png\"));\n        return available;\n    }\n\n    public static boolean isCacheOutdated() {\n        return cacheSourceHash == 0 || sourceHash != cacheSourceHash;\n    }\n\n    public static List<SystemIconSource> getAllSources() {\n        var prefs = AppPrefs.get().getIconSources().getValue();\n        var all = new ArrayList<SystemIconSource>();\n        all.add(SystemIconSource.Directory.builder()\n                .path(DataStorage.getStorageDirectory().resolve(\"icons\"))\n                .id(\"custom\")\n                .build());\n        // For chinese users, GitHub link might be unreliable\n        // So use an alternative chinese mirror they can use\n        all.add(SystemIconSource.GitRepository.builder()\n                .remote(\"https://github.com/selfhst/icons\")\n                .id(\"selfhst\")\n                .build());\n        for (var pref : prefs) {\n            try {\n                pref.checkComplete();\n            } catch (ValidationException e) {\n                // This can be expected for synced directory sources\n                continue;\n            }\n\n            if (!all.contains(pref)) {\n                all.add(pref);\n            }\n        }\n        return all;\n    }\n\n    public static List<SystemIconSource> getEffectiveSources() {\n        var all = getAllSources();\n        var disabled = AppCache.getNonNull(\"disabledIconSources\", Set.class, () -> Set.<String>of());\n        all.removeIf(systemIconSource -> disabled.contains(systemIconSource.getId()));\n        return all;\n    }\n\n    public static Set<SystemIcon> getIcons() {\n        return ICONS;\n    }\n\n    public static synchronized String getAndLoadIconFile(SystemIcon icon) {\n        var id = \"icons/\" + icon.getSource().getId() + \"/\" + icon.getId() + \".svg\";\n        if (loadedIconImages.contains(icon)) {\n            return id;\n        }\n\n        var dir = SystemIconCache.getDirectory(icon.getSource());\n        var res = AppDisplayScale.hasOnlyDefaultDisplayScale() ? List.of(16, 24, 40) : List.of(16, 24, 40, 80);\n        var files = new ArrayList<Path>();\n        for (Integer re : res) {\n            files.add(dir.resolve(icon.getId() + \"-\" + re + \".png\"));\n            files.add(dir.resolve(icon.getId() + \"-\" + re + \"-dark.png\"));\n        }\n        for (var file : files) {\n            if (Files.isRegularFile(file)) {\n                AppImages.loadImage(file, \"icons/\" + icon.getSource().getId() + \"/\" + file.getFileName());\n            }\n        }\n\n        loadedIconImages.add(icon);\n        return id;\n    }\n\n    public static Optional<SystemIcon> getIcon(String id) {\n        var split = id.split(\"/\");\n        if (split.length == 2) {\n            var source = split[0];\n            var foundSource = getAllSources().stream()\n                    .filter(systemIconSource -> systemIconSource.getId().equals(source))\n                    .findFirst();\n            if (foundSource.isEmpty()) {\n                return Optional.empty();\n            }\n\n            var icon = new SystemIcon(foundSource.get(), split[1]);\n            var foundIcon = ICONS.contains(icon);\n            return foundIcon ? Optional.of(icon) : Optional.empty();\n        } else {\n            return Optional.empty();\n        }\n    }\n\n    private static synchronized int calculateSourceHash() {\n        var total = 0;\n        var set = false;\n        for (var e : LOADED_SOURCES.entrySet()) {\n            total += e.getKey().getPath().hashCode();\n            for (SystemIconSourceFile icon : e.getValue().getIcons()) {\n                total += icon.getFile().toString().hashCode();\n                set = true;\n            }\n        }\n\n        if (set) {\n            total += AppPrefs.get().preferMonochromeIcons().get() ? 1 : 0;\n            total += SystemIconCache.VERSION;\n        }\n\n        return total != 0 ? total : set ? -1 : 0;\n    }\n\n    public static void init() throws Exception {\n        cacheSourceHash = SystemIconCache.getCacheSourceHash();\n        reloadSources();\n        sourceHash = calculateSourceHash();\n        AppPrefs.get().preferMonochromeIcons().addListener((observableValue, o, n) -> {\n            sourceHash = calculateSourceHash();\n        });\n        AppPrefs.get().getIconSources().addListener((observableValue, o, n) -> {\n            sourceHash = calculateSourceHash();\n        });\n    }\n\n    public static void initAdditional() {\n        for (var source : getEffectiveSources()) {\n            if (!LOADED_SOURCES.containsKey(source)) {\n                var data = SystemIconSourceData.of(source);\n                LOADED_SOURCES.put(source, data);\n                data.getIcons().forEach(systemIconSourceFile -> {\n                    var icon = new SystemIcon(source, systemIconSourceFile.getName());\n                    ICONS.add(icon);\n                });\n            }\n        }\n        sourceHash = calculateSourceHash();\n    }\n\n    public static synchronized void reloadSources() throws Exception {\n        Files.createDirectories(DIRECTORY);\n\n        LOADED_SOURCES.clear();\n        for (var source : getEffectiveSources()) {\n            LOADED_SOURCES.put(source, SystemIconSourceData.of(source));\n        }\n\n        ICONS.clear();\n        LOADED_SOURCES.forEach((source, systemIconSourceData) -> {\n            systemIconSourceData.getIcons().forEach(systemIconSourceFile -> {\n                var icon = new SystemIcon(source, systemIconSourceFile.getName());\n                ICONS.add(icon);\n            });\n        });\n    }\n\n    public static void prepareUsedIconImages() {\n        for (DataStoreEntry storeEntry : DataStorage.get().getStoreEntries()) {\n            storeEntry.getEffectiveIconFile();\n        }\n    }\n\n    private static synchronized void reloadImages() {\n        AppImages.remove(s -> s.startsWith(\"icons/\"));\n        loadedIconImages.clear();\n\n        try {\n            for (var loadedIconImage : ICONS) {\n                getAndLoadIconFile(loadedIconImage);\n            }\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n        }\n    }\n\n    public static synchronized void rebuild() throws Exception {\n        Files.createDirectories(DIRECTORY);\n        for (var source : getEffectiveSources()) {\n            try {\n                source.refresh();\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).expected().handle();\n            }\n        }\n        reloadSources();\n        sourceHash = calculateSourceHash();\n        SystemIconCache.rebuildCache(LOADED_SOURCES, sourceHash);\n        cacheSourceHash = sourceHash;\n        reloadImages();\n    }\n\n    public static synchronized void reloadSourceHashes() throws Exception {\n        Files.createDirectories(DIRECTORY);\n        reloadSources();\n        sourceHash = calculateSourceHash();\n    }\n\n    public static synchronized void loadAllAvailableIconImages() {\n        for (SystemIcon icon : getIcons()) {\n            getAndLoadIconFile(icon);\n        }\n    }\n\n    public static Path getPoolPath() {\n        return AppProperties.get()\n                .getDataDir()\n                .resolve(\"cache\")\n                .resolve(\"icons\")\n                .resolve(\"pool\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/icon/SystemIconSource.java",
    "content": "package io.xpipe.app.icon;\n\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.ext.ValidationException;\nimport io.xpipe.app.util.DesktopHelper;\nimport io.xpipe.app.util.Hyperlinks;\nimport io.xpipe.app.util.Validators;\nimport io.xpipe.core.FilePath;\n\nimport com.fasterxml.jackson.annotation.JsonSubTypes;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\n@JsonSubTypes({\n    @JsonSubTypes.Type(value = SystemIconSource.Directory.class),\n    @JsonSubTypes.Type(value = SystemIconSource.GitRepository.class)\n})\npublic interface SystemIconSource {\n\n    void checkComplete() throws ValidationException;\n\n    void refresh() throws Exception;\n\n    String getId();\n\n    Path getPath();\n\n    String getIcon();\n\n    String getDisplayName();\n\n    String getDescription();\n\n    void open();\n\n    @Value\n    @Builder\n    @Jacksonized\n    @JsonTypeName(\"directory\")\n    class Directory implements SystemIconSource {\n\n        Path path;\n        String id;\n\n        @Override\n        public void checkComplete() throws ValidationException {\n            Validators.nonNull(path);\n            if (path.getParent() == null) {\n                throw new ValidationException(\"Directory is a root\");\n            }\n            Validators.notEmpty(id);\n        }\n\n        @Override\n        public void refresh() {}\n\n        @Override\n        public Path getPath() {\n            return path;\n        }\n\n        @Override\n        public String getIcon() {\n            return \"mdi2f-folder\";\n        }\n\n        @Override\n        public String getDisplayName() {\n            var name = path.getFileName();\n            return name != null ? name.toString() : path.toString();\n        }\n\n        @Override\n        public String getDescription() {\n            return path.toString();\n        }\n\n        @Override\n        public void open() {\n            if (Files.exists(path)) {\n                DesktopHelper.browseFile(path);\n            }\n        }\n    }\n\n    @Value\n    @Builder\n    @Jacksonized\n    @JsonTypeName(\"git\")\n    class GitRepository implements SystemIconSource {\n\n        String remote;\n        String id;\n\n        @Override\n        public void checkComplete() throws ValidationException {\n            Validators.notEmpty(remote);\n            Validators.notEmpty(id);\n        }\n\n        @Override\n        public void refresh() throws Exception {\n            var dir = SystemIconManager.getPoolPath().resolve(id);\n            if (!Files.exists(dir)) {\n                ProcessControlProvider.get().cloneRepository(remote, dir);\n            } else {\n                ProcessControlProvider.get().pullRepository(dir);\n            }\n        }\n\n        @Override\n        public Path getPath() {\n            return SystemIconManager.getPoolPath().resolve(id);\n        }\n\n        @Override\n        public String getIcon() {\n            return \"mdi2g-git\";\n        }\n\n        @Override\n        public String getDisplayName() {\n            return FilePath.of(remote).getFileName();\n        }\n\n        @Override\n        public String getDescription() {\n            return \"Git repository \" + remote;\n        }\n\n        @Override\n        public void open() {\n            Hyperlinks.open(remote);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/icon/SystemIconSourceData.java",
    "content": "package io.xpipe.app.icon;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\n\nimport lombok.Value;\nimport org.apache.commons.io.FilenameUtils;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\n@Value\npublic class SystemIconSourceData {\n\n    Path directory;\n    List<SystemIconSourceFile> icons;\n\n    public static SystemIconSourceData of(SystemIconSource source) {\n        var target = source.getPath();\n        var list = new ArrayList<SystemIconSourceFile>();\n        walkTree(source, target, list);\n        return new SystemIconSourceData(target, list);\n    }\n\n    private static void walkTree(SystemIconSource source, Path dir, List<SystemIconSourceFile> sourceFiles) {\n        try {\n            if (!Files.isDirectory(dir)) {\n                return;\n            }\n\n            var files = Files.walk(dir).toList();\n            var flatFiles = files.stream()\n                    .filter(path -> Files.isRegularFile(path))\n                    .filter(path -> path.toString().endsWith(\".svg\"))\n                    .map(path -> {\n                        var name = FilenameUtils.getBaseName(path.getFileName().toString());\n                        var cleanedName = name.replaceFirst(\"-light$\", \"\").replaceFirst(\"-dark$\", \"\");\n                        var cleanedPath = path.getParent().resolve(cleanedName + \".svg\");\n                        return cleanedPath;\n                    })\n                    .collect(Collectors.toCollection(LinkedHashSet::new));\n            for (var file : flatFiles) {\n                var name = FilenameUtils.getBaseName(file.getFileName().toString());\n                var displayName = name.toLowerCase(Locale.ROOT);\n                var baseFile = file.getParent().resolve(name + \".svg\");\n                var hasBaseVariant = Files.exists(baseFile);\n                var darkModeFile = file.getParent().resolve(name + \"-light.svg\");\n                var hasDarkModeVariant = Files.exists(darkModeFile);\n                var lightModeFile = file.getParent().resolve(name + \"-dark.svg\");\n                var hasLightModeVariant = Files.exists(lightModeFile);\n\n                if (hasBaseVariant) {\n                    sourceFiles.add(new SystemIconSourceFile(\n                            source, displayName, baseFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));\n                }\n                if (hasLightModeVariant) {\n                    sourceFiles.add(new SystemIconSourceFile(\n                            source, displayName, lightModeFile, SystemIconSourceFile.ColorSchemeData.LIGHT));\n                }\n                if (hasDarkModeVariant) {\n                    sourceFiles.add(new SystemIconSourceFile(\n                            source, displayName, darkModeFile, SystemIconSourceFile.ColorSchemeData.DARK));\n                }\n            }\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).expected().handle();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/icon/SystemIconSourceFile.java",
    "content": "package io.xpipe.app.icon;\n\nimport lombok.Value;\n\nimport java.nio.file.Path;\n\n@Value\npublic class SystemIconSourceFile {\n\n    SystemIconSource source;\n    String name;\n    Path file;\n    ColorSchemeData colorSchemeData;\n\n    public enum ColorSchemeData {\n        LIGHT,\n        DARK,\n        DEFAULT\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/issue/AttachmentHelper.java",
    "content": "package io.xpipe.app.issue;\n\nimport org.apache.commons.io.IOUtils;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipOutputStream;\n\npublic class AttachmentHelper {\n\n    public static Path compressZipfile(Path sourceDir, Path outputFile) throws IOException {\n        ZipOutputStream zipFile = new ZipOutputStream(new FileOutputStream(outputFile.toFile()));\n        compressDirectoryToZipfile(sourceDir, sourceDir, zipFile);\n        IOUtils.closeQuietly(zipFile);\n        return outputFile;\n    }\n\n    private static void compressDirectoryToZipfile(Path rootDir, Path sourceDir, ZipOutputStream out)\n            throws IOException {\n        var files = sourceDir.toFile().listFiles();\n        if (files == null) {\n            return;\n        }\n\n        for (File file : files) {\n            if (file.isDirectory()) {\n                compressDirectoryToZipfile(rootDir, sourceDir.resolve(file.getName()), out);\n            } else {\n                ZipEntry entry = new ZipEntry(\n                        rootDir.relativize(sourceDir).resolve(file.getName()).toString());\n                out.putNextEntry(entry);\n\n                FileInputStream in =\n                        new FileInputStream(sourceDir.resolve(file.getName()).toString());\n                IOUtils.copy(in, out);\n                IOUtils.closeQuietly(in);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/issue/ErrorAction.java",
    "content": "package io.xpipe.app.issue;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.util.Hyperlinks;\nimport io.xpipe.app.util.LicenseProvider;\nimport io.xpipe.core.FailableSupplier;\n\npublic interface ErrorAction {\n\n    static ErrorAction openDocumentation(String link) {\n        return translated(\"openDocumentation\", () -> {\n            Hyperlinks.open(link);\n            return false;\n        });\n    }\n\n    static ErrorAction translated(String key, FailableSupplier<Boolean> r) {\n        return new ErrorAction() {\n            @Override\n            public String getName() {\n                return AppI18n.get(key);\n            }\n\n            @Override\n            public String getDescription() {\n                return AppI18n.get(key + \"Description\");\n            }\n\n            @Override\n            public boolean handle(ErrorEvent event) throws Exception {\n                return r.get();\n            }\n        };\n    }\n\n    static IgnoreAction ignore() {\n        return new IgnoreAction();\n    }\n\n    String getName();\n\n    String getDescription();\n\n    boolean handle(ErrorEvent event) throws Exception;\n\n    class IgnoreAction implements ErrorAction {\n        @Override\n        public String getName() {\n            return AppI18n.get(\"ignoreError\");\n        }\n\n        @Override\n        public String getDescription() {\n            return AppI18n.get(\"ignoreErrorDescription\");\n        }\n\n        @Override\n        public boolean handle(ErrorEvent event) {\n            if (!event.isReportable()\n                    || (LicenseProvider.get() != null && !LicenseProvider.get().shouldReportError())) {\n                return true;\n            }\n\n            SentryErrorHandler.getInstance().handle(event);\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/issue/ErrorEvent.java",
    "content": "package io.xpipe.app.issue;\n\nimport io.xpipe.app.util.DocumentationLink;\n\nimport lombok.Builder;\nimport lombok.Getter;\nimport lombok.Setter;\nimport lombok.Singular;\n\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.concurrent.CopyOnWriteArraySet;\n\n@Builder\n@Getter\npublic class ErrorEvent {\n\n    private static final Set<Throwable> HANDLED = new CopyOnWriteArraySet<>();\n\n    @Builder.Default\n    private final boolean omitted = false;\n\n    @Builder.Default\n    private final boolean reportable = true;\n\n    private final Throwable throwable;\n\n    @Singular\n    private final List<ErrorAction> customActions;\n\n    private String description;\n    private boolean terminal;\n\n    @Setter\n    private boolean shouldSendDiagnostics;\n\n    @Singular\n    private List<Path> attachments;\n\n    private String link;\n\n    private String email;\n    private String userReport;\n    private boolean unhandled;\n\n    public void attachUserReport(String email, String text) {\n        this.email = email;\n        userReport = text;\n    }\n\n    public List<Throwable> getThrowableChain() {\n        var list = new ArrayList<Throwable>();\n        Throwable t = getThrowable();\n        while (t != null) {\n            list.addFirst(t);\n            t = t.getCause();\n        }\n        return list;\n    }\n\n    private boolean shouldIgnore(Throwable throwable) {\n        return (throwable != null && HANDLED.stream().anyMatch(t -> t == throwable) && !terminal)\n                || (throwable != null && throwable.getCause() != throwable && shouldIgnore(throwable.getCause()));\n    }\n\n    public void handle() {\n        // Check object identity to allow for multiple exceptions with same trace\n        if (shouldIgnore(throwable)) {\n            return;\n        }\n\n        EventHandler.get().modify(this);\n        EventHandler.get().handle(this);\n        HANDLED.add(throwable);\n    }\n\n    public void addAttachment(Path file) {\n        attachments = new ArrayList<>(attachments);\n        attachments.add(file);\n    }\n\n    public void clearAttachments() {\n        attachments = new ArrayList<>();\n    }\n\n    public static class ErrorEventBuilder {\n\n        public ErrorEventBuilder documentationLink(DocumentationLink documentationLink) {\n            return link(documentationLink.getLink());\n        }\n\n        public ErrorEventBuilder term() {\n            return terminal(true);\n        }\n\n        public ErrorEventBuilder omit() {\n            return omitted(true);\n        }\n\n        public ErrorEventBuilder expected() {\n            return reportable(false);\n        }\n\n        public ErrorEventBuilder discard() {\n            return omit().expected();\n        }\n\n        public ErrorEventBuilder ignore() {\n            if (throwable != null) {\n                HANDLED.add(throwable);\n            }\n            return this;\n        }\n\n        public ErrorEvent handle() {\n            var event = build();\n            event.handle();\n            return event;\n        }\n\n        public void expectedIfContains(String... s) {\n            var contains = throwable != null\n                    && throwable.getMessage() != null\n                    && Arrays.stream(s).map(String::toLowerCase).anyMatch(string -> throwable\n                            .getMessage()\n                            .toLowerCase(Locale.ROOT)\n                            .endsWith(string));\n            if (contains) {\n                expected();\n            }\n        }\n\n        Throwable getThrowable() {\n            return throwable;\n        }\n\n        String getLink() {\n            return link;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/issue/ErrorEventFactory.java",
    "content": "package io.xpipe.app.issue;\n\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.process.ProcessOutputException;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.core.OsType;\n\nimport java.nio.file.AccessDeniedException;\nimport java.nio.file.NoSuchFileException;\nimport java.util.Arrays;\nimport java.util.IdentityHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport javax.net.ssl.SSLHandshakeException;\n\npublic class ErrorEventFactory {\n\n    private static final Map<Throwable, ErrorEvent.ErrorEventBuilder> EVENT_BASES = new IdentityHashMap<>();\n\n    public static ErrorEvent.ErrorEventBuilder fromThrowable(Throwable t) {\n        var b = retrieveBuilder(t);\n        return b;\n    }\n\n    public static ErrorEvent.ErrorEventBuilder fromThrowable(String msg, Throwable t) {\n        var b = retrieveBuilder(t);\n        return b.description(msg);\n    }\n\n    public static ErrorEvent.ErrorEventBuilder fromMessage(String msg) {\n        return ErrorEvent.builder().description(msg);\n    }\n\n    public static <T extends Throwable> T expectedIfContains(T t, String... s) {\n        return expectedIf(\n                t,\n                t.getMessage() != null\n                        && Arrays.stream(s).map(String::toLowerCase).anyMatch(string -> t.getMessage()\n                                .toLowerCase(Locale.ROOT)\n                                .contains(string)));\n    }\n\n    public static <T extends Throwable> T expectedIf(T t, boolean b) {\n        if (b) {\n            preconfigure(fromThrowable(t).expected());\n        }\n        return t;\n    }\n\n    public static <T extends Throwable> T expected(T t) {\n        preconfigure(fromThrowable(t).expected());\n        return t;\n    }\n\n    public static synchronized void preconfigure(ErrorEvent.ErrorEventBuilder event) {\n        EVENT_BASES.put(event.getThrowable(), event);\n    }\n\n    private static synchronized ErrorEvent.ErrorEventBuilder retrieveBuilder(Throwable t) {\n        var b = EVENT_BASES.remove(t);\n        if (b == null) {\n            b = ErrorEvent.builder().throwable(t);\n        }\n\n        if (t instanceof SSLHandshakeException\n                || (t.getClass().getName().equals(\"sun.security.provider.certpath.SunCertPathBuilderException\"))) {\n            if (b.getLink() == null) {\n                b.documentationLink(DocumentationLink.TLS_DECRYPTION);\n            }\n            b.expected();\n        }\n\n        // Indicates that the session is scheduled to end and new processes won't be started\n        if (OsType.ofLocal() == OsType.WINDOWS\n                && t instanceof ProcessOutputException pex\n                && pex.getExitCode() == -1073741205) {\n            b.expected();\n        }\n\n        // On Linux shutdown, active file descriptors are getting closed. This breaks shell commands\n        if (OsType.ofLocal() == OsType.LINUX\n                && AppOperationMode.isInShutdown()\n                && t instanceof IllegalStateException ise\n                && \"Parent stream is closed\".equals(ise.getMessage())) {\n            b.expected();\n        }\n\n        if (OsType.ofLocal() == OsType.WINDOWS && t.getMessage() != null && t.getMessage().contains(\"The cloud file provider is not running\")) {\n            b.description(\"The OneDrive cloud file provider is not running. Verify that your cloud storage is working and you are logged in.\");\n            b.expected();\n        }\n\n        if (t instanceof AccessDeniedException ade) {\n            b.description(\"Access is denied: \" + ade.getMessage());\n            b.expected();\n        }\n\n        if (t instanceof NoSuchFileException nsfe) {\n            b.description(\"No such file: \" + nsfe.getMessage());\n            b.expected();\n        }\n\n        return b;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/issue/ErrorHandler.java",
    "content": "package io.xpipe.app.issue;\n\npublic interface ErrorHandler {\n\n    void handle(ErrorEvent event);\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java",
    "content": "package io.xpipe.app.issue;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.ButtonComp;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.process.ProcessOutputException;\nimport io.xpipe.app.util.BooleanScope;\nimport io.xpipe.app.util.LicenseRequiredException;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.beans.property.BooleanProperty;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.geometry.Insets;\nimport javafx.geometry.Orientation;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.Separator;\nimport javafx.scene.control.TextArea;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.VBox;\n\nimport lombok.Getter;\n\nimport static atlantafx.base.theme.Styles.ACCENT;\nimport static atlantafx.base.theme.Styles.BUTTON_OUTLINED;\n\npublic class ErrorHandlerComp extends SimpleRegionBuilder {\n\n    private final ErrorEvent event;\n    private final Runnable closeDialogAction;\n\n    @Getter\n    private final Property<ErrorAction> takenAction = new SimpleObjectProperty<>();\n\n    public ErrorHandlerComp(ErrorEvent event, Runnable closeDialogAction) {\n        this.event = event;\n        this.closeDialogAction = closeDialogAction;\n    }\n\n    private Region createActionButtonGraphic(String nameString, String descString) {\n        var header = new Label(nameString);\n        AppFontSizes.xl(header);\n        var desc = new Label(descString);\n        AppFontSizes.xs(desc);\n        var text = new VBox(header, desc);\n        text.setSpacing(2);\n        return text;\n    }\n\n    private Region createActionComp(ErrorAction a, BooleanProperty busy) {\n        var graphic = createActionButtonGraphic(a.getName(), a.getDescription());\n        var b = new ButtonComp(null, graphic, () -> {\n            takenAction.setValue(a);\n            ThreadHelper.runAsync(() -> {\n                try (var ignored = new BooleanScope(busy).start()) {\n                    var r = a.handle(event);\n                    if (r) {\n                        closeDialogAction.run();\n                    }\n                } catch (Exception ignored) {\n                    closeDialogAction.run();\n                }\n            });\n        });\n        b.disable(busy);\n        b.maxWidth(2000);\n        return b.build();\n    }\n\n    private Region createTop() {\n        var desc = getEventDescription();\n\n        // Account for line wrapping of long lines\n        var estimatedLineCount = desc.lines()\n                .mapToInt(s -> Math.max(1, (int) Math.ceil(s.length() / 80.0)))\n                .sum();\n\n        var descriptionField = new TextArea(desc);\n        descriptionField.setPrefRowCount(Math.max(5, Math.min(estimatedLineCount + 2, 14)));\n        descriptionField.setWrapText(true);\n        descriptionField.setEditable(false);\n        descriptionField.setPadding(Insets.EMPTY);\n        descriptionField.getStyleClass().add(\"description\");\n        AppFontSizes.base(descriptionField);\n        var text = new VBox(descriptionField);\n        text.setFillWidth(true);\n        text.setSpacing(8);\n        return text;\n    }\n\n    private String getEventDescription() {\n        var desc = event.getDescription();\n\n        Throwable t = event.getThrowable();\n        while (t != null) {\n            var toAppend = t.getMessage() != null\n                    ? t.getMessage()\n                    : AppI18n.get(\"errorTypeOccured\", t.getClass().getSimpleName());\n            desc = desc != null ? desc + \"\\n\\n\" + toAppend : toAppend;\n            t = t.getCause() != t && !(t instanceof ProcessOutputException) ? t.getCause() : null;\n        }\n\n        if (desc == null && event.getThrowable() != null) {\n            desc = AppI18n.get(\"errorNoExceptionMessage\", event.getThrowable().getClass().getSimpleName());\n        }\n\n        if (desc == null) {\n            desc = AppI18n.get(\"errorNoDetail\");\n        }\n\n        desc = desc.strip();\n\n        if (event.isTerminal()) {\n            desc = desc + \"\\n\\n\" + AppI18n.get(\"terminalErrorDescription\");\n        }\n\n        return desc;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var top = createTop();\n        var content = new VBox(top);\n        var header = new Label(AppI18n.get(\"possibleActions\"));\n        header.setPadding(new Insets(0, 0, 2, 3));\n        AppFontSizes.xl(header);\n        var actionBox = new VBox();\n        actionBox.getStyleClass().add(\"actions\");\n        actionBox.setFillWidth(true);\n\n        if (event.getThrowable() instanceof LicenseRequiredException) {\n            event.getCustomActions().add(new ErrorAction() {\n                @Override\n                public String getName() {\n                    return AppI18n.get(\"upgrade\");\n                }\n\n                @Override\n                public String getDescription() {\n                    return AppI18n.get(\"seeTiers\");\n                }\n\n                @Override\n                public boolean handle(ErrorEvent event) {\n                    AppLayoutModel.get().selectLicense();\n                    return true;\n                }\n            });\n        }\n\n        var custom = event.getCustomActions();\n        var busy = new SimpleBooleanProperty();\n        for (var c : custom) {\n            var ac = createActionComp(c, busy);\n            ac.getStyleClass().addAll(BUTTON_OUTLINED, ACCENT);\n            actionBox.getChildren().add(ac);\n        }\n\n        if (event.getLink() != null) {\n            var ac = createActionComp(ErrorAction.openDocumentation(event.getLink()), busy);\n            ac.getStyleClass().addAll(BUTTON_OUTLINED, ACCENT);\n            actionBox.getChildren().add(ac);\n        }\n\n        var hasCustomActions = event.getCustomActions().size() > 0 || event.getLink() != null;\n        if (hasCustomActions) {\n            actionBox.getChildren().add(createActionComp(ErrorAction.ignore(), busy));\n        }\n\n        if (actionBox.getChildren().size() > 0) {\n            actionBox.getChildren().addFirst(header);\n            content.getChildren().add(new Separator(Orientation.HORIZONTAL));\n            actionBox.getChildren().get(1).getStyleClass().addAll(BUTTON_OUTLINED);\n            content.getChildren().addAll(actionBox);\n        }\n\n        content.getStyleClass().add(\"top\");\n        content.setFillWidth(true);\n        content.setMinHeight(Region.USE_PREF_SIZE);\n\n        var layout = new VBox();\n        layout.getChildren().add(content);\n        layout.getStyleClass().add(\"error-handler-comp\");\n\n        return layout;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/issue/ErrorHandlerDialog.java",
    "content": "package io.xpipe.app.issue;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.process.ProcessOutputException;\nimport io.xpipe.app.util.Deobfuscator;\n\nimport javafx.application.Platform;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.geometry.Insets;\nimport javafx.scene.control.TextArea;\nimport javafx.scene.layout.Region;\n\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.concurrent.atomic.AtomicReference;\n\npublic class ErrorHandlerDialog {\n\n    public static void showAndWait(ErrorEvent event) {\n        // There might be unfortunate freezes when there are errors on the platform\n        // thread on startup\n        if (Platform.isFxApplicationThread() && AppOperationMode.isInStartup()) {\n            ErrorAction.ignore().handle(event);\n        }\n\n        try {\n            var modal = new AtomicReference<ModalOverlay>();\n            var comp = new ErrorHandlerComp(event, () -> {\n                AppDialog.closeDialog(modal.get());\n            });\n            comp.prefWidth(event.getThrowable() != null ? 600 : 500);\n            var headerId = event.isTerminal() ? \"terminalErrorOccured\" : \"errorOccured\";\n            var errorModal = ModalOverlay.of(headerId, comp, new LabelGraphic.NodeGraphic(() -> {\n                var graphic = new FontIcon(\"mdomz-warning\");\n                graphic.getStyleClass().add(\"graphic\");\n                graphic.getStyleClass().add(\"error\");\n                return graphic;\n            }));\n            if (event.getThrowable() != null && event.isReportable()) {\n                errorModal.addButton(new ModalButton(\n                        \"stackTrace\",\n                        () -> {\n                            var detailsModal = ModalOverlay.of(\"errorDetails\", RegionBuilder.of(() -> {\n                                var content = createStackTraceContent(event);\n                                content.setPrefWidth(650);\n                                content.setPrefHeight(750);\n                                return content;\n                            }));\n                            detailsModal.show();\n                        },\n                        false,\n                        false));\n            }\n            if (event.isReportable()) {\n                var reported = new SimpleBooleanProperty();\n                errorModal\n                        .addButton(new ModalButton(\n                                \"report\",\n                                () -> {\n                                    if (UserReportComp.show(event)) {\n                                        reported.set(true);\n                                    }\n                                },\n                                false,\n                                false))\n                        .augment(button -> button.disableProperty().bind(reported));\n                errorModal.addButtonBarComp(RegionBuilder.hspacer());\n            }\n            var hasCustomActions = event.getCustomActions().size() > 0 || event.getLink() != null;\n            var hideOk = hasCustomActions;\n            if (!hideOk) {\n                errorModal.addButton(ModalButton.ok());\n            }\n            modal.set(errorModal);\n            AppDialog.showAndWait(modal.get());\n            if (comp.getTakenAction().getValue() == null) {\n                ErrorAction.ignore().handle(event);\n                comp.getTakenAction().setValue(ErrorAction.ignore());\n            }\n        } catch (Throwable t) {\n            ErrorAction.ignore().handle(ErrorEventFactory.fromThrowable(t).build());\n            ErrorAction.ignore().handle(event);\n        }\n    }\n\n    private static Region createStackTraceContent(ErrorEvent event) {\n        if (event.getThrowable() != null) {\n            String stackTrace = Deobfuscator.deobfuscateToString(event.getThrowable());\n            if (event.getThrowable() instanceof ProcessOutputException pex) {\n                stackTrace = stackTrace.replace(event.getThrowable().getMessage(), pex.getDetailedMessage());\n            }\n            stackTrace = stackTrace.replace(\"\\t\", \"\");\n            var tf = new TextArea(stackTrace);\n            AppFontSizes.xs(tf);\n            tf.setWrapText(true);\n            tf.setEditable(false);\n            tf.setPadding(new Insets(10, 0, 10, 0));\n            return tf;\n        }\n\n        return new Region();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/issue/EventHandler.java",
    "content": "package io.xpipe.app.issue;\n\nimport java.util.ServiceLoader;\n\npublic abstract class EventHandler {\n\n    private static EventHandler INSTANCE;\n\n    private static void init() {\n        if (INSTANCE == null) {\n            INSTANCE = ServiceLoader.load(EventHandler.class).findFirst().orElseThrow();\n        }\n    }\n\n    public static EventHandler get() {\n        init();\n        return INSTANCE;\n    }\n\n    public abstract void handle(TrackEvent te);\n\n    public abstract void handle(ErrorEvent ee);\n\n    public abstract void modify(ErrorEvent ee);\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/issue/EventHandlerImpl.java",
    "content": "package io.xpipe.app.issue;\n\nimport io.xpipe.app.core.AppLogs;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.util.Deobfuscator;\n\nimport java.nio.file.Path;\n\npublic class EventHandlerImpl extends EventHandler {\n\n    public static TrackEvent fromErrorEvent(ErrorEvent ee) {\n        var te = TrackEvent.builder();\n        var prefix = ee.getDescription() != null ? ee.getDescription() + \":\\n\" : \"\";\n        var suffix = ee.getThrowable() != null ? Deobfuscator.deobfuscateToString(ee.getThrowable()) : \"\";\n        te.message(prefix + suffix);\n        te.type(\"error\");\n        te.tag(\"omitted\", ee.isOmitted());\n        te.tag(\"terminal\", ee.isTerminal());\n        te.elements(ee.getAttachments().stream().map(Path::toString).toList());\n        return te.build();\n    }\n\n    @Override\n    public void handle(TrackEvent te) {\n        if (AppLogs.get() != null) {\n            AppLogs.get().logEvent(te);\n        } else {\n            System.out.println(te);\n            System.out.flush();\n        }\n    }\n\n    @Override\n    public void handle(ErrorEvent ee) {\n        if (AppProperties.get() != null && AppProperties.get().isAotTrainMode()) {\n            new LogErrorHandler().handle(ee);\n            if (ee.isTerminal()) {\n                AppOperationMode.halt(1);\n            }\n            return;\n        }\n\n        if (ee.isTerminal()) {\n            new TerminalErrorHandler().handle(ee);\n            return;\n        }\n\n        // Don't block shutdown\n        if (AppOperationMode.isInShutdown()) {\n            handleOnShutdown(ee);\n            return;\n        }\n\n        if (AppOperationMode.get() == null) {\n            AppOperationMode.BACKGROUND.getErrorHandler().handle(ee);\n        } else {\n            AppOperationMode.get().getErrorHandler().handle(ee);\n        }\n    }\n\n    @Override\n    public void modify(ErrorEvent ee) {\n        if (AppLogs.get() != null && AppLogs.get().getSessionLogsDirectory() != null) {\n            ee.addAttachment(AppLogs.get().getSessionLogsDirectory());\n        }\n    }\n\n    private void handleOnShutdown(ErrorEvent ee) {\n        ErrorAction.ignore().handle(ee);\n        handle(fromErrorEvent(ee));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/issue/GuiErrorHandler.java",
    "content": "package io.xpipe.app.issue;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.util.LicenseProvider;\nimport io.xpipe.app.util.LicenseRequiredException;\n\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.time.Duration;\nimport java.util.stream.Stream;\n\npublic class GuiErrorHandler extends GuiErrorHandlerBase implements ErrorHandler {\n\n    private final ErrorHandler log = new LogErrorHandler();\n\n    @Override\n    public void handle(ErrorEvent event) {\n        log.handle(event);\n\n        if (!startupGui(throwable -> {\n            var second = ErrorEventFactory.fromThrowable(throwable).build();\n            log.handle(second);\n            ErrorAction.ignore().handle(second);\n        })) {\n            return;\n        }\n\n        if (event.isOmitted()) {\n            ErrorAction.ignore().handle(event);\n            if (AppLayoutModel.get() != null) {\n                AppLayoutModel.get()\n                        .showQueueEntry(\n                                new AppLayoutModel.QueueEntry(\n                                        AppI18n.observable(\"errorOccurred\"),\n                                        new LabelGraphic.NodeGraphic(() -> {\n                                            var graphic = new FontIcon(\"mdoal-error_outline\");\n                                            graphic.getStyleClass().add(\"graphic\");\n                                            graphic.getStyleClass().add(\"error\");\n                                            return graphic;\n                                        }),\n                                        () -> {\n                                            handleGui(event);\n                                            return true;\n                                        }),\n                                Duration.ofSeconds(10),\n                                true);\n            }\n            return;\n        }\n\n        handleGui(event);\n    }\n\n    private void handleGui(ErrorEvent event) {\n        var lex = event.getThrowableChain().stream()\n                .flatMap(throwable -> throwable instanceof LicenseRequiredException le ? Stream.of(le) : Stream.of())\n                .findFirst();\n        if (lex.isPresent()) {\n            LicenseProvider.get().showLicenseAlert(lex.get());\n            if (!LicenseProvider.get().hasPaidLicense()) {\n                event.clearAttachments();\n                ErrorAction.ignore().handle(event);\n            }\n        } else {\n            ErrorHandlerDialog.showAndWait(event);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/issue/GuiErrorHandlerBase.java",
    "content": "package io.xpipe.app.issue;\n\nimport io.xpipe.app.core.*;\nimport io.xpipe.app.core.window.AppMainWindow;\nimport io.xpipe.app.platform.PlatformInit;\n\nimport java.util.function.Consumer;\n\npublic class GuiErrorHandlerBase {\n\n    protected boolean startupGui(Consumer<Throwable> onFail) {\n        try {\n            AppProperties.init();\n            AppExtensionManager.init();\n\n            if (PlatformInit.isLoadingThread()) {\n                return false;\n            }\n\n            PlatformInit.init(true);\n            AppMainWindow.init(true);\n        } catch (Throwable ex) {\n            onFail.accept(ex);\n            return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/issue/LogErrorHandler.java",
    "content": "package io.xpipe.app.issue;\n\nimport io.xpipe.app.core.AppLogs;\nimport io.xpipe.app.util.Deobfuscator;\n\npublic class LogErrorHandler implements ErrorHandler {\n\n    @Override\n    public void handle(ErrorEvent event) {\n        if (AppLogs.get() != null) {\n            if (event.getThrowable() != null) {\n                AppLogs.get().logException(event.getDescription(), event.getThrowable());\n            } else {\n                AppLogs.get()\n                        .logEvent(TrackEvent.fromMessage(\"error\", event.getDescription())\n                                .build());\n            }\n            AppLogs.get().flush();\n            return;\n        }\n\n        if (event.getDescription() != null) {\n            System.err.println(event.getDescription());\n        }\n        if (event.getThrowable() != null) {\n            var s = Deobfuscator.deobfuscateToString(event.getThrowable());\n            System.err.println(s);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java",
    "content": "package io.xpipe.app.issue;\n\nimport io.xpipe.app.core.AppLogs;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.ProcessOutputException;\nimport io.xpipe.app.update.AppDistributionType;\nimport io.xpipe.app.util.LicenseProvider;\nimport io.xpipe.app.util.LicenseRequiredException;\n\nimport io.sentry.*;\nimport io.sentry.protocol.Geo;\nimport io.sentry.protocol.SentryId;\nimport io.sentry.protocol.User;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.ObjectInputStream;\nimport java.io.ObjectOutputStream;\nimport java.net.URISyntaxException;\nimport java.nio.file.FileSystemException;\nimport java.nio.file.Files;\nimport java.nio.file.InvalidPathException;\nimport java.util.Arrays;\nimport java.util.regex.PatternSyntaxException;\nimport java.util.stream.Collectors;\n\npublic class SentryErrorHandler implements ErrorHandler {\n\n    private static final ErrorHandler INSTANCE = new SyncErrorHandler(new SentryErrorHandler());\n    private boolean init;\n\n    public static ErrorHandler getInstance() {\n        return INSTANCE;\n    }\n\n    private static boolean hasUserReport(ErrorEvent ee) {\n        var email = ee.getEmail();\n        var hasEmail = email != null && !email.isBlank();\n        var text = ee.getUserReport();\n        var hasText = text != null && !text.isBlank();\n        return hasEmail || hasText;\n    }\n\n    private static boolean doesExceedCommentSize(String text) {\n        if (text == null || text.isEmpty()) {\n            return false;\n        }\n\n        return text.length() > 5000;\n    }\n\n    private static Throwable adjustCopy(Throwable throwable, boolean clear) {\n        if (throwable == null) {\n            return null;\n        }\n\n        if (!clear) {\n            return throwable;\n        }\n\n        if (throwable instanceof LicenseRequiredException) {\n            return throwable;\n        }\n\n        try {\n            ByteArrayOutputStream baos = new ByteArrayOutputStream();\n            ObjectOutputStream oos = new ObjectOutputStream(baos);\n            oos.writeObject(throwable);\n\n            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());\n            ObjectInputStream ois = new ObjectInputStream(bais);\n            var copy = (Throwable) ois.readObject();\n\n            if (!(copy instanceof NullPointerException) && !(copy instanceof IndexOutOfBoundsException)) {\n                var msgField = Throwable.class.getDeclaredField(\"detailMessage\");\n                msgField.setAccessible(true);\n                msgField.set(copy, null);\n            }\n\n            if (copy instanceof FileSystemException) {\n                var fileField = FileSystemException.class.getDeclaredField(\"file\");\n                fileField.setAccessible(true);\n                fileField.set(copy, null);\n\n                var otherField = FileSystemException.class.getDeclaredField(\"other\");\n                otherField.setAccessible(true);\n                otherField.set(copy, null);\n            }\n\n            if (copy instanceof InvalidPathException) {\n                var inputField = InvalidPathException.class.getDeclaredField(\"input\");\n                inputField.setAccessible(true);\n                inputField.set(copy, \"\");\n            }\n\n            if (copy instanceof URISyntaxException) {\n                var inputField = URISyntaxException.class.getDeclaredField(\"input\");\n                inputField.setAccessible(true);\n                inputField.set(copy, \"\");\n            }\n\n            if (copy instanceof PatternSyntaxException) {\n                var descField = PatternSyntaxException.class.getDeclaredField(\"desc\");\n                descField.setAccessible(true);\n                descField.set(copy, \"\");\n\n                var patternField = PatternSyntaxException.class.getDeclaredField(\"pattern\");\n                patternField.setAccessible(true);\n                patternField.set(copy, \"\");\n            }\n\n            if (copy instanceof ProcessOutputException) {\n                var prefixField = ProcessOutputException.class.getDeclaredField(\"prefix\");\n                prefixField.setAccessible(true);\n                prefixField.set(copy, null);\n\n                var suffixField = ProcessOutputException.class.getDeclaredField(\"suffix\");\n                suffixField.setAccessible(true);\n                suffixField.set(copy, null);\n\n                var outputField = ProcessOutputException.class.getDeclaredField(\"output\");\n                outputField.setAccessible(true);\n                outputField.set(copy, \"\");\n            }\n\n            var causeField = Throwable.class.getDeclaredField(\"cause\");\n            causeField.setAccessible(true);\n            causeField.set(copy, adjustCopy(throwable.getCause(), true));\n\n            var suppressedField = Throwable.class.getDeclaredField(\"suppressedExceptions\");\n            suppressedField.setAccessible(true);\n            var suppressed = throwable.getSuppressed();\n            if (suppressed.length > 0) {\n                suppressedField.set(\n                        copy,\n                        Arrays.stream(suppressed).map(s -> adjustCopy(s, true)).toList());\n            }\n\n            return copy;\n        } catch (Throwable e) {\n            // This can fail for example when the underlying exception is not serializable\n            // and comes from some third party library\n            if (AppLogs.get() != null) {\n                AppLogs.get().logException(\"Unable to adjust exception\", e);\n            }\n            return throwable;\n        }\n    }\n\n    private static SentryId captureEvent(ErrorEvent ee) {\n        if (!hasUserReport(ee) && \"User Report\".equals(ee.getDescription())) {\n            return null;\n        }\n\n        if (ee.getThrowable() != null) {\n            var adjusted = adjustCopy(ee.getThrowable(), !ee.isShouldSendDiagnostics());\n            return Sentry.captureException(adjusted, sc -> fillScope(ee, sc));\n        }\n\n        if (ee.getDescription() != null) {\n            return Sentry.captureMessage(ee.getDescription(), sc -> fillScope(ee, sc));\n        }\n\n        var event = new SentryEvent();\n        return Sentry.captureEvent(event, sc -> fillScope(ee, sc));\n    }\n\n    private static void fillScope(ErrorEvent ee, IScope s) {\n        if (ee.isShouldSendDiagnostics()) {\n            // Write all buffered output to log files to ensure that we get all information\n            if (AppLogs.get() != null) {\n                AppLogs.get().flush();\n            }\n\n            var atts = ee.getAttachments().stream()\n                    .map(d -> {\n                        try {\n                            var toUse = d;\n                            if (Files.isDirectory(d)) {\n                                toUse = AttachmentHelper.compressZipfile(\n                                        d,\n                                        AppSystemInfo.ofCurrent()\n                                                .getTemp()\n                                                .resolve(d.getFileName().toString() + \".zip\"));\n                            }\n                            return new Attachment(toUse.toString());\n                        } catch (Exception ex) {\n                            ex.printStackTrace();\n                            return null;\n                        }\n                    })\n                    .filter(attachment -> attachment != null)\n                    .toList();\n            atts.forEach(attachment -> s.addAttachment(attachment));\n        }\n\n        if (doesExceedCommentSize(ee.getUserReport())) {\n            try {\n                var report = Files.createTempFile(\"report\", \".txt\");\n                Files.writeString(report, ee.getUserReport());\n                s.addAttachment(new Attachment(report.toString()));\n            } catch (Exception ex) {\n                AppLogs.get().logException(\"Unable to create report file\", ex);\n            }\n        }\n\n        s.setTag(\n                \"hasLicense\",\n                String.valueOf(\n                        LicenseProvider.get() != null ? LicenseProvider.get().hasPaidLicense() : null));\n        s.setTag(\n                \"updatesEnabled\",\n                AppPrefs.get() != null\n                        ? AppPrefs.get().automaticallyUpdate().getValue().toString()\n                        : \"unknown\");\n        s.setTag(\n                \"securityUpdatesEnabled\",\n                AppPrefs.get() != null\n                        ? AppPrefs.get().checkForSecurityUpdates().getValue().toString()\n                        : \"unknown\");\n        s.setTag(\n                \"developerMode\",\n                AppPrefs.get() != null\n                        ? AppPrefs.get().developerMode().getValue().toString()\n                        : \"false\");\n        s.setTag(\"terminal\", Boolean.toString(ee.isTerminal()));\n        s.setTag(\"omitted\", Boolean.toString(ee.isOmitted()));\n        s.setTag(\n                \"logs\",\n                Boolean.toString(\n                        ee.isShouldSendDiagnostics() && !ee.getAttachments().isEmpty()));\n        s.setTag(\"inStartup\", Boolean.toString(AppOperationMode.isInStartup()));\n        s.setTag(\"inShutdown\", Boolean.toString(AppOperationMode.isInShutdown()));\n        s.setTag(\"unhandled\", Boolean.toString(ee.isUnhandled()));\n\n        s.setTag(\"diagnostics\", Boolean.toString(ee.isShouldSendDiagnostics()));\n        s.setTag(\"licenseRequired\", Boolean.toString(ee.getThrowable() instanceof LicenseRequiredException));\n        s.setTag(\n                \"localShell\",\n                AppPrefs.get() != null && AppPrefs.get().localShellDialect().getValue() != null\n                        ? AppPrefs.get().localShellDialect().getValue().getId()\n                        : \"unknown\");\n        s.setTag(\"initial\", AppProperties.get() != null ? AppProperties.get().isInitialLaunch() + \"\" : \"false\");\n\n        var exMessage = ee.getThrowable() != null ? ee.getThrowable().getMessage() : null;\n        if (ee.getDescription() != null\n                && !ee.getDescription().equals(exMessage)\n                && (ee.isShouldSendDiagnostics() || ee.getThrowable() instanceof LicenseRequiredException)) {\n            s.setTag(\"message\", ee.getDescription().lines().collect(Collectors.joining(\" \")));\n        }\n\n        var user = new User();\n        user.setId(AppProperties.get().getUuid().toString());\n        user.setGeo(new Geo());\n        s.setUser(user);\n    }\n\n    public void handle(ErrorEvent ee) {\n        // Assume that this object is wrapped by a synchronous error handler\n        if (!init) {\n            AppProperties.init();\n            if (AppProperties.get().getSentryUrl() != null) {\n                Sentry.init(options -> {\n                    options.setDsn(AppProperties.get().getSentryUrl());\n                    options.setEnableUncaughtExceptionHandler(false);\n                    options.setAttachServerName(false);\n                    options.setRelease(AppProperties.get().getVersion());\n                    options.setEnableShutdownHook(false);\n                    options.setProguardUuid(AppProperties.get().getBuildUuid().toString());\n                    options.setTag(\"os\", System.getProperty(\"os.name\"));\n                    options.setTag(\"osVersion\", System.getProperty(\"os.version\"));\n                    options.setTag(\"arch\", AppProperties.get().getArch());\n                    options.setDist(AppDistributionType.get().getId());\n                    options.setTag(\"staging\", String.valueOf(AppProperties.get().isStaging()));\n                    options.setSendModules(false);\n                    options.setAttachThreads(false);\n                    options.setEnableDeduplication(false);\n                    options.setCacheDirPath(\n                            AppProperties.get().getDataDir().resolve(\"cache\").toString());\n                });\n            }\n            init = true;\n        }\n\n        var id = captureEvent(ee);\n        if (id == null) {\n            return;\n        }\n\n        var email = ee.getEmail();\n        var hasEmail = email != null && !email.isBlank();\n        var text = ee.getUserReport();\n        if (hasUserReport(ee)) {\n            var fb = new UserFeedback(id);\n            if (hasEmail) {\n                fb.setEmail(email);\n            }\n            if (doesExceedCommentSize(text)) {\n                fb.setComments(\"<Attachment>\");\n            } else {\n                fb.setComments(text);\n            }\n            Sentry.captureUserFeedback(fb);\n        }\n        Sentry.flush(3000);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/issue/SyncErrorHandler.java",
    "content": "package io.xpipe.app.issue;\n\nimport java.util.Queue;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\npublic class SyncErrorHandler implements ErrorHandler {\n\n    private final Queue<ErrorEvent> eventQueue = new LinkedBlockingQueue<>();\n    private final ErrorHandler errorHandler;\n    private final AtomicBoolean busy = new AtomicBoolean();\n\n    public SyncErrorHandler(ErrorHandler errorHandler) {\n        this.errorHandler = errorHandler;\n    }\n\n    @Override\n    public void handle(ErrorEvent event) {\n        synchronized (busy) {\n            if (busy.get()) {\n                synchronized (eventQueue) {\n                    eventQueue.add(event);\n                }\n                return;\n            }\n            busy.set(true);\n        }\n\n        errorHandler.handle(event);\n        synchronized (eventQueue) {\n            eventQueue.forEach(errorEvent -> {\n                System.out.println(\"Event happened during error handling: \" + errorEvent.toString());\n            });\n            eventQueue.clear();\n        }\n        busy.set(false);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/issue/TerminalErrorHandler.java",
    "content": "package io.xpipe.app.issue;\n\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.*;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.platform.PlatformInit;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.update.AppDistributionType;\nimport io.xpipe.app.util.Hyperlinks;\nimport io.xpipe.app.util.ThreadHelper;\n\npublic class TerminalErrorHandler extends GuiErrorHandlerBase implements ErrorHandler {\n\n    private final ErrorHandler log = new LogErrorHandler();\n\n    @Override\n    public void handle(ErrorEvent event) {\n        log.handle(event);\n\n        if (event.isOmitted() || AppOperationMode.isInShutdown()) {\n            ErrorAction.ignore().handle(event);\n            // Wait a bit to the beacon the ability to respond to any open requests with an error\n            ThreadHelper.sleep(3000);\n            AppOperationMode.halt(1);\n            return;\n        }\n\n        if (!startupGui(throwable -> {\n            handleWithSecondaryException(event, throwable);\n            ErrorAction.ignore().handle(event);\n        })) {\n            // Exit if we couldn't initialize the GUI\n            // Wait a bit to the beacon the ability to respond to any open requests with an error\n            ThreadHelper.sleep(3000);\n            AppOperationMode.halt(1);\n            return;\n        }\n\n        handleGui(event);\n    }\n\n    private void handleGui(ErrorEvent event) {\n        try {\n            AppProperties.init();\n            AppExtensionManager.init();\n            PlatformInit.init(true);\n            ErrorHandlerDialog.showAndWait(event);\n        } catch (Throwable r) {\n            event.clearAttachments();\n            handleWithSecondaryException(event, r);\n            return;\n        }\n\n        if (AppOperationMode.isInStartup() && !AppProperties.get().isDevelopmentEnvironment()) {\n            handleProbableUpdate();\n        }\n\n        ThreadHelper.sleep(1000);\n        AppOperationMode.halt(1);\n    }\n\n    private void handleWithSecondaryException(ErrorEvent event, Throwable t) {\n        ErrorAction.ignore().handle(event);\n\n        var second = ErrorEventFactory.fromThrowable(t).build();\n        log.handle(second);\n        ErrorAction.ignore().handle(second);\n        ThreadHelper.sleep(1000);\n        AppOperationMode.halt(1);\n    }\n\n    private void handleProbableUpdate() {\n        if (AppProperties.get().isDevelopmentEnvironment()) {\n            return;\n        }\n\n        if (!LocalShell.isInitialized()) {\n            return;\n        }\n\n        try {\n            var rel = AppDistributionType.get().getUpdateHandler().refreshUpdateCheck(false, false);\n            if (rel != null && rel.isUpdate()) {\n                var updateModal = ModalOverlay.of(\n                        \"updateAvailableTitle\",\n                        AppDialog.dialogText(AppI18n.get(\"updateAvailableContent\", rel.getVersion())));\n                updateModal.addButton(\n                        new ModalButton(\"checkOutUpdate\", () -> Hyperlinks.open(rel.getReleaseUrl()), false, true));\n                updateModal.addButton(new ModalButton(\"ignore\", null, true, false));\n                AppDialog.showAndWait(updateModal);\n            }\n        } catch (Throwable t) {\n            var event = ErrorEventFactory.fromThrowable(t).build();\n            log.handle(event);\n            ErrorAction.ignore().handle(event);\n            ThreadHelper.sleep(1000);\n            AppOperationMode.halt(1);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/issue/TrackEvent.java",
    "content": "package io.xpipe.app.issue;\n\nimport lombok.Builder;\nimport lombok.Getter;\nimport lombok.Singular;\n\nimport java.time.Instant;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\n@Builder\n@Getter\npublic class TrackEvent {\n\n    private final Instant instant = Instant.now();\n    private String type;\n    private String message;\n\n    @Singular\n    private Map<String, Object> tags;\n\n    @Singular\n    private List<Object> elements;\n\n    public static TrackEventBuilder fromMessage(String type, String message) {\n        return builder().type(type).message(message);\n    }\n\n    public static TrackEventBuilder withInfo(String message) {\n        return builder().type(\"info\").message(message);\n    }\n\n    public static TrackEventBuilder withWarn(String message) {\n        return builder().type(\"warn\").message(message);\n    }\n\n    public static TrackEventBuilder withTrace(String message) {\n        return builder().type(\"trace\").message(message);\n    }\n\n    public static void info(String message) {\n        builder().type(\"info\").message(message).build().handle();\n    }\n\n    public static void warn(String message) {\n        builder().type(\"warn\").message(message).build().handle();\n    }\n\n    public static TrackEventBuilder withDebug(String message) {\n        return builder().type(\"debug\").message(message);\n    }\n\n    public static void debug(String message) {\n        builder().type(\"debug\").message(message).build().handle();\n    }\n\n    public static void trace(String message) {\n        builder().type(\"trace\").message(message).build().handle();\n    }\n\n    public static TrackEventBuilder withError(String message) {\n        return builder().type(\"error\").message(message);\n    }\n\n    public static void error(String message) {\n        builder().type(\"error\").message(message).build().handle();\n    }\n\n    public void handle() {\n        EventHandler.get().handle(this);\n    }\n\n    @Override\n    public String toString() {\n        var s = new StringBuilder(message != null ? message : \"\");\n        if (tags.size() > 0) {\n            s.append(\" {\\n\");\n            for (var e : tags.entrySet()) {\n                var valueString = e.getValue() != null ? e.getValue().toString() : \"null\";\n                var value = valueString.contains(\"\\n\")\n                        ? \"\\n\"\n                                + (valueString\n                                        .lines()\n                                        .map(line -> \"    | \" + line)\n                                        .collect(Collectors.joining(\"\\n\")))\n                        : valueString;\n                s.append(\"    \").append(e.getKey()).append(\"=\").append(value).append(\"\\n\");\n            }\n            s.append(\"}\");\n        }\n\n        if (elements.size() > 0) {\n            s.append(\" [\\n\");\n            for (var e : elements) {\n                s.append(\"    \").append(e != null ? e.toString() : \"null\").append(\"\\n\");\n            }\n            s.append(\"]\");\n        }\n\n        return s.toString();\n    }\n\n    public static class TrackEventBuilder {\n\n        public void handle() {\n            build().handle();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/issue/UserReportComp.java",
    "content": "package io.xpipe.app.issue;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.*;\nimport io.xpipe.app.prefs.AppPrefs;\n\nimport javafx.beans.property.*;\nimport javafx.collections.FXCollections;\nimport javafx.geometry.Orientation;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.*;\nimport javafx.scene.layout.*;\n\nimport atlantafx.base.controls.Popover;\nimport atlantafx.base.controls.Spacer;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic class UserReportComp extends ModalOverlayContentComp {\n\n    private final StringProperty email = new SimpleStringProperty();\n    private final StringProperty text = new SimpleStringProperty();\n    private final ListProperty<Path> includedDiagnostics;\n    private final ErrorEvent event;\n\n    public UserReportComp(ErrorEvent event) {\n        this.event = event;\n        this.includedDiagnostics = new SimpleListProperty<>(FXCollections.observableArrayList());\n    }\n\n    public static boolean show(ErrorEvent event) {\n        var comp = new UserReportComp(event);\n        var modal = ModalOverlay.of(\"errorHandler\", comp);\n        var sent = new SimpleBooleanProperty();\n        modal.addButtonBarComp(privacyPolicy());\n        modal.addButtonBarComp(RegionBuilder.hspacer());\n        modal.addButton(new ModalButton(\n                \"sendReport\",\n                () -> {\n                    comp.send();\n                    sent.set(true);\n                },\n                true,\n                true));\n        modal.showAndWait();\n        return sent.get();\n    }\n\n    private static BaseRegionBuilder<?, ?> privacyPolicy() {\n        return RegionBuilder.of(() -> {\n            var dataPolicyButton = new Hyperlink(AppI18n.get(\"dataHandlingPolicies\"));\n            AppFontSizes.xs(dataPolicyButton);\n            dataPolicyButton.setOnAction(event1 -> {\n                AppResources.with(AppResources.MAIN_MODULE, \"misc/report_privacy_policy.md\", file -> {\n                    var markDown = new MarkdownComp(Files.readString(file), s -> s, true)\n                            .apply(struc -> struc.setMaxWidth(500))\n                            .apply(struc -> struc.setMaxHeight(400));\n                    var popover = new Popover(markDown.build());\n                    popover.setAutoHide(!AppPrefs.get().limitedTouchscreenMode().get());\n                    popover.setCloseButtonEnabled(true);\n                    popover.setHeaderAlwaysVisible(false);\n                    popover.setDetachable(true);\n                    AppFontSizes.xs(popover.getContentNode());\n                    popover.show(dataPolicyButton);\n                });\n                event1.consume();\n            });\n\n            var agree = new Label(\"Note the issue reporter \");\n            var buttons = new HBox(agree, dataPolicyButton);\n            buttons.setAlignment(Pos.CENTER_LEFT);\n            buttons.setMinWidth(Region.USE_PREF_SIZE);\n            return buttons;\n        });\n    }\n\n    @Override\n    protected Region createSimple() {\n        var emailHeader = new Label(AppI18n.get(\"provideEmail\"));\n        emailHeader.setWrapText(true);\n        var email = new TextField();\n        this.email.bind(email.textProperty());\n        VBox.setVgrow(email, Priority.ALWAYS);\n\n        var infoHeader = new Label(AppI18n.get(\"additionalErrorInfo\"));\n        var tf = new TextArea();\n        text.bind(tf.textProperty());\n        VBox.setVgrow(tf, Priority.ALWAYS);\n\n        var attachmentsHeader = new Label(AppI18n.get(\"additionalErrorAttachments\"));\n        var attachments = new ListSelectorComp<>(\n                        FXCollections.observableList(event.getAttachments()),\n                        file -> {\n                            if (file.equals(AppLogs.get().getSessionLogsDirectory())) {\n                                return AppI18n.get(\"logFilesAttachment\");\n                            }\n\n                            return file.getFileName().toString();\n                        },\n                        file -> null,\n                        includedDiagnostics,\n                        file -> false,\n                        () -> false)\n                .style(\"attachment-list\")\n                .build();\n\n        var reportSection = new VBox(\n                infoHeader,\n                tf,\n                new Spacer(8, Orientation.VERTICAL),\n                attachmentsHeader,\n                new Spacer(3, Orientation.VERTICAL),\n                attachments);\n        reportSection.setSpacing(5);\n        reportSection.getStyleClass().add(\"report\");\n        reportSection.getChildren().addAll(new Spacer(8, Orientation.VERTICAL), emailHeader, email);\n        reportSection.setPrefWidth(600);\n        reportSection.setPrefHeight(550);\n        return reportSection;\n    }\n\n    private void send() {\n        event.clearAttachments();\n        event.setShouldSendDiagnostics(true);\n        includedDiagnostics.forEach(event::addAttachment);\n        event.attachUserReport(email.get(), text.get());\n        SentryErrorHandler.getInstance().handle(event);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/BindingsHelper.java",
    "content": "package io.xpipe.app.platform;\n\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.binding.BooleanBinding;\nimport javafx.beans.binding.ObjectBinding;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.value.ChangeListener;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.layout.Region;\n\nimport lombok.Value;\n\nimport java.lang.ref.WeakReference;\nimport java.util.HashSet;\nimport java.util.Set;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\n\n@SuppressWarnings(\"InfiniteLoopStatement\")\npublic class BindingsHelper {\n\n    private static final Set<ReferenceEntry> REFERENCES = new HashSet<>();\n\n    static {\n        ThreadHelper.createPlatformThread(\"Binding Reference GC\", true, () -> {\n                    while (true) {\n                        synchronized (REFERENCES) {\n                            REFERENCES.removeIf(ReferenceEntry::canGc);\n                        }\n                        ThreadHelper.sleep(1000);\n\n                        // Use for testing\n                        // System.gc();\n                    }\n                })\n                .start();\n    }\n\n    public static void preserve(Object source, Object target) {\n        synchronized (REFERENCES) {\n            REFERENCES.add(new ReferenceEntry(new WeakReference<>(source), target));\n        }\n    }\n\n    public static <T, U> ObjectBinding<U> map(\n            ObservableValue<T> observableValue, Function<? super T, ? extends U> mapper) {\n        return Bindings.createObjectBinding(\n                () -> {\n                    return mapper.apply(observableValue.getValue());\n                },\n                observableValue);\n    }\n\n    public static <T> BooleanBinding mapBoolean(\n            ObservableValue<T> observableValue, Function<? super T, Boolean> mapper) {\n        return Bindings.createBooleanBinding(\n                () -> {\n                    return mapper.apply(observableValue.getValue());\n                },\n                observableValue);\n    }\n\n    public static <T, U> ObservableValue<U> flatMap(\n            ObservableValue<T> observableValue, Function<? super T, ? extends ObservableValue<? extends U>> mapper) {\n        var prop = new SimpleObjectProperty<U>();\n        Runnable runnable = () -> {\n            prop.bind(mapper.apply(observableValue.getValue()));\n        };\n        runnable.run();\n        observableValue.addListener((observable, oldValue, newValue) -> {\n            runnable.run();\n        });\n        preserve(prop, observableValue);\n        return prop;\n    }\n\n    public static <R extends Region, T> void attach(R node, ObservableValue<T> value, Consumer<T> consumer) {\n        var listener = new ChangeListener<T>() {\n            @Override\n            public void changed(ObservableValue<? extends T> observable, T oldValue, T newValue) {\n                consumer.accept(newValue);\n            }\n        };\n        node.sceneProperty().subscribe(scene -> {\n            if (scene != null) {\n                consumer.accept(value.getValue());\n                value.addListener(listener);\n            } else {\n                value.removeListener(listener);\n            }\n        });\n    }\n\n    @Value\n    private static class ReferenceEntry {\n\n        WeakReference<?> source;\n        Object target;\n\n        public boolean canGc() {\n            return source.get() == null;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/BooleanAnimationTimer.java",
    "content": "package io.xpipe.app.platform;\n\nimport javafx.animation.AnimationTimer;\nimport javafx.beans.value.ObservableBooleanValue;\n\nimport java.util.concurrent.atomic.AtomicReference;\n\npublic class BooleanAnimationTimer {\n\n    private final ObservableBooleanValue value;\n    private final int duration;\n    private final Runnable toExecute;\n\n    public BooleanAnimationTimer(ObservableBooleanValue value, int duration, Runnable toExecute) {\n        this.value = value;\n        this.duration = duration;\n        this.toExecute = toExecute;\n    }\n\n    public void start() {\n        var timer = new AtomicReference<AnimationTimer>();\n        value.addListener((observable, oldValue, newValue) -> {\n            if (newValue) {\n                if (timer.get() == null) {\n                    timer.set(new AnimationTimer() {\n\n                        long init = 0;\n\n                        @Override\n                        public void handle(long now) {\n                            if (init == 0) {\n                                init = now;\n                            }\n\n                            var nowMs = now;\n                            if ((nowMs - init) > duration * 1_000_000L) {\n                                toExecute.run();\n                                stop();\n                            }\n                        }\n                    });\n                    timer.get().start();\n                }\n            } else {\n                if (timer.get() != null) {\n                    timer.get().stop();\n                    timer.set(null);\n                }\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/ChainedValidator.java",
    "content": "package io.xpipe.app.platform;\n\nimport javafx.beans.Observable;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.binding.StringBinding;\nimport javafx.beans.property.ReadOnlyBooleanProperty;\nimport javafx.beans.property.ReadOnlyBooleanWrapper;\nimport javafx.beans.property.ReadOnlyObjectProperty;\nimport javafx.beans.property.ReadOnlyObjectWrapper;\n\nimport net.synedra.validatorfx.ValidationMessage;\nimport net.synedra.validatorfx.ValidationResult;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\npublic class ChainedValidator implements Validator {\n\n    private final List<Validator> validators;\n    private final ReadOnlyObjectWrapper<ValidationResult> validationResultProperty =\n            new ReadOnlyObjectWrapper<>(new ValidationResult());\n    private final ReadOnlyBooleanWrapper containsErrorsProperty = new ReadOnlyBooleanWrapper();\n\n    public ChainedValidator(List<Validator> validators) {\n        this.validators = validators;\n        validators.forEach(v -> {\n            v.containsErrorsProperty().addListener((c, o, n) -> {\n                containsErrorsProperty.set(containsErrors());\n            });\n\n            v.validationResultProperty().addListener((c, o, n) -> {\n                validationResultProperty.set(getValidationResult());\n            });\n        });\n    }\n\n    @Override\n    public Check createCheck() {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public void add(Check check) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public void remove(Check check) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public ValidationResult getValidationResult() {\n        var list = new ArrayList<ValidationMessage>();\n        for (var val : validators) {\n            list.addAll(val.getValidationResult().getMessages());\n        }\n\n        var r = new ValidationResult();\n        r.addAll(list);\n        return r;\n    }\n\n    @Override\n    public ReadOnlyObjectProperty<ValidationResult> validationResultProperty() {\n        return validationResultProperty;\n    }\n\n    @Override\n    public ReadOnlyBooleanProperty containsErrorsProperty() {\n        return containsErrorsProperty;\n    }\n\n    @Override\n    public boolean containsErrors() {\n        return validators.stream().anyMatch(Validator::containsErrors);\n    }\n\n    @Override\n    public boolean validate() {\n        var valid = true;\n        for (var val : validators) {\n            if (!val.validate()) {\n                valid = false;\n            }\n        }\n\n        return valid;\n    }\n\n    @Override\n    public StringBinding createStringBinding() {\n        return createStringBinding(\"- \", \"\\n\");\n    }\n\n    @Override\n    public StringBinding createStringBinding(String prefix, String separator) {\n        var list = new ArrayList<Observable>(\n                validators.stream().map(Validator::createStringBinding).toList());\n        Observable[] observables = list.toArray(Observable[]::new);\n        return Bindings.createStringBinding(\n                () -> {\n                    return validators.stream()\n                            .map(v -> v.createStringBinding(prefix, separator).get())\n                            .collect(Collectors.joining(\"\\n\"));\n                },\n                observables);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/Check.java",
    "content": "package io.xpipe.app.platform;\n\nimport javafx.application.Platform;\nimport javafx.beans.property.ReadOnlyObjectProperty;\nimport javafx.beans.property.ReadOnlyObjectWrapper;\nimport javafx.beans.value.ChangeListener;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.Node;\n\nimport lombok.Getter;\nimport net.synedra.validatorfx.Decoration;\nimport net.synedra.validatorfx.DefaultDecoration;\nimport net.synedra.validatorfx.ValidationMessage;\nimport net.synedra.validatorfx.ValidationResult;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\n\n/**\n * A check represents a check for validity in a form.\n *\n * @author r.lichtenberger@synedra.com\n */\npublic class Check {\n\n    private final Map<String, ObservableValue<?>> dependencies = new HashMap<>(1);\n    private final ReadOnlyObjectWrapper<ValidationResult> validationResultProperty = new ReadOnlyObjectWrapper<>();\n\n    @Getter\n    private final List<Node> targets = new ArrayList<>(1);\n\n    private final List<Decoration> decorations = new ArrayList<>();\n    private final ChangeListener<? super Object> dependencyListener;\n    private Consumer<Context> checkMethod;\n    private ValidationResult nextValidationResult = new ValidationResult();\n    private final Function<ValidationMessage, Decoration> decorationFactory;\n\n    public Check() {\n        validationResultProperty.set(new ValidationResult());\n        decorationFactory = DefaultDecoration.getFactory();\n        dependencyListener = (obs, oldv, newv) -> recheck();\n    }\n\n    public Check withMethod(Consumer<Context> checkMethod) {\n        this.checkMethod = checkMethod;\n        return this;\n    }\n\n    public Check dependsOn(String key, ObservableValue<?> dependency) {\n        dependencies.put(key, dependency);\n        return this;\n    }\n\n    public Check decorates(Node target) {\n        targets.add(target);\n        return this;\n    }\n\n    /**\n     * Sets this check to be immediately evaluated if one of its dependencies changes.\n     * This method must be called last.\n     */\n    public Check immediate() {\n        for (ObservableValue<?> dependency : dependencies.values()) {\n            dependency.addListener(dependencyListener);\n        }\n        Platform.runLater(this::recheck); // to circumvent problems with decoration pane vs. dialog\n        return this;\n    }\n\n    /**\n     * Evaluate all dependencies and apply decorations of this check. You should not normally need to call this method directly.\n     */\n    public void recheck() {\n        nextValidationResult = new ValidationResult();\n        checkMethod.accept(new Context());\n        for (Node target : targets) {\n            for (Decoration decoration : decorations) {\n                decoration.remove(target);\n            }\n        }\n        decorations.clear();\n        for (Node target : targets) {\n            for (ValidationMessage validationMessage : nextValidationResult.getMessages()) {\n                Decoration decoration = decorationFactory.apply(validationMessage);\n                decorations.add(decoration);\n                decoration.add(target);\n            }\n        }\n        if (!nextValidationResult.getMessages().equals(getValidationResult().getMessages())) {\n            validationResultProperty.set(nextValidationResult);\n        }\n    }\n\n    /**\n     * Retrieves current validation result\n     *\n     * @return validation result\n     */\n    public ValidationResult getValidationResult() {\n        return validationResultProperty.get();\n    }\n\n    /**\n     * Can be used to track validation result changes\n     *\n     * @return The Validation result property.\n     */\n    public ReadOnlyObjectProperty<ValidationResult> validationResultProperty() {\n        return validationResultProperty.getReadOnlyProperty();\n    }\n\n    public class Context {\n\n        private Context() {}\n\n        /**\n         * Get the current value of a dependency.\n         *\n         * @param <T> The type the value should be casted into\n         * @param key The key the dependency has been given\n         * @return The current value of the given depency\n         */\n        @SuppressWarnings(\"unchecked\")\n        public <T> T get(String key) {\n            return (T) dependencies.get(key).getValue();\n        }\n\n        /**\n         * Emit an error.\n         *\n         * @param message The text to be presented to the user as error message.\n         */\n        public void error(String message) {\n            nextValidationResult.addError(message);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/ClipboardHelper.java",
    "content": "package io.xpipe.app.platform;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.core.SecretValue;\n\nimport javafx.animation.PauseTransition;\nimport javafx.scene.input.Clipboard;\nimport javafx.scene.input.DataFormat;\nimport javafx.util.Duration;\n\nimport java.util.AbstractMap;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.stream.Stream;\n\npublic class ClipboardHelper {\n\n    private static final AppLayoutModel.QueueEntry COPY_QUEUE_ENTRY = new AppLayoutModel.QueueEntry(\n            AppI18n.observable(\"passwordCopied\"),\n            new LabelGraphic.IconGraphic(\"mdi2c-clipboard-check-outline\"),\n            () -> true);\n\n    private static void apply(Map<DataFormat, Object> map, boolean showNotification) {\n        Clipboard clipboard = Clipboard.getSystemClipboard();\n        Map<DataFormat, Object> contents = Stream.of(\n                        DataFormat.PLAIN_TEXT,\n                        DataFormat.URL,\n                        DataFormat.RTF,\n                        DataFormat.HTML,\n                        DataFormat.IMAGE,\n                        DataFormat.FILES)\n                .map(dataFormat -> {\n                    try {\n                        // This can fail if the clipboard data is invalid\n                        return new AbstractMap.SimpleEntry<>(dataFormat, clipboard.getContent(dataFormat));\n                    } catch (Exception e) {\n                        return new AbstractMap.SimpleEntry<>(dataFormat, null);\n                    }\n                })\n                .collect(HashMap::new, (m, v) -> m.put(v.getKey(), v.getValue()), HashMap::putAll);\n        contents.putAll(map);\n        contents.entrySet().removeIf(e -> e.getValue() == null);\n        clipboard.setContent(contents);\n\n        if (showNotification) {\n            AppLayoutModel.get().showQueueEntry(COPY_QUEUE_ENTRY, java.time.Duration.ofSeconds(15), true);\n        }\n    }\n\n    public static void copyPassword(SecretValue pass) {\n        if (pass == null) {\n            return;\n        }\n\n        PlatformThread.runLaterIfNeeded(() -> {\n            Clipboard clipboard = Clipboard.getSystemClipboard();\n            var text = clipboard.getString();\n\n            apply(Map.of(DataFormat.PLAIN_TEXT, pass.getSecretValue()), true);\n\n            var transition = new PauseTransition(Duration.millis(15000));\n            transition.setOnFinished(e -> {\n                var present = clipboard.getString();\n                if (present != null && present.equals(pass.getSecretValue())) {\n                    var map = new HashMap<DataFormat, Object>();\n                    map.put(DataFormat.PLAIN_TEXT, text);\n                    apply(map, false);\n                }\n            });\n            transition.play();\n        });\n    }\n\n    public static void copyText(String s) {\n        PlatformThread.runLaterIfNeeded(() -> {\n            apply(Map.of(DataFormat.PLAIN_TEXT, s), true);\n        });\n    }\n\n    public static void copyUrl(String s) {\n        PlatformThread.runLaterIfNeeded(() -> {\n            apply(Map.of(DataFormat.URL, s, DataFormat.PLAIN_TEXT, s), true);\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/ColorHelper.java",
    "content": "package io.xpipe.app.platform;\n\nimport javafx.scene.paint.Color;\n\npublic class ColorHelper {\n\n    public static String toWeb(Color c) {\n        var hex = String.format(\n                \"#%02X%02X%02X%02X\",\n                (int) (c.getRed() * 255), (int) (c.getGreen() * 255), (int) (c.getBlue() * 255), (int)\n                        (c.getOpacity() * 255));\n        return hex;\n    }\n\n    public static Color withOpacity(Color c, double opacity) {\n        return new Color(c.getRed(), c.getGreen(), c.getBlue(), opacity);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/DerivedObservableList.java",
    "content": "package io.xpipe.app.platform;\n\nimport javafx.beans.Observable;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.value.ObservableBooleanValue;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.FXCollections;\nimport javafx.collections.ListChangeListener;\nimport javafx.collections.ObservableList;\n\nimport lombok.Getter;\n\nimport java.util.*;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\nimport java.util.stream.Stream;\n\n@Getter\npublic class DerivedObservableList<T> {\n\n    private final List<T> backingList;\n    private final ObservableList<T> list;\n    private final boolean unique;\n\n    public DerivedObservableList(List<T> backingList, ObservableList<T> list, boolean unique) {\n        this.backingList = backingList;\n        this.list = list;\n        this.unique = unique;\n    }\n\n    public static <T> DerivedObservableList<T> synchronizedArrayList(boolean unique) {\n        var list = new ArrayList<T>();\n        return new DerivedObservableList<>(\n                list, FXCollections.synchronizedObservableList(FXCollections.observableList(list)), unique);\n    }\n\n    public static <T> DerivedObservableList<T> arrayList(boolean unique) {\n        var list = new ArrayList<T>();\n        return new DerivedObservableList<>(list, FXCollections.observableList(list), unique);\n    }\n\n    public static <T> DerivedObservableList<T> wrap(ObservableList<T> list, boolean unique) {\n        return new DerivedObservableList<>(null, list, unique);\n    }\n\n    private <V> DerivedObservableList<V> createNewDerived() {\n        var name = list.getClass().getSimpleName();\n        var backingList = new ArrayList<V>();\n        var l = name.toLowerCase().contains(\"synchronized\")\n                ? FXCollections.synchronizedObservableList(FXCollections.observableList(backingList))\n                : FXCollections.observableList(backingList);\n        var derived = new DerivedObservableList<>(backingList, l, unique);\n        BindingsHelper.preserve(l, this);\n        return derived;\n    }\n\n    public void setContent(List<? extends T> newList) {\n        synchronized (list) {\n            if (list.equals(newList)) {\n                return;\n            }\n\n            if (list.size() == 0) {\n                list.addAll(newList);\n                return;\n            }\n\n            if (newList.size() == 0) {\n                list.clear();\n                return;\n            }\n        }\n\n        if (unique) {\n            setContentUnique(newList);\n        } else {\n            setContentNonUnique(newList);\n        }\n    }\n\n    private void setContentNonUnique(List<? extends T> newList) {\n        var target = list;\n        var targetSet = new HashSet<T>();\n        synchronized (target) {\n            targetSet.addAll(target);\n\n            var newSet = new HashSet<>(newList);\n\n            // Only add missing element\n            if (target.size() + 1 == newList.size() && newSet.containsAll(targetSet)) {\n                var l = new HashSet<>(newSet);\n                l.removeAll(targetSet);\n                if (l.size() > 0) {\n                    var found = l.iterator().next();\n                    var index = newList.indexOf(found);\n                    target.add(index, found);\n                    return;\n                }\n            }\n\n            // Only remove not needed element\n            if (target.size() - 1 == newList.size() && targetSet.containsAll(newSet)) {\n                var l = new HashSet<>(targetSet);\n                l.removeAll(newSet);\n                if (l.size() > 0) {\n                    target.remove(l.iterator().next());\n                    return;\n                }\n            }\n\n            // Other cases are more difficult\n            target.setAll(newList);\n        }\n    }\n\n    private int indexOfFromStart(List<? extends T> list, T value, int start) {\n        for (int i = start; i < list.size(); i++) {\n            if (Objects.equals(list.get(i), value)) {\n                return i;\n            }\n        }\n        return -1;\n    }\n\n    private void setContentUnique(List<? extends T> newList) {\n        synchronized (list) {\n            // Addition\n            var newSet = new HashSet<>(newList);\n            if (newSet.containsAll(list)) {\n                var listSet = new HashSet<>(list);\n\n                var l = new ArrayList<>(newList);\n                l.removeIf(t -> !listSet.contains(t));\n                // Reordering occurred\n                if (!l.equals(list)) {\n                    list.setAll(newList);\n                    return;\n                }\n\n                var start = 0;\n                for (int end = 0; end <= list.size(); end++) {\n                    var index = end < list.size() ? indexOfFromStart(newList, list.get(end), end) : newList.size();\n                    for (; start < index; start++) {\n                        list.add(start, newList.get(start));\n                    }\n                    start = index + 1;\n                }\n                return;\n            }\n\n            // Removal\n            var listSet = new HashSet<>(list);\n            if (listSet.containsAll(newList)) {\n                var toRemove = new ArrayList<>(list);\n                toRemove.removeIf(t -> newSet.contains(t));\n                list.removeAll(toRemove);\n\n                // Reordering occurred\n                if (!list.equals(newList)) {\n                    list.setAll(newList);\n                    return;\n                }\n\n                return;\n            }\n\n            // Other cases are more difficult\n            list.setAll(newList);\n        }\n    }\n\n    private Stream<T> listStream() {\n        if (backingList != null) {\n            return backingList.stream();\n        }\n\n        return list.stream();\n    }\n\n    public <V> DerivedObservableList<V> mapped(Function<T, V> map) {\n        var cache = new HashMap<T, V>();\n        var l1 = this.<V>createNewDerived();\n        Runnable runnable = () -> {\n            List<V> toApply;\n            synchronized (list) {\n                var listSet = new HashSet<>(list);\n                cache.keySet().removeIf(t -> !listSet.contains(t));\n                toApply = listStream()\n                        .map(v -> {\n                            if (!cache.containsKey(v)) {\n                                cache.put(v, map.apply(v));\n                            }\n\n                            return cache.get(v);\n                        })\n                        .toList();\n            }\n            l1.setContent(toApply);\n        };\n        runnable.run();\n        list.addListener((ListChangeListener<? super T>) c -> {\n            runnable.run();\n        });\n        return l1;\n    }\n\n    public DerivedObservableList<T> filtered(Predicate<T> predicate) {\n        return filtered(new SimpleObjectProperty<>(predicate));\n    }\n\n    public DerivedObservableList<T> filtered(Predicate<T> predicate, Observable... observables) {\n        return filtered(Bindings.createObjectBinding(\n                () -> {\n                    return new Predicate<>() {\n                        @Override\n                        public boolean test(T v) {\n                            return predicate.test(v);\n                        }\n                    };\n                },\n                Arrays.stream(observables).filter(Objects::nonNull).toArray(Observable[]::new)));\n    }\n\n    public DerivedObservableList<T> filtered(ObservableValue<Predicate<T>> predicate) {\n        var d = this.<T>createNewDerived();\n        Runnable runnable = () -> {\n            List<T> toApply;\n            synchronized (list) {\n                toApply = predicate.getValue() != null\n                        ? listStream().filter(predicate.getValue()).toList()\n                        : list;\n            }\n            d.setContent(toApply);\n        };\n        runnable.run();\n        list.addListener((ListChangeListener<? super T>) c -> {\n            runnable.run();\n        });\n        predicate.addListener(observable -> {\n            runnable.run();\n        });\n        return d;\n    }\n\n    public DerivedObservableList<T> sorted(Comparator<T> comp, Observable... observables) {\n        return sorted(Bindings.createObjectBinding(\n                () -> {\n                    return new Comparator<>() {\n                        @Override\n                        public int compare(T o1, T o2) {\n                            return comp.compare(o1, o2);\n                        }\n                    };\n                },\n                observables));\n    }\n\n    public DerivedObservableList<T> sorted(ObservableValue<Comparator<T>> comp) {\n        var d = this.<T>createNewDerived();\n        Runnable runnable = () -> {\n            List<T> toApply;\n            synchronized (list) {\n                toApply = listStream().sorted(comp.getValue()).toList();\n            }\n            d.setContent(toApply);\n        };\n        runnable.run();\n        list.addListener((ListChangeListener<? super T>) c -> {\n            runnable.run();\n        });\n        comp.addListener(observable -> {\n            d.list.sort(comp.getValue());\n        });\n        return d;\n    }\n\n    public DerivedObservableList<T> blockUpdatesIf(ObservableBooleanValue block) {\n        var d = this.<T>createNewDerived();\n        Runnable runnable = () -> {\n            d.setContent(list);\n        };\n        runnable.run();\n        list.addListener((ListChangeListener<? super T>) c -> {\n            if (!block.getValue()) {\n                runnable.run();\n            }\n        });\n        block.addListener(observable -> {\n            if (!block.getValue()) {\n                runnable.run();\n            }\n        });\n        return d;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/ExclusiveValidator.java",
    "content": "package io.xpipe.app.platform;\n\nimport javafx.beans.Observable;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.binding.StringBinding;\nimport javafx.beans.property.ReadOnlyBooleanProperty;\nimport javafx.beans.property.ReadOnlyObjectProperty;\nimport javafx.beans.value.ObservableValue;\n\nimport net.synedra.validatorfx.ValidationResult;\n\nimport java.util.ArrayList;\nimport java.util.Map;\n\npublic final class ExclusiveValidator<T> implements Validator {\n\n    private final Map<T, ? extends Validator> validators;\n    private final ObservableValue<T> obs;\n\n    public ExclusiveValidator(Map<T, ? extends Validator> validators, ObservableValue<T> obs) {\n        this.validators = validators;\n        this.obs = obs;\n    }\n\n    private Validator get() {\n        return validators.get(obs.getValue());\n    }\n\n    @Override\n    public Check createCheck() {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public void add(Check check) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public void remove(Check check) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public ValidationResult getValidationResult() {\n        return get().getValidationResult();\n    }\n\n    @Override\n    public ReadOnlyObjectProperty<ValidationResult> validationResultProperty() {\n        return get().validationResultProperty();\n    }\n\n    @Override\n    public ReadOnlyBooleanProperty containsErrorsProperty() {\n        return get().containsErrorsProperty();\n    }\n\n    @Override\n    public boolean containsErrors() {\n        return get().containsErrors();\n    }\n\n    @Override\n    public boolean validate() {\n        return get().validate();\n    }\n\n    @Override\n    public StringBinding createStringBinding() {\n        return createStringBinding(\"- \", \"\\n\");\n    }\n\n    @Override\n    public StringBinding createStringBinding(String prefix, String separator) {\n        var list = new ArrayList<Observable>(\n                validators.values().stream().map(Validator::createStringBinding).toList());\n        list.add(obs);\n        Observable[] observables = list.toArray(Observable[]::new);\n        return Bindings.createStringBinding(\n                () -> {\n                    return get().createStringBinding(prefix, separator).get();\n                },\n                observables);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/GlobalBooleanProperty.java",
    "content": "package io.xpipe.app.platform;\n\nimport javafx.beans.InvalidationListener;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.value.ChangeListener;\n\npublic class GlobalBooleanProperty extends SimpleBooleanProperty {\n\n    public GlobalBooleanProperty() {}\n\n    public GlobalBooleanProperty(boolean initialValue) {\n        super(initialValue);\n    }\n\n    @Override\n    public synchronized void addListener(InvalidationListener listener) {\n        super.addListener(listener);\n    }\n\n    @Override\n    public synchronized void removeListener(InvalidationListener listener) {\n        super.removeListener(listener);\n    }\n\n    @Override\n    public synchronized void addListener(ChangeListener<? super Boolean> listener) {\n        super.addListener(listener);\n    }\n\n    @Override\n    public synchronized void removeListener(ChangeListener<? super Boolean> listener) {\n        super.removeListener(listener);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/GlobalClipboard.java",
    "content": "package io.xpipe.app.platform;\n\nimport io.xpipe.app.util.ThreadHelper;\n\nimport java.awt.*;\nimport java.awt.datatransfer.Clipboard;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Consumer;\n\npublic class GlobalClipboard {\n\n    private static final List<Consumer<Clipboard>> clipboardListeners = new ArrayList<>();\n\n    public static synchronized void addListener(Consumer<Clipboard> listener) {\n        clipboardListeners.add(listener);\n    }\n\n    public static void init() {\n        // Only access from one thread to fix https://bugs.openjdk.org/browse/JDK-8332271\n        Toolkit.getDefaultToolkit().getSystemClipboard().addFlavorListener(e -> {\n            // Fix clipboard open issues: https://stackoverflow.com/a/51797746\n            ThreadHelper.sleep(20);\n\n            var cp = (Clipboard) e.getSource();\n            ThreadHelper.runFailableAsync(() -> {\n                synchronized (GlobalClipboard.class) {\n                    for (Consumer<Clipboard> clipboardListener : clipboardListeners) {\n                        clipboardListener.accept(cp);\n                    }\n                }\n            });\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/GlobalDoubleProperty.java",
    "content": "package io.xpipe.app.platform;\n\nimport javafx.beans.InvalidationListener;\nimport javafx.beans.property.SimpleDoubleProperty;\nimport javafx.beans.value.ChangeListener;\n\npublic class GlobalDoubleProperty extends SimpleDoubleProperty {\n\n    public GlobalDoubleProperty() {}\n\n    public GlobalDoubleProperty(Double initialValue) {\n        super(initialValue);\n    }\n\n    @Override\n    public synchronized void addListener(InvalidationListener listener) {\n        super.addListener(listener);\n    }\n\n    @Override\n    public synchronized void removeListener(InvalidationListener listener) {\n        super.removeListener(listener);\n    }\n\n    @Override\n    public synchronized void addListener(ChangeListener<? super Number> listener) {\n        super.addListener(listener);\n    }\n\n    @Override\n    public synchronized void removeListener(ChangeListener<? super Number> listener) {\n        super.removeListener(listener);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/GlobalObjectProperty.java",
    "content": "package io.xpipe.app.platform;\n\nimport javafx.beans.InvalidationListener;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.value.ChangeListener;\n\npublic class GlobalObjectProperty<T> extends SimpleObjectProperty<T> {\n\n    public GlobalObjectProperty() {}\n\n    public GlobalObjectProperty(T initialValue) {\n        super(initialValue);\n    }\n\n    @Override\n    public synchronized void addListener(InvalidationListener listener) {\n        super.addListener(listener);\n    }\n\n    @Override\n    public synchronized void removeListener(InvalidationListener listener) {\n        super.removeListener(listener);\n    }\n\n    @Override\n    public synchronized void addListener(ChangeListener<? super T> listener) {\n        super.addListener(listener);\n    }\n\n    @Override\n    public synchronized void removeListener(ChangeListener<? super T> listener) {\n        super.removeListener(listener);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/GlobalStringProperty.java",
    "content": "package io.xpipe.app.platform;\n\nimport javafx.beans.InvalidationListener;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ChangeListener;\n\npublic class GlobalStringProperty extends SimpleStringProperty {\n\n    public GlobalStringProperty() {}\n\n    public GlobalStringProperty(String initialValue) {\n        super(initialValue);\n    }\n\n    @Override\n    public synchronized void addListener(InvalidationListener listener) {\n        super.addListener(listener);\n    }\n\n    @Override\n    public synchronized void removeListener(InvalidationListener listener) {\n        super.removeListener(listener);\n    }\n\n    @Override\n    public synchronized void addListener(ChangeListener<? super String> listener) {\n        super.addListener(listener);\n    }\n\n    @Override\n    public synchronized void removeListener(ChangeListener<? super String> listener) {\n        super.removeListener(listener);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/InputHelper.java",
    "content": "package io.xpipe.app.platform;\n\nimport javafx.event.EventHandler;\nimport javafx.event.EventTarget;\nimport javafx.scene.input.*;\n\nimport java.util.function.Consumer;\n\npublic class InputHelper {\n\n    public static void onKeyCombination(EventTarget target, KeyCombination c, boolean filter, Consumer<KeyEvent> r) {\n        EventHandler<KeyEvent> keyEventEventHandler = event -> {\n            if (c.match(event)) {\n                r.accept(event);\n            }\n        };\n        if (filter) {\n            target.addEventFilter(KeyEvent.KEY_PRESSED, keyEventEventHandler);\n        } else {\n            target.addEventHandler(KeyEvent.KEY_PRESSED, keyEventEventHandler);\n        }\n    }\n\n    public static void onExactKeyCode(EventTarget target, KeyCode code, boolean filter, Consumer<KeyEvent> r) {\n        EventHandler<KeyEvent> keyEventEventHandler = event -> {\n            if (new KeyCodeCombination(code).match(event)) {\n                r.accept(event);\n            }\n        };\n        if (filter) {\n            target.addEventFilter(KeyEvent.KEY_PRESSED, keyEventEventHandler);\n        } else {\n            target.addEventHandler(KeyEvent.KEY_PRESSED, keyEventEventHandler);\n        }\n    }\n\n    public static void onLeft(EventTarget target, boolean filter, Consumer<KeyEvent> r) {\n        EventHandler<KeyEvent> e = event -> {\n            if (new KeyCodeCombination(KeyCode.LEFT).match(event)\n                    || new KeyCodeCombination(KeyCode.NUMPAD4).match(event)) {\n                r.accept(event);\n            }\n        };\n        if (filter) {\n            target.addEventFilter(KeyEvent.KEY_PRESSED, e);\n        } else {\n            target.addEventHandler(KeyEvent.KEY_PRESSED, e);\n        }\n    }\n\n    public static void onRight(EventTarget target, boolean filter, Consumer<KeyEvent> r) {\n        EventHandler<KeyEvent> e = event -> {\n            if (new KeyCodeCombination(KeyCode.RIGHT).match(event)\n                    || new KeyCodeCombination(KeyCode.NUMPAD6).match(event)) {\n                r.accept(event);\n            }\n        };\n        if (filter) {\n            target.addEventFilter(KeyEvent.KEY_PRESSED, e);\n        } else {\n            target.addEventHandler(KeyEvent.KEY_PRESSED, e);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/JfxHelper.java",
    "content": "package io.xpipe.app.platform;\n\nimport io.xpipe.app.comp.base.PrettyImageHelper;\nimport io.xpipe.app.core.AppFontSizes;\n\nimport javafx.beans.value.ObservableValue;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.Label;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.VBox;\n\nimport atlantafx.base.controls.Spacer;\n\npublic class JfxHelper {\n\n    public static Region createNamedEntry(\n            ObservableValue<String> nameString, ObservableValue<String> descString, String image) {\n        var header = new Label();\n        header.textProperty().bind(nameString);\n        AppFontSizes.xl(header);\n        var desc = new Label();\n        desc.textProperty().bind(descString);\n        AppFontSizes.xs(desc);\n        var text = new VBox(header, new Spacer(), desc);\n        text.setAlignment(Pos.CENTER_LEFT);\n\n        if (image == null) {\n            return text;\n        }\n\n        var size = 40;\n        var graphic = PrettyImageHelper.ofFixedSizeSquare(image, size).build();\n\n        var hbox = new HBox(graphic, text);\n        hbox.setAlignment(Pos.CENTER_LEFT);\n        hbox.setFillHeight(true);\n        hbox.setSpacing(10);\n\n        //        graphic.fitWidthProperty().bind(Bindings.createDoubleBinding(() -> header.getHeight() +\n        // desc.getHeight() + 2,\n        //                header.heightProperty(), desc.heightProperty()));\n        //        graphic.fitHeightProperty().bind(Bindings.createDoubleBinding(() -> header.getHeight() +\n        // desc.getHeight() + 2,\n        //                header.heightProperty(), desc.heightProperty()));\n\n        return hbox;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/LabelGraphic.java",
    "content": "package io.xpipe.app.platform;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.base.PrettyImageHelper;\n\nimport javafx.scene.Node;\n\nimport lombok.EqualsAndHashCode;\nimport lombok.Value;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.function.Supplier;\n\npublic abstract class LabelGraphic {\n\n    public static LabelGraphic none() {\n        return new LabelGraphic() {\n\n            @Override\n            public Node createGraphicNode() {\n                return null;\n            }\n        };\n    }\n\n    public abstract Node createGraphicNode();\n\n    @Value\n    @EqualsAndHashCode(callSuper = true)\n    public static class IconGraphic extends LabelGraphic {\n\n        String icon;\n\n        @Override\n        public Node createGraphicNode() {\n            var fi = new FontIcon(icon);\n            fi.getStyleClass().add(\"graphic\");\n            return fi;\n        }\n    }\n\n    @Value\n    @EqualsAndHashCode(callSuper = true)\n    public static class ImageGraphic extends LabelGraphic {\n\n        String file;\n        int size;\n\n        @Override\n        public Node createGraphicNode() {\n            return PrettyImageHelper.ofFixedSizeSquare(file, size)\n                    .style(\"graphic\")\n                    .build();\n        }\n    }\n\n    @Value\n    @EqualsAndHashCode(callSuper = true)\n    public static class CompGraphic extends LabelGraphic {\n\n        BaseRegionBuilder<?, ?> comp;\n\n        @Override\n        public Node createGraphicNode() {\n            return comp.style(\"graphic\").build();\n        }\n    }\n\n    @Value\n    @EqualsAndHashCode(callSuper = true)\n    public static class NodeGraphic extends LabelGraphic {\n\n        Supplier<Node> node;\n\n        @Override\n        public Node createGraphicNode() {\n            var n = node.get();\n            if (n != null) {\n                n.getStyleClass().add(\"graphic\");\n            }\n            return n;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/MacOsPermissions.java",
    "content": "package io.xpipe.app.platform;\n\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.application.Platform;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.scene.control.Alert;\n\nimport java.util.concurrent.atomic.AtomicReference;\n\n@SuppressWarnings(\"unused\")\npublic class MacOsPermissions {\n\n    public static boolean waitForAccessibilityPermissions() throws Exception {\n        AtomicReference<Alert> alert = new AtomicReference<>();\n        var state = new SimpleBooleanProperty(true);\n        try (var pc = LocalShell.getShell().start()) {\n            while (state.get()) {\n                // We can't wait in the platform thread, so just return instantly\n                if (Platform.isFxApplicationThread()) {\n                    pc.osascriptCommand(\"\"\"\n                                        tell application \"System Events\" to keystroke \"t\"\n                                        \"\"\").execute();\n                    return true;\n                }\n\n                var success = pc.osascriptCommand(\"\"\"\n                                                  tell application \"System Events\" to keystroke \"t\"\n                                                  \"\"\").executeAndCheck();\n\n                if (success) {\n                    Platform.runLater(() -> {\n                        if (alert.get() != null) {\n                            alert.get().close();\n                        }\n                    });\n                    return true;\n                } else {\n                    Platform.runLater(() -> {\n                        if (alert.get() != null) {\n                            return;\n                        }\n\n                        //                        AppWindowHelper.showAlert(\n                        //                                a -> {\n                        //                                    a.setAlertType(Alert.AlertType.INFORMATION);\n                        //                                    a.setTitle(AppI18n.get(\"permissionsAlertTitle\"));\n                        //                                    a.setHeaderText(AppI18n.get(\"permissionsAlertHeader\"));\n                        //                                    a.getDialogPane()\n                        //                                            .setContent(AppWindowHelper.alertContentText(\n                        //                                                    AppI18n.get(\"permissionsAlertContent\")));\n                        //                                    a.getButtonTypes().clear();\n                        //                                    a.getButtonTypes().add(ButtonType.CANCEL);\n                        //                                    alert.set(a);\n                        //                                },\n                        //                                buttonType -> {\n                        //                                    alert.get().close();\n                        //                                    state.set(false);\n                        //                                });\n                    });\n                    ThreadHelper.sleep(1000);\n                }\n            }\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/MarkdownHelper.java",
    "content": "package io.xpipe.app.platform;\n\nimport com.vladsch.flexmark.ext.anchorlink.AnchorLinkExtension;\nimport com.vladsch.flexmark.ext.definition.DefinitionExtension;\nimport com.vladsch.flexmark.ext.footnotes.FootnoteExtension;\nimport com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension;\nimport com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension;\nimport com.vladsch.flexmark.ext.tables.TablesExtension;\nimport com.vladsch.flexmark.ext.toc.TocExtension;\nimport com.vladsch.flexmark.ext.yaml.front.matter.YamlFrontMatterExtension;\nimport com.vladsch.flexmark.html.HtmlRenderer;\nimport com.vladsch.flexmark.parser.Parser;\nimport com.vladsch.flexmark.util.ast.Document;\nimport com.vladsch.flexmark.util.data.MutableDataSet;\n\nimport java.util.Arrays;\nimport java.util.function.UnaryOperator;\n\npublic class MarkdownHelper {\n\n    public static String toHtml(\n            String value,\n            UnaryOperator<String> headTransformation,\n            UnaryOperator<String> bodyTransformation,\n            String bodyStyleClass) {\n        MutableDataSet options = new MutableDataSet()\n                .set(\n                        Parser.EXTENSIONS,\n                        Arrays.asList(\n                                StrikethroughExtension.create(),\n                                TaskListExtension.create(),\n                                TablesExtension.create(),\n                                FootnoteExtension.create(),\n                                DefinitionExtension.create(),\n                                AnchorLinkExtension.create(),\n                                YamlFrontMatterExtension.create(),\n                                TocExtension.create()))\n                .set(FootnoteExtension.FOOTNOTE_BACK_LINK_REF_CLASS, \"footnotes\")\n                .set(TablesExtension.WITH_CAPTION, false)\n                .set(TablesExtension.COLUMN_SPANS, false)\n                .set(TablesExtension.MIN_HEADER_ROWS, 1)\n                .set(TablesExtension.MAX_HEADER_ROWS, 1)\n                .set(TablesExtension.APPEND_MISSING_COLUMNS, true)\n                .set(TablesExtension.DISCARD_EXTRA_COLUMNS, true)\n                .set(TablesExtension.HEADER_SEPARATOR_COLUMN_MATCH, true)\n                .set(HtmlRenderer.GENERATE_HEADER_ID, true);\n        Parser parser = Parser.builder(options).build();\n        HtmlRenderer renderer = HtmlRenderer.builder(options).build();\n        Document document = parser.parse(value);\n        var html = renderer.render(document);\n        var result = bodyTransformation.apply(html);\n        var headContent = headTransformation.apply(\"<meta charset=\\\"utf-8\\\"/>\");\n        return \"<html><head>\" + headContent\n                + \"</head><body\"\n                + (bodyStyleClass != null ? \" class=\\\"\" + bodyStyleClass + \"\\\"\" : \"\")\n                + \"><article class=\\\"markdown-body\\\">\"\n                + result\n                + \"</article></body></html>\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/MenuHelper.java",
    "content": "package io.xpipe.app.platform;\n\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.prefs.AppPrefs;\n\nimport javafx.application.Platform;\nimport javafx.geometry.Side;\nimport javafx.scene.Node;\nimport javafx.scene.control.*;\nimport javafx.scene.control.skin.ComboBoxListViewSkin;\nimport javafx.scene.control.skin.ComboBoxPopupControl;\nimport javafx.scene.control.skin.MenuButtonSkin;\nimport javafx.scene.control.skin.MenuButtonSkinBase;\nimport javafx.scene.layout.Region;\n\nimport lombok.SneakyThrows;\n\npublic class MenuHelper {\n\n    @SneakyThrows\n    public static <T> ComboBoxListViewSkin<T> fixComboBoxSkin(ComboBoxListViewSkin<T> skin) {\n        var m = ComboBoxPopupControl.class.getDeclaredMethod(\"getPopup\");\n        m.setAccessible(true);\n        var popup = (PopupControl) m.invoke(skin);\n        popup.setAutoHide(!AppPrefs.get().limitedTouchscreenMode().get());\n        return skin;\n    }\n\n    @SneakyThrows\n    public static MenuButton createMenuButton() {\n        var mb = new MenuButton();\n        var skin = new MenuButtonSkin(mb);\n        mb.setSkin(skin);\n        var field = MenuButtonSkinBase.class.getDeclaredField(\"popup\");\n        field.setAccessible(true);\n        var popup = (PopupControl) field.get(skin);\n        popup.setAutoHide(!AppPrefs.get().limitedTouchscreenMode().get());\n        return mb;\n    }\n\n    @SneakyThrows\n    public static <T> ComboBox<T> createComboBox() {\n        var cb = new ComboBox<T>();\n        var skin = new ComboBoxListViewSkin<>(cb);\n        fixComboBoxSkin(skin);\n        cb.setSkin(skin);\n        return cb;\n    }\n\n    public static ContextMenu createContextMenu() {\n        ContextMenu contextMenu = new ContextMenu();\n        contextMenu.setAutoHide(!AppPrefs.get().limitedTouchscreenMode().get());\n        InputHelper.onLeft(contextMenu, false, e -> {\n            contextMenu.hide();\n            e.consume();\n        });\n        contextMenu.addEventFilter(Menu.ON_SHOWING, e -> {\n            Node content = contextMenu.getSkin().getNode();\n            if (content instanceof Region r) {\n                r.setMaxWidth(500);\n            }\n        });\n        contextMenu.addEventFilter(Menu.ON_SHOWN, e -> {\n            Platform.runLater(() -> {\n                var first = contextMenu.getItems().getFirst();\n                if (first != null) {\n                    var s = first.getStyleableNode();\n                    if (s != null) {\n                        s.requestFocus();\n                    }\n                }\n            });\n        });\n        AppFontSizes.lg(contextMenu.getStyleableNode());\n        return contextMenu;\n    }\n\n    public static MenuItem createMenuItem(LabelGraphic graphic, String nameKey) {\n        var i = new MenuItem();\n        i.textProperty().bind(AppI18n.observable(nameKey));\n        i.setGraphic(graphic.createGraphicNode());\n        return i;\n    }\n\n    public static void toggleMenuShow(ContextMenu contextMenu, Node ref, Side side) {\n        if (!contextMenu.isShowing()) {\n            // Prevent NPE in show()\n            if (contextMenu.getScene() == null || ref == null || ref.getScene() == null) {\n                return;\n            }\n            contextMenu.show(ref, side, 0, 0);\n        } else {\n            contextMenu.hide();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/NativeBridge.java",
    "content": "package io.xpipe.app.platform;\n\nimport io.xpipe.app.core.AppInstallation;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.core.OsType;\n\nimport com.sun.jna.Library;\nimport com.sun.jna.Native;\nimport com.sun.jna.NativeLong;\n\nimport java.util.Map;\nimport java.util.Optional;\n\npublic class NativeBridge {\n\n    private static MacOsLibrary macOsLibrary;\n    private static boolean loadingFailed;\n\n    public static void init() {\n        // Preload\n        if (OsType.ofLocal() == OsType.MACOS && AppProperties.get().getArch().equals(\"arm64\")) {\n            getMacOsLibrary();\n        }\n    }\n\n    public static Optional<MacOsLibrary> getMacOsLibrary() {\n        if (!AppProperties.get().isImage()\n                || !AppProperties.get().isFullVersion()\n                || !AppProperties.get().getArch().equals(\"arm64\")) {\n            return Optional.empty();\n        }\n\n        if (macOsLibrary == null && !loadingFailed) {\n            try {\n                System.setProperty(\n                        \"jna.library.path\",\n                        AppInstallation.ofCurrent()\n                                .getBaseInstallationPath()\n                                .resolve(\"Contents\")\n                                .resolve(\"runtime\")\n                                .resolve(\"Contents\")\n                                .resolve(\"Home\")\n                                .resolve(\"lib\")\n                                .toString());\n                var l = Native.load(\"xpipe_bridge\", MacOsLibrary.class, Map.of());\n                macOsLibrary = l;\n            } catch (Throwable t) {\n                ErrorEventFactory.fromThrowable(t).handle();\n                loadingFailed = true;\n            }\n        }\n        return Optional.ofNullable(macOsLibrary);\n    }\n\n    public interface MacOsLibrary extends Library {\n\n        void setAppearance(NativeLong window, boolean seamlessFrame, boolean dark);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/NativeMacOsWindowControl.java",
    "content": "package io.xpipe.app.platform;\n\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.issue.ErrorEventFactory;\n\nimport javafx.stage.Window;\n\nimport com.sun.jna.NativeLong;\nimport lombok.Getter;\nimport lombok.SneakyThrows;\n\nimport java.lang.reflect.Method;\n\n@Getter\npublic class NativeMacOsWindowControl {\n\n    private final long nsWindow;\n\n    @SneakyThrows\n    public NativeMacOsWindowControl(Window stage) {\n        Method tkStageGetter = Window.class.getDeclaredMethod(\"getPeer\");\n        tkStageGetter.setAccessible(true);\n        Object tkStage = tkStageGetter.invoke(stage);\n        Method getPlatformWindow = tkStage.getClass().getDeclaredMethod(\"getPlatformWindow\");\n        getPlatformWindow.setAccessible(true);\n        Object platformWindow = getPlatformWindow.invoke(tkStage);\n        Method getNativeHandle = platformWindow.getClass().getMethod(\"getNativeHandle\");\n        getNativeHandle.setAccessible(true);\n        Object nativeHandle = getNativeHandle.invoke(platformWindow);\n        this.nsWindow = (long) nativeHandle;\n    }\n\n    public boolean setAppearance(boolean seamlessFrame, boolean darkMode) {\n        if (!AppProperties.get().isImage() || !AppProperties.get().isFullVersion()) {\n            return false;\n        }\n\n        var lib = NativeBridge.getMacOsLibrary();\n        if (lib.isEmpty()) {\n            return false;\n        }\n\n        try {\n            lib.get().setAppearance(new NativeLong(nsWindow), seamlessFrame, darkMode);\n        } catch (Throwable e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/NativeWinWindowControl.java",
    "content": "package io.xpipe.app.platform;\n\nimport io.xpipe.app.util.Rect;\n\nimport io.xpipe.app.util.User32Ex;\nimport javafx.stage.Window;\n\nimport com.sun.jna.Library;\nimport com.sun.jna.Native;\nimport com.sun.jna.Pointer;\nimport com.sun.jna.PointerType;\nimport com.sun.jna.platform.win32.*;\nimport com.sun.jna.ptr.IntByReference;\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.SneakyThrows;\n\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.List;\n\n@Getter\n@EqualsAndHashCode\npublic class NativeWinWindowControl {\n\n    private static final int WS_EX_APPWINDOW = 0x00040000;\n\n    public static NativeWinWindowControl MAIN_WINDOW;\n    private final WinDef.HWND windowHandle;\n\n    @SneakyThrows\n    public NativeWinWindowControl(Window stage) {\n        this.windowHandle = byWindow(stage);\n    }\n\n    public NativeWinWindowControl(WinDef.HWND windowHandle) {\n        this.windowHandle = windowHandle;\n    }\n\n    @SneakyThrows\n    public static WinDef.HWND byWindow(Window window) {\n        Method tkStageGetter = Window.class.getDeclaredMethod(\"getPeer\");\n        tkStageGetter.setAccessible(true);\n        Object tkStage = tkStageGetter.invoke(window);\n        Method getPlatformWindow = tkStage.getClass().getDeclaredMethod(\"getPlatformWindow\");\n        getPlatformWindow.setAccessible(true);\n        Object platformWindow = getPlatformWindow.invoke(tkStage);\n        Method getNativeHandle = platformWindow.getClass().getMethod(\"getNativeHandle\");\n        getNativeHandle.setAccessible(true);\n        Object nativeHandle = getNativeHandle.invoke(platformWindow);\n        var hwnd = new WinDef.HWND(new Pointer((long) nativeHandle));\n        return hwnd;\n    }\n\n    public static List<NativeWinWindowControl> byPid(long pid) {\n        var refs = new ArrayList<NativeWinWindowControl>();\n        User32.INSTANCE.EnumWindows(\n                (hWnd, data) -> {\n                    var visible = User32.INSTANCE.IsWindowVisible(hWnd);\n                    if (!visible) {\n                        return true;\n                    }\n\n                    var wpid = new IntByReference();\n                    User32.INSTANCE.GetWindowThreadProcessId(hWnd, wpid);\n                    if (wpid.getValue() == pid) {\n                        refs.add(new NativeWinWindowControl(hWnd));\n                    }\n                    return true;\n                },\n                null);\n        return refs;\n    }\n\n    public void removeBorders() {\n        var rect = getBounds();\n\n        var style = User32.INSTANCE.GetWindowLong(windowHandle, User32.GWL_STYLE);\n        var mod = style & ~(User32.WS_CAPTION | User32.WS_THICKFRAME | User32.WS_MAXIMIZEBOX);\n        User32.INSTANCE.SetWindowLong(windowHandle, User32.GWL_STYLE, mod);\n\n        User32.INSTANCE.SetWindowPos(windowHandle, null, rect.getX(), rect.getY(), rect.getW() + 1, rect.getH(),\n                User32.SWP_NOACTIVATE | User32.SWP_NOMOVE | User32.SWP_NOZORDER);\n        User32.INSTANCE.SetWindowPos(windowHandle, null, rect.getX(), rect.getY(), rect.getW(), rect.getH(),\n                User32.SWP_NOACTIVATE | User32.SWP_NOMOVE | User32.SWP_NOZORDER);\n    }\n\n    public void restoreBorders() {\n        var rect = getBounds();\n\n        var style = User32.INSTANCE.GetWindowLong(windowHandle, User32.GWL_STYLE);\n        var mod = style | User32.WS_CAPTION | User32.WS_THICKFRAME | User32.WS_MAXIMIZEBOX;\n        User32.INSTANCE.SetWindowLong(windowHandle, User32.GWL_STYLE, mod);\n\n        User32.INSTANCE.SetWindowPos(windowHandle, null, rect.getX(), rect.getY(), rect.getW() + 1, rect.getH(),\n                User32.SWP_NOACTIVATE | User32.SWP_NOMOVE | User32.SWP_NOZORDER);\n        User32.INSTANCE.SetWindowPos(windowHandle, null, rect.getX(), rect.getY(), rect.getW(), rect.getH(),\n                User32.SWP_NOACTIVATE | User32.SWP_NOMOVE | User32.SWP_NOZORDER);\n    }\n\n    public void removeIcon() {\n        var style = User32.INSTANCE.GetWindowLong(windowHandle, User32.GWL_EXSTYLE);\n        var mod = style & ~(WS_EX_APPWINDOW);\n        User32.INSTANCE.SetWindowLong(windowHandle, User32.GWL_EXSTYLE, mod);\n    }\n\n    public void restoreIcon() {\n        var style = User32.INSTANCE.GetWindowLong(windowHandle, User32.GWL_EXSTYLE);\n        var mod = style | WS_EX_APPWINDOW;\n        User32.INSTANCE.SetWindowLong(windowHandle, User32.GWL_EXSTYLE, mod);\n    }\n\n    public void takeOwnership(WinDef.HWND owner) {\n        User32Ex.INSTANCE.SetWindowLongPtr(getWindowHandle(), User32.GWL_HWNDPARENT, owner);\n    }\n\n    public void releaseOwnership() {\n        User32Ex.INSTANCE.SetWindowLongPtr(getWindowHandle(), User32.GWL_HWNDPARENT, (WinDef.HWND) null);\n    }\n\n    public boolean isIconified() {\n        return (User32.INSTANCE.GetWindowLong(windowHandle, User32.GWL_STYLE) & User32.WS_MINIMIZE) != 0;\n    }\n\n    public boolean isVisible() {\n        return User32.INSTANCE.IsWindowVisible(windowHandle);\n    }\n\n    public void moveToFront() {\n        orderRelative(new WinDef.HWND(new Pointer(0)));\n    }\n\n    public void orderRelative(WinDef.HWND predecessor) {\n        User32.INSTANCE.SetWindowPos(\n                windowHandle, predecessor, 0, 0, 0, 0, User32.SWP_NOACTIVATE | User32.SWP_NOMOVE | User32.SWP_NOSIZE);\n    }\n\n    public void show() {\n        User32.INSTANCE.ShowWindow(windowHandle, User32.SW_RESTORE);\n    }\n\n    public void close() {\n        User32.INSTANCE.PostMessage(windowHandle, User32.WM_CLOSE, null, null);\n    }\n\n    public void minimize() {\n        User32.INSTANCE.ShowWindow(windowHandle, User32.SW_MINIMIZE);\n    }\n\n    public void move(Rect bounds) {\n        User32.INSTANCE.SetWindowPos(\n                windowHandle, null, bounds.getX(), bounds.getY(), bounds.getW(), bounds.getH(), User32.SWP_NOACTIVATE | User32.SWP_NOZORDER);\n    }\n\n    public Rect getBounds() {\n        var rect = new WinDef.RECT();\n        User32.INSTANCE.GetWindowRect(windowHandle, rect);\n        return new Rect(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);\n    }\n\n    public boolean setWindowAttribute(int attribute, boolean attributeValue) {\n        var r = Dwm.INSTANCE.DwmSetWindowAttribute(\n                windowHandle, attribute, new WinDef.BOOLByReference(new WinDef.BOOL(attributeValue)), WinDef.BOOL.SIZE);\n        return r.longValue() == 0;\n    }\n\n    public void activate() {\n        User32.INSTANCE.SetForegroundWindow(windowHandle);\n    }\n\n    public boolean setWindowBackdrop(DwmSystemBackDropType backdrop) {\n        var r = Dwm.INSTANCE.DwmSetWindowAttribute(\n                windowHandle,\n                DmwaWindowAttribute.DWMWA_SYSTEMBACKDROP_TYPE.get(),\n                new WinDef.DWORDByReference(new WinDef.DWORD(backdrop.get())),\n                WinDef.DWORD.SIZE);\n        return r.longValue() == 0;\n    }\n\n    public void setWindowsTransitionsEnabled(boolean enabled) {\n        setWindowAttribute(DmwaWindowAttribute.DWMWA_TRANSITIONS_FORCEDISABLED.get(), !enabled);\n    }\n\n    public enum DmwaWindowAttribute {\n        DWMWA_TRANSITIONS_FORCEDISABLED(3),\n        DWMWA_USE_IMMERSIVE_DARK_MODE(20),\n        DWMWA_SYSTEMBACKDROP_TYPE(38);\n\n        private final int value;\n\n        DmwaWindowAttribute(int value) {\n            this.value = value;\n        }\n\n        public int get() {\n            return value;\n        }\n    }\n\n    @SuppressWarnings(\"unused\")\n    public enum DwmSystemBackDropType {\n        // DWMSBT_NONE\n        NONE(1),\n        // DWMSBT_MAINWINDOW\n        MICA(2),\n        // DWMSBT_TRANSIENTWINDOW\n        ACRYLIC(3),\n        // DWMSBT_TABBEDWINDOW\n        MICA_ALT(4);\n\n        private final int value;\n\n        DwmSystemBackDropType(int value) {\n            this.value = value;\n        }\n\n        public int get() {\n            return value;\n        }\n    }\n\n    public interface Dwm extends Library {\n\n        Dwm INSTANCE = Native.load(\"dwmapi\", Dwm.class);\n\n        WinNT.HRESULT DwmSetWindowAttribute(\n                WinDef.HWND hwnd, int dwAttribute, PointerType pvAttribute, int cbAttribute);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/NodeHelper.java",
    "content": "package io.xpipe.app.platform;\n\nimport javafx.scene.Node;\n\npublic class NodeHelper {\n\n    public static boolean isParent(Node parent, Object child) {\n        if (child == null) {\n            return false;\n        }\n\n        if (!(child instanceof Node n)) {\n            return false;\n        }\n\n        var c = n.getParent();\n        while (c != null) {\n            if (c == parent) {\n                return true;\n            }\n\n            c = c.getParent();\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/OptionsBuilder.java",
    "content": "package io.xpipe.app.platform;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.GuiDialog;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.util.BooleanScope;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.app.util.LicenseProvider;\nimport io.xpipe.core.InPlaceSecretValue;\n\nimport javafx.beans.property.*;\nimport javafx.beans.value.ChangeListener;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.ListChangeListener;\nimport javafx.collections.ObservableList;\nimport javafx.geometry.Orientation;\nimport javafx.scene.control.ComboBox;\nimport javafx.scene.layout.Region;\n\nimport atlantafx.base.controls.Spacer;\n\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\n\npublic class OptionsBuilder {\n\n    public <V, T> ObjectProperty<T> map(Property<V> prop, Function<V, T> function) {\n        var mapped = new SimpleObjectProperty<T>();\n        prop.subscribe(v -> {\n            if (mappingUpdate.get()) {\n                return;\n            }\n\n            try (var ignored = new BooleanScope(mappingUpdate).start()) {\n                mapped.setValue(function.apply(v));\n            }\n        });\n        return mapped;\n    }\n\n    public <V, T, R> ObjectProperty<R> map(Property<V> prop, Function<V, T> function, Function<T, R> subFunction) {\n        var mapped = new SimpleObjectProperty<R>();\n        prop.subscribe(v -> {\n            if (mappingUpdate.get()) {\n                return;\n            }\n\n            T t = function.apply(v);\n            R r = t != null ? subFunction.apply(t) : null;\n            try (var ignored = new BooleanScope(mappingUpdate).start()) {\n                mapped.setValue(r);\n            }\n        });\n        return mapped;\n    }\n\n    private final Validator ownValidator;\n    private final List<Validator> allValidators = new ArrayList<>();\n    private final List<Check> allChecks = new ArrayList<>();\n    private final List<OptionsComp.Entry> entries = new ArrayList<>();\n    private final List<Property<?>> props = new ArrayList<>();\n\n    private ObservableValue<String> name;\n    private ObservableValue<String> description;\n    private String documentationLink;\n    private BaseRegionBuilder<?, ?> comp;\n    private BaseRegionBuilder<?, ?> lastCompHeadReference;\n    private ObservableValue<String> lastNameReference;\n    private boolean focusFirstIncomplete = true;\n\n    private final BooleanProperty mappingUpdate = new SimpleBooleanProperty();\n\n    public OptionsBuilder disableFirstIncompleteFocus() {\n        focusFirstIncomplete = false;\n        return this;\n    }\n\n    public OptionsBuilder() {\n        this.ownValidator = new SimpleValidator();\n        this.allValidators.add(ownValidator);\n    }\n\n    public OptionsBuilder(Validator validator) {\n        this.ownValidator = validator;\n        this.allValidators.add(ownValidator);\n    }\n\n    public Validator buildEffectiveValidator() {\n        return new ChainedValidator(allValidators);\n    }\n\n    public OptionsBuilder choice(IntegerProperty selectedIndex, Map<ObservableValue<String>, OptionsBuilder> options) {\n        return choice(selectedIndex, options, null);\n    }\n\n    public OptionsBuilder choice(\n            IntegerProperty selectedIndex,\n            Map<ObservableValue<String>, OptionsBuilder> options,\n            Function<ComboBox<ChoicePaneComp.Entry>, Region> transformer) {\n        var list = options.entrySet().stream()\n                .map(e -> new ChoicePaneComp.Entry(\n                        e.getKey(), e.getValue() != null ? e.getValue().buildComp() : RegionBuilder.empty()))\n                .toList();\n        var validatorList = options.values().stream()\n                .map(builder -> builder != null ? builder.buildEffectiveValidator() : new SimpleValidator())\n                .toList();\n        var selected =\n                new SimpleObjectProperty<>(selectedIndex.getValue() != -1 ? list.get(selectedIndex.getValue()) : null);\n        selected.addListener((observable, oldValue, newValue) -> {\n            selectedIndex.setValue(newValue != null ? list.indexOf(newValue) : null);\n            if (newValue != null) {\n                validatorList.get(list.indexOf(newValue)).validate();\n            }\n        });\n        selectedIndex.addListener((observable, oldValue, newValue) -> {\n            selected.setValue(list.get(newValue.intValue()));\n        });\n        var pane = new ChoicePaneComp(list, selected);\n        if (transformer != null) {\n            pane.setTransformer(transformer);\n        }\n\n        var validatorMap = new LinkedHashMap<ChoicePaneComp.Entry, Validator>();\n        for (int i = 0; i < list.size(); i++) {\n            validatorMap.put(list.get(i), validatorList.get(i));\n        }\n        validatorMap.put(null, new SimpleValidator());\n        var orVal = new ExclusiveValidator<>(validatorMap, selected);\n\n        options.values().forEach(builder -> {\n            if (builder != null) {\n                props.addAll(builder.props);\n            }\n        });\n        props.add(selectedIndex);\n        allValidators.add(orVal);\n        pushComp(pane);\n        return this;\n    }\n\n    private void finishCurrent() {\n        if (comp == null) {\n            return;\n        }\n\n        var entry = new OptionsComp.Entry(description, documentationLink, name, comp);\n        description = null;\n        documentationLink = null;\n        name = null;\n        lastNameReference = null;\n        comp = null;\n        lastCompHeadReference = null;\n        entries.add(entry);\n    }\n\n    public OptionsBuilder nameAndDescription(String key) {\n        return name(key).description(key + \"Description\");\n    }\n\n    public OptionsBuilder nameAndDescription(ObservableValue<String> key) {\n        return name(AppI18n.observable(key))\n                .description(AppI18n.observable(BindingsHelper.map(key, k -> k + \"Description\")));\n    }\n\n    public OptionsBuilder subAdvanced(OptionsBuilder builder) {\n        name(\"advanced\");\n        subExpandable(\"showAdvancedOptions\", builder);\n        return this;\n    }\n\n    public OptionsBuilder subExpandable(String key, OptionsBuilder builder) {\n        sub(builder, null);\n        var subComp = this.comp;\n        var pane = new SimpleTitledPaneComp(AppI18n.observable(key), subComp, true);\n        pane.apply(struc -> struc.setExpanded(false));\n        this.comp = pane;\n        return this;\n    }\n\n    public OptionsBuilder sub(OptionsBuilder builder) {\n        return sub(builder, null);\n    }\n\n    public OptionsBuilder sub(OptionsBuilder builder, Property<?> prop) {\n        props.addAll(builder.props);\n        allValidators.add(builder.buildEffectiveValidator());\n        if (builder.focusFirstIncomplete) {\n            allChecks.addAll(builder.allChecks);\n        }\n        if (prop != null) {\n            props.add(prop);\n        }\n        var c = builder.lastCompHeadReference;\n        var n = builder.lastNameReference;\n        pushComp(builder.buildComp());\n        if (c != null) {\n            lastCompHeadReference = c;\n        }\n        if (n != null) {\n            lastNameReference = n;\n        }\n        return this;\n    }\n\n    public OptionsBuilder addTitle(String titleKey) {\n        finishCurrent();\n        entries.add(new OptionsComp.Entry(\n                null, null, null, new LabelComp(AppI18n.observable(titleKey)).style(\"title-header\")));\n        return this;\n    }\n\n    public OptionsBuilder addTitle(ObservableValue<String> title) {\n        finishCurrent();\n        entries.add(new OptionsComp.Entry(null, null, null, new LabelComp(title).style(\"title-header\")));\n        return this;\n    }\n\n    public OptionsBuilder pref(Object property) {\n        var mapping = AppPrefs.get().getMapping(property);\n        pref(\n                mapping.getKey(),\n                mapping.isRequiresRestart(),\n                mapping.getLicenseFeatureId(),\n                mapping.getDocumentationLink());\n        return this;\n    }\n\n    public OptionsBuilder pref(\n            String key, boolean requiresRestart, String licenseFeatureId, DocumentationLink documentationLink) {\n        var name = key;\n        name(name);\n        if (requiresRestart) {\n            description(AppI18n.observable(name + \"Description\").map(s -> s + \"\\n\\n\" + AppI18n.get(\"requiresRestart\")));\n        } else {\n            description(AppI18n.observable(name + \"Description\"));\n        }\n        if (documentationLink != null) {\n            documentationLink(documentationLink);\n        }\n        if (licenseFeatureId != null) {\n            licenseRequirement(licenseFeatureId);\n        }\n        return this;\n    }\n\n    public OptionsBuilder licenseRequirement(String featureId) {\n        var f = LicenseProvider.get().getFeature(featureId);\n        name = f.suffixObservable(name);\n        lastNameReference = name;\n        return this;\n    }\n\n    public OptionsBuilder check(Function<Validator, Check> c) {\n        var check = c.apply(ownValidator);\n        lastCompHeadReference.apply(s -> {\n            check.decorates(s);\n        });\n        allChecks.add(check);\n        return this;\n    }\n\n    public OptionsBuilder check(Check c) {\n        lastCompHeadReference.apply(s -> c.decorates(s));\n        allChecks.add(c);\n        return this;\n    }\n\n    public OptionsBuilder disable() {\n        lastCompHeadReference.disable(new SimpleBooleanProperty(true));\n        return this;\n    }\n\n    public OptionsBuilder disable(ObservableValue<Boolean> b) {\n        lastCompHeadReference.disable(b);\n        return this;\n    }\n\n    public OptionsBuilder hide(boolean b) {\n        return hide(new SimpleBooleanProperty(b));\n    }\n\n    public OptionsBuilder hide(ObservableValue<Boolean> b) {\n        lastCompHeadReference.hide(b);\n        return this;\n    }\n\n    public OptionsBuilder disable(boolean b) {\n        lastCompHeadReference.disable(new SimpleBooleanProperty(b));\n        return this;\n    }\n\n    public OptionsBuilder nonNull() {\n        var e = lastNameReference;\n        var p = props.getLast();\n        return check(Validator.nonNull(ownValidator, e, p));\n    }\n\n    public OptionsBuilder nonNullIf(ObservableValue<Boolean> b) {\n        var e = lastNameReference;\n        var p = props.getLast();\n        return check(Validator.nonNullIf(ownValidator, e, p, b));\n    }\n\n    private void pushComp(BaseRegionBuilder<?, ?> comp) {\n        finishCurrent();\n        this.comp = comp;\n        this.lastCompHeadReference = comp;\n    }\n\n    public OptionsBuilder addInteger(Property<Integer> prop) {\n        var comp = new IntFieldComp(prop);\n        pushComp(comp);\n        props.add(prop);\n        return this;\n    }\n\n    public OptionsBuilder addToggle(Property<Boolean> prop) {\n        var comp = new ToggleSwitchComp(prop, null, null);\n        pushComp(comp);\n        props.add(prop);\n        return this;\n    }\n\n    public OptionsBuilder addYesNoToggle(Property<Boolean> prop) {\n        var map = new LinkedHashMap<Boolean, ObservableValue<String>>();\n        map.put(Boolean.FALSE, AppI18n.observable(\"no\"));\n        map.put(null, AppI18n.observable(\"inherit\"));\n        map.put(Boolean.TRUE, AppI18n.observable(\"yes\"));\n        var comp = new ToggleGroupComp<>(prop, new SimpleObjectProperty<>(map));\n        pushComp(comp);\n        props.add(prop);\n        return this;\n    }\n\n    public OptionsBuilder addStaticString(Object o) {\n        return addStaticString(new SimpleStringProperty(o != null ? o.toString() : null));\n    }\n\n    public OptionsBuilder addStaticString(ObservableValue<String> s) {\n        var prop = new SimpleStringProperty();\n        s.subscribe(prop::set);\n        var comp = new TextFieldComp(prop, false);\n        BindingsHelper.preserve(comp, s);\n        comp.apply(struc -> {\n            struc.setEditable(false);\n            struc.setOpacity(0.9);\n            struc.setFocusTraversable(false);\n        });\n        pushComp(comp);\n        return this;\n    }\n\n    public OptionsBuilder addString(Property<String> prop) {\n        var comp = new TextFieldComp(prop, false);\n        pushComp(comp);\n        props.add(prop);\n        return this;\n    }\n\n    public OptionsBuilder spacer(double size) {\n        return addComp(RegionBuilder.of(() -> new Spacer(size, Orientation.VERTICAL)));\n    }\n\n    public OptionsBuilder name(String nameKey) {\n        finishCurrent();\n        name = AppI18n.observable(nameKey);\n        lastNameReference = name;\n        return this;\n    }\n\n    public OptionsBuilder name(ObservableValue<String> name) {\n        finishCurrent();\n        this.name = name;\n        lastNameReference = name;\n        return this;\n    }\n\n    public OptionsBuilder description(String descriptionKey) {\n        finishCurrent();\n        description = AppI18n.observable(descriptionKey);\n        return this;\n    }\n\n    public OptionsBuilder description(ObservableValue<String> description) {\n        finishCurrent();\n        this.description = description;\n        return this;\n    }\n\n    public OptionsBuilder documentationLink(String link) {\n        finishCurrent();\n        documentationLink = link;\n        return this;\n    }\n\n    public OptionsBuilder documentationLink(DocumentationLink link) {\n        finishCurrent();\n        documentationLink = link != null ? link.getLink() : null;\n        return this;\n    }\n\n    public OptionsBuilder addComp(BaseRegionBuilder<?, ?> comp) {\n        pushComp(comp);\n        return this;\n    }\n\n    public OptionsBuilder addComp(BaseRegionBuilder<?, ?> comp, Property<?> prop) {\n        pushComp(comp);\n        props.add(prop);\n        return this;\n    }\n\n    public OptionsBuilder addProperty(Property<?> prop) {\n        props.add(prop);\n        return this;\n    }\n\n    public <T> OptionsBuilder addProperty(ObservableList<T> prop) {\n        // For updating the options builder binding on list change, it doesn't support observable lists\n        var listHashProp = new SimpleIntegerProperty(0);\n        prop.addListener((ListChangeListener<T>) c -> {\n            listHashProp.set(c.getList().hashCode());\n        });\n        addProperty(listHashProp);\n        return this;\n    }\n\n    public OptionsBuilder addSecret(Property<InPlaceSecretValue> prop, boolean copy) {\n        var comp = new SecretFieldComp(prop, copy);\n        pushComp(comp);\n        props.add(prop);\n        return this;\n    }\n\n    @SafeVarargs\n    public final <T, V extends T> OptionsBuilder bind(Supplier<V> creator, Property<T>... toSet) {\n        props.forEach(prop -> {\n            prop.addListener((c, o, n) -> {\n                if (mappingUpdate.get()) {\n                    return;\n                }\n\n                for (Property<T> p : toSet) {\n                    p.setValue(creator.get());\n                }\n            });\n        });\n        for (Property<T> p : toSet) {\n            p.setValue(creator.get());\n        }\n        return this;\n    }\n\n    public final <T, V extends T> OptionsBuilder bindChoice(\n            Supplier<Property<? extends V>> creator, Property<T> toSet) {\n        var current = new AtomicReference<Property<? extends V>>(creator.get());\n        var listener = new ChangeListener<V>() {\n            @Override\n            public void changed(ObservableValue<? extends V> observable, V oldValue, V newValue) {\n                if (mappingUpdate.get()) {\n                    return;\n                }\n\n                toSet.setValue(newValue);\n            }\n        };\n        current.get().addListener(listener);\n        props.forEach(prop -> {\n            prop.addListener((c, o, n) -> {\n                if (mappingUpdate.get()) {\n                    return;\n                }\n\n                current.get().removeListener(listener);\n                current.set(creator.get());\n                toSet.setValue(current.get().getValue());\n                current.get().addListener(listener);\n            });\n        });\n        return this;\n    }\n\n    public OptionsComp buildComp() {\n        finishCurrent();\n        var comp = new OptionsComp(entries, focusFirstIncomplete ? allChecks : List.of());\n        return comp;\n    }\n\n    public Region build() {\n        return buildComp().build();\n    }\n\n    public GuiDialog buildDialog() {\n        return new GuiDialog(this);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/OptionsChoiceBuilder.java",
    "content": "package io.xpipe.app.platform;\n\nimport io.xpipe.app.comp.base.ChoicePaneComp;\nimport io.xpipe.app.core.AppI18n;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.ReadOnlyObjectWrapper;\nimport javafx.beans.property.SimpleIntegerProperty;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.control.ComboBox;\nimport javafx.scene.layout.Region;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.SneakyThrows;\n\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.function.Function;\n\n@Builder\npublic class OptionsChoiceBuilder {\n\n    private final Property<?> property;\n    private final List<Class<?>> available;\n    private final Function<ComboBox<ChoicePaneComp.Entry>, Region> transformer;\n    private final boolean allowNull;\n    private final Object customConfiguration;\n\n    @SneakyThrows\n    private String createIdForClass(Class<?> c) {\n        var customPlain = Arrays.stream(c.getDeclaredMethods())\n                .filter(m -> m.getName().equals(\"getOptionsNameKey\") && m.getParameters().length == 0)\n                .findFirst();\n        if (customPlain.isPresent()) {\n            return (String) customPlain.get().invoke(null);\n        }\n\n        var customConfig = Arrays.stream(c.getDeclaredMethods())\n                .filter(m -> m.getName().equals(\"getOptionsNameKey\") && m.getParameters().length == 1)\n                .findFirst();\n        if (customConfig.isPresent()) {\n            return (String) customConfig.get().invoke(null, customConfiguration);\n        }\n\n        var a = c.getAnnotation(JsonTypeName.class);\n        if (a != null) {\n            return a.value();\n        }\n\n        return null;\n    }\n\n    private static Method findCreateOptionsMethod(Class<?> c) {\n        return Arrays.stream(c.getDeclaredMethods())\n                .filter(method -> method.getName().equals(\"createOptions\"))\n                .findFirst()\n                .orElse(null);\n    }\n\n    private static OptionsBuilder createOptionsForClass(\n            Class<?> c, Property<Object> property, Object customConfiguration) {\n        var method = findCreateOptionsMethod(c);\n        if (method == null) {\n            return null;\n        }\n\n        try {\n            method.setAccessible(true);\n            var r = method.getParameters().length == 2\n                    ? method.invoke(null, property, customConfiguration)\n                    : method.invoke(null, property);\n            if (r != null) {\n                return (OptionsBuilder) r;\n            }\n        } catch (Exception ignored) {\n        }\n        return new OptionsBuilder();\n    }\n\n    private static Object createDefaultInstanceForClass(Class<?> c) {\n        try {\n            var cd = c.getDeclaredMethod(\"createDefault\");\n            cd.setAccessible(true);\n            var defValue = cd.invoke(null);\n            return defValue;\n        } catch (Exception ignored) {\n        }\n\n        try {\n            var bm = c.getDeclaredMethod(\"builder\");\n            bm.setAccessible(true);\n            var b = bm.invoke(null);\n            var m = b.getClass().getDeclaredMethod(\"build\");\n            m.setAccessible(true);\n            var defValue = c.cast(m.invoke(b));\n            return defValue;\n        } catch (Exception ignored) {\n        }\n\n        try {\n            var defConstructor = c.getDeclaredConstructor();\n            var defValue = defConstructor.newInstance();\n            return defValue;\n        } catch (Exception e) {\n            return null;\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public <T> OptionsBuilder build() {\n        Property<T> s = (Property<T>) property;\n        var initial = s.getValue();\n        var sub = available;\n        var selectedIndex = s.getValue() == null\n                ? (allowNull ? 0 : -1)\n                : sub.stream()\n                        .filter(c -> c.equals(s.getValue().getClass()))\n                        .findFirst()\n                        .map(c -> sub.indexOf(c) + (allowNull ? 1 : 0))\n                        .orElse(-1);\n        var selected = new SimpleIntegerProperty(selectedIndex);\n\n        var properties = new ArrayList<Property<Object>>();\n        if (allowNull) {\n            properties.add(new SimpleObjectProperty<>());\n        }\n        for (Class<?> aClass : sub) {\n            var compatible = aClass.isInstance(s.getValue());\n            properties.add(\n                    new SimpleObjectProperty<>(compatible ? s.getValue() : createDefaultInstanceForClass(aClass)));\n        }\n\n        property.addListener((obs, oldValue, newValue) -> {\n            if (newValue == null) {\n                return;\n            }\n\n            for (int i = 0; i < sub.size(); i++) {\n                var c = sub.get(i);\n                if (c.isAssignableFrom(newValue.getClass())) {\n                    properties.get(i + (allowNull ? 1 : 0)).setValue(newValue);\n                    selected.setValue(i + (allowNull ? 1 : 0));\n                }\n            }\n        });\n\n        var map = new LinkedHashMap<ObservableValue<String>, OptionsBuilder>();\n        for (int i = 0; i < sub.size(); i++) {\n            map.put(\n                    AppI18n.observable(createIdForClass(sub.get(i))),\n                    createOptionsForClass(sub.get(i), properties.get(i + (allowNull ? 1 : 0)), customConfiguration));\n        }\n        if (allowNull) {\n            var key = AppI18n.observable(\"none\");\n            if (map.containsKey(key)) {\n                map.putFirst(AppI18n.observable(\"empty\"), new OptionsBuilder());\n            } else {\n                map.putFirst(key, new OptionsBuilder());\n            }\n        }\n\n        var options = new OptionsBuilder()\n                .choice(selected, map, transformer)\n                .bindChoice(\n                        () -> {\n                            if (selected.get() == -1) {\n                                return new ReadOnlyObjectWrapper<>(initial);\n                            }\n\n                            var prop = properties.get(selected.get());\n                            return (Property<? extends T>) prop;\n                        },\n                        s);\n        return options;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/PlatformInit.java",
    "content": "package io.xpipe.app.platform;\n\nimport io.xpipe.app.core.*;\nimport io.xpipe.app.core.check.AppAndroidLinuxTerminalCheck;\nimport io.xpipe.app.core.check.AppGpuCheck;\nimport io.xpipe.app.core.window.AppModifiedStage;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.OsType;\n\nimport javafx.application.Application;\nimport javafx.application.Platform;\n\nimport lombok.Getter;\nimport lombok.SneakyThrows;\n\nimport java.util.concurrent.CountDownLatch;\n\npublic class PlatformInit {\n\n    private static final CountDownLatch latch = new CountDownLatch(2);\n    private static Thread loadingThread;\n\n    @Getter\n    private static Throwable error;\n\n    public static boolean isLoadingThread() {\n        return Thread.currentThread() == loadingThread || (loadingThread != null && Platform.isFxApplicationThread());\n    }\n\n    @SneakyThrows\n    public static synchronized void init(boolean wait) {\n        // Already finished\n        if (latch.getCount() == 0) {\n            if (error != null) {\n                throw error;\n            }\n\n            return;\n        }\n\n        // Another thread is loading\n        if (latch.getCount() == 1) {\n            if (Thread.currentThread() == loadingThread) {\n                return;\n            }\n\n            if (wait) {\n                latch.await();\n            }\n\n            if (error != null) {\n                throw error;\n            }\n\n            return;\n        }\n\n        if (latch.getCount() == 2) {\n            latch.countDown();\n        }\n\n        ThreadHelper.runAsync(() -> {\n            loadingThread = Thread.currentThread();\n            initSync();\n            loadingThread = null;\n        });\n        if (wait) {\n            if (error != null) {\n                throw error;\n            }\n            latch.await();\n        }\n    }\n\n    private static void initSync() {\n        if (AppProperties.get().isAotTrainMode() && OsType.ofLocal() == OsType.LINUX) {\n            latch.countDown();\n            return;\n        }\n\n        try {\n            TrackEvent.info(\"Platform init started\");\n            AppModifiedStage.init();\n            PlatformState.initPlatformOrThrow();\n            AppAndroidLinuxTerminalCheck.check();\n            AppGpuCheck.check();\n            AppFont.init();\n            PlatformThread.runLaterIfNeededBlocking(() -> {\n                AppDisplayScale.init();\n                AppStyle.init();\n                AppTheme.init();\n            });\n            AppI18n.init();\n            AppDesktopIntegration.init();\n            GlobalClipboard.init();\n\n            // Must not be called on platform thread\n            // This will not finish until the platform exits, so we use a platform thread to not lose a virtual one\n            ThreadHelper.createPlatformThread(\"app-wait\", false, () -> {\n                        Application.launch(App.class);\n                    })\n                    .start();\n            while (App.getApp() == null) {\n                ThreadHelper.sleep(10);\n            }\n            NativeBridge.init();\n            TrackEvent.info(\"Platform init finished\");\n            latch.countDown();\n        } catch (Throwable t) {\n            error = t;\n            latch.countDown();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/PlatformState.java",
    "content": "package io.xpipe.app.platform;\n\nimport io.xpipe.app.core.check.AppSystemFontCheck;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.OsType;\n\nimport javafx.application.Platform;\nimport javafx.scene.text.Font;\n\nimport lombok.Getter;\nimport lombok.Setter;\nimport lombok.SneakyThrows;\nimport org.apache.commons.lang3.SystemUtils;\n\nimport java.awt.*;\nimport java.util.concurrent.CountDownLatch;\n\npublic enum PlatformState {\n    NOT_INITIALIZED,\n    RUNNING,\n    EXITED;\n\n    @Getter\n    @Setter\n    private static PlatformState current = PlatformState.NOT_INITIALIZED;\n\n    private static Throwable lastError;\n    private static boolean expectedError;\n\n    public static Throwable getLastError() {\n        if (expectedError) {\n            ErrorEventFactory.expected(lastError);\n        }\n        return lastError;\n    }\n\n    public static void teardown() {\n        setCurrent(PlatformState.EXITED);\n\n        // Give other threads, e.g. windows shutdown hook time to properly signal exit state\n        ThreadHelper.sleep(100);\n\n        Platform.exit();\n    }\n\n    public static void initPlatformOrThrow() throws Throwable {\n        if (current == NOT_INITIALIZED) {\n            PlatformState.initPlatform();\n        }\n        if (lastError != null) {\n            throw getLastError();\n        }\n    }\n\n    private static String getErrorMessage(String message) {\n        var header = message != null ? message + \"\\n\\n\" : \"Failed to load graphics support\\n\\n\";\n        var msg =\n                header + \"Please note that XPipe is a desktop application that should be run on your local workstation.\"\n                        + \" It is able to provide the full functionality for all integrations via remote server connections, e.g. via SSH.\"\n                        + \" You don't have to install XPipe on any system like a server, a WSL distribution, a hypervisor, etc.,\"\n                        + \" to have full access to that system, a shell connection to it is enough for XPipe to work from your local machine.\";\n        return msg;\n    }\n\n    private static void initPlatform() {\n        if (current == EXITED) {\n            lastError = new IllegalStateException(\"Platform has already exited\");\n            return;\n        }\n\n        if (current == RUNNING) {\n            return;\n        }\n\n        try {\n            // Weird fix to ensure that macOS quit operation works while in tray.\n            // Maybe related to https://bugs.openjdk.org/browse/JDK-8318129 as it prints the same error if not called\n            GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();\n\n            // Catch more than just the headless exception in case the graphics environment initialization completely\n            // fails\n        } catch (HeadlessException h) {\n            var msg = getErrorMessage(h.getMessage());\n            PlatformState.setCurrent(PlatformState.EXITED);\n            expectedError = true;\n            lastError = new UnsupportedOperationException(msg, h);\n            return;\n        } catch (Throwable t) {\n            PlatformState.setCurrent(PlatformState.EXITED);\n            lastError = t;\n            return;\n        }\n\n        // Check if we have no fonts and set properties to load bundled ones\n        AppSystemFontCheck.init();\n\n        if (AppPrefs.get() != null) {\n            var s = AppPrefs.get().uiScale().getValue();\n            if (s != null) {\n                var i = Math.min(300, Math.max(25, s));\n                var value = i + \"%\";\n                switch (OsType.ofLocal()) {\n                    case OsType.Linux ignored -> {\n                        System.setProperty(\"glass.gtk.uiScale\", value);\n                    }\n                    case OsType.Windows ignored -> {\n                        System.setProperty(\"glass.win.uiScale\", value);\n                    }\n                    default -> {}\n                }\n            }\n        }\n\n        // This issue is now fixed in 27-ea+4\n        // if (SystemUtils.IS_OS_WINDOWS) {\n        // This is primarily intended to fix Windows unified stage transparency issues\n        // (https://bugs.openjdk.org/browse/JDK-8329382)\n        // But apparently it can also occur without a custom stage on Windows\n        // System.setProperty(\"prism.forceUploadingPainter\", \"true\");\n        // }\n\n        if (AppPrefs.get() != null\n                && AppPrefs.get().disableHardwareAcceleration().get()) {\n            System.setProperty(\"prism.order\", \"sw\");\n        }\n\n        try {\n            CountDownLatch latch = new CountDownLatch(1);\n            Platform.setImplicitExit(false);\n            Platform.startup(() -> {\n                latch.countDown();\n            });\n            try {\n                latch.await();\n                PlatformState.setCurrent(PlatformState.RUNNING);\n            } catch (InterruptedException e) {\n                lastError = e;\n                return;\n            }\n        } catch (Throwable t) {\n            // Check if we already exited\n            if (\"Platform.exit has been called\".equals(t.getMessage())) {\n                PlatformState.setCurrent(PlatformState.EXITED);\n                lastError = t;\n                return;\n            } else if (\"Toolkit already initialized\".equals(t.getMessage())) {\n                PlatformState.setCurrent(PlatformState.RUNNING);\n            } else {\n                // Platform initialization has failed in this case\n                var msg = getErrorMessage(t.getMessage());\n                var ex = new UnsupportedOperationException(msg, t);\n                ErrorEventFactory.expected(ex);\n                PlatformState.setCurrent(PlatformState.EXITED);\n                lastError = ex;\n                return;\n            }\n        }\n\n        // We use our own shutdown hook\n        disableToolkitShutdownHook();\n\n        try {\n            // This can fail if the found system fonts can somehow not be loaded\n            Font.getDefault();\n        } catch (Throwable e) {\n            var ex = new IllegalStateException(\"Unable to load fonts. Do you have a valid font package installed?\", e);\n            lastError = ex;\n            PlatformState.setCurrent(PlatformState.EXITED);\n            return;\n        }\n    }\n\n    @SneakyThrows\n    private static void disableToolkitShutdownHook() {\n        var tkClass = Class.forName(\n                ModuleLayer.boot().findModule(\"javafx.graphics\").orElseThrow(), \"com.sun.javafx.tk.Toolkit\");\n        var getToolkitMethod = tkClass.getDeclaredMethod(\"getToolkit\");\n        getToolkitMethod.setAccessible(true);\n        var tk = getToolkitMethod.invoke(null);\n        var shutdownHookField = tk.getClass().getDeclaredField(\"shutdownHook\");\n        shutdownHookField.setAccessible(true);\n        var thread = (Thread) shutdownHookField.get(tk);\n        Runtime.getRuntime().removeShutdownHook(thread);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/PlatformThread.java",
    "content": "package io.xpipe.app.platform;\n\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.issue.ErrorEventFactory;\n\nimport javafx.application.Platform;\nimport javafx.beans.InvalidationListener;\nimport javafx.beans.value.ChangeListener;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.ListChangeListener;\nimport javafx.collections.ObservableList;\n\nimport lombok.NonNull;\n\nimport java.util.*;\nimport java.util.concurrent.CountDownLatch;\n\n@SuppressWarnings(\"unchecked\")\npublic class PlatformThread {\n\n    public static <T> ObservableValue<T> sync(ObservableValue<T> ov) {\n        Objects.requireNonNull(ov);\n        ObservableValue<T> obs = new ObservableValue<>() {\n\n            private final Map<ChangeListener<? super T>, ChangeListener<? super T>> changeListenerMap = new HashMap<>();\n            private final Map<InvalidationListener, InvalidationListener> invListenerMap = new HashMap<>();\n\n            @Override\n            public synchronized void addListener(ChangeListener<? super T> listener) {\n                ChangeListener<? super T> l = (c, o, n) -> {\n                    PlatformThread.runLaterIfNeeded(() -> listener.changed(c, o, n));\n                };\n\n                changeListenerMap.put(listener, l);\n                ov.addListener(l);\n            }\n\n            @Override\n            public synchronized void removeListener(ChangeListener<? super T> listener) {\n                var r = changeListenerMap.remove(listener);\n                if (r != null) {\n                    ov.removeListener(r);\n                }\n            }\n\n            @Override\n            public T getValue() {\n                return ov.getValue();\n            }\n\n            @Override\n            public synchronized void addListener(InvalidationListener listener) {\n                InvalidationListener l = o -> {\n                    PlatformThread.runLaterIfNeeded(() -> listener.invalidated(o));\n                };\n\n                invListenerMap.put(listener, l);\n                ov.addListener(l);\n            }\n\n            @Override\n            public synchronized void removeListener(InvalidationListener listener) {\n                var r = invListenerMap.remove(listener);\n                if (r != null) {\n                    ov.removeListener(r);\n                }\n            }\n        };\n        return obs;\n    }\n\n    public static <T> ObservableList<T> sync(ObservableList<T> ol) {\n        Objects.requireNonNull(ol);\n        ObservableList<T> obs = new ObservableList<>() {\n\n            private final Map<ListChangeListener<? super T>, ListChangeListener<? super T>> listChangeListenerMap =\n                    new HashMap<>();\n            private final Map<InvalidationListener, InvalidationListener> invListenerMap = new HashMap<>();\n\n            @Override\n            public synchronized void addListener(ListChangeListener<? super T> listener) {\n                ListChangeListener<? super T> l = (lc) -> {\n                    PlatformThread.runLaterIfNeeded(() -> listener.onChanged(lc));\n                };\n\n                listChangeListenerMap.put(listener, l);\n                ol.addListener(l);\n            }\n\n            @Override\n            public synchronized void removeListener(ListChangeListener<? super T> listener) {\n                var r = listChangeListenerMap.remove(listener);\n                if (r != null) {\n                    ol.removeListener(r);\n                }\n            }\n\n            @Override\n            public boolean addAll(T... elements) {\n                return ol.addAll(elements);\n            }\n\n            @Override\n            public boolean setAll(T... elements) {\n                return ol.setAll(elements);\n            }\n\n            @Override\n            public boolean setAll(Collection<? extends T> col) {\n                return ol.setAll(col);\n            }\n\n            @Override\n            public boolean removeAll(T... elements) {\n                return ol.removeAll(elements);\n            }\n\n            @Override\n            public boolean retainAll(T... elements) {\n                return ol.retainAll(elements);\n            }\n\n            @Override\n            public void remove(int from, int to) {\n                ol.remove(from, to);\n            }\n\n            @Override\n            public int size() {\n                return ol.size();\n            }\n\n            @Override\n            public boolean isEmpty() {\n                return ol.isEmpty();\n            }\n\n            @Override\n            public boolean contains(Object o) {\n                return ol.contains(o);\n            }\n\n            @Override\n            public @NonNull Iterator<T> iterator() {\n                return ol.iterator();\n            }\n\n            @Override\n            public Object @NonNull [] toArray() {\n                return ol.toArray();\n            }\n\n            @Override\n            public <T1> T1 @NonNull [] toArray(T1 @NonNull [] a) {\n                return ol.toArray(a);\n            }\n\n            @Override\n            public boolean add(T t) {\n                return ol.add(t);\n            }\n\n            @Override\n            public boolean remove(Object o) {\n                return ol.remove(o);\n            }\n\n            @Override\n            public boolean containsAll(@NonNull Collection<?> c) {\n                return ol.containsAll(c);\n            }\n\n            @Override\n            public boolean addAll(@NonNull Collection<? extends T> c) {\n                return ol.addAll(c);\n            }\n\n            @Override\n            public boolean addAll(int index, @NonNull Collection<? extends T> c) {\n                return ol.addAll(index, c);\n            }\n\n            @Override\n            public boolean removeAll(@NonNull Collection<?> c) {\n                return ol.removeAll(c);\n            }\n\n            @Override\n            public boolean retainAll(@NonNull Collection<?> c) {\n                return ol.retainAll(c);\n            }\n\n            @Override\n            public void clear() {\n                ol.clear();\n            }\n\n            @Override\n            public T get(int index) {\n                return ol.get(index);\n            }\n\n            @Override\n            public T set(int index, T element) {\n                return ol.set(index, element);\n            }\n\n            @Override\n            public void add(int index, T element) {\n                ol.add(index, element);\n            }\n\n            @Override\n            public T remove(int index) {\n                return ol.remove(index);\n            }\n\n            @Override\n            public int indexOf(Object o) {\n                return ol.indexOf(o);\n            }\n\n            @Override\n            public int lastIndexOf(Object o) {\n                return ol.lastIndexOf(o);\n            }\n\n            @Override\n            public @NonNull ListIterator<T> listIterator() {\n                return ol.listIterator();\n            }\n\n            @Override\n            public @NonNull ListIterator<T> listIterator(int index) {\n                return ol.listIterator(index);\n            }\n\n            @Override\n            public @NonNull List<T> subList(int fromIndex, int toIndex) {\n                return ol.subList(fromIndex, toIndex);\n            }\n\n            @Override\n            public synchronized void addListener(InvalidationListener listener) {\n                InvalidationListener l = o -> {\n                    PlatformThread.runLaterIfNeeded(() -> listener.invalidated(o));\n                };\n\n                invListenerMap.put(listener, l);\n                ol.addListener(l);\n            }\n\n            @Override\n            public synchronized void removeListener(InvalidationListener listener) {\n                var r = invListenerMap.remove(listener);\n                if (r != null) {\n                    ol.removeListener(r);\n                }\n            }\n        };\n        return obs;\n    }\n\n    private static boolean canRunPlatform() {\n        if (PlatformState.getCurrent() != PlatformState.RUNNING) {\n            return false;\n        }\n\n        if (AppOperationMode.isInShutdown()) {\n            return false;\n        }\n\n        return true;\n    }\n\n    public static void enterNestedEventLoop(Object key) {\n        if (!Platform.canStartNestedEventLoop()) {\n            return;\n        }\n\n        try {\n            Platform.enterNestedEventLoop(key);\n        } catch (IllegalStateException ex) {\n            // We might be in an animation or layout call\n            ErrorEventFactory.fromThrowable(ex).omit().expected().handle();\n        }\n    }\n\n    public static void exitNestedEventLoop(Object key) {\n        try {\n            Platform.exitNestedEventLoop(key, null);\n        } catch (IllegalArgumentException ex) {\n            // The event loop might have died somehow\n            // Or we passed an invalid key\n            ErrorEventFactory.fromThrowable(ex).omit().expected().handle();\n        }\n    }\n\n    public static void runNestedLoopIteration() {\n        if (!Platform.canStartNestedEventLoop()) {\n            return;\n        }\n\n        var key = new Object();\n        Platform.runLater(() -> {\n            exitNestedEventLoop(key);\n        });\n        enterNestedEventLoop(key);\n    }\n\n    public static void runLaterIfNeeded(Runnable r) {\n        if (!canRunPlatform()) {\n            return;\n        }\n\n        Runnable catcher = () -> {\n            try {\n                r.run();\n            } catch (Throwable t) {\n                ErrorEventFactory.fromThrowable(t).handle();\n            }\n        };\n\n        if (Platform.isFxApplicationThread()) {\n            catcher.run();\n        } else {\n            Platform.runLater(catcher);\n        }\n    }\n\n    public static void runLaterIfNeededBlocking(Runnable r) {\n        if (!canRunPlatform()) {\n            return;\n        }\n\n        Runnable catcher = () -> {\n            try {\n                r.run();\n            } catch (Throwable t) {\n                ErrorEventFactory.fromThrowable(t).handle();\n            }\n        };\n\n        if (!Platform.isFxApplicationThread()) {\n            CountDownLatch latch = new CountDownLatch(1);\n            Platform.runLater(() -> {\n                catcher.run();\n                latch.countDown();\n            });\n            try {\n                latch.await();\n            } catch (InterruptedException ignored) {\n            }\n        } else {\n            catcher.run();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/PlatformThreadWatcher.java",
    "content": "package io.xpipe.app.platform;\n\nimport io.xpipe.app.core.AppProperties;\n\nimport javafx.application.Platform;\nimport javafx.beans.value.ChangeListener;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.ListChangeListener;\nimport javafx.scene.Node;\nimport javafx.scene.Parent;\nimport javafx.scene.control.Labeled;\nimport javafx.stage.Window;\n\nimport java.util.HashSet;\nimport java.util.Set;\nimport java.util.function.Consumer;\n\npublic class PlatformThreadWatcher {\n\n    private static final Set<Window> windows = new HashSet<>();\n    private static final Set<Node> nodes = new HashSet<>();\n    // Reuse listener for everything. Disabling generics allows that\n    @SuppressWarnings(\"rawtypes\")\n    private static final ChangeListener listener = new ChangeListener() {\n\n        @Override\n        public void changed(ObservableValue observableValue, Object o, Object t1) {\n            checkPlatformThread();\n        }\n    };\n\n    @SuppressWarnings(\"rawtypes\")\n    private static final ListChangeListener listListener = new ListChangeListener() {\n\n        @Override\n        public void onChanged(Change change) {\n            checkPlatformThread();\n        }\n    };\n\n    public static void init() {\n        if (!AppProperties.get().isDebugPlatformThreadAccess()) {\n            return;\n        }\n\n        Window.getWindows().addListener((ListChangeListener<? super Window>) change -> {\n            for (Window window : change.getList()) {\n                if (!windows.add(window)) {\n                    continue;\n                }\n\n                window.sceneProperty().subscribe(scene -> {\n                    if (scene == null) {\n                        return;\n                    }\n\n                    scene.rootProperty().subscribe(root -> {\n                        if (root != null) {\n                            watchPlatformThreadChanges(root);\n                        }\n                    });\n                });\n            }\n        });\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static void watchPlatformThreadChanges(Node node) {\n        watchGraph(node, c -> {\n            c.sceneProperty().subscribe((oldScene, newScene) -> {\n                var add = oldScene == null && newScene != null;\n                var remove = oldScene != null && newScene == null;\n                if (!add && !remove) {\n                    return;\n                }\n\n                if (add && !nodes.add(c)) {\n                    return;\n                }\n\n                if (remove) {\n                    nodes.remove(c);\n                }\n\n                if (c instanceof Parent p) {\n                    if (add) {\n                        p.getChildrenUnmodifiable().addListener(listListener);\n                    } else {\n                        p.getChildrenUnmodifiable().removeListener(listListener);\n                    }\n                }\n\n                if (add) {\n                    c.visibleProperty().addListener(listener);\n                    c.boundsInParentProperty().addListener(listener);\n                    c.managedProperty().addListener(listener);\n                    c.opacityProperty().addListener(listener);\n                    c.accessibleHelpProperty().addListener(listener);\n                    c.accessibleTextProperty().addListener(listener);\n\n                    if (c instanceof Labeled l) {\n                        l.textProperty().addListener(listener);\n                        l.graphicProperty().addListener(listener);\n                    }\n                } else {\n                    c.visibleProperty().removeListener(listener);\n                    c.boundsInParentProperty().removeListener(listener);\n                    c.managedProperty().removeListener(listener);\n                    c.opacityProperty().removeListener(listener);\n                    c.accessibleHelpProperty().removeListener(listener);\n                    c.accessibleTextProperty().removeListener(listener);\n\n                    if (c instanceof Labeled l) {\n                        l.textProperty().removeListener(listener);\n                        l.graphicProperty().removeListener(listener);\n                    }\n                }\n            });\n        });\n    }\n\n    private static void watchGraph(Node node, Consumer<Node> callback) {\n        if (node instanceof Parent p) {\n            for (Node c : p.getChildrenUnmodifiable()) {\n                watchGraph(c, callback);\n            }\n\n            ListChangeListener<? super Node> childListener = change -> {\n                for (Node c : change.getList()) {\n                    watchGraph(c, callback);\n                }\n            };\n            p.sceneProperty().subscribe(scene -> {\n                if (scene != null) {\n                    p.getChildrenUnmodifiable().addListener(childListener);\n                } else {\n                    p.getChildrenUnmodifiable().removeListener(childListener);\n                }\n            });\n        }\n        callback.accept(node);\n    }\n\n    private static void checkPlatformThread() {\n        if (!Platform.isFxApplicationThread()) {\n            throw new IllegalStateException(\"Not in Fx application thread\");\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/SimpleValidator.java",
    "content": "package io.xpipe.app.platform;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.binding.StringBinding;\nimport javafx.beans.property.ReadOnlyBooleanProperty;\nimport javafx.beans.property.ReadOnlyBooleanWrapper;\nimport javafx.beans.property.ReadOnlyObjectProperty;\nimport javafx.beans.property.ReadOnlyObjectWrapper;\nimport javafx.beans.value.ChangeListener;\n\nimport net.synedra.validatorfx.Severity;\nimport net.synedra.validatorfx.ValidationMessage;\nimport net.synedra.validatorfx.ValidationResult;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\npublic class SimpleValidator implements Validator {\n\n    private final Map<Check, ChangeListener<ValidationResult>> checks = new LinkedHashMap<>();\n    private final ReadOnlyObjectWrapper<ValidationResult> validationResultProperty =\n            new ReadOnlyObjectWrapper<>(new ValidationResult());\n    private final ReadOnlyBooleanWrapper containsErrorsProperty = new ReadOnlyBooleanWrapper();\n\n    /**\n     * Create a check that lives within this checker's domain.\n     *\n     * @return A check object whose dependsOn, decorates, etc. methods can be called\n     */\n    public Check createCheck() {\n        Check check = new Check();\n        add(check);\n        return check;\n    }\n\n    /**\n     * Add another check to the checker. Changes in the check's validationResultProperty will be reflected in the checker.\n     *\n     * @param check The check to add.\n     */\n    public void add(Check check) {\n        ChangeListener<ValidationResult> listener = (obs, oldv, newv) -> refreshProperties();\n        checks.put(check, listener);\n        check.validationResultProperty().addListener(listener);\n    }\n\n    /**\n     * Removes a check from this validator.\n     *\n     * @param check The check to remove from this validator.\n     */\n    public void remove(Check check) {\n        ChangeListener<ValidationResult> listener = checks.remove(check);\n        if (listener != null) {\n            check.validationResultProperty().removeListener(listener);\n        }\n        refreshProperties();\n    }\n\n    /**\n     * Retrieves current validation result\n     *\n     * @return validation result\n     */\n    public ValidationResult getValidationResult() {\n        return validationResultProperty.get();\n    }\n\n    /**\n     * Can be used to track validation result changes\n     *\n     * @return The Validation result property.\n     */\n    public ReadOnlyObjectProperty<ValidationResult> validationResultProperty() {\n        return validationResultProperty.getReadOnlyProperty();\n    }\n\n    /**\n     * A read-only boolean property indicating whether any of the checks of this validator emitted an error.\n     */\n    public ReadOnlyBooleanProperty containsErrorsProperty() {\n        return containsErrorsProperty.getReadOnlyProperty();\n    }\n\n    public boolean containsErrors() {\n        return containsErrorsProperty().get();\n    }\n\n    /**\n     * Run all checks (decorating nodes if appropriate)\n     *\n     * @return true if no errors were found, false otherwise\n     */\n    public boolean validate() {\n        for (Check check : checks.keySet()) {\n            check.recheck();\n        }\n        return !containsErrors();\n    }\n\n    /**\n     * Create a string property that depends on the validation result.\n     * Each error message will be displayed on a separate line prefixed with a bullet.\n     */\n    public StringBinding createStringBinding() {\n        return createStringBinding(\"- \", \"\\n\");\n    }\n\n    @Override\n    public StringBinding createStringBinding(String prefix, String separator) {\n        return Bindings.createStringBinding(\n                () -> {\n                    StringBuilder str = new StringBuilder();\n                    for (ValidationMessage msg : validationResultProperty.get().getMessages()) {\n                        if (str.length() > 0) {\n                            str.append(separator);\n                        }\n                        str.append(prefix).append(msg.getText());\n                    }\n                    return str.toString();\n                },\n                validationResultProperty);\n    }\n\n    private void refreshProperties() {\n        ValidationResult nextResult = new ValidationResult();\n        for (Check check : checks.keySet()) {\n            nextResult.addAll(check.getValidationResult().getMessages());\n        }\n        validationResultProperty.set(nextResult);\n        boolean hasErrors = false;\n        for (ValidationMessage msg : nextResult.getMessages()) {\n            hasErrors = hasErrors || msg.getSeverity() == Severity.ERROR;\n        }\n        containsErrorsProperty.set(hasErrors);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/platform/Validator.java",
    "content": "package io.xpipe.app.platform;\n\nimport io.xpipe.app.core.AppI18n;\n\nimport javafx.beans.binding.StringBinding;\nimport javafx.beans.property.ReadOnlyBooleanProperty;\nimport javafx.beans.property.ReadOnlyListProperty;\nimport javafx.beans.property.ReadOnlyObjectProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.ObservableList;\n\nimport net.synedra.validatorfx.ValidationResult;\n\nimport java.util.function.Predicate;\n\npublic interface Validator {\n\n    static Check nonNull(Validator v, ObservableValue<String> name, ObservableValue<?> s) {\n        return v.createCheck()\n                .dependsOn(\"val\", s)\n                .withMethod(c -> {\n                    if (c.get(\"val\") == null) {\n                        c.error(AppI18n.get(\n                                \"app.mustNotBeEmpty\", name != null ? name.getValue() : AppI18n.get(\"value\")));\n                    }\n                })\n                .immediate();\n    }\n\n    static Check nonNullIf(\n            Validator v, ObservableValue<String> name, ObservableValue<?> s, ObservableValue<Boolean> checkIf) {\n        return v.createCheck()\n                .dependsOn(\"val\", s)\n                .dependsOn(\"if\", checkIf)\n                .withMethod(c -> {\n                    if (Boolean.TRUE.equals(c.get(\"if\")) && c.get(\"val\") == null) {\n                        c.error(AppI18n.get(\n                                \"app.mustNotBeEmpty\", name != null ? name.getValue() : AppI18n.get(\"value\")));\n                    }\n                })\n                .immediate();\n    }\n\n    static Check nonEmpty(Validator v, ObservableValue<String> name, ReadOnlyListProperty<?> s) {\n        return v.createCheck()\n                .dependsOn(\"val\", s)\n                .withMethod(c -> {\n                    if (((ObservableList<?>) c.get(\"val\")).size() == 0) {\n                        c.error(AppI18n.get(\n                                \"app.mustNotBeEmpty\", name != null ? name.getValue() : AppI18n.get(\"value\")));\n                    }\n                })\n                .immediate();\n    }\n\n    static <T> Check create(Validator v, ObservableValue<String> message, ObservableValue<T> s, Predicate<T> p) {\n        return v.createCheck()\n                .dependsOn(\"val\", s)\n                .withMethod(c -> {\n                    if (!p.test(c.get(\"val\"))) {\n                        c.error(message.getValue());\n                    }\n                })\n                .immediate();\n    }\n\n    Check createCheck();\n\n    /**\n     * Add another check to the checker. Changes in the check's validationResultProperty will be reflected in the checker.\n     *\n     * @param check The check to add.\n     */\n    void add(Check check);\n\n    /**\n     * Removes a check from this validator.\n     *\n     * @param check The check to remove from this validator.\n     */\n    void remove(Check check);\n\n    /**\n     * Retrieves current validation result\n     *\n     * @return validation result\n     */\n    ValidationResult getValidationResult();\n\n    /**\n     * Can be used to track validation result changes\n     *\n     * @return The Validation result property.\n     */\n    ReadOnlyObjectProperty<ValidationResult> validationResultProperty();\n\n    /**\n     * A read-only boolean property indicating whether any of the checks of this validator emitted an error.\n     */\n    ReadOnlyBooleanProperty containsErrorsProperty();\n\n    boolean containsErrors();\n\n    /**\n     * Run all checks (decorating nodes if appropriate)\n     *\n     * @return true if no errors were found, false otherwise\n     */\n    boolean validate();\n\n    /**\n     * Create a string property that depends on the validation result.\n     * Each error message will be displayed on a separate line prefixed with a bullet.\n     */\n    StringBinding createStringBinding();\n\n    /**\n     * Create a string property that depends on the validation result.\n     *\n     * @param prefix    The string to prefix each validation message with\n     * @param separator The string to separate consecutive validation messages with\n     */\n    StringBinding createStringBinding(String prefix, String separator);\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/AboutCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.RegionDescriptor;\nimport io.xpipe.app.comp.base.LabelComp;\nimport io.xpipe.app.comp.base.VerticalComp;\nimport io.xpipe.app.core.AppNames;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.platform.JfxHelper;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.update.AppDistributionType;\n\nimport javafx.beans.property.ReadOnlyStringWrapper;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.geometry.Insets;\n\nimport atlantafx.base.theme.Styles;\n\nimport java.util.List;\n\npublic class AboutCategory extends AppPrefsCategory {\n\n    @Override\n    protected String getId() {\n        return \"about\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2i-information-outline\");\n    }\n\n    @Override\n    protected BaseRegionBuilder<?, ?> create() {\n        var props = createProperties();\n        var update = new UpdateCheckComp().prefWidth(600);\n        return new VerticalComp(List.of(\n                        props,\n                        RegionBuilder.vspacer(1),\n                        update,\n                        RegionBuilder.vspacer(5),\n                        RegionBuilder.hseparator().padding(Insets.EMPTY).maxWidth(600)))\n                .apply(s -> s.setFillWidth(true))\n                .apply(struc -> struc.setSpacing(12))\n                .style(\"information\")\n                .style(\"about-tab\");\n    }\n\n    private BaseRegionBuilder<?, ?> createProperties() {\n        var title = RegionBuilder.of(() -> {\n            return JfxHelper.createNamedEntry(\n                    new ReadOnlyStringWrapper(AppNames.ofCurrent().getName() + \" Desktop\"),\n                    new SimpleStringProperty(\"Version \" + AppProperties.get().getVersion() + \" (\"\n                            + AppProperties.get().getArch() + \")\"),\n                    \"logo/logo.png\");\n        });\n\n        title.style(Styles.TEXT_BOLD);\n\n        var section = new OptionsBuilder()\n                .addComp(RegionBuilder.vspacer(40))\n                .addComp(title, null)\n                .addComp(RegionBuilder.vspacer(10))\n                .name(\"build\")\n                .addComp(\n                        new LabelComp(AppProperties.get().getBuild())\n                                .describe(d ->\n                                        d.focusTraversal(RegionDescriptor.FocusTraversal.ENABLED_FOR_ACCESSIBILITY)),\n                        null)\n                .name(\"distribution\")\n                .addComp(new LabelComp(AppDistributionType.get().toTranslatedString())\n                        .describe(d -> d.focusTraversal(RegionDescriptor.FocusTraversal.ENABLED_FOR_ACCESSIBILITY)))\n                .name(\"virtualMachine\")\n                .addComp(\n                        new LabelComp(System.getProperty(\"java.vm.vendor\") + \" \"\n                                        + System.getProperty(\"java.vm.name\")\n                                        + \" \"\n                                        + System.getProperty(\"java.vm.version\"))\n                                .describe(d ->\n                                        d.focusTraversal(RegionDescriptor.FocusTraversal.ENABLED_FOR_ACCESSIBILITY)),\n                        null)\n                .buildComp();\n        return section.style(\"properties-comp\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/ApiCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.base.TextFieldComp;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\n\npublic class ApiCategory extends AppPrefsCategory {\n\n    @Override\n    protected String getId() {\n        return \"api\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2c-code-json\");\n    }\n\n    @Override\n    protected BaseRegionBuilder<?, ?> create() {\n        var prefs = AppPrefs.get();\n\n        return new OptionsBuilder()\n                .addTitle(\"httpServer\")\n                .sub(new OptionsBuilder()\n                        .pref(prefs.enableHttpApi)\n                        .addToggle(prefs.enableHttpApi)\n                        .pref(prefs.apiKey)\n                        .addComp(new TextFieldComp(prefs.apiKey).maxWidth(getCompWidth()), prefs.apiKey)\n                        .pref(prefs.disableApiAuthentication)\n                        .addToggle(prefs.disableApiAuthentication))\n                .buildComp();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/AppPrefs.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.core.*;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.ext.PrefsHandler;\nimport io.xpipe.app.ext.PrefsProvider;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.icon.SystemIconSource;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.*;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ShellDialect;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.app.pwman.PasswordManager;\nimport io.xpipe.app.rdp.ExternalRdpClient;\nimport io.xpipe.app.spice.ExternalSpiceClient;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStorageUserHandler;\nimport io.xpipe.app.terminal.ExternalTerminalType;\nimport io.xpipe.app.terminal.TerminalMultiplexer;\nimport io.xpipe.app.terminal.TerminalPrompt;\nimport io.xpipe.app.terminal.TerminalSplitStrategy;\nimport io.xpipe.app.update.AppDistributionType;\nimport io.xpipe.app.util.*;\nimport io.xpipe.app.vnc.ExternalVncClient;\nimport io.xpipe.app.vnc.InternalVncClient;\nimport io.xpipe.app.vnc.VncCategory;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport javafx.application.Platform;\nimport javafx.beans.property.*;\nimport javafx.beans.value.ObservableBooleanValue;\nimport javafx.beans.value.ObservableDoubleValue;\nimport javafx.beans.value.ObservableStringValue;\nimport javafx.beans.value.ObservableValue;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.JavaType;\nimport com.fasterxml.jackson.databind.type.SimpleType;\nimport com.fasterxml.jackson.databind.type.TypeFactory;\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Getter;\nimport lombok.Value;\n\nimport java.nio.file.Files;\nimport java.util.*;\n\npublic final class AppPrefs {\n\n    private static AppPrefs INSTANCE;\n    private final List<Mapping> mapping = new ArrayList<>();\n\n    public static void initLocal() {\n        INSTANCE = new AppPrefs();\n        PrefsProvider.getAll().forEach(prov -> prov.addPrefs(INSTANCE.extensionHandler));\n        INSTANCE.loadLocal();\n        INSTANCE.vaultStorageHandler =\n                new AppPrefsStorageHandler(DataStorage.getStorageDirectory().resolve(\"preferences.json\"));\n        INSTANCE.fixLocalValues();\n    }\n\n    public static void initSynced() throws Exception {\n        INSTANCE.loadSharedRemote();\n        INSTANCE.encryptAllVaultData.addListener((observableValue, aBoolean, t1) -> {\n            if (DataStorage.get() != null) {\n                DataStorage.get().forceRewrite();\n            }\n        });\n    }\n\n    public static void initStorage() {\n        INSTANCE.vaultAuthentication.set(DataStorageUserHandler.getInstance().getVaultAuthenticationType());\n    }\n\n    public static void reset() {\n        INSTANCE.save();\n\n        // Keep instance as we might need some values on shutdown, e.g. on update with terminals\n        // INSTANCE = null;\n    }\n\n    public static AppPrefs get() {\n        return INSTANCE;\n    }\n\n    @Getter\n    private final BooleanProperty requiresRestart = new GlobalBooleanProperty(false);\n\n    final BooleanProperty disableHardwareAcceleration = map(Mapping.builder()\n            .property(new GlobalBooleanProperty(false))\n            .key(\"disableHardwareAcceleration\")\n            .valueClass(Boolean.class)\n            .requiresRestart(true)\n            .build());\n    final BooleanProperty preferMonochromeIcons = map(Mapping.builder()\n            .property(new GlobalBooleanProperty(false))\n            .key(\"preferMonochromeIcons\")\n            .valueClass(Boolean.class)\n            .requiresRestart(false)\n            .build());\n    final BooleanProperty pinLocalMachineOnStartup = map(Mapping.builder()\n            .property(new GlobalBooleanProperty(false))\n            .key(\"pinLocalMachineOnStartup\")\n            .valueClass(Boolean.class)\n            .requiresRestart(true)\n            .build());\n    final BooleanProperty enableHttpApi = map(Mapping.builder()\n            .property(new GlobalBooleanProperty(false))\n            .key(\"enableHttpApi\")\n            .valueClass(Boolean.class)\n            .requiresRestart(false)\n            .documentationLink(DocumentationLink.API)\n            .build());\n    final BooleanProperty enableMcpServer = map(Mapping.builder()\n            .property(new GlobalBooleanProperty(false))\n            .key(\"enableMcpServer\")\n            .valueClass(Boolean.class)\n            .requiresRestart(false)\n            .documentationLink(DocumentationLink.MCP)\n            .build());\n    final BooleanProperty enableMcpMutationTools = map(Mapping.builder()\n            .property(new GlobalBooleanProperty(false))\n            .key(\"enableMcpMutationTools\")\n            .valueClass(Boolean.class)\n            .build());\n    final StringProperty mcpAdditionalContext = map(Mapping.builder()\n            .property(new GlobalStringProperty(null))\n            .key(\"mcpAdditionalContext\")\n            .valueClass(String.class)\n            .requiresRestart(true)\n            .build());\n    final BooleanProperty dontAutomaticallyStartVmSshServer =\n            mapVaultShared(new GlobalBooleanProperty(false), \"dontAutomaticallyStartVmSshServer\", Boolean.class, false);\n    final BooleanProperty dontAcceptNewHostKeys =\n            mapVaultShared(new GlobalBooleanProperty(false), \"dontAcceptNewHostKeys\", Boolean.class, false);\n    public final BooleanProperty performanceMode = map(Mapping.builder()\n            .property(new GlobalBooleanProperty())\n            .key(\"performanceMode\")\n            .valueClass(Boolean.class)\n            .build());\n    final BooleanProperty limitedTouchscreenMode = map(Mapping.builder()\n            .property(new GlobalBooleanProperty())\n            .key(\"limitedTouchscreenMode\")\n            .valueClass(Boolean.class)\n            .requiresRestart(true)\n            .build());\n    public final ObjectProperty<AppTheme.Theme> theme = map(Mapping.builder()\n            .property(new GlobalObjectProperty<>())\n            .key(\"theme\")\n            .valueClass(AppTheme.Theme.class)\n            .build());\n    final BooleanProperty useSystemFont = map(Mapping.builder()\n            .property(new GlobalBooleanProperty(OsType.ofLocal() != OsType.MACOS))\n            .key(\"useSystemFont\")\n            .valueClass(Boolean.class)\n            .build());\n    final Property<Integer> uiScale = map(Mapping.builder()\n            .property(new GlobalObjectProperty<>())\n            .key(\"uiScale\")\n            .valueClass(Integer.class)\n            .requiresRestart(true)\n            .build());\n    final BooleanProperty saveWindowLocation = map(Mapping.builder()\n            .property(new GlobalBooleanProperty(true))\n            .key(\"saveWindowLocation\")\n            .valueClass(Boolean.class)\n            .requiresRestart(false)\n            .build());\n    final BooleanProperty preferTerminalTabs = map(Mapping.builder()\n            .property(new GlobalBooleanProperty(true))\n            .key(\"preferTerminalTabs\")\n            .valueClass(Boolean.class)\n            .requiresRestart(false)\n            .build());\n    final ObjectProperty<ExternalTerminalType> terminalType = map(Mapping.builder()\n            .property(new GlobalObjectProperty<>())\n            .key(\"terminalType\")\n            .valueClass(ExternalTerminalType.class)\n            .requiresRestart(false)\n            .documentationLink(DocumentationLink.TERMINAL)\n            .build());\n    final ObjectProperty<ExternalRdpClient> rdpClientType = map(Mapping.builder()\n            .property(new GlobalObjectProperty<>())\n            .key(\"rdpClientType\")\n            .valueClass(ExternalRdpClient.class)\n            .requiresRestart(false)\n            .documentationLink(DocumentationLink.RDP)\n            .build());\n    final StringProperty notesTemplate = new GlobalStringProperty(null);\n    final DoubleProperty windowOpacity = map(Mapping.builder()\n            .property(new GlobalDoubleProperty(1.0))\n            .key(\"windowOpacity\")\n            .valueClass(Double.class)\n            .requiresRestart(false)\n            .build());\n    final StringProperty customTerminalCommand = map(Mapping.builder()\n            .property(new GlobalStringProperty(null))\n            .key(\"customTerminalCommand\")\n            .valueClass(String.class)\n            .requiresRestart(false)\n            .build());\n    final BooleanProperty clearTerminalOnInit = map(Mapping.builder()\n            .property(new GlobalBooleanProperty(true))\n            .key(\"clearTerminalOnInit\")\n            .valueClass(Boolean.class)\n            .requiresRestart(false)\n            .build());\n    final Property<List<SystemIconSource>> iconSources = map(Mapping.builder()\n            .property(new GlobalObjectProperty<>(new ArrayList<>()))\n            .key(\"iconSources\")\n            .valueType(TypeFactory.defaultInstance().constructType(new TypeReference<List<SystemIconSource>>() {}))\n            .vaultSpecific(true)\n            .build());\n    public final BooleanProperty disableCertutilUse = map(Mapping.builder()\n            .property(new GlobalBooleanProperty(false))\n            .key(\"disableCertutilUse\")\n            .valueClass(Boolean.class)\n            .build());\n    public final BooleanProperty useLocalFallbackShell = map(Mapping.builder()\n            .property(new GlobalBooleanProperty(false))\n            .key(\"useLocalFallbackShell\")\n            .valueClass(Boolean.class)\n            .requiresRestart(true)\n            .build());\n    final Property<ShellDialect> localShellDialect = map(Mapping.builder()\n            .property(new GlobalObjectProperty<>(\n                    ProcessControlProvider.get().getAvailableLocalDialects().getFirst()))\n            .key(\"localShellDialect\")\n            .valueClass(ShellDialect.class)\n            .vaultSpecific(false)\n            .requiresRestart(true)\n            .build());\n\n    public final BooleanProperty disableTerminalRemotePasswordPreparation = mapVaultShared(\n            new GlobalBooleanProperty(false), \"disableTerminalRemotePasswordPreparation\", Boolean.class, false);\n    public final Property<Boolean> alwaysConfirmElevation =\n            mapVaultShared(new GlobalObjectProperty<>(false), \"alwaysConfirmElevation\", Boolean.class, false);\n    public final BooleanProperty focusWindowOnNotifications = map(Mapping.builder()\n            .property(new GlobalBooleanProperty(true))\n            .key(\"focusWindowOnNotifications\")\n            .valueClass(Boolean.class)\n            .build());\n    public final BooleanProperty dontCachePasswords =\n            mapVaultShared(new GlobalBooleanProperty(false), \"dontCachePasswords\", Boolean.class, false);\n    public final Property<ExternalVncClient> vncClient = map(Mapping.builder()\n            .property(new GlobalObjectProperty<>())\n            .key(\"vncClient\")\n            .valueClass(ExternalVncClient.class)\n            .documentationLink(DocumentationLink.VNC)\n            .build());\n    public final Property<ExternalSpiceClient> spiceClient = map(Mapping.builder()\n            .property(new GlobalObjectProperty<>())\n            .key(\"spiceClient\")\n            .valueClass(ExternalSpiceClient.class)\n            .build());\n    final Property<PasswordManager> passwordManager = map(Mapping.builder()\n            .property(new GlobalObjectProperty<>())\n            .key(\"passwordManager\")\n            .valueClass(PasswordManager.class)\n            .log(false)\n            .documentationLink(DocumentationLink.PASSWORD_MANAGER)\n            .build());\n    final Property<ShellScript> terminalInitScript = map(Mapping.builder()\n            .property(new GlobalObjectProperty<>(null))\n            .key(\"terminalInitScript\")\n            .valueClass(ShellScript.class)\n            .log(false)\n            .build());\n    final Property<UUID> terminalProxy = map(Mapping.builder()\n            .property(new GlobalObjectProperty<>())\n            .key(\"terminalProxy\")\n            .valueClass(UUID.class)\n            .requiresRestart(false)\n            .build());\n    final Property<TerminalMultiplexer> terminalMultiplexer = map(Mapping.builder()\n            .property(new GlobalObjectProperty<>(null))\n            .key(\"terminalMultiplexer\")\n            .valueClass(TerminalMultiplexer.class)\n            .log(false)\n            .documentationLink(DocumentationLink.TERMINAL_MULTIPLEXER)\n            .build());\n    final Property<Boolean> terminalAlwaysPauseOnExit = map(Mapping.builder()\n            .property(new GlobalBooleanProperty(true))\n            .key(\"terminalAlwaysPauseOnExit\")\n            .valueClass(Boolean.class)\n            .build());\n    final Property<TerminalSplitStrategy> terminalSplitStrategy = map(Mapping.builder()\n            .property(new GlobalObjectProperty<>(TerminalSplitStrategy.BALANCED))\n            .key(\"terminalSplitStrategy\")\n            .valueClass(TerminalSplitStrategy.class)\n            .build());\n    final Property<TerminalPrompt> terminalPrompt = map(Mapping.builder()\n            .property(new GlobalObjectProperty<>(null))\n            .key(\"terminalPrompt\")\n            .valueClass(TerminalPrompt.class)\n            .log(false)\n            .documentationLink(DocumentationLink.TERMINAL_PROMPT)\n            .build());\n    final ObjectProperty<VaultAuthentication> vaultAuthentication = new GlobalObjectProperty<>();\n\n    final ObjectProperty<StartupBehaviour> startupBehaviour = map(Mapping.builder()\n            .property(new GlobalObjectProperty<>(StartupBehaviour.GUI))\n            .key(\"startupBehaviour\")\n            .valueClass(StartupBehaviour.class)\n            .requiresRestart(true)\n            .build());\n    public final BooleanProperty enableGitStorage = map(Mapping.builder()\n            .property(new GlobalBooleanProperty(false))\n            .key(\"enableGitStorage\")\n            .valueClass(Boolean.class)\n            .requiresRestart(true)\n            .documentationLink(DocumentationLink.SYNC)\n            .build());\n    final StringProperty storageGitRemote = map(Mapping.builder()\n            .property(new GlobalStringProperty(\"\"))\n            .key(\"storageGitRemote\")\n            .valueClass(String.class)\n            .requiresRestart(true)\n            .documentationLink(DocumentationLink.SYNC)\n            .build());\n    final ObjectProperty<SyncMode> syncMode = map(Mapping.builder()\n            .property(new GlobalObjectProperty<>(SyncMode.INSTANT))\n            .key(\"syncMode\")\n            .valueClass(SyncMode.class)\n            .requiresRestart(true)\n            .documentationLink(DocumentationLink.SYNC_MODE)\n            .build());\n    final ObjectProperty<CloseBehaviour> closeBehaviour = map(Mapping.builder()\n            .property(new GlobalObjectProperty<>(CloseBehaviour.QUIT))\n            .key(\"closeBehaviour\")\n            .valueClass(CloseBehaviour.class)\n            .build());\n    final ObjectProperty<ExternalEditorType> externalEditor =\n            mapLocal(new GlobalObjectProperty<>(), \"externalEditor\", ExternalEditorType.class, false);\n    final StringProperty customEditorCommand =\n            mapLocal(new GlobalStringProperty(\"\"), \"customEditorCommand\", String.class, false);\n    final BooleanProperty customEditorCommandInTerminal =\n            mapLocal(new GlobalBooleanProperty(false), \"customEditorCommandInTerminal\", Boolean.class, false);\n    final BooleanProperty automaticallyCheckForUpdates =\n            mapLocal(new GlobalBooleanProperty(true), \"automaticallyCheckForUpdates\", Boolean.class, false);\n    final BooleanProperty encryptAllVaultData =\n            mapVaultShared(new GlobalBooleanProperty(false), \"encryptAllVaultData\", Boolean.class, true);\n    final BooleanProperty enableTerminalLogging = map(Mapping.builder()\n            .property(new GlobalBooleanProperty(false))\n            .key(\"enableTerminalLogging\")\n            .valueClass(Boolean.class)\n            .licenseFeatureId(\"logging\")\n            .documentationLink(DocumentationLink.TERMINAL_LOGGING)\n            .build());\n    final BooleanProperty enableTerminalStartupBell = map(Mapping.builder()\n            .property(new GlobalBooleanProperty(false))\n            .key(\"enableTerminalStartupBell\")\n            .valueClass(Boolean.class)\n            .build());\n    final BooleanProperty checkForSecurityUpdates =\n            mapLocal(new GlobalBooleanProperty(true), \"checkForSecurityUpdates\", Boolean.class, false);\n    final BooleanProperty disableHttpsTlsCheck =\n            mapLocal(new GlobalBooleanProperty(false), \"disableHttpsTlsCheck\", Boolean.class, true);\n    final BooleanProperty condenseConnectionDisplay =\n            mapLocal(new GlobalBooleanProperty(false), \"condenseConnectionDisplay\", Boolean.class, false);\n    final BooleanProperty showChildCategoriesInParentCategory =\n            mapLocal(new GlobalBooleanProperty(true), \"showChildrenConnectionsInParentCategory\", Boolean.class, false);\n    final Property<HibernateBehaviour> hibernateBehaviour =\n            mapLocal(new GlobalObjectProperty<>(), \"hibernateBehaviour\", HibernateBehaviour.class, false);\n    final BooleanProperty openConnectionSearchWindowOnConnectionCreation = mapLocal(\n            new GlobalBooleanProperty(true), \"openConnectionSearchWindowOnConnectionCreation\", Boolean.class, false);\n    final ObjectProperty<FilePath> downloadsDirectory =\n            mapLocal(new GlobalObjectProperty<>(), \"downloadsDirectory\", FilePath.class, false);\n    final BooleanProperty developerMode =\n            mapLocal(new GlobalBooleanProperty(false), \"developerMode\", Boolean.class, true);\n    final BooleanProperty developerDisableUpdateVersionCheck =\n            mapLocal(new GlobalBooleanProperty(false), \"developerDisableUpdateVersionCheck\", Boolean.class, false);\n    final BooleanProperty developerForceSshTty =\n            mapLocal(new GlobalBooleanProperty(false), \"developerForceSshTty\", Boolean.class, false);\n    final BooleanProperty developerDisableSshTunnelGateways =\n            mapLocal(new GlobalBooleanProperty(false), \"developerDisableSshTunnelGateways\", Boolean.class, false);\n    final BooleanProperty developerPrintInitFiles =\n            mapLocal(new GlobalBooleanProperty(false), \"developerPrintInitFiles\", Boolean.class, false);\n    final BooleanProperty developerShowSensitiveCommands =\n            mapLocal(new GlobalBooleanProperty(false), \"developerShowSensitiveCommands\", Boolean.class, false);\n    final BooleanProperty disableSshPinCaching =\n            mapLocal(new GlobalBooleanProperty(false), \"disableSshPinCaching\", Boolean.class, false);\n    final ObjectProperty<SupportedLocale> language =\n            mapLocal(new GlobalObjectProperty<>(SupportedLocale.ENGLISH), \"language\", SupportedLocale.class, false);\n    final ObjectProperty<FilePath> sshAgentSocket = map(Mapping.builder()\n            .property(new GlobalObjectProperty<>())\n            .key(\"sshAgentSocket\")\n            .valueClass(FilePath.class)\n            .requiresRestart(false)\n            .build());\n\n    final ObjectProperty<FilePath> defaultSshAgentSocket = new SimpleObjectProperty<>();\n\n    final BooleanProperty requireDoubleClickForConnections =\n            mapLocal(new GlobalBooleanProperty(false), \"requireDoubleClickForConnections\", Boolean.class, false);\n    final BooleanProperty editFilesWithDoubleClick =\n            mapLocal(new GlobalBooleanProperty(false), \"editFilesWithDoubleClick\", Boolean.class, false);\n    final BooleanProperty enableFileBrowserTerminalDocking =\n            mapLocal(new GlobalBooleanProperty(true), \"enableFileBrowserTerminalDocking\", Boolean.class, false);\n    final BooleanProperty enableConnectionHubTerminalDocking =\n            mapLocal(new GlobalBooleanProperty(true), \"enableConnectionHubTerminalDocking\", Boolean.class, false);\n    final BooleanProperty censorMode = mapLocal(new GlobalBooleanProperty(false), \"censorMode\", Boolean.class, false);\n    final BooleanProperty sshVerboseOutput = map(Mapping.builder()\n            .property(new GlobalBooleanProperty(false))\n            .key(\"sshVerboseOutput\")\n            .valueClass(Boolean.class)\n            .documentationLink(DocumentationLink.SSH_TROUBLESHOOT)\n            .build());\n    final StringProperty apiKey =\n            mapVaultShared(new GlobalStringProperty(UUID.randomUUID().toString()), \"apiKey\", String.class, true);\n    final BooleanProperty disableApiAuthentication =\n            mapLocal(new GlobalBooleanProperty(false), \"disableApiAuthentication\", Boolean.class, false);\n\n    @Getter\n    private final StringProperty lockCrypt =\n            mapVaultShared(new GlobalStringProperty(), \"workspaceLock\", String.class, true);\n\n    @Getter\n    private final List<AppPrefsCategory> categories = List.of(\n            new AboutCategory(),\n            new PersonalizationCategory(),\n            new VaultCategory(),\n            new SyncCategory(),\n            new PasswordManagerCategory(),\n            new TerminalCategory(),\n            new LoggingCategory(),\n            new EditorCategory(),\n            new RdpCategory(),\n            new VncCategory(),\n            new SshCategory(),\n            new ConnectionHubCategory(),\n            new FileBrowserCategory(),\n            new IconsCategory(),\n            new DisplayCategory(),\n            new SystemCategory(),\n            new ApiCategory(),\n            new McpCategory(),\n            new UpdatesCategory(),\n            new SecurityCategory(),\n            new WorkspacesCategory(),\n            new DeveloperCategory(),\n            new TroubleshootCategory(),\n            new LinksCategory());\n\n    private final AppPrefsStorageHandler globalStorageHandler = new AppPrefsStorageHandler(\n            AppProperties.get().getDataDir().resolve(\"settings\").resolve(\"preferences.json\"));\n    private final Map<Mapping, OptionsBuilder> customEntries = new LinkedHashMap<>();\n\n    @Getter\n    private final Property<AppPrefsCategory> selectedCategory = new GlobalObjectProperty<>(categories.getFirst());\n\n    private final PrefsHandler extensionHandler = new PrefsHandlerImpl();\n    private AppPrefsStorageHandler vaultStorageHandler;\n\n    private AppPrefs() {}\n\n    public ObservableValue<Boolean> disableHttpsTlsCheck() {\n        return disableHttpsTlsCheck;\n    }\n\n    public ObservableValue<VaultAuthentication> vaultAuthentication() {\n        return vaultAuthentication;\n    }\n\n    public ObservableValue<TerminalSplitStrategy> terminalSplitStrategy() {\n        return terminalSplitStrategy;\n    }\n\n    public ObservableStringValue notesTemplate() {\n        return notesTemplate;\n    }\n\n    public ObservableBooleanValue disableHardwareAcceleration() {\n        return disableHardwareAcceleration;\n    }\n\n    public ObservableBooleanValue enableTerminalStartupBell() {\n        return enableTerminalStartupBell;\n    }\n\n    public ObservableBooleanValue preferTerminalTabs() {\n        return preferTerminalTabs;\n    }\n\n    public ObservableValue<List<SystemIconSource>> getIconSources() {\n        return iconSources;\n    }\n\n    public ObservableValue<TerminalPrompt> terminalPrompt() {\n        return terminalPrompt;\n    }\n\n    public ObservableValue<UUID> terminalProxy() {\n        return terminalProxy;\n    }\n\n    public ObservableValue<Boolean> terminalAlwaysPauseOnExit() {\n        return terminalAlwaysPauseOnExit;\n    }\n\n    public ObservableValue<FilePath> sshAgentSocket() {\n        return sshAgentSocket;\n    }\n\n    public ObservableValue<FilePath> defaultSshAgentSocket() {\n        return defaultSshAgentSocket;\n    }\n\n    public ObservableBooleanValue preferMonochromeIcons() {\n        return preferMonochromeIcons;\n    }\n\n    public ObservableBooleanValue editFilesWithDoubleClick() {\n        return editFilesWithDoubleClick;\n    }\n\n    public ObservableBooleanValue sshVerboseOutput() {\n        return sshVerboseOutput;\n    }\n\n    public ObservableValue<SyncMode> syncMode() {\n        return syncMode;\n    }\n\n    public ObservableBooleanValue censorMode() {\n        return censorMode;\n    }\n\n    public ObservableBooleanValue requireDoubleClickForConnections() {\n        return requireDoubleClickForConnections;\n    }\n\n    public ObservableBooleanValue enableFileBrowserTerminalDocking() {\n        return enableFileBrowserTerminalDocking;\n    }\n\n    public ObservableBooleanValue enableConnectionHubTerminalDocking() {\n        return enableConnectionHubTerminalDocking;\n    }\n\n    public ObservableBooleanValue disableSshPinCaching() {\n        return disableSshPinCaching;\n    }\n\n    public ObservableBooleanValue focusWindowOnNotifications() {\n        return focusWindowOnNotifications;\n    }\n\n    public ObservableValue<AppTheme.Theme> theme() {\n        return theme;\n    }\n\n    public ObservableBooleanValue developerPrintInitFiles() {\n        return developerPrintInitFiles;\n    }\n\n    public ObservableBooleanValue developerShowSensitiveCommands() {\n        return developerShowSensitiveCommands;\n    }\n\n    public ObservableBooleanValue checkForSecurityUpdates() {\n        return checkForSecurityUpdates;\n    }\n\n    public ObservableBooleanValue enableTerminalLogging() {\n        return enableTerminalLogging;\n    }\n\n    public ObservableStringValue apiKey() {\n        return apiKey;\n    }\n\n    public ObservableBooleanValue disableApiAuthentication() {\n        return disableApiAuthentication;\n    }\n\n    public ObservableBooleanValue enableHttpApi() {\n        return enableHttpApi;\n    }\n\n    public ObservableBooleanValue enableMcpServer() {\n        return enableMcpServer;\n    }\n\n    public ObservableBooleanValue enableMcpMutationTools() {\n        return enableMcpMutationTools;\n    }\n\n    public ObservableValue<String> mcpAdditionalContext() {\n        return mcpAdditionalContext;\n    }\n\n    public ObservableBooleanValue pinLocalMachineOnStartup() {\n        return pinLocalMachineOnStartup;\n    }\n\n    public ObservableValue<PasswordManager> passwordManager() {\n        return passwordManager;\n    }\n\n    public ObservableValue<TerminalMultiplexer> terminalMultiplexer() {\n        return terminalMultiplexer;\n    }\n\n    @SuppressWarnings(\"unused\")\n    public ObservableValue<ShellScript> terminalInitScript() {\n        return terminalInitScript;\n    }\n\n    public ObservableValue<SupportedLocale> language() {\n        return language;\n    }\n\n    public ObservableBooleanValue dontAutomaticallyStartVmSshServer() {\n        return dontAutomaticallyStartVmSshServer;\n    }\n\n    public ObservableBooleanValue dontAcceptNewHostKeys() {\n        return dontAcceptNewHostKeys;\n    }\n\n    public ObservableBooleanValue performanceMode() {\n        return performanceMode;\n    }\n\n    public ObservableBooleanValue limitedTouchscreenMode() {\n        return limitedTouchscreenMode;\n    }\n\n    public ObservableValue<Boolean> useSystemFont() {\n        return useSystemFont;\n    }\n\n    public ReadOnlyProperty<Integer> uiScale() {\n        return uiScale;\n    }\n\n    public ReadOnlyBooleanProperty clearTerminalOnInit() {\n        return clearTerminalOnInit;\n    }\n\n    public ObservableBooleanValue disableCertutilUse() {\n        return disableCertutilUse;\n    }\n\n    public ObservableValue<ShellDialect> localShellDialect() {\n        return localShellDialect;\n    }\n\n    public ObservableBooleanValue disableTerminalRemotePasswordPreparation() {\n        return disableTerminalRemotePasswordPreparation;\n    }\n\n    public ObservableValue<HibernateBehaviour> hibernateBehaviour() {\n        return hibernateBehaviour;\n    }\n\n    public ObservableValue<Boolean> alwaysConfirmElevation() {\n        return alwaysConfirmElevation;\n    }\n\n    public ObservableBooleanValue dontCachePasswords() {\n        return dontCachePasswords;\n    }\n\n    public ObservableBooleanValue enableGitStorage() {\n        return enableGitStorage;\n    }\n\n    public ObservableStringValue storageGitRemote() {\n        return storageGitRemote;\n    }\n\n    public ObservableBooleanValue encryptAllVaultData() {\n        return encryptAllVaultData;\n    }\n\n    public ObservableBooleanValue condenseConnectionDisplay() {\n        return condenseConnectionDisplay;\n    }\n\n    public ObservableBooleanValue showChildCategoriesInParentCategory() {\n        return showChildCategoriesInParentCategory;\n    }\n\n    public ObservableBooleanValue openConnectionSearchWindowOnConnectionCreation() {\n        return openConnectionSearchWindowOnConnectionCreation;\n    }\n\n    public ReadOnlyProperty<CloseBehaviour> closeBehaviour() {\n        return closeBehaviour;\n    }\n\n    public ReadOnlyProperty<ExternalEditorType> externalEditor() {\n        return externalEditor;\n    }\n\n    public ObservableValue<String> customEditorCommand() {\n        return customEditorCommand;\n    }\n\n    public ObservableBooleanValue customEditorCommandInTerminal() {\n        return customEditorCommandInTerminal;\n    }\n\n    public ReadOnlyProperty<StartupBehaviour> startupBehaviour() {\n        return startupBehaviour;\n    }\n\n    public ReadOnlyBooleanProperty automaticallyUpdate() {\n        return automaticallyCheckForUpdates;\n    }\n\n    public ObservableValue<ExternalTerminalType> terminalType() {\n        return terminalType;\n    }\n\n    public ObservableValue<ExternalRdpClient> rdpClientType() {\n        return rdpClientType;\n    }\n\n    public ObservableValue<String> customTerminalCommand() {\n        return customTerminalCommand;\n    }\n\n    public ObservableValue<FilePath> downloadsDirectory() {\n        return downloadsDirectory;\n    }\n\n    public ObservableValue<Boolean> developerMode() {\n        return AppProperties.get().isDeveloperMode() ? new ReadOnlyBooleanWrapper(true) : developerMode;\n    }\n\n    public ObservableDoubleValue windowOpacity() {\n        return windowOpacity;\n    }\n\n    public ObservableBooleanValue saveWindowLocation() {\n        return saveWindowLocation;\n    }\n\n    public ObservableBooleanValue developerDisableUpdateVersionCheck() {\n        return developerDisableUpdateVersionCheck;\n    }\n\n    public ObservableBooleanValue developerForceSshTty() {\n        return developerForceSshTty;\n    }\n\n    public ObservableBooleanValue developerDisableSshTunnelGateways() {\n        return developerDisableSshTunnelGateways;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private <T> T map(Mapping m) {\n        mapping.add(m);\n        m.property.addListener((observable, oldValue, newValue) -> {\n            var running = AppOperationMode.get() == AppOperationMode.GUI;\n            if (running && m.requiresRestart) {\n                AppPrefs.get().requiresRestart.set(true);\n            }\n        });\n        return (T) m.getProperty();\n    }\n\n    private <T> T mapLocal(Property<?> o, String name, Class<?> clazz, boolean requiresRestart) {\n        return map(new Mapping(name, o, clazz, false, requiresRestart, true, null));\n    }\n\n    private <T> T mapVaultShared(Property<?> o, String name, Class<?> clazz, boolean requiresRestart) {\n        return map(new Mapping(name, o, clazz, true, requiresRestart, true, null));\n    }\n\n    public <T> void setFromExternal(ObservableValue<T> prop, T newValue) {\n        var writable = (Property<T>) prop;\n\n        // Prior to GUI init, we can set whatever we like\n        if (AppLayoutModel.get() == null) {\n            writable.setValue(newValue);\n            return;\n        }\n\n        PlatformThread.runLaterIfNeededBlocking(() -> {\n            writable.setValue(newValue);\n        });\n\n        if (mapping.stream().anyMatch(m -> m.property == prop)) {\n            save();\n        }\n    }\n\n    private void fixLocalValues() {\n        if (AppDistributionType.get() == AppDistributionType.WEBTOP) {\n            performanceMode.setValue(true);\n        } else if (System.getProperty(\"os.name\").toLowerCase().contains(\"server\")) {\n            performanceMode.setValue(true);\n        }\n\n        if (!AppProperties.get().isDevelopmentEnvironment()) {\n            developerForceSshTty.setValue(false);\n            developerDisableSshTunnelGateways.setValue(false);\n        }\n\n        if (OsType.ofLocal() == OsType.MACOS\n                && AppProperties.get()\n                        .getCanonicalVersion()\n                        .map(appVersion -> appVersion.getMajor() == 18)\n                        .orElse(false)) {\n            useSystemFont.set(false);\n        }\n\n        if (useLocalFallbackShell.get()) {\n            localShellDialect.setValue(\n                    ProcessControlProvider.get().getAvailableLocalDialects().get(1));\n            useLocalFallbackShell.set(false);\n        }\n\n        if (localShellDialect.getValue() == null\n                || !ProcessControlProvider.get().getAvailableLocalDialects().contains(localShellDialect.getValue())) {\n            localShellDialect.setValue(\n                    ProcessControlProvider.get().getAvailableLocalDialects().getFirst());\n        }\n\n        if (sshVerboseOutput.get()) {\n            sshVerboseOutput.set(false);\n        }\n\n        PrefsProvider.getAll().forEach(prov -> prov.fixLocalValues());\n    }\n\n    public void initDefaultValues() {\n        externalEditor.setValue(ExternalEditorType.determineDefault(externalEditor.get()));\n        terminalType.set(ExternalTerminalType.determineDefault(terminalType.get()));\n        rdpClientType.setValue(ExternalRdpClient.determineDefault(rdpClientType.get()));\n        spiceClient.setValue(ExternalSpiceClient.determineDefault(spiceClient.getValue()));\n        vncClient.setValue(ExternalVncClient.determineDefault(vncClient.getValue()));\n\n        PrefsProvider.getAll().forEach(prov -> prov.initDefaultValues());\n    }\n\n    public OptionsBuilder getCustomOptions(String id) {\n        return customEntries.entrySet().stream()\n                .filter(e -> e.getKey().getKey().equals(id))\n                .findFirst()\n                .map(Map.Entry::getValue)\n                .orElseThrow();\n    }\n\n    private void loadLocal() {\n        for (Mapping value : mapping) {\n            if (value.isVaultSpecific()) {\n                continue;\n            }\n\n            loadValue(globalStorageHandler, value);\n        }\n    }\n\n    private void loadSharedRemote() throws Exception {\n        for (Mapping value : mapping) {\n            if (!value.isVaultSpecific()) {\n                continue;\n            }\n\n            loadValue(vaultStorageHandler, value);\n        }\n\n        if (OsType.ofLocal() != OsType.WINDOWS) {\n            // On Linux and macOS, we prefer the shell variable compared to any global env variable\n            // as the one is set by default and might not be the right one\n            // This happens for example with homebrew ssh\n            var shellVariable = LocalShell.getShell().view().getEnvironmentVariable(\"SSH_AUTH_SOCK\");\n            var socketEnvVariable = shellVariable.isEmpty() ? System.getenv(\"SSH_AUTH_SOCK\") : shellVariable.get();\n            defaultSshAgentSocket.setValue(FilePath.parse(socketEnvVariable));\n        }\n\n        try {\n            var file = AppProperties.get().getDataDir().resolve(\"storage\").resolve(\"notes.md\");\n            if (Files.exists(file)) {\n                notesTemplate.set(Files.readString(file));\n            }\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private <T> void loadValue(AppPrefsStorageHandler handler, Mapping value) {\n        T def = (T) value.getProperty().getValue();\n        Property<T> property = (Property<T>) value.getProperty();\n        var val = handler.loadObject(value.getKey(), value.getValueType(), def, value.isLog());\n        property.setValue(val);\n    }\n\n    public synchronized void save() {\n        for (Mapping m : mapping) {\n            AppPrefsStorageHandler handler = m.isVaultSpecific() ? vaultStorageHandler : globalStorageHandler;\n            // It might be possible that we save while the vault handler is not initialized yet / has no file or\n            // directory\n            if (!handler.isInitialized()) {\n                continue;\n            }\n            handler.updateObject(m.getKey(), m.getProperty().getValue(), m.getValueType());\n        }\n        if (vaultStorageHandler.isInitialized()) {\n            vaultStorageHandler.save();\n        }\n        if (globalStorageHandler.isInitialized()) {\n            globalStorageHandler.save();\n        }\n\n        if (notesTemplate.get() != null) {\n            try {\n                Files.writeString(\n                        AppProperties.get().getDataDir().resolve(\"storage\", \"notes.md\"), notesTemplate.getValue());\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).handle();\n            }\n        }\n    }\n\n    public void selectCategory(String id) {\n        var found = categories.stream()\n                .filter(appPrefsCategory -> appPrefsCategory.getId().equals(id))\n                .findFirst();\n        found.ifPresent(appPrefsCategory -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                AppLayoutModel.get().selectSettings();\n\n                Platform.runLater(() -> {\n                    // Reset scroll in case the target category is already somewhat in focus\n                    selectedCategory.setValue(null);\n                    selectedCategory.setValue(appPrefsCategory);\n                });\n            });\n        });\n    }\n\n    public Mapping getMapping(Object property) {\n        return mapping.stream().filter(m -> m.property == property).findFirst().orElseThrow();\n    }\n\n    @Value\n    @Builder\n    @AllArgsConstructor\n    public static class Mapping {\n\n        String key;\n        Property<?> property;\n        JavaType valueType;\n        boolean vaultSpecific;\n        boolean requiresRestart;\n        String licenseFeatureId;\n        boolean log;\n        DocumentationLink documentationLink;\n\n        public Mapping(\n                String key,\n                Property<?> property,\n                Class<?> valueType,\n                boolean vaultSpecific,\n                boolean requiresRestart,\n                boolean log,\n                DocumentationLink documentationLink) {\n            this.key = key;\n            this.property = property;\n            this.valueType = SimpleType.constructUnsafe(valueType);\n            this.vaultSpecific = vaultSpecific;\n            this.requiresRestart = requiresRestart;\n            this.log = log;\n            this.documentationLink = documentationLink;\n            this.licenseFeatureId = null;\n        }\n\n        public Mapping(\n                String key,\n                Property<?> property,\n                JavaType valueType,\n                boolean vaultSpecific,\n                boolean requiresRestart,\n                boolean log,\n                DocumentationLink documentationLink) {\n            this.key = key;\n            this.property = property;\n            this.valueType = valueType;\n            this.vaultSpecific = vaultSpecific;\n            this.requiresRestart = requiresRestart;\n            this.log = log;\n            this.documentationLink = documentationLink;\n            this.licenseFeatureId = null;\n        }\n\n        public static class MappingBuilder {\n\n            MappingBuilder valueClass(Class<?> clazz) {\n                this.valueType(TypeFactory.defaultInstance().constructType(clazz));\n                return this;\n            }\n        }\n    }\n\n    @Getter\n    private class PrefsHandlerImpl implements PrefsHandler {\n\n        @Override\n        public <T> void addSetting(\n                String id,\n                JavaType t,\n                Property<T> property,\n                OptionsBuilder builder,\n                boolean requiresRestart,\n                boolean log) {\n            var m = new Mapping(id, property, t, false, requiresRestart, log, null);\n            customEntries.put(m, builder);\n            map(m);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/AppPrefsCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.platform.LabelGraphic;\n\npublic abstract class AppPrefsCategory {\n\n    protected int getCompWidth() {\n        return 600;\n    }\n\n    protected boolean show() {\n        return true;\n    }\n\n    protected abstract String getId();\n\n    protected abstract LabelGraphic getIcon();\n\n    protected abstract BaseRegionBuilder<?, ?> create();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.VerticalComp;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.util.BooleanScope;\n\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.geometry.Insets;\nimport javafx.scene.control.ScrollPane;\nimport javafx.scene.layout.*;\n\nimport net.synedra.validatorfx.GraphicDecorationStackPane;\n\npublic class AppPrefsComp extends SimpleRegionBuilder {\n\n    @Override\n    protected Region createSimple() {\n        var categories = AppPrefs.get().getCategories().stream()\n                .filter(appPrefsCategory -> appPrefsCategory.show())\n                .toList();\n        var list = categories.stream()\n                .map(appPrefsCategory -> {\n                    var r = appPrefsCategory.create().style(\"prefs-container\").style(appPrefsCategory.getId());\n                    return r;\n                })\n                .toList();\n        var boxComp = new VerticalComp(list);\n        boxComp.apply(struc -> {\n            struc.getStyleClass().add(\"prefs-box\");\n        });\n        boxComp.maxWidth(850);\n        var box = boxComp.build();\n\n        var pane = new GraphicDecorationStackPane();\n        pane.getChildren().add(box);\n\n        var scrollPane = new ScrollPane(pane);\n\n        var externalUpdate = new SimpleBooleanProperty();\n\n        scrollPane.vvalueProperty().addListener((observable, oldValue, newValue) -> {\n            if (externalUpdate.get()) {\n                return;\n            }\n\n            BooleanScope.executeExclusive(externalUpdate, () -> {\n                var offset = newValue.doubleValue();\n                if (offset == 1.0) {\n                    AppPrefs.get().getSelectedCategory().setValue(categories.getLast());\n                    return;\n                }\n\n                for (int i = categories.size() - 1; i >= 0; i--) {\n                    var category = categories.get(i);\n                    var min = computeCategoryOffset(box, scrollPane, category);\n                    if (offset + (100.0 / box.getHeight()) > min) {\n                        AppPrefs.get().getSelectedCategory().setValue(category);\n                        return;\n                    }\n                }\n            });\n        });\n\n        AppPrefs.get().getSelectedCategory().addListener((observable, oldValue, val) -> {\n            if (val == null) {\n                return;\n            }\n\n            PlatformThread.runLaterIfNeeded(() -> {\n                if (externalUpdate.get()) {\n                    return;\n                }\n\n                BooleanScope.executeExclusive(externalUpdate, () -> {\n                    // This value is off initially if we haven't opened the settings before\n                    // Perhaps it's the layout that is not done yet?\n                    var off = computeCategoryOffset(box, scrollPane, val);\n                    scrollPane.setVvalue(off);\n                });\n            });\n        });\n        scrollPane.setFitToWidth(true);\n        HBox.setHgrow(scrollPane, Priority.ALWAYS);\n\n        var sidebar = new AppPrefsSidebarComp().build();\n        sidebar.setMinWidth(260);\n        sidebar.setPrefWidth(260);\n        sidebar.setMaxWidth(260);\n        sidebar.setMinHeight(0);\n\n        var split = new HBox(sidebar, scrollPane);\n        HBox.setMargin(sidebar, new Insets(4));\n        split.setFillHeight(true);\n        split.getStyleClass().add(\"prefs\");\n        return split;\n    }\n\n    private double computeCategoryOffset(Region box, ScrollPane scrollPane, AppPrefsCategory val) {\n        var node = val != null ? box.lookup(\".\" + val.getId()) : null;\n        if (node != null && scrollPane.getHeight() > 0.0) {\n            var s = Math.min(\n                            box.getHeight(),\n                            node.getBoundsInParent().getMinY() > 0.0\n                                    ? node.getBoundsInParent().getMinY() + 20\n                                    : 0.0)\n                    / box.getHeight();\n            var off = (scrollPane.getHeight() * s * 1.02) / box.getHeight();\n            return s + off;\n        } else {\n            return 0;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/AppPrefsSidebarComp.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.ButtonComp;\nimport io.xpipe.app.comp.base.VerticalComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppRestart;\nimport io.xpipe.app.platform.PlatformThread;\n\nimport javafx.beans.property.ReadOnlyObjectWrapper;\nimport javafx.css.PseudoClass;\nimport javafx.geometry.Insets;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.Button;\nimport javafx.scene.layout.Region;\nimport javafx.scene.text.TextAlignment;\n\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.ArrayList;\nimport java.util.stream.Collectors;\n\npublic class AppPrefsSidebarComp extends SimpleRegionBuilder {\n\n    @Override\n    protected Region createSimple() {\n        var effectiveCategories = AppPrefs.get().getCategories().stream()\n                .filter(appPrefsCategory -> appPrefsCategory.show())\n                .toList();\n        var buttons = effectiveCategories.stream()\n                .<BaseRegionBuilder<?, ?>>map(appPrefsCategory -> {\n                    return new ButtonComp(\n                                    AppI18n.observable(appPrefsCategory.getId()),\n                                    new ReadOnlyObjectWrapper<>(appPrefsCategory.getIcon()),\n                                    () -> {\n                                        AppPrefs.get().getSelectedCategory().setValue(appPrefsCategory);\n                                    })\n                            .apply(struc -> {\n                                struc.setGraphicTextGap(9);\n                                struc.setTextAlignment(TextAlignment.LEFT);\n                                struc.setAlignment(Pos.CENTER_LEFT);\n                                AppPrefs.get().getSelectedCategory().subscribe(val -> {\n                                    struc.pseudoClassStateChanged(\n                                            PseudoClass.getPseudoClass(\"selected\"), appPrefsCategory.equals(val));\n                                });\n                            })\n                            .maxWidth(2000);\n                })\n                .collect(Collectors.toCollection(ArrayList::new));\n\n        var restartButton = new ButtonComp(AppI18n.observable(\"restartApp\"), new FontIcon(\"mdi2r-restart\"), () -> {\n            AppRestart.restart();\n        });\n        restartButton.maxWidth(2000);\n        restartButton.visible(AppPrefs.get().getRequiresRestart());\n        restartButton.padding(new Insets(6, 10, 6, 6));\n        buttons.add(RegionBuilder.vspacer());\n        buttons.add(restartButton);\n\n        var vbox = new VerticalComp(buttons).style(\"sidebar\").style(\"color-box\").style(\"gray\");\n        vbox.apply(struc -> {\n            AppPrefs.get().getSelectedCategory().subscribe(val -> {\n                PlatformThread.runLaterIfNeeded(() -> {\n                    var index = val != null ? effectiveCategories.indexOf(val) : 0;\n                    if (index >= struc.getChildren().size()) {\n                        return;\n                    }\n\n                    ((Button) struc.getChildren().get(index)).fire();\n                });\n            });\n        });\n        return vbox.build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/AppPrefsStorageHandler.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.ext.PrefsChoiceValue;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.core.JacksonMapper;\n\nimport com.fasterxml.jackson.databind.JavaType;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.JsonNodeFactory;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.fasterxml.jackson.databind.node.TextNode;\nimport com.fasterxml.jackson.databind.util.TokenBuffer;\nimport lombok.SneakyThrows;\nimport org.apache.commons.io.FileUtils;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport static io.xpipe.app.ext.PrefsChoiceValue.getAll;\nimport static io.xpipe.app.ext.PrefsChoiceValue.getSupported;\n\npublic class AppPrefsStorageHandler {\n\n    private final Path file;\n    private ObjectNode content;\n\n    public AppPrefsStorageHandler(Path file) {\n        this.file = file;\n    }\n\n    boolean isInitialized() {\n        return content != null;\n    }\n\n    private JsonNode getContent(String key) {\n        loadIfNeeded();\n        return content.get(key);\n    }\n\n    private void loadIfNeeded() {\n        if (content == null) {\n            if (Files.exists(file)) {\n                try {\n                    var s = Files.readString(file);\n                    if (!s.isEmpty()) {\n                        var read = JacksonMapper.getDefault().readTree(s);\n                        if (read.isObject()) {\n                            content = (ObjectNode) read;\n                        }\n                    }\n                } catch (IOException e) {\n                    ErrorEventFactory.fromThrowable(e).handle();\n                }\n            }\n\n            if (content == null) {\n                content = JsonNodeFactory.instance.objectNode();\n            }\n        }\n    }\n\n    private void setContent(String key, JsonNode value) {\n        content.set(key, value);\n    }\n\n    void save() {\n        try {\n            FileUtils.forceMkdir(file.getParent().toFile());\n            JacksonMapper.getDefault().writeValue(file.toFile(), content);\n        } catch (IOException e) {\n            ErrorEventFactory.fromThrowable(e).expected().handle();\n        }\n    }\n\n    @SneakyThrows\n    public void updateObject(String key, Object object, JavaType type) {\n        if (object instanceof PrefsChoiceValue prefsChoiceValue) {\n            setContent(key, new TextNode(prefsChoiceValue.getId()));\n            return;\n        }\n\n        if (object == null) {\n            setContent(key, JsonNodeFactory.instance.nullNode());\n            return;\n        }\n\n        var mapper = JacksonMapper.getDefault();\n        TokenBuffer buf = new TokenBuffer(mapper, false);\n        mapper.writerFor(type).writeValue(buf, object);\n        var tree = mapper.readTree(buf.asParser());\n        setContent(key, (JsonNode) tree);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    @SneakyThrows\n    public <T> T loadObject(String id, JavaType type, T defaultObject, boolean log) {\n        var tree = getContent(id);\n        if (tree == null) {\n            TrackEvent.withDebug(\"Preferences value not found\")\n                    .tag(\"id\", id)\n                    .tag(\"default\", defaultObject)\n                    .handle();\n            return defaultObject;\n        }\n\n        if (tree.isNull()) {\n            return null;\n        }\n\n        if (PrefsChoiceValue.class.isAssignableFrom(type.getRawClass())) {\n            List<T> all = (List<T>) getAll(type.getRawClass());\n            if (all != null) {\n                Class<PrefsChoiceValue> cast = (Class<PrefsChoiceValue>) type.getRawClass();\n                var in = tree.asText();\n                var found = all.stream()\n                        .filter(t -> ((PrefsChoiceValue) t).getId().equalsIgnoreCase(in))\n                        .findAny();\n                if (found.isEmpty()) {\n                    if (log) {\n                        TrackEvent.withWarn(\"Invalid prefs value found\")\n                                .tag(\"key\", id)\n                                .tag(\"value\", in)\n                                .handle();\n                    }\n                    return defaultObject;\n                }\n\n                var supported = getSupported(cast);\n                if (!supported.contains(found.get())) {\n                    if (log) {\n                        TrackEvent.withWarn(\"Unsupported prefs value found\")\n                                .tag(\"key\", id)\n                                .tag(\"value\", in)\n                                .handle();\n                    }\n                    return defaultObject;\n                }\n\n                if (log) {\n                    TrackEvent.debug(\"Loading preferences value for key \" + id + \" from value \" + found.get());\n                }\n                return found.get();\n            }\n        }\n\n        try {\n            if (log) {\n                TrackEvent.debug(\"Loading preferences value for key \" + id + \" from value \" + tree);\n            }\n            T value = JacksonMapper.getDefault().treeToValue(tree, type);\n            if (value instanceof List<?> l) {\n                var mod = l.stream().filter(v -> v != null).collect(Collectors.toCollection(ArrayList::new));\n                return (T) mod;\n            }\n            return value;\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).expected().omit().handle();\n            return defaultObject;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/CloseBehaviour.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.ext.PrefsChoiceValue;\n\nimport lombok.Getter;\n\n@Getter\npublic enum CloseBehaviour implements PrefsChoiceValue {\n    QUIT(\"app.quit\") {\n        @Override\n        public void run() {\n            AppOperationMode.shutdown(false);\n        }\n    },\n\n    MINIMIZE_TO_TRAY(\"app.minimizeToTray\") {\n        @Override\n        public void run() {\n            AppOperationMode.switchToAsync(AppOperationMode.TRAY);\n        }\n\n        @Override\n        public boolean isSelectable() {\n            return AppOperationMode.TRAY.isSupported();\n        }\n    },\n\n    CONTINUE_IN_BACKGROUND(\"app.continueInBackground\") {\n        @Override\n        public void run() {\n            AppOperationMode.switchToAsync(AppOperationMode.BACKGROUND);\n        }\n\n        @Override\n        public boolean isSelectable() {\n            return !AppOperationMode.TRAY.isSupported();\n        }\n    };\n\n    private final String id;\n\n    CloseBehaviour(String id) {\n        this.id = id;\n    }\n\n    public abstract void run();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/CloseBehaviourDialog.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.LabelComp;\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.comp.base.VerticalComp;\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.ext.PrefsChoiceValue;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.scene.control.RadioButton;\nimport javafx.scene.control.ToggleGroup;\nimport javafx.scene.layout.VBox;\n\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\npublic class CloseBehaviourDialog {\n\n    public static boolean showIfNeeded() {\n        if (AppOperationMode.isInShutdown()) {\n            return true;\n        }\n\n        boolean set = AppCache.getBoolean(\"closeBehaviourSet\", false);\n        if (set) {\n            return true;\n        }\n\n        Property<CloseBehaviour> prop =\n                new SimpleObjectProperty<>(AppPrefs.get().closeBehaviour().getValue());\n        var label = new LabelComp(AppI18n.observable(\"closeBehaviourAlertTitleHeader\"));\n        label.apply(struc -> {\n            struc.setWrapText(true);\n        });\n        var content = new VerticalComp(List.of(label, RegionBuilder.of(() -> {\n                    ToggleGroup group = new ToggleGroup();\n                    var vb = new VBox();\n                    vb.setSpacing(7);\n                    for (var cb : PrefsChoiceValue.getSupported(CloseBehaviour.class)) {\n                        RadioButton rb = new RadioButton(cb.toTranslatedString().getValue());\n                        rb.setToggleGroup(group);\n                        rb.selectedProperty().addListener((c, o, n) -> {\n                            if (n) {\n                                prop.setValue(cb);\n                            }\n                        });\n                        if (prop.getValue().equals(cb)) {\n                            rb.setSelected(true);\n                        }\n                        vb.getChildren().add(rb);\n                    }\n                    return vb;\n                })))\n                .spacing(15)\n                .prefWidth(500);\n        var oked = new AtomicBoolean();\n        var modal = ModalOverlay.of(\"closeBehaviourAlertTitle\", content);\n        modal.addButton(ModalButton.cancel());\n        modal.addButton(ModalButton.ok(() -> {\n            AppCache.update(\"closeBehaviourSet\", true);\n            AppPrefs.get().setFromExternal(AppPrefs.get().closeBehaviour(), prop.getValue());\n            oked.set(true);\n        }));\n        modal.showAndWait();\n        return oked.get();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/ConnectionHubCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.base.ButtonComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppResources;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.util.FileOpener;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.property.ReadOnlyObjectWrapper;\n\nimport java.nio.file.Files;\nimport java.util.UUID;\nimport java.util.concurrent.atomic.AtomicReference;\n\npublic class ConnectionHubCategory extends AppPrefsCategory {\n\n    @Override\n    protected String getId() {\n        return \"connectionHub\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2c-connection\");\n    }\n\n    @Override\n    protected BaseRegionBuilder<?, ?> create() {\n        var prefs = AppPrefs.get();\n        var connectionsBuilder = new OptionsBuilder()\n                .pref(prefs.enableConnectionHubTerminalDocking)\n                .addToggle(prefs.enableConnectionHubTerminalDocking)\n                .hide(OsType.ofLocal() != OsType.WINDOWS)\n                .nameAndDescription(\"connectionNotesTemplate\")\n                .addComp(\n                        new ButtonComp(\n                                AppI18n.observable(\"connectionNotesButton\"),\n                                new ReadOnlyObjectWrapper<>(\n                                        new LabelGraphic.IconGraphic(\"mdi2a-application-edit-outline\")),\n                                () -> {\n                                    editNotesTemplate();\n                                }),\n                        prefs.notesTemplate)\n                .pref(prefs.condenseConnectionDisplay)\n                .addToggle(prefs.condenseConnectionDisplay)\n                .pref(prefs.showChildCategoriesInParentCategory)\n                .addToggle(prefs.showChildCategoriesInParentCategory)\n                .pref(prefs.openConnectionSearchWindowOnConnectionCreation)\n                .addToggle(prefs.openConnectionSearchWindowOnConnectionCreation)\n                .pref(prefs.requireDoubleClickForConnections)\n                .addToggle(prefs.requireDoubleClickForConnections);\n        var options = new OptionsBuilder().addTitle(\"connectionHub\").sub(connectionsBuilder);\n        return options.buildComp();\n    }\n\n    private static final UUID NOTES_UUID = UUID.randomUUID();\n\n    private void editNotesTemplate() {\n        AtomicReference<String> val =\n                new AtomicReference<>(AppPrefs.get().notesTemplate.getValue());\n        if (val.get() == null) {\n            AppResources.with(AppResources.MAIN_MODULE, \"misc/notes_default.md\", f -> {\n                val.set(Files.readString(f));\n            });\n        }\n\n        FileOpener.openString(\"notes\", NOTES_UUID, val.get(), s -> {\n            AppPrefs.get().notesTemplate.set(s);\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/DeveloperCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.base.ButtonComp;\nimport io.xpipe.app.comp.base.InputGroupComp;\nimport io.xpipe.app.comp.base.TextFieldComp;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ProcessOutputException;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.geometry.Insets;\nimport javafx.geometry.Pos;\n\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.List;\n\npublic class DeveloperCategory extends AppPrefsCategory {\n\n    @Override\n    protected boolean show() {\n        return AppPrefs.get().developerMode().getValue();\n    }\n\n    @Override\n    protected String getId() {\n        return \"developer\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2c-code-tags\");\n    }\n\n    @Override\n    protected BaseRegionBuilder<?, ?> create() {\n        var prefs = AppPrefs.get();\n        var localCommand = new SimpleStringProperty();\n        Runnable test = () -> {\n            var cmd = localCommand.get();\n            if (cmd == null) {\n                return;\n            }\n\n            ThreadHelper.runFailableAsync(() -> {\n                try {\n                    TrackEvent.info(LocalShell.getShell().executeSimpleStringCommand(cmd));\n                } catch (ProcessOutputException ex) {\n                    TrackEvent.error(ex.getOutput());\n                }\n            });\n        };\n\n        var runLocalCommand = new InputGroupComp(List.of(\n                        new TextFieldComp(localCommand)\n                                .apply(struc -> struc.setPromptText(\"Local command\"))\n                                .hgrow(),\n                        new ButtonComp(null, new FontIcon(\"mdi2p-play\"), test)))\n                .setMainReference(0)\n                .padding(new Insets(15, 0, 0, 0))\n                .apply(struc -> struc.setAlignment(Pos.CENTER_LEFT))\n                .apply(struc -> struc.setFillHeight(true))\n                .maxWidth(600);\n        var sub = new OptionsBuilder()\n                .nameAndDescription(\"developerDisableUpdateVersionCheck\")\n                .addToggle(prefs.developerDisableUpdateVersionCheck)\n                .nameAndDescription(\"developerPrintInitFiles\")\n                .addToggle(prefs.developerPrintInitFiles)\n                .nameAndDescription(\"developerShowSensitiveCommands\")\n                .addToggle(prefs.developerShowSensitiveCommands);\n        if (AppProperties.get().isDevelopmentEnvironment()) {\n            sub.nameAndDescription(\"developerForceSshTty\").addToggle(prefs.developerForceSshTty);\n            sub.nameAndDescription(\"developerDisableSshTunnelGateways\")\n                    .addToggle(prefs.developerDisableSshTunnelGateways);\n        }\n        sub.nameAndDescription(\"shellCommandTest\").addComp(runLocalCommand);\n        return new OptionsBuilder().addTitle(\"developer\").sub(sub).buildComp();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/DisplayCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.IntFieldComp;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.scene.control.Slider;\n\nimport atlantafx.base.controls.ProgressSliderSkin;\nimport atlantafx.base.theme.Styles;\n\npublic class DisplayCategory extends AppPrefsCategory {\n\n    @Override\n    protected String getId() {\n        return \"display\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2m-monitor-screenshot\");\n    }\n\n    @Override\n    protected BaseRegionBuilder<?, ?> create() {\n        var prefs = AppPrefs.get();\n        return new OptionsBuilder()\n                .addTitle(\"displayOptions\")\n                .sub(new OptionsBuilder()\n                        .pref(prefs.uiScale)\n                        .addComp(\n                                new IntFieldComp(prefs.uiScale).maxWidth(100).apply(struc -> {\n                                    struc.setPromptText(\"100\");\n                                }),\n                                prefs.uiScale)\n                        .hide(new SimpleBooleanProperty(OsType.ofLocal() == OsType.MACOS))\n                        .pref(prefs.performanceMode)\n                        .addToggle(prefs.performanceMode)\n                        .pref(prefs.useSystemFont)\n                        .addToggle(prefs.useSystemFont)\n                        .pref(prefs.censorMode)\n                        .addToggle(prefs.censorMode)\n                        .pref(prefs.limitedTouchscreenMode)\n                        .addToggle(prefs.limitedTouchscreenMode)\n                        .hide(OsType.ofLocal() != OsType.LINUX))\n                .addTitle(\"windowOptions\")\n                .sub(new OptionsBuilder()\n                        .pref(prefs.windowOpacity)\n                        .addComp(\n                                RegionBuilder.of(() -> {\n                                            var s = new Slider(0.3, 1.0, prefs.windowOpacity.get());\n                                            s.getStyleClass().add(Styles.SMALL);\n                                            prefs.windowOpacity.bind(s.valueProperty());\n                                            s.setSkin(new ProgressSliderSkin(s));\n                                            return s;\n                                        })\n                                        .maxWidth(getCompWidth()),\n                                prefs.windowOpacity)\n                        .pref(prefs.saveWindowLocation)\n                        .addToggle(prefs.saveWindowLocation)\n                        .pref(prefs.focusWindowOnNotifications)\n                        .addToggle(prefs.focusWindowOnNotifications))\n                .buildComp();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/EditorCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.PrefsChoiceValue;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.util.*;\n\nimport javafx.geometry.Insets;\nimport javafx.geometry.Pos;\nimport javafx.scene.layout.Region;\n\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.List;\n\npublic class EditorCategory extends AppPrefsCategory {\n\n    public static OptionsBuilder editorChoice() {\n        var prefs = AppPrefs.get();\n        var editorTest = new ButtonComp(AppI18n.observable(\"test\"), new FontIcon(\"mdi2p-play\"), () -> {\n                    ThreadHelper.runFailableAsync(() -> {\n                        var editor = AppPrefs.get().externalEditor().getValue();\n                        if (editor != null) {\n                            FileOpener.openReadOnlyString(\"If you can read this, the editor integration is working\");\n                        }\n                    });\n                })\n                .padding(new Insets(6, 11, 6, 5))\n                .apply(struc -> struc.setAlignment(Pos.CENTER_LEFT));\n\n        var choice = ChoiceComp.ofTranslatable(\n                prefs.externalEditor, PrefsChoiceValue.getSupported(ExternalEditorType.class), false);\n\n        var visit = new ButtonComp(AppI18n.observable(\"website\"), new FontIcon(\"mdi2w-web\"), () -> {\n                    var t = prefs.externalEditor().getValue();\n                    if (t == null || t.getWebsite() == null) {\n                        return;\n                    }\n\n                    Hyperlinks.open(t.getWebsite());\n                })\n                .minWidth(Region.USE_PREF_SIZE);\n\n        var h = new HorizontalComp(List.of(choice.hgrow(), visit)).apply(struc -> {\n            struc.setAlignment(Pos.CENTER_LEFT);\n            struc.setSpacing(10);\n        });\n        h.maxWidth(600);\n\n        var builder = new OptionsBuilder()\n                .nameAndDescription(\"editorProgram\")\n                .addComp(h)\n                .nameAndDescription(\"customEditorCommand\")\n                .addComp(new TextFieldComp(prefs.customEditorCommand, true)\n                        .apply(struc -> struc.setPromptText(\"myeditor $FILE\")))\n                .hide(prefs.externalEditor.isNotEqualTo(ExternalEditorType.CUSTOM))\n                .addComp(editorTest)\n                .nameAndDescription(\"customEditorCommandInTerminal\")\n                .addToggle(prefs.customEditorCommandInTerminal)\n                .hide(prefs.externalEditor.isNotEqualTo(ExternalEditorType.CUSTOM));\n        return builder;\n    }\n\n    @Override\n    protected String getId() {\n        return \"editor\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2f-file-document-edit-outline\");\n    }\n\n    @Override\n    protected BaseRegionBuilder<?, ?> create() {\n        return new OptionsBuilder()\n                .addTitle(\"editorConfiguration\")\n                .sub(editorChoice())\n                .buildComp();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/ExternalApplicationHelper.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.CommandSupport;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.core.FilePath;\n\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.Locale;\nimport java.util.stream.Collectors;\n\npublic class ExternalApplicationHelper {\n\n    public static String replaceVariableArgument(String format, String variable, String value) {\n        // Support for legacy variables that were not upper case\n        variable = variable.toUpperCase(Locale.ROOT);\n        format = format.replace(\"$\" + variable.toLowerCase(Locale.ROOT), \"$\" + variable.toUpperCase(Locale.ROOT));\n\n        var fileString = value.contains(\" \") ? \"\\\"\" + value + \"\\\"\" : value;\n        // Check if the variable is already quoted\n        return format.replace(\"\\\"$\" + variable + \"\\\"\", fileString).replace(\"$\" + variable, fileString);\n    }\n\n    public static void startAsync(String raw) throws Exception {\n        if (raw == null) {\n            return;\n        }\n\n        raw = raw.strip();\n        var split = Arrays.asList(raw.split(\"\\\\s+\"));\n        if (split.size() == 0) {\n            return;\n        }\n\n        String exec;\n        String args;\n        if (raw.startsWith(\"\\\"\")) {\n            var end = raw.substring(1).indexOf(\"\\\"\");\n            if (end == -1) {\n                return;\n            }\n            end++;\n            exec = raw.substring(1, end);\n            args = raw.substring(end + 1).strip();\n        } else {\n            exec = split.getFirst();\n            args = split.stream().skip(1).collect(Collectors.joining(\" \"));\n        }\n\n        startAsync(CommandBuilder.of().addFile(exec).add(args));\n    }\n\n    public static void startAsync(CommandBuilder b) throws Exception {\n        try (var sc = LocalShell.getShell().start()) {\n            var base = b.buildBaseParts(sc);\n            var exec = base.getFirst();\n            if (exec.startsWith(\"\\\"\") && exec.endsWith(\"\\\"\")) {\n                exec = exec.substring(1, exec.length() - 1);\n            } else if (exec.startsWith(\"'\") && exec.endsWith(\"'\")) {\n                exec = exec.substring(1, exec.length() - 1);\n            }\n\n            // Some commands might be joined together without splitting into multiple elements\n            // e.g. user-provided commands\n            if (!exec.contains(\" \")) {\n                var execFile = FilePath.of(exec);\n                if (execFile.isAbsolute()) {\n                    if (!sc.view().fileExists(execFile)) {\n                        throw ErrorEventFactory.expected(new IOException(\"Executable \" + execFile + \" does not exist\"));\n                    }\n                } else {\n                    CommandSupport.isInPathOrThrow(sc, exec);\n                }\n            }\n\n            var cmd = sc.getShellDialect().launchAsync(b, true);\n            TrackEvent.withDebug(\"Executing local application\")\n                    .tag(\"command\", b.buildFull(sc))\n                    .tag(\"adjusted\", cmd.buildFull(sc))\n                    .handle();\n            try (var c = sc.command(cmd).start()) {\n                c.discardOrThrow();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.ext.PrefsValue;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.CommandControl;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.util.FlatpakCache;\nimport io.xpipe.app.util.Translatable;\nimport io.xpipe.core.OsType;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.InvalidPathException;\nimport java.nio.file.Path;\nimport java.util.Optional;\n\npublic interface ExternalApplicationType extends PrefsValue {\n\n    boolean isAvailable();\n\n    interface MacApplication extends ExternalApplicationType {\n\n        default CommandControl launchCommand(CommandBuilder builder, boolean args) {\n            if (args) {\n                builder.add(0, \"--args\");\n            }\n            builder.addQuoted(0, getApplicationName());\n            builder.add(0, \"open\", \"-a\");\n            return LocalShell.getShell().command(builder);\n        }\n\n        @Override\n        default boolean isAvailable() {\n            try {\n                return findApp().isPresent();\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).handle();\n                return false;\n            }\n        }\n\n        String getApplicationName();\n\n        default Optional<Path> findApp() throws Exception {\n            // Perform a quick check because mdfind is slow\n            var applicationsDef = Path.of(\"/Applications/\" + getApplicationName() + \".app\");\n            if (Files.exists(applicationsDef)) {\n                return Optional.of(applicationsDef);\n            }\n            var systemApplicationsDef = Path.of(\"/System/Applications/\" + getApplicationName() + \".app\");\n            if (Files.exists(systemApplicationsDef)) {\n                return Optional.of(systemApplicationsDef);\n            }\n            var userApplicationsDef =\n                    AppSystemInfo.ofCurrent().getUserHome().resolve(\"Applications\", getApplicationName() + \".app\");\n            if (Files.exists(userApplicationsDef)) {\n                return Optional.of(userApplicationsDef);\n            }\n\n            try (ShellControl pc = LocalShell.getShell().start()) {\n                var out = pc.command(String.format(\n                                \"mdfind -literal 'kMDItemFSName = \\\"%s.app\\\"' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications\",\n                                getApplicationName()))\n                        .readStdoutIfPossible();\n                return out.isPresent() && !out.get().isBlank() && out.get().contains(getApplicationName() + \".app\")\n                        ? out.map(s -> Path.of(s))\n                        : Optional.empty();\n            }\n        }\n\n        default void focus() {\n            try (ShellControl pc = LocalShell.getShell().start()) {\n                pc.command(String.format(\"open -a \\\"%s.app\\\"\", getApplicationName()))\n                        .execute();\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).handle();\n            }\n        }\n\n        @Override\n        default boolean isSelectable() {\n            return OsType.ofLocal() == OsType.MACOS;\n        }\n    }\n\n    interface PathApplication extends ExternalApplicationType {\n\n        String getExecutable();\n\n        boolean detach();\n\n        default boolean isAvailable() {\n            try (ShellControl pc = LocalShell.getShell()) {\n                String name = getExecutable();\n                return pc.view().findProgram(name).isPresent();\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).omit().handle();\n                return false;\n            }\n        }\n\n        default void launch(CommandBuilder args) throws Exception {\n            try (ShellControl pc = LocalShell.getShell()) {\n                String executable = getExecutable();\n                if (!pc.view().findProgram(executable).isPresent()) {\n                    throw ErrorEventFactory.expected(new IOException(\"Executable \" + getExecutable()\n                            + \" not found in PATH. Either add it to the PATH and refresh the environment by restarting XPipe, or specify an absolute \"\n                            + \"executable path using the custom terminal setting.\"));\n                }\n\n                args.add(0, getExecutable());\n                if (detach()) {\n                    ExternalApplicationHelper.startAsync(args);\n                } else {\n                    pc.executeSimpleCommand(args);\n                }\n            }\n        }\n    }\n\n    interface LinuxApplication extends PathApplication {\n\n        String getFlatpakId() throws Exception;\n\n        default CommandBuilder getCommandBase() throws Exception {\n            if (getFlatpakId() == null\n                    || LocalShell.getShell().view().findProgram(getExecutable()).isPresent()) {\n                return CommandBuilder.of().add(getExecutable());\n            }\n\n            var app = FlatpakCache.getApp(getFlatpakId());\n            if (app.isEmpty()) {\n                throw ErrorEventFactory.expected(new IOException(\n                        \"Executable \" + getExecutable() + \" not found in PATH nor as a flatkpak \" + getFlatpakId()\n                                + \" not installed. Install it and refresh the environment by restarting XPipe\"));\n            }\n\n            return FlatpakCache.getRunCommand(getFlatpakId());\n        }\n\n        @Override\n        default boolean isAvailable() {\n            try (ShellControl pc = LocalShell.getShell().start()) {\n                if (getFlatpakId() != null) {\n                    var app = FlatpakCache.getApp(getFlatpakId());\n                    if (app.isPresent()) {\n                        return true;\n                    }\n                }\n\n                String name = getExecutable();\n                return pc.view().findProgram(name).isPresent();\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).omit().handle();\n                return false;\n            }\n        }\n\n        @Override\n        default void launch(CommandBuilder args) throws Exception {\n            if (getFlatpakId() == null\n                    || LocalShell.getShell().view().findProgram(getExecutable()).isPresent()) {\n                PathApplication.super.launch(args);\n                return;\n            }\n\n            var app = FlatpakCache.getApp(getFlatpakId());\n            if (app.isEmpty()) {\n                throw ErrorEventFactory.expected(new IOException(\n                        \"Executable \" + getExecutable() + \" not found in PATH nor as a flatkpak \" + getFlatpakId()\n                                + \" not installed. Install it and refresh the environment by restarting XPipe\"));\n            }\n\n            args.add(0, FlatpakCache.getRunCommand(getFlatpakId()));\n            if (detach()) {\n                ExternalApplicationHelper.startAsync(args);\n            } else {\n                LocalShell.getShell().command(args).execute();\n            }\n        }\n    }\n\n    interface InstallLocationType extends ExternalApplicationType {\n\n        String getExecutable();\n\n        Optional<Path> determineInstallation();\n\n        default Optional<Path> determineFromPath() {\n            // Try to locate if it is in the Path\n            try (var sc = LocalShell.getShell().start()) {\n                String name = getExecutable();\n                var out = sc.view().findProgram(name);\n                if (out.isPresent()) {\n                    return out.flatMap(filePath -> {\n                        try {\n                            return Optional.of(Path.of(filePath.toString()));\n                        } catch (InvalidPathException ex) {\n                            ErrorEventFactory.fromThrowable(ex).omit().handle();\n                            return Optional.empty();\n                        }\n                    });\n                }\n            } catch (Exception ex) {\n                ErrorEventFactory.fromThrowable(ex).omit().handle();\n            }\n            return Optional.empty();\n        }\n\n        default Path findExecutable() {\n            var location = determineFromPath();\n            if (location.isEmpty()) {\n                location = determineInstallation();\n                if (location.isEmpty()) {\n                    var name = this instanceof Translatable t\n                            ? t.toTranslatedString().getValue()\n                            : getExecutable();\n                    throw ErrorEventFactory.expected(\n                            new UnsupportedOperationException(\"Unable to find installation of \" + name));\n                }\n            }\n            return location.get();\n        }\n\n        @Override\n        default boolean isAvailable() {\n            var path = determineFromPath();\n            if (path.isPresent() && Files.exists(path.get())) {\n                return true;\n            }\n\n            var installation = determineInstallation();\n            return installation.isPresent() && Files.exists(installation.get());\n        }\n    }\n\n    interface WindowsType extends InstallLocationType {\n\n        boolean detach();\n\n        default void launch(CommandBuilder builder) throws Exception {\n            var location = findExecutable();\n            builder.add(0, sc -> {\n                return sc != null ? sc.getShellDialect().fileArgument(location.toString()) : \"\\\"\" + location + \"\\\"\";\n            });\n            if (detach()) {\n                ExternalApplicationHelper.startAsync(builder);\n            } else {\n                LocalShell.getShell().executeSimpleCommand(builder);\n            }\n        }\n\n        @Override\n        default boolean isSelectable() {\n            return OsType.ofLocal() == OsType.WINDOWS;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.ext.PrefsChoiceValue;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.*;\nimport io.xpipe.app.terminal.TerminalLaunch;\nimport io.xpipe.app.util.FlatpakCache;\nimport io.xpipe.app.util.WindowsRegistry;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.AllArgsConstructor;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Optional;\nimport java.util.function.Supplier;\n\npublic interface ExternalEditorType extends PrefsChoiceValue {\n    ExternalEditorType NOTEPAD = new WindowsType() {\n\n        @Override\n        public String getWebsite() {\n            return \"https://apps.microsoft.com/detail/9msmlrh6lzf3?hl=en-US&gl=US\";\n        }\n\n        @Override\n        public String getId() {\n            return \"app.notepad\";\n        }\n\n        @Override\n        public boolean detach() {\n            return false;\n        }\n\n        @Override\n        public String getExecutable() {\n            return \"notepad\";\n        }\n\n        @Override\n        public Optional<Path> determineInstallation() {\n            return Optional.of(AppSystemInfo.ofWindows().getSystemRoot().resolve(\"\\\\System32\\\\notepad.exe\"));\n        }\n    };\n\n    WindowsType VSCODIUM_WINDOWS = new VsCodeWindowsType(\n            \"app.vscodium\",\n            \"https://vscodium.com/\",\n            () -> AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Programs\")\n                    .resolve(\"VSCodium\")\n                    .resolve(\"bin\")\n                    .resolve(\"codium.cmd\"),\n            () -> AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Programs\")\n                    .resolve(\"VSCodium\")\n                    .resolve(\"VSCodium.exe\"));\n\n    WindowsType ANTIGRAVITY_WINDOWS = new VsCodeWindowsType(\n            \"app.antigravity\",\n            \"https://antigravity.google/\",\n            () -> AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Programs\")\n                    .resolve(\"Antigravity\")\n                    .resolve(\"bin\")\n                    .resolve(\"antigravity.cmd\"),\n            () -> AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Programs\")\n                    .resolve(\"Antigravity\")\n                    .resolve(\"Antigravity.exe\"));\n\n    WindowsType CURSOR_WINDOWS = new VsCodeWindowsType(\n            \"app.cursor\",\n            \"https://cursor.com/\",\n            () -> AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Programs\")\n                    .resolve(\"cursor\")\n                    .resolve(\"bin\")\n                    .resolve(\"cursor.cmd\"),\n            () -> AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Programs\")\n                    .resolve(\"cursor\")\n                    .resolve(\"Cursor.exe\"));\n\n    WindowsType VOID_WINDOWS = new VsCodeWindowsType(\n            \"app.void\",\n            \"https://voideditor.com/\",\n            () -> AppSystemInfo.ofWindows()\n                    .getProgramFiles()\n                    .resolve(\"Void\")\n                    .resolve(\"bin\")\n                    .resolve(\"void.cmd\"),\n            () -> AppSystemInfo.ofWindows().getProgramFiles().resolve(\"Void\").resolve(\"Void.exe\"));\n\n    WindowsType WINDSURF_WINDOWS = new VsCodeWindowsType(\n            \"app.windsurf\",\n            \"https://windsurf.com/editor\",\n            () -> AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Programs\")\n                    .resolve(\"Windsurf\")\n                    .resolve(\"bin\")\n                    .resolve(\"windsurf.cmd\"),\n            () -> AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Programs\")\n                    .resolve(\"Windsurf\")\n                    .resolve(\"Windsurf.exe\"));\n\n    WindowsType KIRO_WINDOWS = new VsCodeWindowsType(\n            \"app.kiro\",\n            \"https://kiro.dev/\",\n            () -> AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Programs\")\n                    .resolve(\"Kiro\")\n                    .resolve(\"bin\")\n                    .resolve(\"kiro.cmd\"),\n            () -> AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Programs\")\n                    .resolve(\"Kiro\")\n                    .resolve(\"Kiro.exe\"));\n\n    // Cli is broken, keep inactive\n    @SuppressWarnings(\"unused\")\n    WindowsType THEIAIDE_WINDOWS = new WindowsType() {\n\n        @Override\n        public String getWebsite() {\n            return \"https://theia-ide.org/\";\n        }\n\n        @Override\n        public String getId() {\n            return \"app.theiaide\";\n        }\n\n        @Override\n        public boolean detach() {\n            return true;\n        }\n\n        @Override\n        public String getExecutable() {\n            return \"Theiaide\";\n        }\n\n        @Override\n        public Optional<Path> determineInstallation() {\n            return Optional.of(AppSystemInfo.ofWindows()\n                            .getLocalAppData()\n                            .resolve(\"Programs\")\n                            .resolve(\"TheiaIDE\")\n                            .resolve(\"TheiaIDE.exe\"))\n                    .filter(path -> Files.exists(path));\n        }\n    };\n\n    WindowsType TRAE_WINDOWS = new VsCodeWindowsType(\n            \"app.trae\",\n            \"https://www.trae.ai/\",\n            () -> AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Programs\")\n                    .resolve(\"Trae\")\n                    .resolve(\"bin\")\n                    .resolve(\"trae.cmd\"),\n            () -> AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Programs\")\n                    .resolve(\"Trae\")\n                    .resolve(\"Trae.exe\"));\n\n    WindowsType VSCODE_WINDOWS = new VsCodeWindowsType(\n            \"app.vscode\",\n            \"https://code.visualstudio.com/\",\n            () -> AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Programs\")\n                    .resolve(\"Microsoft VS Code\")\n                    .resolve(\"bin\")\n                    .resolve(\"code.cmd\"),\n            () -> AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Programs\")\n                    .resolve(\"Microsoft VS Code\")\n                    .resolve(\"Code.exe\"));\n\n    WindowsType VSCODE_INSIDERS_WINDOWS = new VsCodeWindowsType(\n            \"app.vscodeInsiders\",\n            \"https://code.visualstudio.com/insiders/\",\n            () -> AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Programs\")\n                    .resolve(\"Microsoft VS Code Insiders\")\n                    .resolve(\"bin\")\n                    .resolve(\"code-insiders.cmd\"),\n            () -> AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Programs\")\n                    .resolve(\"Microsoft VS Code Insiders\")\n                    .resolve(\"Code - Insiders.exe\"));\n\n    ExternalEditorType NOTEPADPLUSPLUS = new WindowsType() {\n\n        @Override\n        public String getWebsite() {\n            return \"https://notepad-plus-plus.org/\";\n        }\n\n        @Override\n        public String getId() {\n            return \"app.notepad++\";\n        }\n\n        @Override\n        public boolean detach() {\n            return false;\n        }\n\n        @Override\n        public String getExecutable() {\n            return \"notepad++\";\n        }\n\n        @Override\n        public Optional<Path> determineInstallation() {\n            var found = WindowsRegistry.local()\n                    .readStringValueIfPresent(WindowsRegistry.HKEY_LOCAL_MACHINE, \"SOFTWARE\\\\Notepad++\", null);\n\n            // Check 32 bit install\n            if (found.isEmpty()) {\n                found = WindowsRegistry.local()\n                        .readStringValueIfPresent(\n                                WindowsRegistry.HKEY_LOCAL_MACHINE, \"WOW6432Node\\\\SOFTWARE\\\\Notepad++\", null);\n            }\n            return found.map(p -> p + \"\\\\notepad++.exe\").map(Path::of);\n        }\n    };\n\n    LinuxType VSCODE_LINUX =\n            new LinuxType(\"app.vscode\", \"code\", \"https://code.visualstudio.com/\", \"com.visualstudio.code\") {\n                @Override\n                public void launch(Path file) throws Exception {\n                    var exec = CommandSupport.isInLocalPath(getExecutable())\n                            ? CommandBuilder.of().addFile(getExecutable())\n                            : FlatpakCache.getRunCommand(getFlatpakId());\n\n                    if (FlatpakCache.getApp(getFlatpakId()).isEmpty()) {\n                        CommandSupport.isInPathOrThrow(LocalShell.getShell(), getExecutable());\n                    }\n\n                    var builder = CommandBuilder.of()\n                            .fixedEnvironment(\"DONT_PROMPT_WSL_INSTALL\", \"No_Prompt_please\")\n                            .add(exec)\n                            .addFile(file.toString());\n                    ExternalApplicationHelper.startAsync(builder);\n                }\n            };\n\n    LinuxPathType WINDSURF_LINUX = new LinuxPathType(\"app.windsurf\", \"windsurf\", \"https://windsurf.com/editor\");\n\n    LinuxPathType CURSOR_LINUX = new LinuxPathType(\"app.cursor\", \"cursor\", \"https://cursor.com/\");\n\n    LinuxPathType KIRO_LINUX = new LinuxPathType(\"app.kiro\", \"kiro\", \"https://kiro.dev/\");\n\n    LinuxType NEOVIM_LINUX = new LinuxType(\"app.neovim\", \"nvim\", \"https://neovim.io/\", null) {\n        @Override\n        public void launch(Path file) throws Exception {\n            TerminalLaunch.builder()\n                    .title(file.toString())\n                    .localScript(sc -> new ShellScript(CommandBuilder.of()\n                            .addFile(getExecutable())\n                            .addFile(file.toString())\n                            .buildFull(sc)))\n                    .logIfEnabled(false)\n                    .preferTabs(false)\n                    .pauseOnExit(false)\n                    .launch();\n        }\n    };\n\n    WindowsType NEOVIM_WINDOWS = new WindowsType() {\n        @Override\n        public String getId() {\n            return \"app.neovim\";\n        }\n\n        @Override\n        public boolean detach() {\n            return false;\n        }\n\n        @Override\n        public String getExecutable() {\n            return \"nvim\";\n        }\n\n\n        @Override\n        public String getWebsite() {\n            return \"https://neovim.io/\";\n        }\n\n\n        @Override\n        public Optional<Path> determineInstallation() {\n            var programFiles = AppSystemInfo.ofWindows().getProgramFiles().resolve(\"Neovim\", \"bin\").resolve(\"nvim.exe\");\n            if (Files.exists(programFiles)) {\n                return Optional.of(programFiles);\n            }\n            return Optional.empty();\n        }\n\n        @Override\n        public void launch(Path file) throws Exception {\n            TerminalLaunch.builder()\n                    .title(file.toString())\n                    .localScript(sc -> new ShellScript(CommandBuilder.of()\n                            .addFile(findExecutable().toString())\n                            .addFile(file)\n                            .buildFull(sc)))\n                    .logIfEnabled(false)\n                    .preferTabs(false)\n                    .pauseOnExit(false)\n                    .launch();\n        }\n\n    };\n\n    WindowsType ZED_WINDOWS = new WindowsType() {\n\n        @Override\n        public String getWebsite() {\n            return \"https://zed.dev/\";\n        }\n\n        @Override\n        public String getExecutable() {\n            return \"Zed.exe\";\n        }\n\n        @Override\n        public Optional<Path> determineInstallation() {\n            var nightly = AppSystemInfo.ofWindows().getLocalAppData().resolve(\"Programs\", \"Zed Nightly\", \"Zed.exe\");\n            if (Files.exists(nightly)) {\n                return Optional.of(nightly);\n            }\n\n            var regular = AppSystemInfo.ofWindows().getLocalAppData().resolve(\"Programs\", \"Zed\", \"Zed.exe\");\n            if (Files.exists(regular)) {\n                return Optional.of(regular);\n            }\n\n            return Optional.empty();\n        }\n\n        @Override\n        public boolean detach() {\n            return false;\n        }\n\n        @Override\n        public String getId() {\n            return \"app.zed\";\n        }\n    };\n\n    LinuxType ZED_LINUX = new LinuxType(\"app.zed\", \"zed\", \"https://zed.dev/\", \"dev.zed.Zed\");\n\n    ExternalEditorType ZED_MACOS = new MacOsEditor(\"app.zed\", \"Zed\", \"https://zed.dev/\");\n\n    LinuxType VSCODIUM_LINUX = new LinuxType(\"app.vscodium\", \"codium\", \"https://vscodium.com/\", \"com.vscodium.codium\");\n\n    LinuxType ANTIGRAVITY_LINUX = new LinuxType(\"app.antigravity\", \"antigravity\", \"https://antigravity.google/\", null);\n\n    LinuxType GNOME = new LinuxType(\"app.gnomeTextEditor\", \"gnome-text-editor\", \"LinuxType\", \"org.gnome.TextEditor\");\n\n    LinuxType KATE = new LinuxType(\"app.kate\", \"kate\", \"https://kate-editor.org\", \"org.kde.kate\");\n\n    LinuxType GEDIT = new LinuxType(\"app.gedit\", \"gedit\", \"https://gedit-text-editor.org/\", \"org.gnome.gedit\");\n\n    LinuxPathType LEAFPAD = new LinuxPathType(\"app.leafpad\", \"leafpad\", \"https://snapcraft.io/leafpad\");\n\n    LinuxType MOUSEPAD =\n            new LinuxType(\"app.mousepad\", \"mousepad\", \"https://docs.xfce.org/apps/mousepad/start\", \"org.xfce.mousepad\");\n\n    LinuxPathType PLUMA = new LinuxPathType(\"app.pluma\", \"pluma\", \"https://github.com/mate-desktop/pluma\");\n    LinuxPathType COSMIC_EDIT =\n            new LinuxPathType(\"app.cosmicEdit\", \"cosmic-edit\", \"https://github.com/pop-os/cosmic-edit\");\n    LinuxPathType WESTON_EDITOR =\n            new LinuxPathType(\"app.westonEditor\", \"weston-editor\", \"https://wayland.pages.freedesktop.org/weston/\");\n    ExternalEditorType TEXT_EDIT =\n            new MacOsEditor(\"app.textEdit\", \"TextEdit\", \"https://support.apple.com/en-gb/guide/textedit/welcome/mac\");\n    ExternalEditorType BBEDIT = new MacOsEditor(\"app.bbedit\", \"BBEdit\", \"https://www.barebones.com/products/bbedit/\");\n    ExternalEditorType SUBLIME_MACOS = new MacOsEditor(\"app.sublime\", \"Sublime Text\", \"https://www.sublimetext.com/\");\n    ExternalEditorType VSCODE_MACOS =\n            new MacOsEditor(\"app.vscode\", \"Visual Studio Code\", \"https://code.visualstudio.com/\");\n    ExternalEditorType VSCODIUM_MACOS = new MacOsEditor(\"app.vscodium\", \"VSCodium\", \"https://vscodium.com/\");\n    ExternalEditorType ANTIGRAVITY_MACOS =\n            new MacOsEditor(\"app.antigravity\", \"Antigravity\", \"https://antigravity.google/\");\n    ExternalEditorType CURSOR_MACOS = new MacOsEditor(\"app.cursor\", \"Cursor\", \"https://cursor.com/\");\n    ExternalEditorType VOID_MACOS = new MacOsEditor(\"app.void\", \"Void\", \"https://voideditor.com/\");\n    ExternalEditorType WINDSURF_MACOS = new MacOsEditor(\"app.windsurf\", \"Windsurf\", \"https://windsurf.com/editor\");\n    ExternalEditorType KIRO_MACOS = new MacOsEditor(\"app.kiro\", \"Kiro\", \"https://kiro.dev/\");\n    ExternalEditorType TRAE_MACOS = new MacOsEditor(\"app.trae\", \"Trae\", \"https://www.trae.ai/\");\n    ExternalEditorType NEOVIM_MACOS = new MacOsEditor(\"app.neovim\", \"Neovim\", \"https://neovim.io/\") {\n        @Override\n        public void launch(Path file) throws Exception {\n            TerminalLaunch.builder()\n                    .title(file.toString())\n                    .localScript(sc -> new ShellScript(CommandBuilder.of()\n                            .addFile(\"nvim\")\n                            .addFile(file.toString())\n                            .buildFull(sc)))\n                    .logIfEnabled(false)\n                    .preferTabs(false)\n                    .pauseOnExit(false)\n                    .launch();\n        }\n    };\n    ExternalEditorType CUSTOM = new ExternalEditorType() {\n\n        @Override\n        public String getWebsite() {\n            return null;\n        }\n\n        @Override\n        public void launch(Path file) throws Exception {\n            var customCommand = AppPrefs.get().customEditorCommand().getValue();\n            if (customCommand == null || customCommand.isBlank()) {\n                throw ErrorEventFactory.expected(new IllegalStateException(\"No custom editor command specified\"));\n            }\n\n            var format =\n                    customCommand.toLowerCase(Locale.ROOT).contains(\"$file\") ? customCommand : customCommand + \" $FILE\";\n            var command = CommandBuilder.of()\n                    .add(ExternalApplicationHelper.replaceVariableArgument(format, \"FILE\", file.toString()));\n            if (AppPrefs.get().customEditorCommandInTerminal().get()) {\n                TerminalLaunch.builder()\n                        .title(file.toString())\n                        .localScript(sc -> new ShellScript(command.buildFull(sc)))\n                        .logIfEnabled(false)\n                        .preferTabs(false)\n                        .pauseOnExit(false)\n                        .launch();\n            } else {\n                ExternalApplicationHelper.startAsync(command);\n            }\n        }\n\n        @Override\n        public ObservableValue<String> toTranslatedString() {\n            var customCommand = AppPrefs.get().customEditorCommand().getValue();\n            if (customCommand == null\n                    || customCommand.isBlank()\n                    || customCommand.replace(\"$FILE\", \"\").strip().contains(\" \")) {\n                return ExternalEditorType.super.toTranslatedString();\n            }\n\n            return new SimpleStringProperty(customCommand);\n        }\n\n        @Override\n        public String getId() {\n            return \"app.custom\";\n        }\n    };\n    ExternalEditorType FLEET = new GenericPathType(\"app.fleet\", \"fleet\", false, \"https://www.jetbrains.com/fleet/\");\n    ExternalEditorType INTELLIJ = new GenericPathType(\"app.intellij\", \"idea\", false, \"https://www.jetbrains.com/idea/\");\n    ExternalEditorType PYCHARM =\n            new GenericPathType(\"app.pycharm\", \"pycharm\", false, \"https://www.jetbrains.com/pycharm/\");\n    ExternalEditorType WEBSTORM =\n            new GenericPathType(\"app.webstorm\", \"webstorm\", false, \"https://www.jetbrains.com/webstorm/\");\n    ExternalEditorType CLION = new GenericPathType(\"app.clion\", \"clion\", false, \"https://www.jetbrains.com/clion/\");\n    List<ExternalEditorType> WINDOWS_EDITORS = List.of(\n            ZED_WINDOWS,\n            VOID_WINDOWS,\n            CURSOR_WINDOWS,\n            WINDSURF_WINDOWS,\n            TRAE_WINDOWS,\n            KIRO_WINDOWS,\n            ANTIGRAVITY_WINDOWS,\n            VSCODIUM_WINDOWS,\n            VSCODE_INSIDERS_WINDOWS,\n            VSCODE_WINDOWS,\n            NOTEPADPLUSPLUS,\n            NOTEPAD,\n            NEOVIM_WINDOWS);\n    List<GenericPathType> LINUX_EDITORS = List.of(\n            ExternalEditorType.WINDSURF_LINUX,\n            ExternalEditorType.KIRO_LINUX,\n            VSCODIUM_LINUX,\n            ANTIGRAVITY_LINUX,\n            VSCODE_LINUX,\n            ZED_LINUX,\n            KATE,\n            GEDIT,\n            PLUMA,\n            LEAFPAD,\n            MOUSEPAD,\n            NEOVIM_LINUX,\n            GNOME,\n            ExternalEditorType.COSMIC_EDIT,\n            ExternalEditorType.WESTON_EDITOR,\n            ExternalEditorType.CURSOR_LINUX);\n    List<ExternalEditorType> MACOS_EDITORS = List.of(\n            VOID_MACOS,\n            CURSOR_MACOS,\n            WINDSURF_MACOS,\n            KIRO_MACOS,\n            TRAE_MACOS,\n            BBEDIT,\n            VSCODIUM_MACOS,\n            ANTIGRAVITY_MACOS,\n            VSCODE_MACOS,\n            SUBLIME_MACOS,\n            ZED_MACOS,\n            NEOVIM_MACOS,\n            TEXT_EDIT);\n    List<ExternalEditorType> CROSS_PLATFORM_EDITORS = List.of(FLEET, INTELLIJ, PYCHARM, WEBSTORM, CLION);\n\n    @SuppressWarnings({\"unused\", \"TrivialFunctionalExpressionUsage\"})\n    List<ExternalEditorType> ALL = ((Supplier<List<ExternalEditorType>>) () -> {\n                var all = new ArrayList<ExternalEditorType>();\n                if (OsType.ofLocal() == OsType.WINDOWS) {\n                    all.addAll(WINDOWS_EDITORS);\n                }\n                if (OsType.ofLocal() == OsType.LINUX) {\n                    all.addAll(LINUX_EDITORS);\n                }\n                if (OsType.ofLocal() == OsType.MACOS) {\n                    all.addAll(MACOS_EDITORS);\n                }\n                all.addAll(CROSS_PLATFORM_EDITORS);\n                all.add(CUSTOM);\n                return all;\n            })\n            .get();\n\n    static ExternalEditorType determineDefault(ExternalEditorType existing) {\n        // Verify that our selection is still valid\n        if (existing != null && existing.isAvailable()) {\n            return existing;\n        }\n\n        if (OsType.ofLocal() == OsType.WINDOWS) {\n            return WINDOWS_EDITORS.stream()\n                    .filter(PrefsChoiceValue::isAvailable)\n                    .findFirst()\n                    .orElse(NOTEPAD);\n        }\n\n        if (OsType.ofLocal() == OsType.LINUX) {\n            return LINUX_EDITORS.stream()\n                    .filter(ExternalApplicationType.PathApplication::isAvailable)\n                    .findFirst()\n                    .orElse(null);\n        }\n\n        if (OsType.ofLocal() == OsType.MACOS) {\n            return MACOS_EDITORS.stream()\n                    .filter(PrefsChoiceValue::isAvailable)\n                    .findFirst()\n                    .orElse(TEXT_EDIT);\n        }\n\n        return null;\n    }\n\n    String getWebsite();\n\n    void launch(Path file) throws Exception;\n\n    interface WindowsType extends ExternalApplicationType.WindowsType, ExternalEditorType {\n\n        @Override\n        default void launch(Path file) throws Exception {\n            var location = findExecutable();\n            // Use quotes for file in case editor does not like single quotes or other\n            var builder = CommandBuilder.of().addFile(location.toString()).addQuoted(file.toString());\n            if (detach()) {\n                ExternalApplicationHelper.startAsync(builder);\n            } else {\n                LocalShell.getShell().executeSimpleCommand(builder);\n            }\n        }\n    }\n\n    @AllArgsConstructor\n    class VsCodeWindowsType implements WindowsType {\n\n        private final String id;\n        private final String link;\n        private final Supplier<Path> script;\n        private final Supplier<Path> exe;\n\n        @Override\n        public String getId() {\n            return id;\n        }\n\n        @Override\n        public boolean detach() {\n            // Launching the exe requires detach\n            return LocalShell.getDialect() != ShellDialects.CMD;\n        }\n\n        @Override\n        public String getExecutable() {\n            // Use .cmd script in cmd\n            return LocalShell.getDialect() == ShellDialects.CMD\n                    ? script.get().getFileName().toString()\n                    : exe.get().getFileName().toString();\n        }\n\n        @Override\n        public Optional<Path> determineInstallation() {\n            return Optional.of(LocalShell.getDialect() != ShellDialects.CMD ? exe.get() : script.get())\n                    .filter(Files::exists);\n        }\n\n        @Override\n        public String getWebsite() {\n            return link;\n        }\n    }\n\n    class MacOsEditor implements ExternalApplicationType.MacApplication, ExternalEditorType {\n\n        private final String id;\n        private final String appName;\n        private final String website;\n\n        public MacOsEditor(String id, String appName, String website) {\n            this.id = id;\n            this.appName = appName;\n            this.website = website;\n        }\n\n        @Override\n        public String getWebsite() {\n            return website;\n        }\n\n        @Override\n        public void launch(Path file) throws Exception {\n            try (var sc = LocalShell.getShell().start()) {\n                sc.executeSimpleCommand(CommandBuilder.of()\n                        .add(\"open\", \"-a\")\n                        .addQuoted(getApplicationName())\n                        .addFile(file.toString()));\n            }\n        }\n\n        @Override\n        public String getApplicationName() {\n            return appName;\n        }\n\n        @Override\n        public String getId() {\n            return id;\n        }\n    }\n\n    class GenericPathType implements ExternalApplicationType.PathApplication, ExternalEditorType {\n\n        private final String id;\n        private final String executable;\n        private final boolean async;\n        private final String website;\n\n        public GenericPathType(String id, String executable, boolean async, String website) {\n            this.id = id;\n            this.executable = executable;\n            this.async = async;\n            this.website = website;\n        }\n\n        @Override\n        public String getWebsite() {\n            return website;\n        }\n\n        @Override\n        public void launch(Path file) throws Exception {\n            var builder = CommandBuilder.of().addFile(getExecutable()).addFile(file.toString());\n            if (detach()) {\n                ExternalApplicationHelper.startAsync(builder);\n            } else {\n                LocalShell.getShell().executeSimpleCommand(builder);\n            }\n        }\n\n        @Override\n        public String getExecutable() {\n            return executable;\n        }\n\n        @Override\n        public boolean detach() {\n            return async;\n        }\n\n        @Override\n        public String getId() {\n            return id;\n        }\n    }\n\n    class LinuxPathType extends GenericPathType {\n\n        public LinuxPathType(String id, String executable, String website) {\n            super(id, executable, true, website);\n        }\n\n        @Override\n        public boolean isSelectable() {\n            return OsType.ofLocal() == OsType.LINUX;\n        }\n    }\n\n    class LinuxType extends GenericPathType implements ExternalApplicationType.LinuxApplication {\n\n        private final String flatpakId;\n\n        public LinuxType(String id, String executable, String website, String flatpakId) {\n            super(id, executable, true, website);\n            this.flatpakId = flatpakId;\n        }\n\n        @Override\n        public void launch(Path file) throws Exception {\n            if (CommandSupport.isInLocalPath(getExecutable())) {\n                var builder = CommandBuilder.of().add(getExecutable()).addFile(file.toString());\n                if (detach()) {\n                    ExternalApplicationHelper.startAsync(builder);\n                } else {\n                    LocalShell.getShell().command(builder).execute();\n                }\n            } else {\n                if (flatpakId == null || FlatpakCache.getApp(flatpakId).isEmpty()) {\n                    CommandSupport.isInPathOrThrow(LocalShell.getShell(), getExecutable());\n                }\n\n                var builder = FlatpakCache.getRunCommand(getFlatpakId()).addFile(file.toString());\n                ExternalApplicationHelper.startAsync(builder);\n            }\n        }\n\n        @Override\n        public String getFlatpakId() {\n            return flatpakId;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/FileBrowserCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.base.ContextualFileReferenceChoiceComp;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.property.ReadOnlyObjectWrapper;\n\nimport java.util.List;\n\npublic class FileBrowserCategory extends AppPrefsCategory {\n\n    @Override\n    protected String getId() {\n        return \"fileBrowser\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2f-file-cabinet\");\n    }\n\n    @Override\n    protected BaseRegionBuilder<?, ?> create() {\n        var prefs = AppPrefs.get();\n        return new OptionsBuilder()\n                .addTitle(\"fileBrowser\")\n                .sub(new OptionsBuilder()\n                        .pref(prefs.editFilesWithDoubleClick)\n                        .addToggle(prefs.editFilesWithDoubleClick)\n                        .pref(prefs.downloadsDirectory)\n                        .addComp(\n                                new ContextualFileReferenceChoiceComp(\n                                                new ReadOnlyObjectWrapper<>(DataStorage.get()\n                                                        .local()\n                                                        .ref()),\n                                                prefs.downloadsDirectory,\n                                                null,\n                                                List.of(),\n                                                e -> e.equals(DataStorage.get().local()),\n                                                true)\n                                        .maxWidth(getCompWidth()),\n                                prefs.downloadsDirectory)\n                        .pref(prefs.enableFileBrowserTerminalDocking)\n                        .addToggle(prefs.enableFileBrowserTerminalDocking)\n                        .hide(OsType.ofLocal() != OsType.WINDOWS)\n                        .pref(prefs.pinLocalMachineOnStartup)\n                        .addToggle(prefs.pinLocalMachineOnStartup))\n                .buildComp();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/HibernateBehaviour.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.core.AppRestart;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.ext.PrefsChoiceValue;\nimport io.xpipe.app.storage.DataStorageUserHandler;\n\nimport lombok.Getter;\n\n@Getter\npublic enum HibernateBehaviour implements PrefsChoiceValue {\n    LOCK_VAULT(\"lockVault\") {\n        @Override\n        public void runOnWake() {\n            AppRestart.restart();\n        }\n\n        @Override\n        public void runOnSleep() {\n            AppOperationMode.switchToAsync(AppOperationMode.BACKGROUND);\n        }\n\n        @Override\n        public boolean isAvailable() {\n            var handler = DataStorageUserHandler.getInstance();\n            return handler != null && handler.getActiveUser() != null;\n        }\n    },\n\n    RESTART(\"restart\") {\n        @Override\n        public void runOnWake() {\n            AppRestart.restart();\n        }\n\n        @Override\n        public void runOnSleep() {\n            AppOperationMode.switchToAsync(AppOperationMode.BACKGROUND);\n        }\n    };\n\n    private final String id;\n\n    HibernateBehaviour(String id) {\n        this.id = id;\n    }\n\n    public abstract void runOnSleep();\n\n    public abstract void runOnWake();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/IconsCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.RegionDescriptor;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.icon.SystemIconManager;\nimport io.xpipe.app.icon.SystemIconSource;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.process.OsFileSystem;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.util.*;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.collections.FXCollections;\nimport javafx.geometry.Insets;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.CheckBox;\nimport javafx.scene.control.TextField;\nimport javafx.scene.input.MouseEvent;\nimport javafx.scene.layout.Region;\n\nimport org.apache.commons.io.FilenameUtils;\n\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.util.*;\n\npublic class IconsCategory extends AppPrefsCategory {\n\n    @Override\n    protected String getId() {\n        return \"icons\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2v-view-grid-plus-outline\");\n    }\n\n    @Override\n    protected BaseRegionBuilder<?, ?> create() {\n        return new OptionsBuilder()\n                .addTitle(\"customIcons\")\n                .sub(new OptionsBuilder()\n                        .nameAndDescription(\"iconSources\")\n                        .documentationLink(DocumentationLink.ICONS)\n                        .addComp(createOverview().maxWidth(getCompWidth()))\n                        .nameAndDescription(\"preferMonochromeIcons\")\n                        .addToggle(AppPrefs.get().preferMonochromeIcons))\n                .buildComp();\n    }\n\n    private BaseRegionBuilder<?, ?> createOverview() {\n        var sources = FXCollections.<SystemIconSource>observableArrayList();\n        AppPrefs.get().getIconSources().subscribe((newValue) -> {\n            sources.setAll(SystemIconManager.getAllSources());\n        });\n        var box = new ListBoxViewComp<>(sources, sources, s -> createSourceEntry(s, sources), false);\n        box.apply(struc -> {\n            struc.minHeightProperty().bind(((Region) struc.getContent()).heightProperty());\n        });\n\n        var busy = new SimpleBooleanProperty(false);\n        var refreshButton = new TileButtonComp(\"refreshSources\", \"refreshSourcesDescription\", \"mdi2r-refresh\", e -> {\n            ThreadHelper.runFailableAsync(() -> {\n                try (var ignored = new BooleanScope(busy).start()) {\n                    SystemIconManager.rebuild();\n                }\n            });\n            e.consume();\n        });\n        refreshButton.disable(PlatformThread.sync(busy.or(Bindings.isEmpty(sources))));\n        refreshButton.maxWidth(2000);\n\n        var addGitButton =\n                new TileButtonComp(\"addGitIconSource\", \"addGitIconSourceDescription\", \"mdi2a-access-point-plus\", e -> {\n                    var remote = new SimpleStringProperty();\n                    var modal = ModalOverlay.of(\n                            \"repositoryUrl\",\n                            RegionBuilder.of(() -> {\n                                        var creationName = new TextField();\n                                        creationName.textProperty().bindBidirectional(remote);\n                                        return creationName;\n                                    })\n                                    .prefWidth(350));\n                    modal.withDefaultButtons(() -> {\n                        if (remote.get() == null || remote.get().isBlank()) {\n                            return;\n                        }\n\n                        // Don't use the git sync repo itself ...\n                        if (remote.get()\n                                .equals(AppPrefs.get().storageGitRemote().get())) {\n                            return;\n                        }\n\n                        String id = null;\n                        try {\n                            var uri = URI.create(remote.get());\n                            var path = uri.getPath();\n                            if (path != null) {\n                                var name = FilenameUtils.getBaseName(path);\n                                if (!name.isBlank()) {\n                                    // Windows has the most strict file name rules\n                                    id = OsFileSystem.of(OsType.WINDOWS).makeFileSystemCompatible(name);\n                                }\n                            }\n                        } catch (Exception ignored) {\n                        }\n\n                        if (id == null) {\n                            id = UUID.randomUUID().toString();\n                        }\n\n                        var source = SystemIconSource.GitRepository.builder()\n                                .remote(remote.get())\n                                .id(id)\n                                .build();\n                        if (sources.stream()\n                                .noneMatch(s -> s instanceof SystemIconSource.GitRepository g\n                                        && g.getRemote().equals(remote.get()))) {\n                            sources.add(source);\n                            var nl = new ArrayList<>(\n                                    AppPrefs.get().getIconSources().getValue());\n                            nl.add(source);\n                            AppPrefs.get().iconSources.setValue(nl);\n                        }\n                    });\n                    modal.show();\n                    e.consume();\n                });\n        addGitButton.maxWidth(2000);\n\n        var addDirectoryButton = new TileButtonComp(\n                \"addDirectoryIconSource\", \"addDirectoryIconSourceDescription\", \"mdi2f-folder-plus\", e -> {\n                    var dir = new SimpleObjectProperty<FilePath>();\n                    var modal = ModalOverlay.of(\n                            \"iconDirectory\",\n                            new ContextualFileReferenceChoiceComp(\n                                            new SimpleObjectProperty<>(\n                                                    DataStorage.get().local().ref()),\n                                            dir,\n                                            null,\n                                            List.of(),\n                                            en -> en.equals(DataStorage.get().local()),\n                                            true)\n                                    .prefWidth(350));\n                    modal.withDefaultButtons(() -> {\n                        if (dir.get() == null) {\n                            return;\n                        }\n\n                        var path = dir.get().asLocalPathIfPossible();\n                        if (path.isEmpty() || Files.isRegularFile(path.get())) {\n                            throw ErrorEventFactory.expected(\n                                    new IllegalArgumentException(\n                                            \"A custom icon source must be a directory containing .svg files, not a single file\"));\n                        }\n\n                        var source = SystemIconSource.Directory.builder()\n                                .path(path.get())\n                                .id(UUID.randomUUID().toString())\n                                .build();\n                        if (sources.stream()\n                                .noneMatch(s -> s instanceof SystemIconSource.Directory d\n                                        && d.getPath().equals(path.get()))) {\n                            sources.add(source);\n                            var nl = new ArrayList<>(\n                                    AppPrefs.get().getIconSources().getValue());\n                            nl.add(source);\n                            AppPrefs.get().iconSources.setValue(nl);\n                        }\n                    });\n                    modal.show();\n                    e.consume();\n                });\n        addDirectoryButton.maxWidth(2000);\n\n        var vbox = new VerticalComp(List.of(\n                RegionBuilder.vspacer(10),\n                box,\n                RegionBuilder.hseparator(),\n                refreshButton,\n                RegionBuilder.hseparator(),\n                addDirectoryButton,\n                addGitButton));\n        vbox.spacing(10);\n        return vbox;\n    }\n\n    private BaseRegionBuilder<?, ?> createSourceEntry(SystemIconSource source, List<SystemIconSource> sources) {\n        var delete = new IconButtonComp(new LabelGraphic.IconGraphic(\"mdal-delete_outline\"), () -> {\n            if (!AppDialog.confirm(\"iconSourceDeletion\")) {\n                return;\n            }\n\n            var nl = new ArrayList<>(AppPrefs.get().getIconSources().getValue());\n            nl.remove(source);\n            AppPrefs.get().iconSources.setValue(nl);\n            sources.remove(source);\n        });\n        if (!AppPrefs.get().getIconSources().getValue().contains(source)) {\n            delete.disable(new SimpleBooleanProperty(true));\n        }\n\n        var disabled = AppCache.getNonNull(\"disabledIconSources\", Set.class, () -> Set.<String>of());\n        var enabled = RegionBuilder.of(() -> {\n            var cb = new CheckBox();\n            RegionDescriptor.builder().nameKey(\"enabled\").build().apply(cb);\n            cb.setSelected(!disabled.contains(source.getId()));\n            cb.selectedProperty().addListener((observable, oldValue, newValue) -> {\n                var set = new LinkedHashSet<>(\n                        AppCache.getNonNull(\"disabledIconSources\", Set.class, () -> Set.<String>of()));\n                if (newValue) {\n                    set.remove(source.getId());\n                } else {\n                    set.add(source.getId());\n                }\n                AppCache.update(\"disabledIconSources\", set);\n            });\n            cb.setAlignment(Pos.BOTTOM_CENTER);\n            cb.setPadding(new Insets(0, 0, 1, 0));\n            AppFontSizes.sm(cb);\n            cb.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {\n                cb.setSelected(!cb.isSelected());\n                e.consume();\n            });\n            return cb;\n        });\n\n        var buttons = new HorizontalComp(List.of(enabled, delete));\n        buttons.apply(struc -> struc.setFillHeight(true));\n        buttons.spacing(15);\n\n        var tile = new TileButtonComp(\n                new SimpleStringProperty(\n                        AppPrefs.get().getIconSources().getValue().contains(source)\n                                ? source.getDisplayName()\n                                : source.getId()),\n                new SimpleStringProperty(source.getDescription()),\n                new SimpleObjectProperty<>(source.getIcon()),\n                actionEvent -> {\n                    ThreadHelper.runFailableAsync(() -> {\n                        source.open();\n                    });\n                });\n        tile.setRight(buttons);\n        tile.setIconSize(1.0);\n        tile.maxWidth(2000);\n        return tile;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/LinksCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.comp.base.TileButtonComp;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.app.util.Hyperlinks;\nimport io.xpipe.app.util.LicenseProvider;\n\npublic class LinksCategory extends AppPrefsCategory {\n\n    private BaseRegionBuilder<?, ?> createLinks() {\n        return new OptionsBuilder()\n                .addTitle(\"links\")\n                .addComp(RegionBuilder.vspacer(19))\n                .addComp(\n                        new TileButtonComp(\"activeLicense\", \"activeLicenseDescription\", \"mdi2k-key-outline\", e -> {\n                                    AppLayoutModel.get().selectLicense();\n                                    e.consume();\n                                })\n                                .maxWidth(2000),\n                        null)\n                .hide(LicenseProvider.get().hasPaidLicense())\n                .addComp(\n                        new TileButtonComp(\"discord\", \"discordDescription\", \"bi-discord\", e -> {\n                                    Hyperlinks.open(Hyperlinks.DISCORD);\n                                    e.consume();\n                                })\n                                .maxWidth(2000),\n                        null)\n                .addComp(\n                        new TileButtonComp(\"reddit\", \"redditDescription\", \"mdi2r-reddit\", e -> {\n                                    Hyperlinks.open(Hyperlinks.REDDIT);\n                                    e.consume();\n                                })\n                                .maxWidth(2000),\n                        null)\n                .addComp(\n                        new TileButtonComp(\n                                        \"documentation\", \"documentationDescription\", \"mdi2b-book-open-variant\", e -> {\n                                            Hyperlinks.open(DocumentationLink.getRoot());\n                                            e.consume();\n                                        })\n                                .maxWidth(2000),\n                        null)\n                .addComp(\n                        new TileButtonComp(\"tryPtb\", \"tryPtbDescription\", \"mdoal-insights\", e -> {\n                                    Hyperlinks.open(Hyperlinks.GITHUB_PTB);\n                                    e.consume();\n                                })\n                                .maxWidth(2000),\n                        null)\n                .addComp(\n                        new TileButtonComp(\"privacy\", \"privacyDescription\", \"mdomz-privacy_tip\", e -> {\n                                    DocumentationLink.PRIVACY.open();\n                                    e.consume();\n                                })\n                                .maxWidth(2000),\n                        null)\n                .addComp(\n                        new TileButtonComp(\"thirdParty\", \"thirdPartyDescription\", \"mdi2o-open-source-initiative\", e -> {\n                                    var comp = new ThirdPartyDependencyListComp()\n                                            .prefWidth(650)\n                                            .style(\"open-source-notices\");\n                                    var modal = ModalOverlay.of(\"openSourceNotices\", comp);\n                                    modal.show();\n                                })\n                                .maxWidth(2000))\n                .addComp(\n                        new TileButtonComp(\"eula\", \"eulaDescription\", \"mdi2c-card-text-outline\", e -> {\n                                    DocumentationLink.EULA.open();\n                                    e.consume();\n                                })\n                                .maxWidth(2000),\n                        null)\n                .addComp(RegionBuilder.vspacer(40))\n                .buildComp();\n    }\n\n    @Override\n    protected String getId() {\n        return \"links\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2l-link-box-outline\");\n    }\n\n    @Override\n    protected BaseRegionBuilder<?, ?> create() {\n        return createLinks().style(\"information\").style(\"about-tab\").apply(struc -> struc.setPrefWidth(600));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/LoggingCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.util.*;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\n\npublic class LoggingCategory extends AppPrefsCategory {\n\n    @Override\n    protected String getId() {\n        return \"logging\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2t-text-box-search-outline\");\n    }\n\n    @Override\n    protected BaseRegionBuilder<?, ?> create() {\n        var prefs = AppPrefs.get();\n        return new OptionsBuilder()\n                .addTitle(\"sessionLogging\")\n                .sub(new OptionsBuilder()\n                        .pref(prefs.enableTerminalLogging)\n                        .addToggle(prefs.enableTerminalLogging)\n                        .nameAndDescription(\"terminalLoggingDirectory\")\n                        .documentationLink(DocumentationLink.TERMINAL_LOGGING_FILES)\n                        .addComp(new ButtonComp(AppI18n.observable(\"openSessionLogs\"), () -> {\n                                    var dir = AppProperties.get().getDataDir().resolve(\"sessions\");\n                                    try {\n                                        Files.createDirectories(dir);\n                                        DesktopHelper.browseFile(dir);\n                                    } catch (IOException e) {\n                                        ErrorEventFactory.fromThrowable(e).handle();\n                                    }\n                                })\n                                .disable(prefs.enableTerminalLogging.not())))\n                .buildComp();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/McpCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport atlantafx.base.theme.Styles;\nimport io.xpipe.app.beacon.AppBeaconServer;\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.IntegratedTextAreaComp;\nimport io.xpipe.app.comp.base.TextAreaComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppNames;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.control.Tab;\nimport javafx.scene.control.TabPane;\nimport javafx.scene.control.TextArea;\n\npublic class McpCategory extends AppPrefsCategory {\n\n    @Override\n    protected String getId() {\n        return \"mcp\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2c-chat-processing-outline\");\n    }\n\n    private ObservableValue<String> createMcpConfig(String format) {\n        var prefs = AppPrefs.get();\n        return Bindings.createStringBinding(\n                () -> {\n                    return format.formatted(\n                                    AppNames.ofCurrent().getKebapName(),\n                                    AppBeaconServer.get().getPort(),\n                                    prefs.apiKey().get() != null\n                                            ? prefs.apiKey().get()\n                                            : \"?\")\n                            .strip();\n                },\n                prefs.apiKey());\n    }\n\n    @Override\n    protected BaseRegionBuilder<?, ?> create() {\n        var prefs = AppPrefs.get();\n\n        var vsCodeTemplate = createMcpConfig(\"\"\"\n             {\n               \"servers\": {\n                 \"%s\": {\n                   \"type\": \"http\",\n                   \"url\": \"http://localhost:%s/mcp\",\n                   \"headers\": {\n                     \"Authorization\": \"Bearer %s\"\n                   }\n                 }\n               }\n             }\n             \"\"\");\n\n        var cursorTemplate = createMcpConfig(\"\"\"\n               {\n                 \"mcpServers\": {\n                   \"%s\": {\n                     \"type\": \"streamable-http\",\n                     \"url\": \"http://localhost:%s/mcp\",\n                     \"headers\": {\n                       \"Authorization\": \"Bearer %s\"\n                     }\n                   }\n                 }\n               }\n               \"\"\");\n\n        var warpTemplate = createMcpConfig(\"\"\"\n               {\n                 \"%s\": {\n                   \"serverUrl\": \"http://localhost:%s/mcp\",\n                   \"headers\": {\n                     \"Authorization\": \"Bearer %s\"\n                   }\n                 }\n               }\n               \"\"\");\n\n        var claudeCodeTemplate = createMcpConfig(\"\"\"\n               $ claude mcp add %s --transport http \"http://localhost:%s/mcp\" --header \"Authorization: Bearer %s\"\n               \"\"\");\n\n        var tabComp = RegionBuilder.of(() -> {\n            var vsCode = new TextArea();\n            vsCode.setEditable(false);\n            vsCode.textProperty().bind(vsCodeTemplate);\n            vsCode.setPrefRowCount(12);\n            var vsCodeTab = new Tab();\n            vsCodeTab.textProperty().bind(AppI18n.observable(\"vscode\"));\n            vsCodeTab.setContent(vsCode);\n            vsCodeTab.setClosable(false);\n\n            var cursor = new TextArea();\n            cursor.setEditable(false);\n            cursor.textProperty().bind(cursorTemplate);\n            cursor.setPrefRowCount(12);\n            var cursorTab = new Tab();\n            cursorTab.textProperty().bind(AppI18n.observable(\"cursor\"));\n            cursorTab.setContent(cursor);\n            cursorTab.setClosable(false);\n\n            var warp = new TextArea();\n            warp.setEditable(false);\n            warp.textProperty().bind(warpTemplate);\n            warp.setPrefRowCount(12);\n            var warpTab = new Tab();\n            warpTab.textProperty().bind(AppI18n.observable(\"warp\"));\n            warpTab.setContent(warp);\n            warpTab.setClosable(false);\n\n            var claude = new TextArea();\n            claude.setEditable(false);\n            claude.textProperty().bind(claudeCodeTemplate);\n            claude.setPrefRowCount(12);\n            var claudeTab = new Tab();\n            claudeTab.textProperty().bind(AppI18n.observable(\"claudeCode\"));\n            claudeTab.setContent(claude);\n            claudeTab.setClosable(false);\n\n            var tabPane = new TabPane();\n            tabPane.getTabs().addAll(vsCodeTab, cursorTab, warpTab, claudeTab);\n            return tabPane;\n        });\n\n        return new OptionsBuilder()\n                .addTitle(\"mcpServer\")\n                .sub(new OptionsBuilder()\n                        .pref(prefs.enableMcpServer)\n                        .addToggle(prefs.enableMcpServer)\n                        .nameAndDescription(\"mcpClientConfigurationDetails\")\n                        .addComp(tabComp)\n                        .hide(prefs.enableMcpServer.not())\n                        .pref(prefs.enableMcpMutationTools)\n                        .addToggle(prefs.enableMcpMutationTools)\n                        .hide(prefs.enableMcpServer.not())\n                        .pref(prefs.mcpAdditionalContext)\n                        .addComp(new IntegratedTextAreaComp(prefs.mcpAdditionalContext, false, \"prompt\", new SimpleStringProperty(\"txt\")).applyStructure(structure -> {\n                            structure.getTextArea().promptTextProperty().bind(AppI18n.observable(\"mcpAdditionalContextSample\"));\n                        }))\n                        .hide(prefs.enableMcpServer.not())\n                )\n                .buildComp();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/PasswordManagerCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.base.ButtonComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.platform.OptionsChoiceBuilder;\nimport io.xpipe.app.pwman.PasswordManager;\nimport io.xpipe.app.util.*;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Priority;\nimport javafx.scene.layout.Region;\n\nimport org.kordamp.ikonli.javafx.FontIcon;\n\npublic class PasswordManagerCategory extends AppPrefsCategory {\n\n    @Override\n    protected String getId() {\n        return \"passwordManager\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdomz-vpn_key\");\n    }\n\n    @Override\n    protected BaseRegionBuilder<?, ?> create() {\n        var prefs = AppPrefs.get();\n        var testPasswordManagerValue = new SimpleStringProperty();\n\n        var choiceBuilder = OptionsChoiceBuilder.builder()\n                .property(prefs.passwordManager)\n                .available(PasswordManager.getClasses())\n                .allowNull(true)\n                .transformer(entryComboBox -> {\n                    var websiteLinkButton =\n                            new ButtonComp(AppI18n.observable(\"website\"), new FontIcon(\"mdi2w-web\"), () -> {\n                                var l = prefs.passwordManager.getValue().getWebsite();\n                                if (l != null) {\n                                    Hyperlinks.open(l);\n                                }\n                            });\n                    websiteLinkButton.minWidth(Region.USE_PREF_SIZE);\n                    websiteLinkButton.disable(Bindings.createBooleanBinding(\n                            () -> {\n                                return prefs.passwordManager.getValue() == null\n                                        || prefs.passwordManager.getValue().getWebsite() == null;\n                            },\n                            prefs.passwordManager));\n\n                    var hbox = new HBox(entryComboBox, websiteLinkButton.build());\n                    HBox.setHgrow(entryComboBox, Priority.ALWAYS);\n                    hbox.setSpacing(10);\n                    return hbox;\n                })\n                .build();\n        var choice = choiceBuilder.build().buildComp().maxWidth(600);\n\n        var testInput = new PasswordManagerTestComp(testPasswordManagerValue, true);\n        testInput.maxWidth(getCompWidth());\n        testInput.hgrow();\n\n        return new OptionsBuilder()\n                .addTitle(\"passwordManager\")\n                .sub(new OptionsBuilder()\n                        .pref(prefs.passwordManager)\n                        .addComp(choice)\n                        .nameAndDescription(\"passwordManagerCommandTest\")\n                        .addComp(testInput)\n                        .hide(BindingsHelper.map(prefs.passwordManager, p -> p == null)))\n                .buildComp();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/PasswordManagerTestComp.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.ButtonComp;\nimport io.xpipe.app.comp.base.HorizontalComp;\nimport io.xpipe.app.comp.base.LabelComp;\nimport io.xpipe.app.comp.base.TextFieldComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.util.GlobalTimer;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.property.StringProperty;\nimport javafx.geometry.Pos;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.layout.Region;\n\nimport atlantafx.base.theme.Styles;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicInteger;\n\npublic class PasswordManagerTestComp extends SimpleRegionBuilder {\n\n    private final StringProperty value;\n    private final boolean handleEnter;\n    private final AtomicInteger counter = new AtomicInteger(0);\n\n    public PasswordManagerTestComp(StringProperty value, boolean handleEnter) {\n        this.value = value;\n        this.handleEnter = handleEnter;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var prefs = AppPrefs.get();\n        var testPasswordManagerResult = new SimpleStringProperty();\n\n        var field = new TextFieldComp(value)\n                .apply(struc -> struc.promptTextProperty()\n                        .bind(Bindings.createStringBinding(\n                                () -> {\n                                    return prefs.passwordManager.getValue() != null\n                                            ? prefs.passwordManager.getValue().getKeyPlaceholder()\n                                            : \"?\";\n                                },\n                                prefs.passwordManager)))\n                .style(Styles.LEFT_PILL)\n                .hgrow();\n        if (handleEnter) {\n            field.apply(struc -> struc.setOnKeyPressed(event -> {\n                if (event.getCode() == KeyCode.ENTER) {\n                    testPasswordManager(value.get(), testPasswordManagerResult);\n                    event.consume();\n                }\n            }));\n        }\n\n        var button = new ButtonComp(null, new FontIcon(\"mdi2p-play\"), () -> {\n                    testPasswordManager(value.get(), testPasswordManagerResult);\n                })\n                .describe(d -> d.nameKey(\"test\"))\n                .style(Styles.RIGHT_PILL);\n\n        var testInput = new HorizontalComp(List.of(field, button));\n        testInput.apply(struc -> {\n            struc.setFillHeight(true);\n            var first = ((Region) struc.getChildren().get(0));\n            var second = ((Region) struc.getChildren().get(1));\n            second.minHeightProperty().bind(first.heightProperty());\n            second.maxHeightProperty().bind(first.heightProperty());\n            second.prefHeightProperty().bind(first.heightProperty());\n        });\n        testInput.hgrow();\n\n        var testPasswordManager = new HorizontalComp(List.of(\n                        testInput, new LabelComp(testPasswordManagerResult).apply(struc -> struc.setOpacity(0.8))))\n                .apply(struc -> struc.setAlignment(Pos.CENTER_LEFT))\n                .apply(struc -> struc.setFillHeight(true));\n        return testPasswordManager.build();\n    }\n\n    private void testPasswordManager(String key, StringProperty testPasswordManagerResult) {\n        var currentIndex = counter.incrementAndGet();\n        var prefs = AppPrefs.get();\n        ThreadHelper.runFailableAsync(() -> {\n            if (prefs.passwordManager.getValue() == null || key == null) {\n                return;\n            }\n\n            Platform.runLater(() -> {\n                testPasswordManagerResult.set(\"    \" + AppI18n.get(\"querying\"));\n            });\n\n            var r = prefs.passwordManager.getValue().retrieveCredentials(key);\n            if (r == null) {\n                Platform.runLater(() -> {\n                    testPasswordManagerResult.set(null);\n                });\n                return;\n            }\n\n            var pass = r.getPassword() != null ? r.getPassword().getSecretValue() : \"?\";\n            var format = (r.getUsername() != null ? r.getUsername() + \" [\" + pass + \"]\" : pass);\n            Platform.runLater(() -> {\n                testPasswordManagerResult.set(\"    \" + AppI18n.get(\"retrievedPassword\", format));\n            });\n            GlobalTimer.delay(\n                    () -> {\n                        Platform.runLater(() -> {\n                            if (counter.get() == currentIndex) {\n                                testPasswordManagerResult.set(null);\n                            }\n                        });\n                    },\n                    Duration.ofSeconds(5));\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/PersonalizationCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.base.ButtonComp;\nimport io.xpipe.app.comp.base.ChoiceComp;\nimport io.xpipe.app.comp.base.HorizontalComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppTheme;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.util.Hyperlinks;\n\nimport javafx.geometry.Pos;\nimport javafx.scene.control.ListCell;\nimport javafx.scene.layout.StackPane;\nimport javafx.scene.paint.Color;\nimport javafx.scene.shape.Rectangle;\n\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.function.Supplier;\n\npublic class PersonalizationCategory extends AppPrefsCategory {\n\n    public static OptionsBuilder themeChoice() {\n        var prefs = AppPrefs.get();\n        var c = ChoiceComp.ofTranslatable(prefs.theme, AppTheme.Theme.ALL, false)\n                .style(\"theme-switcher\");\n        c.apply(struc -> {\n            Supplier<ListCell<AppTheme.Theme>> cell = () -> new ListCell<>() {\n                @Override\n                protected void updateItem(AppTheme.Theme theme, boolean empty) {\n                    super.updateItem(theme, empty);\n                    if (theme == null) {\n                        setText(null);\n                        setGraphic(null);\n                        return;\n                    }\n\n                    setText(theme.toTranslatedString().getValue());\n\n                    var b = new Rectangle(8, 8);\n                    b.setArcWidth(theme.getDisplayBorderRadius());\n                    b.setArcHeight(theme.getDisplayBorderRadius());\n                    b.getStyleClass().add(\"dot\");\n                    b.setFill(theme.getBaseColor());\n\n                    var d = new Rectangle(10, 10);\n                    d.setArcWidth(theme.getDisplayBorderRadius() + 2);\n                    d.setArcHeight(theme.getDisplayBorderRadius() + 2);\n                    d.getStyleClass().add(\"dot\");\n                    d.setFill(theme.getBorderColor());\n                    d.setFill(Color.GRAY);\n\n                    var s = new StackPane(d, b);\n                    setGraphic(s);\n                    setGraphicTextGap(8);\n                }\n            };\n            struc.setButtonCell(cell.get());\n            struc.setCellFactory(themeListView -> {\n                return cell.get();\n            });\n        });\n        c.maxWidth(600.0 / 2);\n        return new OptionsBuilder().pref(prefs.theme).addComp(c, prefs.theme);\n    }\n\n    public static OptionsBuilder languageChoice() {\n        var prefs = AppPrefs.get();\n        var c = ChoiceComp.ofTranslatable(prefs.language, Arrays.asList(SupportedLocale.values()), false);\n        c.maxWidth(600.0 / 2);\n        c.hgrow();\n        var visit = new ButtonComp(AppI18n.observable(\"translate\"), new FontIcon(\"mdi2w-web\"), () -> {\n            Hyperlinks.open(Hyperlinks.TRANSLATE);\n        });\n        var h = new HorizontalComp(List.of(c, visit)).apply(struc -> {\n            struc.setAlignment(Pos.CENTER_LEFT);\n            struc.setSpacing(10);\n        });\n        return new OptionsBuilder().pref(prefs.language).addComp(h, prefs.language);\n    }\n\n    @Override\n    protected String getId() {\n        return \"personalization\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2b-brush\");\n    }\n\n    @Override\n    protected BaseRegionBuilder<?, ?> create() {\n        return new OptionsBuilder()\n                .addTitle(\"personalization\")\n                .sub(new OptionsBuilder().sub(languageChoice()).sub(themeChoice()))\n                .buildComp();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/RdpCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.base.ButtonComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.platform.OptionsChoiceBuilder;\nimport io.xpipe.app.rdp.ExternalRdpClient;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.app.util.Hyperlinks;\n\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Priority;\nimport javafx.scene.layout.Region;\n\nimport org.kordamp.ikonli.javafx.FontIcon;\n\npublic class RdpCategory extends AppPrefsCategory {\n\n    @Override\n    protected String getId() {\n        return \"rdp\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2r-remote-desktop\");\n    }\n\n    @Override\n    protected BaseRegionBuilder<?, ?> create() {\n        var prefs = AppPrefs.get();\n\n        var choiceBuilder = OptionsChoiceBuilder.builder()\n                .property(prefs.rdpClientType)\n                .available(ExternalRdpClient.getClasses())\n                .allowNull(false)\n                .transformer(entryComboBox -> {\n                    var websiteLinkButton =\n                            new ButtonComp(AppI18n.observable(\"website\"), new FontIcon(\"mdi2w-web\"), () -> {\n                                var c = prefs.rdpClientType.getValue();\n                                if (c != null && c.getWebsite() != null) {\n                                    Hyperlinks.open(c.getWebsite());\n                                }\n                            });\n                    websiteLinkButton.minWidth(Region.USE_PREF_SIZE);\n\n                    var hbox = new HBox(entryComboBox, websiteLinkButton.build());\n                    hbox.setMaxWidth(600);\n                    HBox.setHgrow(entryComboBox, Priority.ALWAYS);\n                    hbox.setSpacing(10);\n                    return hbox;\n                })\n                .build();\n\n        return new OptionsBuilder()\n                .addTitle(\"rdpConfiguration\")\n                .sub(new OptionsBuilder()\n                        .nameAndDescription(\"rdpClient\")\n                        .documentationLink(DocumentationLink.RDP)\n                        .sub(choiceBuilder.build(), prefs.rdpClientType))\n                .buildComp();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/SecurityCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\n\npublic class SecurityCategory extends AppPrefsCategory {\n\n    @Override\n    protected String getId() {\n        return \"security\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2s-security-network\");\n    }\n\n    public BaseRegionBuilder<?, ?> create() {\n        var prefs = AppPrefs.get();\n        var builder = new OptionsBuilder();\n        builder.addTitle(\"security\")\n                .sub(new OptionsBuilder()\n                        .pref(prefs.alwaysConfirmElevation)\n                        .addToggle(prefs.alwaysConfirmElevation)\n                        .pref(prefs.dontCachePasswords)\n                        .addToggle(prefs.dontCachePasswords)\n                        .pref(prefs.disableCertutilUse)\n                        .addToggle(prefs.disableCertutilUse)\n                        .pref(prefs.dontAcceptNewHostKeys)\n                        .addToggle(prefs.dontAcceptNewHostKeys)\n                        .pref(prefs.disableSshPinCaching)\n                        .addToggle(prefs.disableSshPinCaching)\n                        .pref(prefs.dontAutomaticallyStartVmSshServer)\n                        .addToggle(prefs.dontAutomaticallyStartVmSshServer)\n                        .pref(prefs.disableTerminalRemotePasswordPreparation)\n                        .addToggle(prefs.disableTerminalRemotePasswordPreparation)\n                        .pref(prefs.disableHttpsTlsCheck)\n                        .addToggle(prefs.disableHttpsTlsCheck));\n        return builder.buildComp();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/SshCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport atlantafx.base.theme.Styles;\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.base.ButtonComp;\nimport io.xpipe.app.comp.base.ContextualFileReferenceChoiceComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.terminal.TerminalLaunch;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.OsType;\n\nimport javafx.application.Platform;\nimport javafx.beans.property.ReadOnlyObjectWrapper;\nimport javafx.geometry.Insets;\nimport javafx.geometry.Pos;\nimport javafx.scene.layout.Region;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicReference;\n\npublic class SshCategory extends AppPrefsCategory {\n\n    @Override\n    protected String getId() {\n        return \"ssh\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2c-console-network-outline\");\n    }\n\n    @Override\n    protected BaseRegionBuilder<?, ?> create() {\n        var prefs = AppPrefs.get();\n        var options = new OptionsBuilder().addTitle(\"sshConfiguration\");\n        if (OsType.ofLocal() == OsType.WINDOWS) {\n            options.addComp(prefs.getCustomOptions(\"x11WslInstance\").buildComp());\n        }\n\n        AtomicReference<Region> button = new AtomicReference<>();\n        var agentTest = new ButtonComp(AppI18n.observable(\"test\"), new FontIcon(\"mdi2p-play\"), () -> {\n            ThreadHelper.runFailableAsync(() -> {\n                var agent = prefs.sshAgentSocket().getValue();\n                if (agent == null) {\n                    agent = prefs.defaultSshAgentSocket().getValue();\n                }\n\n                if (agent == null) {\n                    return;\n                }\n\n                try {\n                    ProcessControlProvider.get().checkSshAgent(LocalShell.getShell(), agent);\n                } catch (Exception e) {\n                    ErrorEventFactory.fromThrowable(e).expected().handle();\n                    return;\n                }\n\n                Platform.runLater(() -> {\n                    button.get().getStyleClass().add(Styles.SUCCESS);\n                });\n            });\n        })\n                .padding(new Insets(6, 11, 6, 5))\n                .apply(struc -> struc.setAlignment(Pos.CENTER_LEFT));\n        agentTest.apply(struc -> button.set(struc));\n\n\n        if (OsType.ofLocal() != OsType.WINDOWS) {\n            var choice = new ContextualFileReferenceChoiceComp(\n                    new ReadOnlyObjectWrapper<>(DataStorage.get().local().ref()),\n                    prefs.sshAgentSocket,\n                    null,\n                    List.of(),\n                    e -> e.equals(DataStorage.get().local()),\n                    false);\n            choice.setPrompt(prefs.defaultSshAgentSocket);\n            choice.maxWidth(600);\n            options.sub(\n                    new OptionsBuilder().nameAndDescription(\"sshAgentSocket\").addComp(choice, prefs.sshAgentSocket).addComp(agentTest));\n        }\n        return options.buildComp();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/StartupBehaviour.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.ext.PrefsChoiceValue;\nimport io.xpipe.core.XPipeDaemonMode;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Getter;\n\n@Getter\n@AllArgsConstructor\npublic enum StartupBehaviour implements PrefsChoiceValue {\n    GUI(\"app.startGui\", XPipeDaemonMode.GUI) {},\n    TRAY(\"app.startInTray\", XPipeDaemonMode.TRAY) {\n        public boolean isSelectable() {\n            return AppOperationMode.TRAY.isSupported();\n        }\n    },\n    BACKGROUND(\"app.startInBackground\", XPipeDaemonMode.BACKGROUND) {\n        public boolean isSelectable() {\n            return !AppOperationMode.TRAY.isSupported();\n        }\n    };\n\n    private final String id;\n    private final XPipeDaemonMode mode;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/SupportedLocale.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.ext.PrefsChoiceValue;\n\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Getter;\n\nimport java.util.Arrays;\nimport java.util.Locale;\n\n@AllArgsConstructor\n@Getter\npublic enum SupportedLocale implements PrefsChoiceValue {\n    ENGLISH(Locale.ENGLISH, \"en\"),\n    GERMAN(Locale.GERMAN, \"de\"),\n    DUTCH(Locale.of(\"nl\"), \"nl\"),\n    SPANISH(Locale.of(\"es\"), \"es\"),\n    FRENCH(Locale.FRENCH, \"fr\"),\n    ITALIAN(Locale.ITALIAN, \"it\"),\n    PORTUGUESE(Locale.of(\"pt\"), \"pt\"),\n    RUSSIAN(Locale.of(\"ru\"), \"ru\"),\n    JAPANESE(Locale.of(\"ja\"), \"ja\"),\n    CHINESE_SIMPLIFIED(Locale.SIMPLIFIED_CHINESE, \"zh-Hans\"),\n    CHINESE_TRADITIONAL(Locale.TRADITIONAL_CHINESE, \"zh-Hant\"),\n    DANISH(Locale.of(\"da\"), \"da\"),\n    INDONESIAN(Locale.of(\"id\"), \"id\"),\n    SWEDISH(Locale.of(\"sv\"), \"sv\"),\n    POLISH(Locale.of(\"pl\"), \"pl\"),\n    KOREAN(Locale.of(\"ko\"), \"ko\"),\n    TURKISH(Locale.of(\"tr\"), \"tr\"),\n    VIETNAMESE(Locale.of(\"vi\"), \"vi\");\n\n    private final Locale locale;\n    private final String id;\n\n    public static SupportedLocale getEnglish() {\n        return Arrays.stream(values())\n                .filter(supportedLocale -> supportedLocale.getId().equals(\"en\"))\n                .findFirst()\n                .orElseThrow();\n    }\n\n    @Override\n    public ObservableValue<String> toTranslatedString() {\n        return new SimpleStringProperty(locale.getDisplayName(locale));\n    }\n\n    @Override\n    public String getId() {\n        return id;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/SyncCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStorageSyncHandler;\nimport io.xpipe.app.terminal.TerminalLaunch;\nimport io.xpipe.app.util.*;\nimport io.xpipe.core.FilePath;\n\nimport javafx.application.Platform;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.geometry.Insets;\nimport javafx.geometry.Pos;\nimport javafx.scene.layout.Region;\n\nimport atlantafx.base.theme.Styles;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicReference;\n\npublic class SyncCategory extends AppPrefsCategory {\n\n    @Override\n    protected String getId() {\n        return \"vaultSync\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdrmz-vpn_lock\");\n    }\n\n    public BaseRegionBuilder<?, ?> create() {\n        var prefs = AppPrefs.get();\n        AtomicReference<Region> button = new AtomicReference<>();\n\n        var canRestart = new SimpleBooleanProperty(false);\n        var testButton = new ButtonComp(AppI18n.observable(\"test\"), new FontIcon(\"mdi2p-play\"), () -> {\n            ThreadHelper.runAsync(() -> {\n                var r = DataStorageSyncHandler.getInstance().validateConnection();\n                if (r) {\n                    Platform.runLater(() -> {\n                        button.get().getStyleClass().add(Styles.SUCCESS);\n                        canRestart.set(true);\n                    });\n                }\n            });\n        });\n        testButton.apply(struc -> button.set(struc));\n        testButton.padding(new Insets(6, 10, 6, 6));\n\n        var testRow = new HorizontalComp(List.of(testButton))\n                .spacing(10)\n                .padding(new Insets(10, 0, 0, 0))\n                .apply(struc -> struc.setAlignment(Pos.CENTER_LEFT));\n\n        var remoteRepo = new TextFieldComp(prefs.storageGitRemote).hgrow();\n        remoteRepo.apply(textField -> textField.setPromptText(\"https://... | ssh://... | directory path\"));\n        remoteRepo.disable(prefs.enableGitStorage.not());\n\n        var builder = new OptionsBuilder();\n        builder.addTitle(\"gitSync\")\n                .sub(new OptionsBuilder()\n                        .pref(prefs.enableGitStorage)\n                        .addToggle(prefs.enableGitStorage)\n                        .pref(prefs.storageGitRemote)\n                        .addComp(remoteRepo.maxWidth(getCompWidth()), prefs.storageGitRemote)\n                        .addComp(testRow)\n                        .disable(prefs.storageGitRemote.isNull().or(prefs.enableGitStorage.not()))\n                        .sub(prefs.getCustomOptions(\"syncToPlainDirectory\"))\n                        .sub(prefs.getCustomOptions(\"gitUsername\"))\n                        .sub(prefs.getCustomOptions(\"gitPassword\"))\n                        .sub(prefs.getCustomOptions(\"gitVaultIdentityStrategy\"))\n                        .pref(prefs.syncMode)\n                        .addComp(\n                                ChoiceComp.ofTranslatable(prefs.syncMode, Arrays.asList(SyncMode.values()), false).maxWidth(getCompWidth()),\n                                prefs.syncMode)\n                        .addComp(createManualControls())\n                        .hide(prefs.syncMode.isNotEqualTo(SyncMode.MANUAL).or(prefs.enableGitStorage.not()))\n                        .nameAndDescription(\"browseVault\")\n                        .addComp(new ButtonComp(AppI18n.observable(\"browseVaultButton\"), () -> {\n                            DesktopHelper.browseFile(DataStorage.get().getStorageDir());\n                        })));\n        return builder.buildComp();\n    }\n\n    private RegionBuilder<?> createManualControls() {\n        var busy = new SimpleBooleanProperty();\n        var busyIcon = new LoadingIconComp(busy, AppFontSizes::base);\n\n        var pullButton = new ButtonComp(\n                AppI18n.observable(\"pullChanges\"), new LabelGraphic.IconGraphic(\"mdi2d-download\"), () -> {\n                    ThreadHelper.runFailableAsync(() -> {\n                        BooleanScope.executeExclusive(busy, () -> {\n                            DataStorage.get().pullManually();\n                        });\n                    });\n                });\n\n        var pushButton =\n                new ButtonComp(AppI18n.observable(\"pushChanges\"), new LabelGraphic.IconGraphic(\"mdi2u-upload\"), () -> {\n                    ThreadHelper.runFailableAsync(() -> {\n                        BooleanScope.executeExclusive(busy, () -> {\n                            DataStorage.get().pushManually();\n                        });\n                    });\n                });\n\n        var terminalButton = new ButtonComp(\n                AppI18n.observable(\"openTerminal\"), new LabelGraphic.IconGraphic(\"mdi2c-console\"), () -> {\n                    ThreadHelper.runFailableAsync(() -> {\n                        BooleanScope.executeExclusive(busy, () -> {\n                            TerminalLaunch.builder()\n                                    .command(LocalShell.getShell())\n                                    .directory(FilePath.of(DataStorage.get().getStorageDir()))\n                                    .launch();\n                        });\n                    });\n                });\n\n        var box = new HorizontalComp(List.of(pullButton, pushButton, terminalButton, busyIcon))\n                .spacing(10)\n                .apply(struc -> struc.setAlignment(Pos.CENTER_LEFT));\n        return box;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/SyncMode.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.PrefsChoiceValue;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Getter;\n\n@Getter\n@AllArgsConstructor\npublic enum SyncMode implements PrefsChoiceValue {\n    INSTANT {\n        @Override\n        public String getId() {\n            return \"instant\";\n        }\n\n        @Override\n        public ObservableValue<String> toTranslatedString() {\n            return AppI18n.observable(\"syncModeInstant\");\n        }\n    },\n    SESSION {\n        @Override\n        public String getId() {\n            return \"session\";\n        }\n\n        @Override\n        public ObservableValue<String> toTranslatedString() {\n            return AppI18n.observable(\"syncModeSession\");\n        }\n    },\n    MANUAL {\n        @Override\n        public String getId() {\n            return \"manual\";\n        }\n\n        @Override\n        public ObservableValue<String> toTranslatedString() {\n            return AppI18n.observable(\"syncModeManual\");\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/SystemCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.base.ChoiceComp;\nimport io.xpipe.app.ext.PrefsChoiceValue;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.ext.ShellDialectChoiceComp;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\n\npublic class SystemCategory extends AppPrefsCategory {\n\n    @Override\n    protected String getId() {\n        return \"system\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2d-desktop-classic\");\n    }\n\n    public BaseRegionBuilder<?, ?> create() {\n        var prefs = AppPrefs.get();\n        var builder = new OptionsBuilder();\n        builder.addTitle(\"system\")\n                .sub(new OptionsBuilder()\n                        .pref(prefs.startupBehaviour)\n                        .addComp(ChoiceComp.ofTranslatable(\n                                        prefs.startupBehaviour,\n                                        PrefsChoiceValue.getSupported(StartupBehaviour.class),\n                                        false)\n                                .maxWidth(getCompWidth()))\n                        .pref(prefs.closeBehaviour)\n                        .addComp(ChoiceComp.ofTranslatable(\n                                        prefs.closeBehaviour,\n                                        PrefsChoiceValue.getSupported(CloseBehaviour.class),\n                                        false)\n                                .maxWidth(getCompWidth()))\n                        .pref(prefs.hibernateBehaviour)\n                        .addComp(ChoiceComp.ofTranslatable(\n                                        prefs.hibernateBehaviour,\n                                        PrefsChoiceValue.getSupported(HibernateBehaviour.class).stream()\n                                                .filter(b -> b.isAvailable())\n                                                .toList(),\n                                        true)\n                                .maxWidth(getCompWidth()))\n                        .pref(prefs.localShellDialect)\n                        .addComp(\n                                new ShellDialectChoiceComp(\n                                                ProcessControlProvider.get().getAvailableLocalDialects(),\n                                                prefs.localShellDialect,\n                                                ShellDialectChoiceComp.NullHandling.NULL_DISABLED)\n                                        .maxWidth(getCompWidth()),\n                                prefs.localShellDialect)\n                        .pref(prefs.developerMode)\n                        .addToggle(prefs.developerMode));\n        return builder.buildComp();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/TerminalCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.PrefsChoiceValue;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.hub.comp.StoreChoiceComp;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.platform.OptionsChoiceBuilder;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.terminal.*;\nimport io.xpipe.app.util.*;\nimport io.xpipe.core.OsType;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.geometry.Insets;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.ListCell;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Priority;\nimport javafx.scene.layout.Region;\n\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.Arrays;\nimport java.util.List;\n\npublic class TerminalCategory extends AppPrefsCategory {\n\n    public static OptionsBuilder terminalChoice(boolean docsLink) {\n        var prefs = AppPrefs.get();\n        var c = ChoiceComp.ofTranslatable(\n                prefs.terminalType, PrefsChoiceValue.getSupported(ExternalTerminalType.class), false);\n        c.maxWidth(1000);\n        c.apply(struc -> {\n            struc.setCellFactory(param -> {\n                return new ListCell<>() {\n\n                    {\n                        // Update recommended state on changes\n                        prefs.terminalMultiplexer().addListener((observable, oldValue, newValue) -> {\n                            updateItem(getItem(), isEmpty());\n                        });\n                    }\n\n                    @Override\n                    protected void updateItem(ExternalTerminalType item, boolean empty) {\n                        super.updateItem(item, empty);\n                        if (empty) {\n                            return;\n                        }\n\n                        setText(item.toTranslatedString().getValue());\n                        if (item != ExternalTerminalType.CUSTOM) {\n                            var graphic = new FontIcon(\n                                    item.isRecommended() ? \"mdi2c-check-decagram\" : \"mdi2a-alert-circle-check\");\n                            graphic.getStyleClass().add(\"graphic\");\n                            graphic.getStyleClass().add(item.isRecommended() ? \"supported\" : \"unsupported\");\n                            setGraphic(graphic);\n                        } else {\n                            setGraphic(new FontIcon(\"mdi2m-minus-circle\"));\n                        }\n                    }\n                };\n            });\n        });\n\n        var visit = new ButtonComp(AppI18n.observable(\"website\"), new FontIcon(\"mdi2w-web\"), () -> {\n                    var t = prefs.terminalType().getValue();\n                    if (t == null || t.getWebsite() == null) {\n                        return;\n                    }\n\n                    Hyperlinks.open(t.getWebsite());\n                })\n                .minWidth(Region.USE_PREF_SIZE);\n\n        var visitVisible = Bindings.createBooleanBinding(\n                () -> {\n                    var t = prefs.terminalType().getValue();\n                    if (t == null || t.getWebsite() == null) {\n                        return false;\n                    }\n\n                    return true;\n                },\n                prefs.terminalType());\n        visit.visible(visitVisible);\n\n        var h = new HorizontalComp(List.of(c.hgrow(), visit)).apply(struc -> {\n            struc.setAlignment(Pos.CENTER_LEFT);\n            struc.setSpacing(10);\n        });\n        h.maxWidth(600);\n\n        var terminalTest = new ButtonComp(AppI18n.observable(\"test\"), new FontIcon(\"mdi2p-play\"), () -> {\n                    ThreadHelper.runFailableAsync(() -> {\n                        var term = AppPrefs.get().terminalType().getValue();\n                        if (term != null) {\n                            // Don't use tabs to not use multiplexer stuff\n                            TerminalLaunch.builder()\n                                    .title(\"Test\")\n                                    .localScript(new ShellScript(ProcessControlProvider.get()\n                                            .getEffectiveLocalDialect()\n                                            .getEchoCommand(\n                                                    \"If you can read this, the terminal integration works\", false)))\n                                    .preferTabs(false)\n                                    .logIfEnabled(false)\n                                    .pauseOnExit(true)\n                                    .launch();\n                        }\n                    });\n                })\n                .padding(new Insets(6, 11, 6, 5))\n                .apply(struc -> struc.setAlignment(Pos.CENTER_LEFT));\n\n        var builder = new OptionsBuilder().pref(prefs.terminalType);\n        if (!docsLink) {\n            builder.documentationLink((DocumentationLink) null);\n        }\n        builder.addComp(h, prefs.terminalType);\n        builder.pref(prefs.customTerminalCommand)\n                .addComp(new TextFieldComp(prefs.customTerminalCommand, true)\n                        .apply(struc -> struc.setPromptText(\"myterminal -e $CMD\"))\n                        .hide(prefs.terminalType.isNotEqualTo(ExternalTerminalType.CUSTOM)))\n                .addComp(terminalTest);\n        return builder;\n    }\n\n    @Override\n    protected String getId() {\n        return \"terminal\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2c-console\");\n    }\n\n    @Override\n    protected BaseRegionBuilder<?, ?> create() {\n        var prefs = AppPrefs.get();\n        prefs.enableTerminalLogging.addListener((observable, oldValue, newValue) -> {\n            var feature = LicenseProvider.get().getFeature(\"logging\");\n            if (newValue && !feature.isSupported()) {\n                try {\n                    // Disable it again so people don't forget that they left it on\n                    Platform.runLater(() -> {\n                        prefs.enableTerminalLogging.set(false);\n                    });\n                    feature.throwIfUnsupported();\n                } catch (LicenseRequiredException ex) {\n                    ErrorEventFactory.fromThrowable(ex).handle();\n                }\n            }\n        });\n\n        var tabsSettingSupported = Bindings.createBooleanBinding(\n                () -> {\n                    return prefs.terminalType().getValue() != null\n                            && prefs.terminalType().getValue().getOpenFormat()\n                                    == TerminalOpenFormat.NEW_WINDOW_OR_TABBED;\n                },\n                prefs.terminalType());\n        var splitViewSupported = Bindings.isNotNull(TerminalSplitStrategy.getEffectiveSplitStrategyObservable());\n\n        return new OptionsBuilder()\n                .addTitle(\"terminalConfiguration\")\n                .sub(terminalChoice(true))\n                .sub(terminalPrompt())\n                .sub(terminalProxy())\n                .sub(terminalMultiplexer())\n                // .sub(terminalInitScript())\n                .addTitle(\"terminalBehaviour\")\n                .sub(\n                        new OptionsBuilder()\n                                .pref(prefs.enableConnectionHubTerminalDocking)\n                                .addToggle(prefs.enableConnectionHubTerminalDocking)\n                                .hide(OsType.ofLocal() != OsType.WINDOWS)\n                                .pref(prefs.enableFileBrowserTerminalDocking)\n                                .addToggle(prefs.enableFileBrowserTerminalDocking)\n                                .hide(OsType.ofLocal() != OsType.WINDOWS)\n                                .name(\"terminalSplitStrategy\")\n                                .description(Bindings.createStringBinding(\n                                        () -> {\n                                            return AppI18n.get(\n                                                    splitViewSupported.get()\n                                                            ? \"terminalSplitStrategyDescription\"\n                                                            : \"terminalSplitStrategyDisabledDescription\");\n                                        },\n                                        splitViewSupported,\n                                        AppI18n.activeLanguage()))\n                                .documentationLink(DocumentationLink.TERMINAL_SPLIT)\n                                .addComp(\n                                        ChoiceComp.ofTranslatable(\n                                                        prefs.terminalSplitStrategy,\n                                                        Arrays.asList(TerminalSplitStrategy.values()),\n                                                        false)\n                                                .maxWidth(getCompWidth()),\n                                        prefs.terminalSplitStrategy)\n                                .disable(splitViewSupported.not())\n                                .pref(prefs.terminalAlwaysPauseOnExit)\n                                .addToggle(prefs.terminalAlwaysPauseOnExit)\n                                .pref(prefs.clearTerminalOnInit)\n                                .addToggle(prefs.clearTerminalOnInit)\n                                .pref(prefs.preferTerminalTabs)\n                                .addToggle(prefs.preferTerminalTabs)\n                                .hide(tabsSettingSupported.not())\n                                .pref(prefs.enableTerminalStartupBell)\n                                .addToggle(prefs.enableTerminalStartupBell)\n                                .hide(OsType.ofLocal() == OsType.WINDOWS)\n                        //                        .pref(prefs.terminalPromptForRestart)\n                        //                        .addToggle(prefs.terminalPromptForRestart)\n                        )\n                .buildComp();\n    }\n\n    private OptionsBuilder terminalProxy() {\n        var prefs = AppPrefs.get();\n        var ref = new SimpleObjectProperty<DataStoreEntryRef<ShellStore>>(\n                prefs.terminalProxy().getValue() != null\n                        ? DataStorage.get()\n                                .getStoreEntryIfPresent(prefs.terminalProxy().getValue())\n                                .orElse(DataStorage.get().local())\n                                .ref()\n                        : DataStorage.get().local().ref());\n        ref.addListener((observable, oldValue, newValue) -> {\n            prefs.terminalProxy.setValue(\n                    newValue != null && !newValue.get().equals(DataStorage.get().local())\n                            ? newValue.get().getUuid()\n                            : null);\n        });\n        var proxyChoice = new DelayedInitComp(\n                RegionBuilder.of(() -> {\n                    var comp = new StoreChoiceComp<>(\n                            null,\n                            ref,\n                            ShellStore.class,\n                            r -> r.get().equals(DataStorage.get().local()) || TerminalProxyManager.canUseAsProxy(r),\n                            StoreViewState.get().getAllConnectionsCategory());\n                    return comp.build();\n                }),\n                () -> StoreViewState.get() != null && StoreViewState.get().isInitialized());\n        proxyChoice.maxWidth(getCompWidth());\n        return new OptionsBuilder()\n                .nameAndDescription(\"terminalEnvironment\")\n                .documentationLink(DocumentationLink.TERMINAL_ENVIRONMENT)\n                .addComp(proxyChoice, ref)\n                .hide(OsType.ofLocal() != OsType.WINDOWS);\n    }\n\n    @SuppressWarnings(\"unused\")\n    private OptionsBuilder terminalInitScript() {\n        var prefs = AppPrefs.get();\n        var ref = new SimpleObjectProperty<DataStoreEntryRef<ShellStore>>();\n        prefs.terminalProxy().subscribe(uuid -> {\n            ref.set(\n                    uuid != null\n                            ? DataStorage.get()\n                                    .getStoreEntryIfPresent(uuid)\n                                    .orElse(DataStorage.get().local())\n                                    .ref()\n                            : DataStorage.get().local().ref());\n        });\n        return new OptionsBuilder()\n                .nameAndDescription(\"terminalInitScript\")\n                .addComp(\n                        IntegratedTextAreaComp.script(ref, prefs.terminalInitScript)\n                                .maxWidth(getCompWidth()),\n                        prefs.terminalInitScript);\n    }\n\n    private OptionsBuilder terminalMultiplexer() {\n        var prefs = AppPrefs.get();\n        var choiceBuilder = OptionsChoiceBuilder.builder()\n                .property(prefs.terminalMultiplexer)\n                .allowNull(true)\n                .available(TerminalMultiplexer.getClasses())\n                .transformer(entryComboBox -> {\n                    var websiteLinkButton =\n                            new ButtonComp(AppI18n.observable(\"website\"), new FontIcon(\"mdi2w-web\"), () -> {\n                                var l = prefs.terminalMultiplexer().getValue().getDocsLink();\n                                if (l != null) {\n                                    Hyperlinks.open(l);\n                                }\n                            });\n                    websiteLinkButton.minWidth(Region.USE_PREF_SIZE);\n                    websiteLinkButton.disable(Bindings.createBooleanBinding(\n                            () -> {\n                                return prefs.terminalMultiplexer.getValue() == null\n                                        || prefs.terminalMultiplexer.getValue().getDocsLink() == null;\n                            },\n                            prefs.terminalMultiplexer));\n\n                    var hbox = new HBox(entryComboBox, websiteLinkButton.build());\n                    HBox.setHgrow(entryComboBox, Priority.ALWAYS);\n                    hbox.setSpacing(10);\n                    return hbox;\n                })\n                .build();\n        var choice = choiceBuilder.build().buildComp();\n        choice.maxWidth(getCompWidth());\n        var options = new OptionsBuilder()\n                .name(\"terminalMultiplexer\")\n                .description(\n                        OsType.ofLocal() == OsType.WINDOWS\n                                ? \"terminalMultiplexerWindowsDescription\"\n                                : \"terminalMultiplexerDescription\")\n                .documentationLink(DocumentationLink.TERMINAL_MULTIPLEXER)\n                .addComp(choice);\n        if (OsType.ofLocal() == OsType.WINDOWS) {\n            options.disable(BindingsHelper.map(prefs.terminalProxy(), uuid -> uuid == null));\n        }\n        return options;\n    }\n\n    private OptionsBuilder terminalPrompt() {\n        var prefs = AppPrefs.get();\n        var choiceBuilder = OptionsChoiceBuilder.builder()\n                .property(prefs.terminalPrompt)\n                .allowNull(true)\n                .available(TerminalPrompt.getClasses())\n                .transformer(entryComboBox -> {\n                    var websiteLinkButton =\n                            new ButtonComp(AppI18n.observable(\"website\"), new FontIcon(\"mdi2w-web\"), () -> {\n                                var l = prefs.terminalPrompt().getValue().getDocsLink();\n                                if (l != null) {\n                                    Hyperlinks.open(l);\n                                }\n                            });\n                    websiteLinkButton.minWidth(Region.USE_PREF_SIZE);\n                    websiteLinkButton.disable(Bindings.createBooleanBinding(\n                            () -> {\n                                return prefs.terminalPrompt.getValue() == null\n                                        || prefs.terminalPrompt.getValue().getDocsLink() == null;\n                            },\n                            prefs.terminalPrompt));\n\n                    var hbox = new HBox(entryComboBox, websiteLinkButton.build());\n                    HBox.setHgrow(entryComboBox, Priority.ALWAYS);\n                    hbox.setSpacing(10);\n                    return hbox;\n                })\n                .build();\n        var choice = choiceBuilder.build().buildComp();\n        choice.maxWidth(getCompWidth());\n        return new OptionsBuilder()\n                .nameAndDescription(\"terminalPrompt\")\n                .documentationLink(DocumentationLink.TERMINAL_PROMPT)\n                .addComp(choice, prefs.terminalPrompt);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/ThirdPartyDependency.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.core.AppResources;\n\nimport org.apache.commons.io.FilenameUtils;\n\nimport java.nio.file.Files;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Properties;\n\npublic record ThirdPartyDependency(String name, String version, String licenseName, String licenseText, String link) {\n\n    private static final List<ThirdPartyDependency> ALL = new ArrayList<>();\n\n    public static void init() {\n        AppResources.with(AppResources.MAIN_MODULE, \"third-party\", path -> {\n            if (!Files.exists(path)) {\n                return;\n            }\n\n            try (var list = Files.list(path)) {\n                for (var p : list.filter(p -> p.getFileName().toString().endsWith(\".properties\"))\n                        .sorted(Comparator.comparing(path1 -> path1.toString()))\n                        .toList()) {\n                    var props = new Properties();\n                    try (var in = Files.newInputStream(p)) {\n                        props.load(in);\n                    }\n\n                    var textFile = p.resolveSibling(\n                            FilenameUtils.getBaseName(p.getFileName().toString()) + \".license\");\n                    var text = Files.readString(textFile);\n                    ALL.add(new ThirdPartyDependency(\n                            props.getProperty(\"name\"),\n                            props.getProperty(\"version\"),\n                            props.getProperty(\"license\"),\n                            text,\n                            props.getProperty(\"link\")));\n                }\n            }\n        });\n    }\n\n    public static List<ThirdPartyDependency> getAll() {\n        if (ALL.size() == 0) {\n            init();\n        }\n        return ALL;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/ThirdPartyDependencyListComp.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.RegionDescriptor;\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.util.Hyperlinks;\n\nimport javafx.beans.property.ReadOnlyStringWrapper;\nimport javafx.geometry.Insets;\nimport javafx.geometry.Pos;\nimport javafx.scene.control.*;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\n\npublic class ThirdPartyDependencyListComp extends SimpleRegionBuilder {\n\n    private TitledPane createPane(ThirdPartyDependency t) {\n        var tp = new TitledPane();\n        RegionDescriptor.builder()\n                .name(new ReadOnlyStringWrapper(t.name()))\n                .build()\n                .apply(tp);\n        tp.setExpanded(false);\n        var link = new Hyperlink(t.name() + \" @ \" + t.version());\n        link.setOnAction(e -> {\n            Hyperlinks.open(t.link());\n        });\n        tp.setPadding(Insets.EMPTY);\n        tp.setGraphic(link);\n        tp.setAlignment(Pos.CENTER_LEFT);\n        AppFontSizes.xs(tp);\n\n        var licenseName = new Label(\"(\" + t.licenseName() + \")\");\n        var sp = new StackPane(link, licenseName);\n        StackPane.setAlignment(licenseName, Pos.CENTER_RIGHT);\n        StackPane.setAlignment(link, Pos.CENTER_LEFT);\n        sp.prefWidthProperty().bind(tp.widthProperty().subtract(65));\n        tp.setGraphic(sp);\n\n        var text = new TextArea();\n        text.setEditable(false);\n        text.setText(t.licenseText());\n        text.setWrapText(true);\n        text.setPrefHeight(300);\n        text.maxWidthProperty().bind(tp.widthProperty());\n        AppFontSizes.xs(text);\n        tp.setContent(text);\n        AppFontSizes.xs(tp);\n        return tp;\n    }\n\n    @Override\n    public Region createSimple() {\n        var tps = ThirdPartyDependency.getAll().stream().map(this::createPane).toArray(TitledPane[]::new);\n        var acc = new Accordion(tps);\n        acc.getStyleClass().add(\"third-party-dependency-list-comp\");\n        acc.setPrefWidth(500);\n        var sp = new ScrollPane(acc);\n        sp.setFitToWidth(true);\n        sp.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);\n        return sp;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/TroubleshootCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.comp.base.TileButtonComp;\nimport io.xpipe.app.core.*;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.UserReportComp;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.app.terminal.TerminalLaunch;\nimport io.xpipe.app.update.AppDistributionType;\nimport io.xpipe.app.util.*;\nimport io.xpipe.core.OsType;\n\nimport com.sun.management.HotSpotDiagnosticMXBean;\nimport lombok.SneakyThrows;\nimport org.apache.commons.io.FileUtils;\n\nimport java.lang.management.ManagementFactory;\nimport java.nio.file.Files;\nimport javax.management.MBeanServer;\n\npublic class TroubleshootCategory extends AppPrefsCategory {\n\n    @SneakyThrows\n    private static void heapDump() {\n        var file =\n                AppSystemInfo.ofCurrent().getDesktop().resolve(AppNames.ofMain().getSnakeName() + \".hprof\");\n        FileUtils.deleteQuietly(file.toFile());\n        MBeanServer server = ManagementFactory.getPlatformMBeanServer();\n        HotSpotDiagnosticMXBean mxBean = ManagementFactory.newPlatformMXBeanProxy(\n                server, \"com.sun.management:type=HotSpotDiagnostic\", HotSpotDiagnosticMXBean.class);\n        mxBean.dumpHeap(file.toString(), true);\n        DesktopHelper.browseFileInDirectory(file);\n    }\n\n    @Override\n    protected String getId() {\n        return \"troubleshoot\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdoal-bug_report\");\n    }\n\n    @Override\n    protected BaseRegionBuilder<?, ?> create() {\n        var prefs = AppPrefs.get();\n        OptionsBuilder b = new OptionsBuilder()\n                .addTitle(\"troubleshootingOptions\")\n                .sub(new OptionsBuilder().pref(prefs.sshVerboseOutput).addToggle(prefs.sshVerboseOutput))\n                .spacer(19)\n                .addComp(\n                        new TileButtonComp(\"reportIssue\", \"reportIssueDescription\", \"mdal-bug_report\", e -> {\n                                    var event = ErrorEventFactory.fromMessage(\"User Report\");\n                                    if (AppLogs.get().isWriteToFile()) {\n                                        event.attachment(AppLogs.get().getSessionLogsDirectory());\n                                    }\n                                    UserReportComp.show(event.build());\n                                    e.consume();\n                                })\n                                .maxWidth(2000),\n                        null)\n                .addComp(\n                        new TileButtonComp(\"launchDebugMode\", \"launchDebugModeDescription\", \"mdmz-refresh\", e -> {\n                                    AppOperationMode.executeAfterShutdown(() -> {\n                                        var script = AppInstallation.ofCurrent().getDaemonDebugScriptPath();\n                                        TerminalLaunch.builder()\n                                                .title(AppNames.ofCurrent().getName() + \" Debug\")\n                                                .pauseOnExit(true)\n                                                .localScript(sc -> new ShellScript(\n                                                        sc.getShellDialect().runScriptCommand(sc, script.toString())))\n                                                .launch();\n                                    });\n                                    e.consume();\n                                })\n                                .maxWidth(2000),\n                        null);\n\n        if (AppLogs.get().isWriteToFile()) {\n            b.addComp(\n                    new TileButtonComp(\n                                    \"openCurrentLogFile\", \"openCurrentLogFileDescription\", \"mdmz-text_snippet\", e -> {\n                                        AppLogs.get().flush();\n                                        ThreadHelper.sleep(100);\n                                        FileOpener.openInTextEditor(AppLogs.get()\n                                                .getSessionLogsDirectory()\n                                                .resolve(AppNames.ofMain().getKebapName() + \".log\")\n                                                .toString());\n                                        e.consume();\n                                    })\n                            .maxWidth(2000),\n                    null);\n        }\n\n        b.addComp(\n                        new TileButtonComp(\n                                        \"openInstallationDirectory\",\n                                        \"openInstallationDirectoryDescription\",\n                                        \"mdomz-snippet_folder\",\n                                        e -> {\n                                            DesktopHelper.browseFile(\n                                                    AppInstallation.ofCurrent().getBaseInstallationPath());\n                                            e.consume();\n                                        })\n                                .maxWidth(2000),\n                        null)\n                .addComp(\n                        new TileButtonComp(\n                                        \"clearUserData\", \"clearUserDataDescription\", \"mdi2t-trash-can-outline\", e -> {\n                                            var modal = ModalOverlay.of(\n                                                    \"clearUserDataTitle\",\n                                                    AppDialog.dialogTextKey(\"clearUserDataContent\"));\n                                            modal.withDefaultButtons(() -> {\n                                                ThreadHelper.runFailableAsync(() -> {\n                                                    var dir =\n                                                            AppProperties.get().getDataDir();\n                                                    try (var stream = Files.list(dir)) {\n                                                        var dirs = stream.toList();\n                                                        for (var path : dirs) {\n                                                            if (path.getFileName()\n                                                                            .toString()\n                                                                            .equals(\"logs\")\n                                                                    || path.getFileName()\n                                                                            .toString()\n                                                                            .equals(\"shell\")) {\n                                                                continue;\n                                                            }\n\n                                                            FileUtils.deleteQuietly(path.toFile());\n                                                        }\n                                                    }\n                                                    AppOperationMode.halt(0);\n                                                });\n                                            });\n                                            modal.show();\n                                            e.consume();\n                                        })\n                                .maxWidth(2000),\n                        null)\n                .addComp(\n                        new TileButtonComp(\"clearCaches\", \"clearCachesDescription\", \"mdi2t-trash-can-outline\", e -> {\n                                    var modal = ModalOverlay.of(\n                                            \"clearCachesAlertTitle\",\n                                            AppDialog.dialogTextKey(\"clearCachesAlertContent\"));\n                                    modal.withDefaultButtons(() -> {\n                                        AppCache.clear();\n                                    });\n                                    modal.show();\n                                    e.consume();\n                                })\n                                .maxWidth(2000),\n                        null)\n                .addComp(\n                        new TileButtonComp(\"createHeapDump\", \"createHeapDumpDescription\", \"mdi2m-memory\", e -> {\n                                    heapDump();\n                                    e.consume();\n                                })\n                                .maxWidth(2000),\n                        null);\n\n        if (OsType.ofLocal() == OsType.MACOS && AppDistributionType.get() == AppDistributionType.NATIVE_INSTALLATION) {\n            b.addComp(\n                    new TileButtonComp(\n                                    \"uninstallApplication\",\n                                    \"uninstallApplicationDescription\",\n                                    \"mdi2d-dump-truck\",\n                                    e -> {\n                                        var file = AppInstallation.ofCurrent()\n                                                .getBaseInstallationPath()\n                                                .resolve(\"Contents\")\n                                                .resolve(\"Resources\")\n                                                .resolve(\"scripts\")\n                                                .resolve(\"uninstall.sh\");\n                                        AppOperationMode.executeAfterShutdown(() -> {\n                                            TerminalLaunch.builder()\n                                                    .title(\"Uninstall\")\n                                                    .localScript(sc -> ShellScript.lines(\n                                                            \"echo \\\"+ sudo \" + file + \"\\\"\",\n                                                            \"sudo \\\"\" + file + \"\\\"\",\n                                                            ProcessControlProvider.get()\n                                                                    .getEffectiveLocalDialect()\n                                                                    .getPauseCommand()))\n                                                    .launch();\n                                        });\n                                        e.consume();\n                                    })\n                            .maxWidth(2000),\n                    null);\n        }\n\n        return b.buildComp();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/UpdateCheckComp.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.TileButtonComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.update.AppDistributionType;\nimport io.xpipe.app.update.UpdateAvailableDialog;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.scene.layout.Region;\n\npublic class UpdateCheckComp extends SimpleRegionBuilder {\n\n    private void showDialog() {\n        ThreadHelper.runFailableAsync(() -> {\n            AppDistributionType.get().getUpdateHandler().refreshUpdateCheckSilent(false, false);\n            UpdateAvailableDialog.showIfNeeded(false);\n        });\n    }\n\n    private void refresh() {\n        ThreadHelper.runFailableAsync(() -> {\n            AppDistributionType.get().getUpdateHandler().refreshUpdateCheck(false, false);\n            AppDistributionType.get().getUpdateHandler().prepareUpdate();\n        });\n    }\n\n    @Override\n    protected Region createSimple() {\n        var uh = AppDistributionType.get().getUpdateHandler();\n        var name = Bindings.createStringBinding(\n                () -> {\n                    if (uh.getBusy().getValue()) {\n                        var available = uh.getLastUpdateCheckResult().getValue();\n                        if (available != null) {\n                            return AppI18n.get(\"downloadingUpdate\", available.getVersion());\n                        }\n\n                        return AppI18n.get(\"checkingForUpdates\");\n                    }\n\n                    if (uh.getPreparedUpdate().getValue() != null) {\n                        var prefix = !uh.supportsDirectInstallation()\n                                ? AppI18n.get(\"updateReadyPortable\")\n                                : AppI18n.get(\"updateReady\");\n                        var version =\n                                \"Version \" + uh.getPreparedUpdate().getValue().getVersion();\n                        return prefix + \" (\" + version + \")\";\n                    }\n\n                    return AppI18n.get(\"checkForUpdates\");\n                },\n                AppI18n.activeLanguage(),\n                uh.getLastUpdateCheckResult(),\n                uh.getPreparedUpdate(),\n                uh.getBusy());\n        var description = Bindings.createStringBinding(\n                () -> {\n                    if (uh.getBusy().getValue()) {\n                        var available = uh.getLastUpdateCheckResult().getValue();\n                        if (available != null) {\n                            return AppI18n.get(\"downloadingUpdateDescription\");\n                        }\n\n                        return AppI18n.get(\"checkingForUpdatesDescription\");\n                    }\n\n                    if (uh.getPreparedUpdate().getValue() != null) {\n                        return AppDistributionType.get() == AppDistributionType.PORTABLE\n                                ? AppI18n.get(\"updateReadyDescriptionPortable\")\n                                : AppI18n.get(\"updateReadyDescription\");\n                    }\n\n                    return AppI18n.get(\"checkForUpdatesDescription\");\n                },\n                AppI18n.activeLanguage(),\n                uh.getLastUpdateCheckResult(),\n                uh.getPreparedUpdate(),\n                uh.getBusy());\n        var graphic = Bindings.createObjectBinding(\n                () -> {\n                    if (uh.getPreparedUpdate().getValue() != null) {\n                        return \"mdi2b-button-cursor\";\n                    }\n\n                    if (uh.getBusy().getValue() && uh.getLastUpdateCheckResult().getValue() != null) {\n                        return \"mdi2d-download\";\n                    }\n\n                    return \"mdi2r-refresh\";\n                },\n                uh.getPreparedUpdate(),\n                uh.getBusy(),\n                uh.getLastUpdateCheckResult());\n        return new TileButtonComp(name, description, graphic, actionEvent -> {\n                    actionEvent.consume();\n                    if (uh.getPreparedUpdate().getValue() != null) {\n                        showDialog();\n                        return;\n                    }\n\n                    refresh();\n                })\n                .style(\"update-button\")\n                .disable(uh.getBusy())\n                .build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/UpdatesCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\n\npublic class UpdatesCategory extends AppPrefsCategory {\n\n    @Override\n    protected String getId() {\n        return \"updates\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2d-download-box-outline\");\n    }\n\n    public BaseRegionBuilder<?, ?> create() {\n        var prefs = AppPrefs.get();\n        var builder = new OptionsBuilder();\n        builder.addTitle(\"updates\")\n                .sub(new OptionsBuilder()\n                        .pref(prefs.automaticallyCheckForUpdates)\n                        .addToggle(prefs.automaticallyCheckForUpdates)\n                        .pref(prefs.checkForSecurityUpdates)\n                        .addToggle(prefs.checkForSecurityUpdates));\n        return builder.buildComp();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/VaultAuthentication.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.ext.PrefsChoiceValue;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Getter;\n\n@Getter\n@AllArgsConstructor\npublic enum VaultAuthentication implements PrefsChoiceValue {\n    USER(\"userAuth\"),\n    GROUP(\"groupAuth\");\n\n    private final String id;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/VaultCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.ButtonComp;\nimport io.xpipe.app.comp.base.ChoiceComp;\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.storage.DataStorageSyncHandler;\nimport io.xpipe.app.storage.DataStorageUserHandler;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.app.util.LicenseProvider;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleObjectProperty;\n\nimport lombok.SneakyThrows;\n\nimport java.util.Arrays;\n\npublic class VaultCategory extends AppPrefsCategory {\n\n    @Override\n    protected String getId() {\n        return \"vault\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2d-database-lock-outline\");\n    }\n\n    @SneakyThrows\n    public BaseRegionBuilder<?, ?> create() {\n        var prefs = AppPrefs.get();\n        var builder = new OptionsBuilder();\n\n        var encryptVault = new SimpleBooleanProperty(prefs.encryptAllVaultData().get());\n        encryptVault.addListener((observable, oldValue, newValue) -> {\n            if (!newValue) {\n                var modal = ModalOverlay.of(\n                        \"confirmVaultUnencryptTitle\", AppDialog.dialogTextKey(\"confirmVaultUnencryptContent\"));\n                modal.addButton(ModalButton.cancel(() -> {\n                    Platform.runLater(() -> {\n                        encryptVault.set(true);\n                    });\n                }));\n                modal.addButton(ModalButton.ok(() -> {\n                    prefs.encryptAllVaultData.setValue(false);\n                }));\n                modal.showAndWait();\n            } else {\n                prefs.encryptAllVaultData.setValue(true);\n            }\n        });\n\n        var uh = DataStorageUserHandler.getInstance();\n        var vaultTypeKey = uh.getUserCount() == 0\n                ? \"Default\"\n                : uh.getUserCount() == 1 && uh.getVaultAuthenticationType() != VaultAuthentication.GROUP\n                        ? (uh.getActiveUser() != null && uh.getActiveUser().equals(\"legacy\") ? \"Legacy\" : \"Personal\")\n                        : \"Team\";\n\n        var authChoice =\n                ChoiceComp.ofTranslatable(prefs.vaultAuthentication, Arrays.asList(VaultAuthentication.values()), true);\n        authChoice.apply(struc -> struc.setOpacity(1.0));\n        authChoice.maxWidth(600);\n\n        var groupStrategy =\n                new SimpleObjectProperty<>(uh.getActiveUser() != null ? uh.getGroupStrategy(uh.getActiveUser()) : null);\n        groupStrategy.addListener((obs, ov, nv) -> {\n            uh.setCurrentGroupStrategy(nv);\n        });\n\n        builder.addTitle(\"vault\")\n                .sub(new OptionsBuilder()\n                        .name(\"vaultTypeName\" + vaultTypeKey)\n                        .description(\"vaultTypeContent\" + vaultTypeKey)\n                        .documentationLink(DocumentationLink.TEAM_VAULTS)\n                        .addComp(RegionBuilder.empty())\n                        .licenseRequirement(\"team\")\n                        .nameAndDescription(\"vaultAuthentication\")\n                        .addComp(authChoice, prefs.vaultAuthentication)\n                        .nameAndDescription(Bindings.createStringBinding(\n                                () -> {\n                                    var empty = uh.getUserCount() == 0;\n                                    if (prefs.vaultAuthentication.get() == VaultAuthentication.GROUP) {\n                                        return empty ? \"groupManagementEmpty\" : \"groupManagement\";\n                                    }\n\n                                    return empty ? \"userManagementEmpty\" : \"userManagement\";\n                                },\n                                prefs.vaultAuthentication))\n                        .addComp(uh.createOverview().maxWidth(getCompWidth()))\n                        .addComp(uh.createGroupStrategyOptions(groupStrategy).buildComp(), groupStrategy)\n                        .hide(prefs.vaultAuthentication.isNotEqualTo(VaultAuthentication.GROUP))\n                        .nameAndDescription(\"syncVault\")\n                        .addComp(new ButtonComp(AppI18n.observable(\"enableGitSync\"), () -> AppPrefs.get()\n                                .selectCategory(\"vaultSync\")))\n                        .hide(new SimpleBooleanProperty(\n                                DataStorageSyncHandler.getInstance().supportsSync()))\n                        .nameAndDescription(\"teamVaults\")\n                        .addComp(RegionBuilder.empty())\n                        .licenseRequirement(\"team\")\n                        .disable(!LicenseProvider.get().getFeature(\"team\").isSupported())\n                        .hide(uh.getUserCount() > 1));\n        builder.sub(new OptionsBuilder().pref(prefs.encryptAllVaultData).addToggle(encryptVault));\n        return builder.buildComp();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/WorkspaceCreationDialog.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.core.AppRestart;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.util.*;\n\nimport javafx.beans.property.SimpleObjectProperty;\n\nimport java.nio.file.Path;\n\npublic class WorkspaceCreationDialog {\n\n    public static void showAsync() {\n        LicenseProvider.get().getFeature(\"workspaces\").throwIfUnsupported();\n        ThreadHelper.runFailableAsync(() -> {\n            show();\n        });\n    }\n\n    private static void show() {\n        var base = AppProperties.get().getDataDir().toString();\n        var name = new SimpleObjectProperty<>(\"new-workspace\");\n        var path = new SimpleObjectProperty<>(base + \"-new-workspace\");\n        name.subscribe((v) -> {\n            if (v != null && path.get() != null && path.get().startsWith(base)) {\n                var newPath = path.get().substring(0, base.length()) + \"-\"\n                        + v.replaceAll(\" \", \"-\").toLowerCase();\n                path.set(newPath);\n            }\n        });\n        var content = new OptionsBuilder()\n                .nameAndDescription(\"workspaceName\")\n                .addString(name)\n                .nameAndDescription(\"workspacePath\")\n                .addString(path)\n                .buildComp()\n                .prefWidth(500)\n                .apply(struc -> AppFontSizes.xs(struc));\n        var modal = ModalOverlay.of(\"workspaceCreationAlertTitle\", content);\n        modal.addButton(ModalButton.ok(() -> {\n            ThreadHelper.runAsync(() -> {\n                if (name.get() == null || path.get() == null) {\n                    return;\n                }\n\n                try {\n                    var shortcutName = name.get();\n                    var file = DesktopShortcuts.createOpen(\n                            shortcutName,\n                            \"open -d \\\"\" + path.get() + \"\\\" --accept-eula\",\n                            \"-Dio.xpipe.app.dataDir=\\\"\" + path.get() + \"\\\" -Dio.xpipe.app.acceptEula=true\");\n                    showConfirmModal(file, Path.of(path.get()));\n                } catch (Exception e) {\n                    ErrorEventFactory.fromThrowable(e).handle();\n                }\n            });\n        }));\n        modal.show();\n    }\n\n    private static void showConfirmModal(Path shortcut, Path workspaceDir) {\n        var modal = ModalOverlay.of(\n                \"workspaceRestartTitle\", AppDialog.dialogText(AppI18n.observable(\"workspaceRestartContent\", shortcut)));\n        modal.addButton(new ModalButton(\n                \"browseShortcut\",\n                () -> {\n                    DesktopHelper.browseFileInDirectory(shortcut);\n                },\n                false,\n                false));\n        modal.addButton(new ModalButton(\n                \"restart\",\n                () -> {\n                    AppRestart.restart(workspaceDir);\n                },\n                true,\n                true));\n        modal.show();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/prefs/WorkspacesCategory.java",
    "content": "package io.xpipe.app.prefs;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.base.ButtonComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.util.LicenseProvider;\n\npublic class WorkspacesCategory extends AppPrefsCategory {\n\n    @Override\n    protected String getId() {\n        return \"workspaces\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdal-corporate_fare\");\n    }\n\n    @Override\n    protected BaseRegionBuilder<?, ?> create() {\n        return new OptionsBuilder()\n                .addTitle(\"manageWorkspaces\")\n                .sub(new OptionsBuilder()\n                        .nameAndDescription(\"workspaceAdd\")\n                        .licenseRequirement(\"workspaces\")\n                        .addComp(\n                                new ButtonComp(AppI18n.observable(\"addWorkspace\"), WorkspaceCreationDialog::showAsync)))\n                .disable(!LicenseProvider.get().getFeature(\"workspaces\").isSupported())\n                .buildComp();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/BaseElevationHandler.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.secret.SecretManager;\nimport io.xpipe.app.secret.SecretQuery;\nimport io.xpipe.app.secret.SecretRetrievalStrategy;\nimport io.xpipe.app.storage.DataStorage;\n\nimport java.util.List;\nimport java.util.UUID;\n\npublic class BaseElevationHandler implements ElevationHandler {\n\n    private final DataStore dataStore;\n    private final SecretRetrievalStrategy password;\n\n    public BaseElevationHandler(DataStore dataStore, SecretRetrievalStrategy password) {\n        this.dataStore = dataStore;\n        this.password = password;\n    }\n\n    @Override\n    public boolean handleRequest(UUID requestId, CountDown countDown, boolean confirmIfNeeded, boolean interactive) {\n        var ref = getSecretRef();\n        if (ref == null) {\n            return false;\n        }\n\n        SecretManager.expectAskpass(\n                requestId,\n                ref.getSecretId(),\n                List.of(SecretQuery.confirmElevationIfNeeded(password.query(), confirmIfNeeded)),\n                SecretQuery.prompt(false),\n                List.of(),\n                List.of(),\n                countDown,\n                interactive);\n        return true;\n    }\n\n    @Override\n    public SecretReference getSecretRef() {\n        var id = DataStorage.get()\n                .getStoreEntryIfPresent(dataStore, true)\n                .or(() -> {\n                    return DataStorage.get().getStoreEntryInProgressIfPresent(dataStore);\n                })\n                .map(e -> e.getUuid())\n                .orElse(UUID.randomUUID());\n        return password != null && password.expectsQuery() ? new SecretReference(id, 0) : null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/CommandBuilder.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.core.FailableConsumer;\nimport io.xpipe.core.FailableFunction;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport lombok.Getter;\nimport lombok.Setter;\nimport lombok.SneakyThrows;\n\nimport java.nio.file.Path;\nimport java.util.*;\n\npublic class CommandBuilder {\n\n    private final List<Element> elements = new ArrayList<>();\n\n    @Getter\n    private final Map<String, Element> environmentVariables = new LinkedHashMap<>();\n\n    private final List<FailableConsumer<ShellControl, Exception>> setup = new ArrayList<>();\n\n    @Getter\n    @Setter\n    private CountDown countDown;\n\n    @Getter\n    private UUID uuid;\n\n    private CommandBuilder() {}\n\n    public static CommandBuilder of() {\n        return new CommandBuilder();\n    }\n\n    public static CommandBuilder ofString(String s) {\n        return new CommandBuilder().add(s);\n    }\n\n    public static CommandBuilder ofFunction(FailableFunction<ShellControl, String, Exception> command) {\n        return CommandBuilder.of().add(sc -> command.apply(sc));\n    }\n\n    public CommandBuilder setup(FailableConsumer<ShellControl, Exception> consumer) {\n        setup.add(consumer);\n        return this;\n    }\n\n    public CommandBuilder fixedEnvironment(String k, String v) {\n        environmentVariables.put(k, new Fixed(v));\n        return this;\n    }\n\n    public CommandBuilder addToPath(FilePath dir, boolean append) {\n        return addToEnvironmentPath(\"PATH\", dir, append);\n    }\n\n    public CommandBuilder addToEnvironmentPath(String name, FilePath dir, boolean append) {\n        environmentVariables.put(name, sc -> {\n            var sep = sc.getOsType() == OsType.WINDOWS ? \";\" : \":\";\n            var var = sc.view().getEnvironmentVariable(name);\n            return append ? var + sep + dir : dir + sep + var;\n        });\n        return this;\n    }\n\n    public CommandBuilder environment(String k, Element v) {\n        environmentVariables.put(k, v);\n        return this;\n    }\n\n    public CommandBuilder fixedEnvironment(Map<String, String> map) {\n        map.forEach((s, s2) -> fixedEnvironment(s, s2));\n        return this;\n    }\n\n    public CommandBuilder environment(Map<String, Element> map) {\n        environmentVariables.putAll(map);\n        return this;\n    }\n\n    public CommandBuilder discardStdoutOutput() {\n        elements.add(sc -> sc.getShellDialect().getDiscardStdoutOperator());\n        return this;\n    }\n\n    public CommandBuilder discardAllOutput() {\n        elements.add(sc -> sc.getShellDialect().getDiscardAllOperator());\n        return this;\n    }\n\n    public CommandBuilder addIf(boolean b, String... s) {\n        if (b) {\n            for (String s1 : s) {\n                elements.add(new Fixed(s1));\n            }\n        }\n        return this;\n    }\n\n    public CommandBuilder add(String... s) {\n        for (String s1 : s) {\n            elements.add(new Fixed(s1));\n        }\n        return this;\n    }\n\n    public CommandBuilder addQuotedKeyValue(String key, String value) {\n        return add(sc -> {\n            var v = sc.getShellDialect().quoteArgument(value);\n            return key + \"=\" + v;\n        });\n    }\n\n    public CommandBuilder add(int index, String... s) {\n        for (String s1 : s) {\n            elements.add(index++, new Fixed(s1));\n        }\n        return this;\n    }\n\n    public CommandBuilder add(int index, Element... s) {\n        for (var s1 : s) {\n            elements.add(index++, s1);\n        }\n        return this;\n    }\n\n    public CommandBuilder addQuoted(String s) {\n        elements.add(sc -> {\n            if (s == null) {\n                return null;\n            }\n\n            if (sc == null) {\n                return \"\\\"\" + s + \"\\\"\";\n            }\n\n            return sc.getShellDialect().quoteArgument(s);\n        });\n        return this;\n    }\n\n    public CommandBuilder addQuoted(int index, String s) {\n        elements.add(index, sc -> {\n            if (s == null) {\n                return null;\n            }\n\n            if (sc == null) {\n                return \"\\\"\" + s + \"\\\"\";\n            }\n\n            return sc.getShellDialect().quoteArgument(s);\n        });\n        return this;\n    }\n\n    public CommandBuilder add(CommandBuilder sub) {\n        elements.addAll(sub.elements);\n        environmentVariables.putAll(sub.environmentVariables);\n        return this;\n    }\n\n    public CommandBuilder add(int index, CommandBuilder sub) {\n        elements.addAll(index, sub.elements);\n        environmentVariables.putAll(sub.environmentVariables);\n        return this;\n    }\n\n    public CommandBuilder add(Element e) {\n        elements.add(e);\n        return this;\n    }\n\n    public CommandBuilder addAll(List<String> s) {\n        for (String s1 : s) {\n            elements.add(new Fixed(s1));\n        }\n        return this;\n    }\n\n    public CommandBuilder addAll(FailableFunction<ShellControl, List<String>, Exception> f) {\n        elements.add(sc -> String.join(\" \", f.apply(sc)));\n        return this;\n    }\n\n    public CommandBuilder addFile(FailableFunction<ShellControl, FilePath, Exception> f) {\n        elements.add(sc -> {\n            if (f == null) {\n                return null;\n            }\n\n            if (sc == null) {\n                return \"\\\"\" + f.apply(null) + \"\\\"\";\n            }\n\n            return sc.getShellDialect().fileArgument(f.apply(sc));\n        });\n        return this;\n    }\n\n    public CommandBuilder addFile(String s) {\n        elements.add(sc -> {\n            if (s == null) {\n                return null;\n            }\n\n            if (sc == null) {\n                return \"\\\"\" + s + \"\\\"\";\n            }\n\n            return sc.getShellDialect().fileArgument(s);\n        });\n        return this;\n    }\n\n    public CommandBuilder addFile(FilePath s) {\n        return addFile(s != null ? s.toString() : null);\n    }\n\n    public CommandBuilder addFile(Path s) {\n        return addFile(s != null ? s.toString() : null);\n    }\n\n    public CommandBuilder addLiteral(String s) {\n        elements.add(sc -> {\n            if (s == null) {\n                return null;\n            }\n\n            if (sc == null) {\n                return \"\\\"\" + s + \"\\\"\";\n            }\n\n            return sc.getShellDialect().literalArgument(s);\n        });\n        return this;\n    }\n\n    public CommandBuilder addFiles(SequencedCollection<String> s) {\n        s.forEach(this::addFile);\n        return this;\n    }\n\n    public String buildBase(ShellControl sc) throws Exception {\n        return String.join(\" \", buildBaseParts(sc));\n    }\n\n    public List<String> buildBaseParts(ShellControl sc) throws Exception {\n        if (countDown == null) {\n            countDown = CountDown.of();\n        }\n        uuid = UUID.randomUUID();\n\n        for (FailableConsumer<ShellControl, Exception> s : setup) {\n            s.accept(sc);\n        }\n\n        List<String> list = new ArrayList<>();\n        for (Element element : elements) {\n            String evaluate = element.evaluate(sc);\n            if (evaluate == null) {\n                continue;\n            }\n\n            list.add(evaluate);\n        }\n        return list;\n    }\n\n    public Map<String, String> buildEnvironmentVariables(ShellControl sc) throws Exception {\n        LinkedHashMap<String, String> map = new LinkedHashMap<>();\n        for (var e : environmentVariables.entrySet()) {\n            var v = e.getValue().evaluate(sc);\n            if (v != null) {\n                map.put(e.getKey(), v);\n            }\n        }\n        return map;\n    }\n\n    public String buildFull(ShellControl sc) throws Exception {\n        if (sc == null) {\n            return buildSimple();\n        }\n\n        var s = buildBase(sc);\n        Map<String, String> map = buildEnvironmentVariables(sc);\n        return sc.getShellDialect().assembleCommand(s, map);\n    }\n\n    public CommandControl build(ShellControl sc) {\n        return sc.command(this);\n    }\n\n    @SneakyThrows\n    public String buildSimple() {\n        List<String> list = new ArrayList<>();\n        for (Element element : elements) {\n            String evaluate = element.evaluate(null);\n            if (evaluate == null) {\n                continue;\n            }\n\n            list.add(evaluate);\n        }\n        return String.join(\" \", list);\n    }\n\n    public interface Element {\n\n        String evaluate(ShellControl sc) throws Exception;\n    }\n\n    public static class Fixed implements Element {\n\n        private final String string;\n\n        public Fixed(String string) {\n            this.string = string;\n        }\n\n        @Override\n        public String evaluate(ShellControl sc) {\n            return string;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/CommandConfiguration.java",
    "content": "package io.xpipe.app.process;\n\npublic interface CommandConfiguration {\n\n    String rawCommand();\n\n    String fullCommand(ShellControl shellControl);\n\n    CommandConfiguration withRawCommand(String newCommand);\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/CommandControl.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.app.util.GlobalTimer;\nimport io.xpipe.core.FilePath;\n\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\nimport java.time.Duration;\nimport java.util.Optional;\nimport java.util.function.Function;\n\npublic interface CommandControl extends ProcessControl {\n\n    // Keep these out of a normal exit code range\n    // They have to be in range of 0 - 255 in order to work on all systems\n    int UNASSIGNED_EXIT_CODE = 160;\n    int EXIT_TIMEOUT_EXIT_CODE = 161;\n    int START_FAILED_EXIT_CODE = 162;\n    int INTERNAL_ERROR_EXIT_CODE = 163;\n    int ELEVATION_FAILED_EXIT_CODE = 164;\n\n    String getDisplayCommand();\n\n    CommandBuilder getTerminalCommand();\n\n    CommandControl sensitive();\n\n    CommandControl withExceptionConverter(ProcessExceptionConverter converter);\n\n    CommandControl start() throws Exception;\n\n    CommandControl withErrorFormatter(Function<String, String> formatter);\n\n    CommandControl notComplex();\n\n    CommandControl withWorkingDirectory(FilePath directory);\n\n    default void execute() throws Exception {\n        discardOrThrow();\n    }\n\n    default boolean executeAndCheck() throws Exception {\n        return discardAndCheckExit();\n    }\n\n    ShellControl getParent();\n\n    InputStream startExternalStdout() throws Exception;\n\n    OutputStream startExternalStdin() throws Exception;\n\n    void setExitTimeout(Duration duration);\n\n    void setStartTimeout(Duration duration);\n\n    boolean waitFor();\n\n    CommandControl withCustomCharset(Charset charset);\n\n    long getExitCode();\n\n    CommandControl elevated(ElevationFunction function);\n\n    String[] readStdoutAndStderr() throws Exception;\n\n    void discardOrThrow() throws Exception;\n\n    byte[] readRawBytesOrThrow() throws Exception;\n\n    String readStdoutOrThrow() throws Exception;\n\n    Optional<String> readStdoutIfPossible() throws Exception;\n\n    default void killOnTimeout(CountDown countDown) {\n        GlobalTimer.scheduleUntil(Duration.ofSeconds(1), false, () -> {\n            if (!isRunning(true)) {\n                return true;\n            }\n\n            if (!countDown.countDown()) {\n                killExternal();\n                return true;\n            }\n\n            return false;\n        });\n    }\n\n    default boolean discardAndCheckExit() throws ProcessOutputException {\n        try {\n            discardOrThrow();\n            return true;\n        } catch (ProcessOutputException ex) {\n            if (ex.isIrregularExit()) {\n                throw ex;\n            }\n\n            return false;\n        } catch (Exception ex) {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/CommandSupport.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.core.FailableSupplier;\n\nimport java.io.IOException;\n\npublic class CommandSupport {\n\n    public static void isInPathOrThrow(ShellControl processControl, String executable) throws Exception {\n        isInPathOrThrow(processControl, executable, null);\n    }\n\n    public static void isInPathOrThrow(ShellControl processControl, String executable, String displayName)\n            throws Exception {\n        var source = processControl.getSourceStoreId();\n        if (source.isPresent()) {\n            var entry = DataStorage.get().getStoreEntryIfPresent(source.get());\n            if (entry.isPresent()) {\n                isInPathOrThrow(processControl, executable, displayName, entry.get());\n                return;\n            }\n        }\n        isInPathOrThrow(processControl, executable, displayName, null);\n    }\n\n    public static void isInPathOrThrow(\n            ShellControl processControl, String executable, String displayName, DataStoreEntry connection)\n            throws Exception {\n        if (processControl.view().findProgram(executable).isEmpty()) {\n            var prefix = displayName != null ? displayName + \" executable \" + executable : executable + \" executable\";\n            throw ErrorEventFactory.expected(new IOException(\n                    prefix + \" not found in PATH\" + (connection != null ? \" on system \" + connection.getName() : \"\")));\n        }\n    }\n\n    public static void isSupported(FailableSupplier<Boolean> supplier, String displayName, DataStoreEntry connection)\n            throws Exception {\n        if (!supplier.get()) {\n            throw ErrorEventFactory.expected(new IOException(displayName + \" is not available\"\n                    + (connection != null ? \" on system \" + connection.getName() : \"\")));\n        }\n    }\n\n    public static boolean isInLocalPath(String executable) throws Exception {\n        try (var sc = LocalShell.getShell().start()) {\n            return sc.view().findProgram(executable).isPresent();\n        }\n    }\n\n    public static void isInLocalPathOrThrow(String displayName, String executable) throws Exception {\n        var present = isInLocalPath(executable);\n        var prefix = displayName != null\n                ? displayName + \" executable \\\"\" + executable + \"\\\"\"\n                : \"\\\"\" + executable + \"\\\" executable\";\n        if (present) {\n            return;\n        }\n        throw ErrorEventFactory.expected(\n                new IOException(\n                        prefix\n                                + \" not found in PATH. Install the executable, add it to the PATH, and refresh the environment by restarting XPipe to fix this.\"));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/CommandView.java",
    "content": "package io.xpipe.app.process;\n\nimport java.util.function.Consumer;\n\npublic abstract class CommandView implements AutoCloseable {\n\n    @SuppressWarnings(\"unused\")\n    protected abstract CommandControl build(Consumer<CommandBuilder> builder) throws Exception;\n\n    protected abstract ShellControl getShellControl();\n\n    @SuppressWarnings(\"unused\")\n    public abstract CommandView start() throws Exception;\n\n    @Override\n    public void close() throws Exception {\n        getShellControl().close();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/CommandViewBase.java",
    "content": "package io.xpipe.app.process;\n\nimport lombok.Getter;\n\nimport java.util.function.Consumer;\n\n@Getter\npublic abstract class CommandViewBase extends CommandView {\n\n    protected final ShellControl shellControl;\n\n    public CommandViewBase(ShellControl shellControl) {\n        this.shellControl = shellControl;\n    }\n\n    protected abstract CommandControl build(Consumer<CommandBuilder> builder) throws Exception;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/CountDown.java",
    "content": "package io.xpipe.app.process;\n\nimport lombok.Getter;\nimport lombok.Setter;\n\npublic class CountDown {\n\n    private volatile long lastMillis = -1;\n    private volatile long millisecondsLeft;\n\n    @Setter\n    private volatile boolean active;\n\n    @Getter\n    private volatile long maxMillis;\n\n    private CountDown() {}\n\n    public static CountDown of() {\n        return new CountDown();\n    }\n\n    public synchronized CountDown start(long millisecondsLeft) {\n        this.millisecondsLeft = millisecondsLeft;\n        this.maxMillis = millisecondsLeft;\n        lastMillis = System.currentTimeMillis();\n        active = true;\n        return this;\n    }\n\n    public void pause() {\n        lastMillis = System.currentTimeMillis();\n        setActive(false);\n    }\n\n    public void resume() {\n        lastMillis = System.currentTimeMillis();\n        setActive(true);\n    }\n\n    public synchronized boolean countDown() {\n        var ml = System.currentTimeMillis();\n        if (!active) {\n            lastMillis = ml;\n            return true;\n        }\n\n        var diff = ml - lastMillis;\n        lastMillis = ml;\n        millisecondsLeft -= diff;\n        if (millisecondsLeft < 0) {\n            return false;\n        }\n        return true;\n    }\n\n    public synchronized long getMillisecondsElapsed() {\n        return maxMillis - millisecondsLeft;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ElevationFunction.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.core.FailableFunction;\nimport io.xpipe.core.OsType;\n\npublic interface ElevationFunction {\n\n    static ElevationFunction ifNotRoot(ElevationFunction function) {\n        return new ElevationFunction() {\n            @Override\n            public String getPrefix() {\n                return function.getPrefix();\n            }\n\n            @Override\n            public boolean isSpecified() {\n                return true;\n            }\n\n            @Override\n            public boolean apply(ShellControl shellControl) throws Exception {\n                if (shellControl.getOsType() == OsType.WINDOWS) {\n                    return false;\n                }\n\n                var isRoot = shellControl.view().isRoot();\n                if (isRoot) {\n                    return false;\n                }\n\n                return function.apply(shellControl);\n            }\n        };\n    }\n\n    static ElevationFunction of(String prefix, FailableFunction<ShellControl, Boolean, Exception> f) {\n        return new ElevationFunction() {\n            @Override\n            public String getPrefix() {\n                return prefix;\n            }\n\n            @Override\n            public boolean isSpecified() {\n                return true;\n            }\n\n            @Override\n            public boolean apply(ShellControl shellControl) throws Exception {\n                return f.apply(shellControl);\n            }\n        };\n    }\n\n    static ElevationFunction elevated(String prefix) {\n        return new ElevationFunction() {\n            @Override\n            public String getPrefix() {\n                return prefix;\n            }\n\n            @Override\n            public boolean isSpecified() {\n                return true;\n            }\n\n            @Override\n            public boolean apply(ShellControl shellControl) {\n                return true;\n            }\n        };\n    }\n\n    static ElevationFunction cached(String key, ElevationFunction elevationFunction) {\n        return new ElevationFunction() {\n            @Override\n            public String getPrefix() {\n                return elevationFunction.getPrefix();\n            }\n\n            @Override\n            public boolean isSpecified() {\n                return elevationFunction.isSpecified();\n            }\n\n            @Override\n            public boolean apply(ShellControl shellControl) throws Exception {\n                var view = shellControl.view();\n                return view.getCachedPredicate(key, () -> {\n                    return elevationFunction.apply(shellControl);\n                });\n            }\n        };\n    }\n\n    static ElevationFunction none() {\n        return new ElevationFunction() {\n            @Override\n            public String getPrefix() {\n                return null;\n            }\n\n            @Override\n            public boolean isSpecified() {\n                return false;\n            }\n\n            @Override\n            public boolean apply(ShellControl shellControl) {\n                return false;\n            }\n        };\n    }\n\n    String getPrefix();\n\n    boolean isSpecified();\n\n    boolean apply(ShellControl shellControl) throws Exception;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ElevationHandler.java",
    "content": "package io.xpipe.app.process;\n\nimport java.util.UUID;\n\npublic interface ElevationHandler {\n\n    default ElevationHandler orElse(ElevationHandler other) {\n        return new ElevationHandler() {\n\n            @Override\n            public boolean handleRequest(\n                    UUID requestId, CountDown countDown, boolean confirmIfNeeded, boolean interactive) {\n                var r = ElevationHandler.this.handleRequest(requestId, countDown, confirmIfNeeded, interactive);\n                return r || other.handleRequest(requestId, countDown, confirmIfNeeded, interactive);\n            }\n\n            @Override\n            public SecretReference getSecretRef() {\n                var r = ElevationHandler.this.getSecretRef();\n                return r != null ? r : other.getSecretRef();\n            }\n        };\n    }\n\n    boolean handleRequest(UUID requestId, CountDown countDown, boolean confirmIfNeeded, boolean interactive);\n\n    SecretReference getSecretRef();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/LocalProcessInputStream.java",
    "content": "package io.xpipe.app.process;\n\nimport java.io.FilterInputStream;\nimport java.io.InputStream;\n\npublic abstract class LocalProcessInputStream extends FilterInputStream {\n\n    protected LocalProcessInputStream(InputStream in) {\n        super(in);\n    }\n\n    public abstract boolean bufferedAvailable();\n\n    public abstract boolean isClosed();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/LocalProcessOutputStream.java",
    "content": "package io.xpipe.app.process;\n\nimport java.io.FilterOutputStream;\nimport java.io.OutputStream;\n\npublic abstract class LocalProcessOutputStream extends FilterOutputStream {\n\n    protected LocalProcessOutputStream(OutputStream out) {\n        super(out);\n    }\n\n    public abstract boolean isClosed();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/LocalShell.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.issue.ErrorEventFactory;\n\nimport lombok.SneakyThrows;\n\nimport java.util.Optional;\n\npublic class LocalShell {\n\n    private static ShellControl local;\n    private static ShellControl localPowershell;\n    private static boolean powershellInitialized;\n\n    public static synchronized boolean isInitialized() {\n        return local != null;\n    }\n\n    public static synchronized ShellControl init() throws Exception {\n        if (local == null) {\n            local = ProcessControlProvider.get()\n                    .createLocalProcessControl(false)\n                    .start();\n        }\n        return local;\n    }\n\n    public static synchronized void reset(boolean force) {\n        if (local != null) {\n            if (!force) {\n                try {\n                    local.exitAndWait();\n                } catch (Exception e) {\n                    ErrorEventFactory.fromThrowable(e).omit().handle();\n                    local.kill();\n                }\n            } else {\n                local.kill();\n            }\n            local = null;\n        }\n        if (localPowershell != null) {\n            if (!force) {\n                try {\n                    localPowershell.exitAndWait();\n                } catch (Exception e) {\n                    ErrorEventFactory.fromThrowable(e).omit().handle();\n                    localPowershell.kill();\n                }\n            } else {\n                localPowershell.kill();\n            }\n            localPowershell = null;\n        }\n    }\n\n    public static synchronized Optional<ShellControl> getLocalPowershell() {\n        if (local != null && ShellDialects.isPowershell(local)) {\n            return Optional.of(local);\n        }\n\n        if (powershellInitialized) {\n            return Optional.ofNullable(localPowershell);\n        }\n\n        try {\n            powershellInitialized = true;\n            localPowershell = ProcessControlProvider.get()\n                    .createLocalProcessControl(false)\n                    .subShell(ShellDialects.POWERSHELL)\n                    .start();\n            localPowershell.getShellDialect().getDumbMode().throwIfUnsupported();\n        } catch (Exception ex) {\n            localPowershell = null;\n            ErrorEventFactory.fromThrowable(ex)\n                    .description(\"Failed to start local powershell process\")\n                    .handle();\n        }\n\n        return Optional.ofNullable(localPowershell);\n    }\n\n    @SneakyThrows\n    public static ShellControl getShell() {\n        if (local == null) {\n            throw new IllegalStateException(\"Local shell not initialized yet\");\n        }\n\n        return local.start();\n    }\n\n    public static ShellDialect getDialect() {\n        return ProcessControlProvider.get().getEffectiveLocalDialect();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/NewLine.java",
    "content": "package io.xpipe.app.process;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport lombok.Getter;\n\npublic enum NewLine {\n    @JsonProperty(\"lf\")\n    LF(\"\\n\", \"lf\"),\n    @JsonProperty(\"crlf\")\n    CRLF(\"\\r\\n\", \"crlf\");\n\n    private final String newLine;\n\n    @Getter\n    private final String id;\n\n    NewLine(String newLine, String id) {\n        this.newLine = newLine;\n        this.id = id;\n    }\n\n    public String getNewLineString() {\n        return newLine;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/OsFileSystem.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.regex.Pattern;\n\npublic interface OsFileSystem {\n\n    Windows WINDOWS = new Windows();\n    Unix UNIX = new Unix();\n    MacOs MACOS = new MacOs();\n\n    static OsFileSystem ofLocal() {\n        return switch (OsType.ofLocal()) {\n            case OsType.Windows ignored -> WINDOWS;\n            case OsType.Linux ignored -> UNIX;\n            case OsType.MacOs ignored -> MACOS;\n        };\n    }\n\n    static OsFileSystem of(OsType.Any osType) {\n        return switch (osType) {\n            case OsType.Windows ignored -> WINDOWS;\n            case OsType.Bsd ignored -> UNIX;\n            case OsType.Linux ignored -> UNIX;\n            case OsType.MacOs ignored -> MACOS;\n            case OsType.Solaris ignored -> UNIX;\n            case OsType.Aix ignored -> UNIX;\n            case OsType.OtherUnix ignored -> UNIX;\n        };\n    }\n\n    default FilePath makeFileSystemCompatible(FilePath name) {\n        var split = name.split();\n        var needsReplacement = split.stream()\n                .skip(hasMultipleRoots() && name.isAbsolute() ? 1 : 0)\n                .anyMatch(s -> !s.equals(makeFileSystemCompatible(s)));\n        if (!needsReplacement) {\n            return name;\n        }\n\n        var p = Pattern.compile(\"[^/\\\\\\\\]+\");\n        var m = p.matcher(name.toString());\n        var first = new AtomicBoolean(true);\n        var replaced = m.replaceAll(matchResult -> {\n            if (first.getAndSet(false) && hasMultipleRoots() && name.isAbsolute()) {\n                return matchResult.group();\n            }\n\n            return makeFileSystemCompatible(matchResult.group());\n        });\n        return FilePath.of(replaced);\n    }\n\n    boolean isProbableFilePath(String s);\n\n    String makeFileSystemCompatible(String name);\n\n    String getUserHomeDirectory(ShellControl pc) throws Exception;\n\n    String getFileSystemSeparator();\n\n    boolean hasMultipleRoots();\n\n    final class Windows implements OsFileSystem {\n\n        public boolean isProbableFilePath(String s) {\n            if (s.length() >= 2 && s.charAt(1) == ':') {\n                return true;\n            }\n\n            return false;\n        }\n\n        @Override\n        public String makeFileSystemCompatible(String name) {\n            var r = name.replaceAll(\"[<>:\\\"/\\\\\\\\|?*]\", \"_\").replaceAll(\"\\\\p{C}\", \"\");\n            return r.strip();\n        }\n\n        @Override\n        public String getUserHomeDirectory(ShellControl pc) throws Exception {\n            var profile = pc.view().getEnvironmentVariable(\"USERPROFILE\");\n            if (profile.isPresent()) {\n                return profile.get();\n            }\n\n            var name = pc.view().getEnvironmentVariable(\"USERNAME\");\n            if (name.isPresent()) {\n                return \"C:\\\\Users\\\\\" + name.get();\n            }\n\n            return \"C:\\\\Users\\\\User\";\n        }\n\n        @Override\n        public String getFileSystemSeparator() {\n            return \"\\\\\";\n        }\n\n        @Override\n        public boolean hasMultipleRoots() {\n            return true;\n        }\n    }\n\n    class Unix implements OsFileSystem {\n\n        public boolean isProbableFilePath(String s) {\n            return s.startsWith(\"/\");\n        }\n\n        @Override\n        public String makeFileSystemCompatible(String name) {\n            // Technically the backslash is supported, but it causes all kinds of troubles, so we also exclude it\n            return name.replaceAll(\"[/\\\\\\\\]\", \"_\").replaceAll(\"\\0\", \"\");\n        }\n\n        @Override\n        public String getUserHomeDirectory(ShellControl pc) throws Exception {\n            var r = pc.view().getEnvironmentVariable(\"HOME\");\n            if (r.isEmpty()) {\n                var user = pc.view().user();\n                var eval = pc.command(\"eval echo ~\" + user).readStdoutIfPossible();\n                if (eval.isPresent() && !eval.get().isBlank()) {\n                    return eval.get();\n                }\n\n                if (user.equals(\"root\")) {\n                    return \"/root\";\n                } else {\n                    return \"/home/\" + user;\n                }\n            } else {\n                return r.get();\n            }\n        }\n\n        @Override\n        public String getFileSystemSeparator() {\n            return \"/\";\n        }\n\n        @Override\n        public boolean hasMultipleRoots() {\n            return false;\n        }\n    }\n\n    final class MacOs implements OsFileSystem {\n\n        public boolean isProbableFilePath(String s) {\n            return s.startsWith(\"/\");\n        }\n\n        @Override\n        public String makeFileSystemCompatible(String name) {\n            // Technically the backslash is supported, but it causes all kinds of troubles, so we also exclude it\n            return name.replaceAll(\"[\\\\\\\\/:]\", \"_\").replaceAll(\"\\0\", \"\");\n        }\n\n        @Override\n        public String getUserHomeDirectory(ShellControl pc) throws Exception {\n            return pc.view().getEnvironmentVariableOrThrow(\"HOME\");\n        }\n\n        @Override\n        public String getFileSystemSeparator() {\n            return \"/\";\n        }\n\n        @Override\n        public boolean hasMultipleRoots() {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ParentSystemAccess.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.core.FilePath;\n\npublic interface ParentSystemAccess {\n\n    static ParentSystemAccess none() {\n        return new ParentSystemAccess() {\n            @Override\n            public boolean samePermissions() {\n                return false;\n            }\n\n            @Override\n            public boolean supportsSameUsers() {\n                return false;\n            }\n\n            @Override\n            public boolean supportsFileSystemAccess() {\n                return false;\n            }\n\n            @Override\n            public boolean supportsExecutables() {\n                return false;\n            }\n\n            @Override\n            public boolean supportsExecutableEnvironment() {\n                return false;\n            }\n\n            @Override\n            public FilePath translateFromLocalSystemPath(FilePath path) {\n                throw new UnsupportedOperationException();\n            }\n\n            @Override\n            public FilePath translateToLocalSystemPath(FilePath path) {\n                throw new UnsupportedOperationException();\n            }\n\n            @Override\n            public boolean isIdentity() {\n                return false;\n            }\n        };\n    }\n\n    static ParentSystemAccess identity() {\n        return new ParentSystemAccess() {\n            @Override\n            public boolean samePermissions() {\n                return true;\n            }\n\n            @Override\n            public boolean supportsSameUsers() {\n                return true;\n            }\n\n            @Override\n            public boolean supportsFileSystemAccess() {\n                return true;\n            }\n\n            @Override\n            public boolean supportsExecutables() {\n                return true;\n            }\n\n            @Override\n            public boolean supportsExecutableEnvironment() {\n                return true;\n            }\n\n            @Override\n            public FilePath translateFromLocalSystemPath(FilePath path) {\n                return path;\n            }\n\n            @Override\n            public FilePath translateToLocalSystemPath(FilePath path) {\n                return path;\n            }\n\n            @Override\n            public boolean isIdentity() {\n                return true;\n            }\n        };\n    }\n\n    static ParentSystemAccess combine(ParentSystemAccess a1, ParentSystemAccess a2) {\n        return new ParentSystemAccess() {\n            @Override\n            public boolean samePermissions() {\n                return a1.samePermissions() && a2.samePermissions();\n            }\n\n            @Override\n            public boolean supportsSameUsers() {\n                return a1.supportsSameUsers() && a2.supportsSameUsers();\n            }\n\n            @Override\n            public boolean supportsFileSystemAccess() {\n                return a1.supportsFileSystemAccess() && a2.supportsFileSystemAccess();\n            }\n\n            @Override\n            public boolean supportsExecutables() throws Exception {\n                return a1.supportsExecutables() && a2.supportsExecutables();\n            }\n\n            @Override\n            public boolean supportsExecutableEnvironment() {\n                return a1.supportsExecutableEnvironment() && a2.supportsExecutableEnvironment();\n            }\n\n            @Override\n            public FilePath translateFromLocalSystemPath(FilePath path) throws Exception {\n                return a2.translateFromLocalSystemPath(a1.translateFromLocalSystemPath(path));\n            }\n\n            @Override\n            public FilePath translateToLocalSystemPath(FilePath path) throws Exception {\n                return a1.translateToLocalSystemPath(a2.translateToLocalSystemPath(path));\n            }\n\n            @Override\n            public boolean isIdentity() {\n                return a1.isIdentity() && a2.isIdentity();\n            }\n        };\n    }\n\n    static ParentSystemAccess userChange() {\n        return new ParentSystemAccess() {\n            @Override\n            public boolean samePermissions() {\n                return false;\n            }\n\n            @Override\n            public boolean supportsSameUsers() {\n                return true;\n            }\n\n            @Override\n            public boolean supportsFileSystemAccess() {\n                return true;\n            }\n\n            @Override\n            public boolean supportsExecutables() {\n                return true;\n            }\n\n            @Override\n            public boolean supportsExecutableEnvironment() {\n                return true;\n            }\n\n            @Override\n            public FilePath translateFromLocalSystemPath(FilePath path) {\n                return path;\n            }\n\n            @Override\n            public FilePath translateToLocalSystemPath(FilePath path) {\n                return path;\n            }\n\n            @Override\n            public boolean isIdentity() {\n                return false;\n            }\n        };\n    }\n\n    default boolean supportsAnyAccess() {\n        return supportsFileSystemAccess();\n    }\n\n    boolean samePermissions();\n\n    boolean supportsSameUsers();\n\n    boolean supportsFileSystemAccess();\n\n    boolean supportsExecutables() throws Exception;\n\n    boolean supportsExecutableEnvironment();\n\n    FilePath translateFromLocalSystemPath(FilePath path) throws Exception;\n\n    FilePath translateToLocalSystemPath(FilePath path) throws Exception;\n\n    boolean isIdentity();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ProcessControl.java",
    "content": "package io.xpipe.app.process;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\nimport java.util.UUID;\n\npublic interface ProcessControl extends AutoCloseable {\n\n    UUID getUuid();\n\n    String prepareTerminalOpen(TerminalInitScriptConfig config, WorkingDirectoryFunction workingDirectory)\n            throws Exception;\n\n    void refreshRunningState();\n\n    void closeStdin() throws IOException;\n\n    boolean isAnyStreamClosed();\n\n    boolean isRunning(boolean refresh);\n\n    ShellDialect getShellDialect();\n\n    @Override\n    void close() throws Exception;\n\n    void shutdown() throws Exception;\n\n    void kill();\n\n    void killExternal();\n\n    InputStream getStdout();\n\n    OutputStream getStdin();\n\n    InputStream getStderr();\n\n    Charset getCharset();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ProcessExceptionConverter.java",
    "content": "package io.xpipe.app.process;\n\n@FunctionalInterface\npublic interface ProcessExceptionConverter {\n\n    Throwable convert(Throwable t);\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ProcessOutputException.java",
    "content": "package io.xpipe.app.process;\n\nimport lombok.Getter;\n\nimport java.util.Arrays;\nimport java.util.stream.Collectors;\n\n@Getter\npublic class ProcessOutputException extends Exception {\n\n    private final String command;\n    private final long exitCode;\n    private String output;\n    private final String prefix;\n    private final String suffix;\n\n    private ProcessOutputException(\n            String command, long exitCode, String output, String prefix, String suffix, Exception cause) {\n        super(cause);\n        this.exitCode = exitCode;\n        this.output = output;\n        this.prefix = prefix;\n        this.suffix = suffix;\n        this.command = command;\n    }\n\n    public void replaceOutput(String newOutput) {\n        this.output = newOutput;\n        if (getCause() instanceof ProcessOutputException p) {\n            p.replaceOutput(newOutput);\n        }\n    }\n\n    @Override\n    public String getMessage() {\n        if (prefix == null && suffix == null && \"\".equals(output)) {\n            return null;\n        }\n\n        var messagePrefix = prefix != null ? prefix + \"\\n\\n\" : \"\";\n        var messageSuffix = suffix != null ? \"\\n\\n\" + suffix : \"\";\n        var message = messagePrefix + output + messageSuffix;\n        return message;\n    }\n\n    public static ProcessOutputException withPrefix(String customPrefix, ProcessOutputException ex) {\n        var joined = customPrefix + (ex.prefix != null ? \"\\n\" + ex.prefix : \"\");\n        return new ProcessOutputException(ex.getCommand(), ex.getExitCode(), ex.getOutput(), joined, null, ex);\n    }\n\n    public static ProcessOutputException withSuffix(String customSuffix, ProcessOutputException ex) {\n        var joined = (ex.suffix != null ? ex.suffix + \"\\n\" : \"\") + customSuffix;\n        return new ProcessOutputException(ex.getCommand(), ex.getExitCode(), ex.getOutput(), null, joined, ex);\n    }\n\n    public static ProcessOutputException of(long exitCode, String... messages) {\n        return of(null, exitCode, messages);\n    }\n\n    private static String formatMessage(String command, long exitCode, boolean includeCommand) {\n        var start = command != null && includeCommand ? \"Command\\n\" + command + \"\\n\" : \"Process \";\n        var center = command != null && includeCommand ? \"command\\n\" + command + \"\\n\" : \"process \";\n        var message =\n                switch ((int) exitCode) {\n                    case CommandControl.START_FAILED_EXIT_CODE ->\n                        start + \"did not start up properly and had to be killed\";\n                    case CommandControl.EXIT_TIMEOUT_EXIT_CODE -> \"Wait for exit of \" + center + \"timed out\";\n                    case CommandControl.UNASSIGNED_EXIT_CODE -> start + \"exited but failed to provide an exit code.\";\n                    case CommandControl.INTERNAL_ERROR_EXIT_CODE -> start + \"execution failed\";\n                    case CommandControl.ELEVATION_FAILED_EXIT_CODE -> start + \"elevation failed\";\n                    default -> start + \"failed with exit code \" + exitCode;\n                };\n        return message;\n    }\n\n    public String getDetailedMessage() {\n        return formatMessage(command, exitCode, true);\n    }\n\n    public static ProcessOutputException of(String command, long exitCode, String... messages) {\n        var combinedError = Arrays.stream(messages)\n                .filter(s -> s != null && !s.isBlank())\n                .map(s -> s.strip())\n                .collect(Collectors.joining(\"\\n\\n\"))\n                .replaceAll(\"\\r\\n\", \"\\n\");\n        var message = formatMessage(command, exitCode, false);\n        return new ProcessOutputException(command, exitCode, combinedError, message, null, null);\n    }\n\n    public boolean isIrregularExit() {\n        return exitCode == CommandControl.EXIT_TIMEOUT_EXIT_CODE\n                || exitCode == CommandControl.START_FAILED_EXIT_CODE\n                || exitCode == CommandControl.UNASSIGNED_EXIT_CODE\n                || exitCode == CommandControl.INTERNAL_ERROR_EXIT_CODE\n                || exitCode == CommandControl.ELEVATION_FAILED_EXIT_CODE;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/PropertiesFormatsParser.java",
    "content": "package io.xpipe.app.process;\n\nimport lombok.SneakyThrows;\n\nimport java.io.BufferedReader;\nimport java.io.StringReader;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.regex.Pattern;\n\npublic class PropertiesFormatsParser {\n\n    @SneakyThrows\n    public static Map<String, String> parseLine(String line, String split, String quotes) {\n        var map = new LinkedHashMap<String, String>();\n        var pattern = Pattern.compile(\"(\\\\w+?)\\\\s*\" + split + \"\\\\s*\" + quotes + \"(.+?)\" + quotes);\n        var matcher = pattern.matcher(line);\n        while (matcher.find()) {\n            map.put(matcher.group(1), matcher.group(2));\n        }\n        return map;\n    }\n\n    @SneakyThrows\n    public static Map<String, String> parse(String text, String split) {\n        var map = new LinkedHashMap<String, String>();\n\n        var reader = new BufferedReader(new StringReader(text));\n        String line;\n\n        String currentKey = null;\n        StringBuilder currentValue = new StringBuilder();\n        while ((line = reader.readLine()) != null) {\n            if (line.startsWith(\" \") || line.startsWith(\"\\t\")) {\n                currentValue.append(line);\n                continue;\n            }\n\n            if (!line.contains(split)) {\n                continue;\n            }\n\n            var keyName = line.substring(0, line.indexOf(split)).strip();\n            var value = line.substring(line.indexOf(split) + 1).strip();\n            if (value.startsWith(\"\\\"\") && value.endsWith(\"\\\"\")) {\n                value = value.substring(1, value.length() - 1);\n            }\n\n            if (currentKey != null) {\n                map.put(currentKey, currentValue.toString());\n            }\n\n            currentKey = keyName;\n            currentValue = new StringBuilder(value);\n        }\n\n        if (currentKey != null) {\n            map.put(currentKey, currentValue.toString());\n        }\n\n        return map;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ScriptHelper.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.core.FilePath;\n\nimport lombok.SneakyThrows;\n\nimport java.util.Objects;\n\npublic class ScriptHelper {\n\n    public static int getScriptHash(ShellControl sc, String content) throws Exception {\n        return Math.abs(Objects.hash(content, sc.view().user()));\n    }\n\n    @SneakyThrows\n    public static FilePath createLocalExecScript(String content) {\n        try (var l = LocalShell.getShell().start()) {\n            return createExecScript(l, content);\n        }\n    }\n\n    @SneakyThrows\n    public static FilePath createExecScript(ShellControl processControl, String content) {\n        return createExecScript(processControl.getShellDialect(), processControl, content);\n    }\n\n    @SneakyThrows\n    public static FilePath createExecScript(ShellDialect type, ShellControl processControl, String content) {\n        content = type.prepareScriptContent(processControl, content);\n        var fileName = \"xpipe-\" + getScriptHash(processControl, content);\n        var temp = processControl.getSystemTemporaryDirectory();\n        var file = temp.join(fileName + \".\" + type.getScriptFileEnding());\n        return createExecScriptRaw(processControl, file, content);\n    }\n\n    @SneakyThrows\n    public static FilePath createExecScriptRaw(ShellControl processControl, FilePath file, String content) {\n        if (processControl.view().fileExists(file)) {\n            return file;\n        }\n\n        TrackEvent.withTrace(\"Writing exec script\")\n                .tag(\"file\", file)\n                .tag(\"content\", content)\n                .handle();\n        processControl.view().writeScriptFile(file, content);\n        return file;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/SecretReference.java",
    "content": "package io.xpipe.app.process;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Value;\n\nimport java.util.UUID;\n\n@Value\n@AllArgsConstructor\npublic class SecretReference {\n\n    UUID secretId;\n    int subId;\n\n    public static SecretReference ofUuid(UUID secretId) {\n        return new SecretReference(secretId, 0);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ShellControl.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.StatefulDataStore;\nimport io.xpipe.app.util.LicensedFeature;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.FailableConsumer;\nimport io.xpipe.core.FailableFunction;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport lombok.NonNull;\n\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.UUID;\nimport java.util.concurrent.locks.ReentrantLock;\nimport java.util.function.Consumer;\n\npublic interface ShellControl extends ProcessControl {\n\n    void setExitTimeout(int timeout);\n\n    void setUser(String user);\n\n    boolean isExiting();\n\n    boolean isInitializing();\n\n    void setDumbOpen(ShellOpenFunction openFunction);\n\n    void setTerminalOpen(ShellOpenFunction openFunction);\n\n    void writeLine(String line) throws IOException;\n\n    void writeLine(String line, boolean log) throws IOException;\n\n    boolean isSubShellActive();\n\n    void setSubShellActive(boolean active);\n\n    default void waitForSubShellExit() {\n        while (isSubShellActive()) {\n            ThreadHelper.sleep(10);\n        }\n    }\n\n    ShellView view();\n\n    Optional<ShellControl> getParentControl();\n\n    ShellTtyState getTtyState();\n\n    void setNonInteractive();\n\n    boolean isInteractive();\n\n    ElevationHandler getElevationHandler();\n\n    void setElevationHandler(ElevationHandler ref);\n\n    void closeStdout() throws IOException;\n\n    List<UUID> getExitUuids();\n\n    void setWorkingDirectory(WorkingDirectoryFunction workingDirectory);\n\n    Optional<DataStore> getSourceStore();\n\n    Optional<UUID> getSourceStoreId();\n\n    ShellControl withSourceStore(DataStore store);\n\n    ParentSystemAccess getParentSystemAccess();\n\n    void setParentSystemAccess(ParentSystemAccess access);\n\n    ParentSystemAccess getLocalSystemAccess();\n\n    boolean isLocal();\n\n    default boolean canHaveSubshells() {\n        return true;\n    }\n\n    String getOsName();\n\n    ReentrantLock getLock();\n\n    void requireLicensedFeature(LicensedFeature f);\n\n    ShellDialect getOriginalShellDialect();\n\n    void setOriginalShellDialect(ShellDialect dialect);\n\n    ShellControl onInit(FailableConsumer<ShellControl, Exception> pc);\n\n    default <T extends ShellStoreState> ShellControl withShellStateInit(StatefulDataStore<T> store) {\n        return onInit(shellControl -> {\n            var or = shellControl.getOriginalShellDialect();\n            var oldState = store.getState();\n            var s = oldState.toBuilder()\n                    .osType(shellControl.getOsType())\n                    .shellDialect(or.isMarkerDialect() ? oldState.getShellDialect() : or)\n                    .ttyState(shellControl.getTtyState())\n                    .running(true)\n                    .osName(shellControl.getOsName())\n                    .build();\n            store.setState(s.asNeeded());\n        });\n    }\n\n    default <T extends ShellStoreState> ShellControl withShellStateFail(StatefulDataStore<T> store) {\n        return onStartupFail(t -> {\n            // Ugly\n            if (t.getClass().getSimpleName().equals(\"LicenseRequiredException\")) {\n                return;\n            }\n\n            var s = store.getState().toBuilder().running(false).build();\n            store.setState(s.asNeeded());\n        });\n    }\n\n    ShellControl onExit(Consumer<ShellControl> pc);\n\n    ShellControl onKill(Runnable pc);\n\n    ShellControl onStartupFail(Consumer<Throwable> t);\n\n    ShellControl withExceptionConverter(ProcessExceptionConverter converter);\n\n    ShellControl start() throws Exception;\n\n    @Override\n    LocalProcessInputStream getStdout();\n\n    @Override\n    LocalProcessOutputStream getStdin();\n\n    @Override\n    LocalProcessInputStream getStderr();\n\n    void checkLicenseOrThrow();\n\n    String prepareIntermediateTerminalOpen(\n            TerminalInitFunction content, TerminalInitScriptConfig config, WorkingDirectoryFunction workingDirectory)\n            throws Exception;\n\n    FilePath getSystemTemporaryDirectory();\n\n    default CommandControl osascriptCommand(String script) {\n        return command(String.format(\"\"\"\n                                     osascript - \"$@\" <<EOF\n                                     %s\n                                     EOF\n                                     \"\"\", script));\n    }\n\n    default String executeSimpleStringCommand(String command) throws Exception {\n        return command(command).readStdoutOrThrow();\n    }\n\n    default boolean executeSimpleBooleanCommand(String command) throws Exception {\n        return command(command).discardAndCheckExit();\n    }\n\n    default void executeSimpleCommand(CommandBuilder command) throws Exception {\n        command(command).discardOrThrow();\n    }\n\n    default void executeSimpleCommand(String command) throws Exception {\n        command(command).discardOrThrow();\n    }\n\n    ShellControl withSecurityPolicy(ShellSecurityPolicy policy);\n\n    ShellSecurityPolicy getEffectiveSecurityPolicy();\n\n    String buildElevatedCommand(\n            CommandConfiguration input, String prefix, UUID requestId, CountDown countDown, String user)\n            throws Exception;\n\n    void restart() throws Exception;\n\n    OsType.Any getOsType();\n\n    ShellControl elevated(ElevationFunction elevationFunction);\n\n    ShellControl withInitSnippet(ShellTerminalInitCommand snippet, boolean append);\n\n    Optional<ShellControl> getActiveReplacementBackgroundSession() throws Exception;\n\n    default ShellControl subShell(@NonNull ShellDialect type) {\n        var o = new ShellOpenFunction() {\n\n            @Override\n            public CommandBuilder prepareWithoutInitCommand() {\n                return CommandBuilder.of().addAll(sc -> type.getLaunchCommand().loginCommand(sc.getOsType()));\n            }\n\n            @Override\n            public CommandBuilder prepareWithInitCommand(@NonNull String command) {\n                return CommandBuilder.ofString(command);\n            }\n        };\n        var s = subShell();\n        s.setDumbOpen(o);\n        s.setTerminalOpen(o);\n        s.setParentSystemAccess(ParentSystemAccess.identity());\n        return s;\n    }\n\n    default ShellControl identicalDialectSubShell() {\n        var o = new ShellOpenFunction() {\n\n            @Override\n            public CommandBuilder prepareWithoutInitCommand() {\n                return CommandBuilder.of()\n                        .addAll(sc -> sc.getShellDialect().getLaunchCommand().loginCommand(sc.getOsType()));\n            }\n\n            @Override\n            public CommandBuilder prepareWithInitCommand(@NonNull String command) {\n                return CommandBuilder.ofString(command);\n            }\n        };\n        var sc = subShell();\n        sc.setDumbOpen(o);\n        sc.setTerminalOpen(o);\n        sc.withSourceStore(getSourceStore().orElse(null));\n        sc.setParentSystemAccess(ParentSystemAccess.identity());\n        return sc;\n    }\n\n    default ShellControl elevateIfNeeded(ElevationFunction function) throws Exception {\n        if (function.apply(this)) {\n            return identicalDialectSubShell().elevated(ElevationFunction.elevated(function.getPrefix()));\n        } else {\n            return new StubShellControl(this);\n        }\n    }\n\n    default <T> T enforceDialect(@NonNull ShellDialect type, FailableFunction<ShellControl, T, Exception> sc)\n            throws Exception {\n        if (type.equals(getShellDialect())) {\n            return sc.apply(this);\n        } else {\n            try (var sub = subShell(type).start()) {\n                return sc.apply(sub);\n            }\n        }\n    }\n\n    ShellControl subShell();\n\n    default CommandControl command(String command) {\n        return command(CommandBuilder.ofFunction(shellProcessControl -> command));\n    }\n\n    default CommandControl command(ShellScript command) {\n        return command(CommandBuilder.of().add(command.getValue()));\n    }\n\n    default CommandControl command(Consumer<CommandBuilder> builder) {\n        var b = CommandBuilder.of();\n        builder.accept(b);\n        return command(b);\n    }\n\n    CommandControl command(CommandBuilder builder);\n\n    void exitAndWait() throws IOException;\n\n    SudoCache getSudoCache();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ShellDialect.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.app.ext.FileEntry;\nimport io.xpipe.app.ext.FileSystem;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.StreamCharset;\n\nimport java.nio.charset.Charset;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.UUID;\nimport java.util.stream.Stream;\n\npublic interface ShellDialect {\n\n    String setEnvironmentVariableFromScriptCommand(String name, FilePath script);\n\n    default boolean isMarkerDialect() {\n        return false;\n    }\n\n    String unsetEnvironmentVariableCommand(String var);\n\n    CommandBuilder launchAsync(CommandBuilder cmd, boolean window);\n\n    default String getLicenseFeatureId() {\n        return null;\n    }\n\n    String terminalLauncherScript(UUID request, String name, boolean alwaysPromptRestart);\n\n    String getExecutableName();\n\n    default boolean isCompatibleTo(ShellDialect other) {\n        return this.equals(other);\n    }\n\n    default boolean isSourceCompatibleTo(ShellDialect other) {\n        return this.equals(other);\n    }\n\n    String getCatchAllVariable();\n\n    String queryVersion(ShellControl shellControl) throws Exception;\n\n    CommandControl queryFileSize(ShellControl shellControl, String file) throws Exception;\n\n    long queryDirectorySize(ShellControl shellControl, String file) throws Exception;\n\n    CommandControl prepareUserTempDirectory(ShellControl shellControl, String directory);\n\n    FilePath getInitFileName(ShellControl sc, int hash) throws Exception;\n\n    CommandControl directoryExists(ShellControl shellControl, String directory);\n\n    CommandControl evaluateExpression(ShellControl shellControl, String s);\n\n    CommandControl resolveDirectory(ShellControl shellControl, String directory);\n\n    String literalArgument(String s);\n\n    String prepareEnvironmentForCustomTerminalScripts();\n\n    String fileArgument(String s);\n\n    default String fileArgument(FilePath s) {\n        return fileArgument(s.toString());\n    }\n\n    String quoteArgument(String s);\n\n    String prepareTerminalEnvironmentCommands();\n\n    String addToPathVariableCommand(List<String> entries, boolean append);\n\n    default String applyInitFileCommand(ShellControl sc) throws Exception {\n        return null;\n    }\n\n    String changeTitleCommand(String newTitle);\n\n    CommandControl createStreamFileWriteCommand(ShellControl shellControl, String file, long totalBytes)\n            throws Exception;\n\n    default String getCdCommand(String directory) {\n        return \"cd \\\"\" + directory + \"\\\"\";\n    }\n\n    String getScriptFileEnding();\n\n    String assembleCommand(String command, Map<String, String> variables);\n\n    Stream<FileEntry> listFiles(FileSystem fs, ShellControl control, String path, boolean sub) throws Exception;\n\n    Stream<String> listRoots(ShellControl control) throws Exception;\n\n    String getPauseCommand();\n\n    String prepareScriptContent(ShellControl sc, String content);\n\n    default String getPassthroughExitCommand() {\n        return \"exit\";\n    }\n\n    default String getNormalExitCommand() {\n        return \"exit 0\";\n    }\n\n    String environmentVariable(String name);\n\n    default String getConcatenationOperator() {\n        return \";\";\n    }\n\n    String getDiscardStdoutOperator();\n\n    String getDiscardAllOperator();\n\n    String nullStdin(String command);\n\n    ShellDialectAskpass getAskpass();\n\n    String getSetEnvironmentVariableCommand(String variable, String value);\n\n    String getEchoCommand(String s, boolean toErrorStream);\n\n    String getPrintVariableCommand(String name);\n\n    CommandControl printUsernameCommand(ShellControl shellControl);\n\n    String getPrintStartEchoCommand(String prefix);\n\n    Optional<String> executeRobustBootstrapOutputCommand(ShellControl shellControl, String original) throws Exception;\n\n    String getPrintExitCodeCommand(String id, String prefix, String suffix);\n\n    int assignMissingExitCode();\n\n    default String getPrintEnvironmentVariableCommand(String name) {\n        return getPrintVariableCommand(name);\n    }\n\n    CommandBuilder getOpenScriptCommand(String file);\n\n    String terminalInitCommand(ShellControl shellControl, String file, boolean exit);\n\n    String runScriptCommand(ShellControl parent, String file);\n\n    String sourceScriptCommand(ShellControl sc, String file);\n\n    String executeCommandWithShell(String cmd);\n\n    String getMkdirsCommand(String dirs);\n\n    CommandControl getFileReadCommand(ShellControl parent, String file);\n\n    String getPrintWorkingDirectoryCommand();\n\n    StreamCharset getTextCharset();\n\n    default StreamCharset getScriptCharset() {\n        return getTextCharset();\n    }\n\n    CommandControl getFileCopyCommand(ShellControl parent, String oldFile, String newFile);\n\n    CommandControl getFileMoveCommand(ShellControl parent, String oldFile, String newFile);\n\n    default boolean requiresScript(String content) {\n        return content.contains(\"\\n\");\n    }\n\n    CommandControl createTextFileWriteCommand(ShellControl parent, String content, String file);\n\n    CommandControl createScriptTextFileWriteCommand(ShellControl parent, String content, String file);\n\n    CommandControl deleteFileOrDirectory(ShellControl sc, String file);\n\n    String clearDisplayCommand();\n\n    ShellLaunchCommand getLaunchCommand();\n\n    ShellDumbMode getDumbMode();\n\n    CommandControl createFileExistsCommand(ShellControl sc, String file) throws Exception;\n\n    CommandControl symbolicLink(ShellControl sc, String linkFile, String targetFile);\n\n    CommandControl getFileDeleteCommand(ShellControl parent, String file);\n\n    CommandControl getFileTouchCommand(ShellControl parent, String file);\n\n    String whichCommand(ShellControl sc, String executable) throws Exception;\n\n    Charset determineCharset(ShellControl control) throws Exception;\n\n    NewLine getNewLine();\n\n    String getId();\n\n    String getDisplayName();\n\n    boolean doesEchoInputByDefault();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ShellDialectAskpass.java",
    "content": "package io.xpipe.app.process;\n\nimport java.util.List;\nimport java.util.UUID;\n\npublic interface ShellDialectAskpass {\n\n    String prepareStderrPassthroughContent(UUID requestId, String prefix);\n\n    String prepareFixedContent(ShellControl sc, String fileName, List<String> s) throws Exception;\n\n    String elevateDumbCommand(\n            ShellControl shellControl,\n            UUID requestId,\n            ElevationHandler handler,\n            CountDown countDown,\n            String message,\n            String user,\n            CommandConfiguration command)\n            throws Exception;\n\n    String elevateTerminalCommandWithPreparedAskpass(\n            ShellControl shellControl, ElevationHandler handler, String command, String prefix, String user)\n            throws Exception;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ShellDialects.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.core.ModuleLayerLoader;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.ServiceLoader;\n\npublic class ShellDialects {\n\n    public static final List<ShellDialect> ALL = new ArrayList<>();\n    public static ShellDialect OPNSENSE;\n    public static ShellDialect PFSENSE;\n    public static ShellDialect POWERSHELL;\n    public static ShellDialect POWERSHELL_CORE;\n    public static ShellDialect CMD;\n    public static ShellDialect ASH;\n    public static ShellDialect SH;\n    public static ShellDialect DASH;\n    public static ShellDialect BASH;\n    public static ShellDialect ZSH;\n    public static ShellDialect CSH;\n    public static ShellDialect FISH;\n    public static ShellDialect NUSHELL;\n    public static ShellDialect XONSH;\n\n    public static ShellDialect NO_INTERACTION;\n    public static ShellDialect CISCO_IOS;\n    public static ShellDialect CISCO_IOS_XE;\n    public static ShellDialect CISCO_NXOS;\n    public static ShellDialect MIKROTIK;\n    public static ShellDialect PALO_ALTO;\n    public static ShellDialect RBASH;\n    public static ShellDialect CONSTRAINED_POWERSHELL;\n    public static ShellDialect OVH_BASTION;\n    public static ShellDialect HETZNER_BOX;\n    public static ShellDialect SFTP;\n\n    public static List<ShellDialect> getStartableDialects() {\n        return ALL.stream()\n                .filter(dialect ->\n                        dialect.getDumbMode().supportsAnyPossibleInteraction() && dialect.getLaunchCommand() != null)\n                .toList();\n    }\n\n    private static ShellDialect byId(String name) {\n        return ALL.stream()\n                .filter(shellType -> shellType.getId().equals(name))\n                .findFirst()\n                .orElseThrow();\n    }\n\n    public static boolean isPowershell(ShellControl sc) {\n        if (sc.getShellDialect() == null) {\n            return false;\n        }\n\n        return isPowershell(sc.getShellDialect());\n    }\n\n    public static boolean isPowershell(ShellDialect d) {\n        return d == POWERSHELL || d == POWERSHELL_CORE;\n    }\n\n    public static Optional<ShellDialect> byIdIfPresent(String name) {\n        return ALL.stream().filter(shellType -> shellType.getId().equals(name)).findFirst();\n    }\n\n    public static class Loader implements ModuleLayerLoader {\n\n        @Override\n        public void init(ModuleLayer layer) {\n            var services = layer != null\n                    ? ServiceLoader.load(layer, ShellDialect.class)\n                    : ServiceLoader.load(ShellDialect.class);\n            services.stream().forEach(moduleLayerLoaderProvider -> {\n                ALL.add(moduleLayerLoaderProvider.get());\n            });\n\n            if (ALL.isEmpty()) {\n                return;\n            }\n\n            CMD = byId(\"cmd\");\n            POWERSHELL = byId(\"powershell\");\n            POWERSHELL_CORE = byId(\"pwsh\");\n            OPNSENSE = byId(\"opnsense\");\n            PFSENSE = byId(\"pfsense\");\n            FISH = byId(\"fish\");\n            DASH = byId(\"dash\");\n            BASH = byId(\"bash\");\n            ZSH = byId(\"zsh\");\n            CSH = byId(\"csh\");\n            ASH = byId(\"ash\");\n            SH = byId(\"sh\");\n            NUSHELL = byId(\"nushell\");\n            XONSH = byId(\"xonsh\");\n            NO_INTERACTION = byId(\"noInteraction\");\n            CISCO_IOS = byId(\"ciscoIos\");\n            CISCO_IOS_XE = byId(\"ciscoIosXe\");\n            CISCO_NXOS = byId(\"ciscoNxOs\");\n            MIKROTIK = byId(\"mikrotik\");\n            PALO_ALTO = byId(\"paloAlto\");\n            RBASH = byId(\"rbash\");\n            CONSTRAINED_POWERSHELL = byId(\"constrainedPowershell\");\n            OVH_BASTION = byId(\"ovhBastion\");\n            HETZNER_BOX = byId(\"hetznerBox\");\n            SFTP = byId(\"sftp\");\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ShellDumbMode.java",
    "content": "package io.xpipe.app.process;\n\nimport java.io.IOException;\n\npublic interface ShellDumbMode {\n\n    default boolean supportsAnyPossibleInteraction() {\n        return true;\n    }\n\n    default void throwIfUnsupported() {}\n\n    default ShellDialect getSwitchDialect() {\n        return null;\n    }\n\n    default CommandBuilder prepareInlineDumbCommand(ShellControl self, ShellOpenFunction function) throws Exception {\n        return function.prepareWithoutInitCommand();\n    }\n\n    default void prepareInlineShellSwitch(ShellControl shellControl) throws Exception {}\n\n    default void prepareImmediateDumbInit(ShellControl shellControl) throws Exception {}\n\n    default void prepareDumbInit(ShellControl shellControl) throws Exception {}\n\n    default void prepareDumbExit(ShellControl shellControl) throws IOException {\n        shellControl.writeLine(\" exit\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ShellLaunchCommand.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.core.OsType;\n\nimport java.util.List;\n\npublic interface ShellLaunchCommand {\n\n    String inlineCdCommand(String cd);\n\n    List<String> localCommand();\n\n    default String loginCommand() {\n        return String.join(\" \", loginCommand(null));\n    }\n\n    List<String> loginCommand(OsType.Any os);\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ShellOpenFunction.java",
    "content": "package io.xpipe.app.process;\n\nimport lombok.NonNull;\n\npublic interface ShellOpenFunction {\n\n    static ShellOpenFunction unsupported() {\n        return new ShellOpenFunction() {\n            @Override\n            public CommandBuilder prepareWithoutInitCommand() {\n                throw new UnsupportedOperationException();\n            }\n\n            @Override\n            public CommandBuilder prepareWithInitCommand(@NonNull String command) {\n                throw new UnsupportedOperationException();\n            }\n        };\n    }\n\n    CommandBuilder prepareWithoutInitCommand() throws Exception;\n\n    CommandBuilder prepareWithInitCommand(@NonNull String command) throws Exception;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ShellScript.java",
    "content": "package io.xpipe.app.process;\n\nimport lombok.Value;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n@Value\npublic class ShellScript {\n\n    String value;\n\n    public static ShellScript empty() {\n        return new ShellScript(\"\");\n    }\n\n    public static ShellScript of(String s) {\n        return s != null ? new ShellScript(s) : null;\n    }\n\n    public static ShellScript lines(String... lines) {\n        return new ShellScript(Arrays.stream(lines).filter(s -> s != null).collect(Collectors.joining(\"\\n\")));\n    }\n\n    public static ShellScript lines(List<String> lines) {\n        return new ShellScript(String.join(\"\\n\", lines));\n    }\n\n    public ShellScript withoutShebang() {\n        var shebang = value.startsWith(\"#!\");\n        if (shebang) {\n            return new ShellScript(value.lines().skip(1).collect(Collectors.joining(\"\\n\")));\n        } else {\n            return this;\n        }\n    }\n\n    public ShellScript withShebang(ShellDialect dialect) {\n        return new ShellScript(\"#!/usr/bin/env \" + dialect.getExecutableName() + \"\\n\" + withoutShebang());\n    }\n\n    @Override\n    public String toString() {\n        return value;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ShellSecurityPolicy.java",
    "content": "package io.xpipe.app.process;\n\npublic interface ShellSecurityPolicy {\n\n    boolean permitTempScriptCreation();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ShellSpawnException.java",
    "content": "package io.xpipe.app.process;\n\nimport lombok.experimental.StandardException;\n\n@StandardException\npublic class ShellSpawnException extends Exception {}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ShellStoreState.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.app.ext.DataStoreState;\nimport io.xpipe.core.OsType;\n\nimport lombok.AccessLevel;\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.experimental.FieldDefaults;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\n@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)\n@Getter\n@EqualsAndHashCode(callSuper = true)\n@SuperBuilder(toBuilder = true)\n@Jacksonized\npublic class ShellStoreState extends DataStoreState implements SystemState {\n\n    OsType.Any osType;\n    String osName;\n    ShellDialect shellDialect;\n    ShellTtyState ttyState;\n    Boolean running;\n\n    @Override\n    public DataStoreState mergeCopy(DataStoreState newer) {\n        var shellStoreState = (ShellStoreState) newer;\n        var b = toBuilder();\n        mergeBuilder(shellStoreState, b);\n        return b.build();\n    }\n\n    // Do this with an object to fix javadoc compile issues\n    protected void mergeBuilder(ShellStoreState shellStoreState, Object builder) {\n        ShellStoreStateBuilder<?, ?> b = (ShellStoreStateBuilder<?, ?>) builder;\n        b.osType(useNewer(osType, shellStoreState.getOsType()))\n                .osName(useNewer(osName, shellStoreState.getOsName()))\n                .shellDialect(useNewer(shellDialect, shellStoreState.getShellDialect()))\n                .ttyState(useNewer(ttyState, shellStoreState.getTtyState()))\n                .running(useNewer(running, shellStoreState.getRunning()));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ShellTemp.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.app.core.AppNames;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport java.io.IOException;\n\npublic class ShellTemp {\n\n    public static FilePath createUserSpecificTempDataDirectory(ShellControl proc, String sub) throws Exception {\n        FilePath base;\n        // On Windows and macOS, we already have user specific temp directories\n        // Even on macOS as root it is technically unique as only root will use /tmp\n        if (proc.getOsType() != OsType.WINDOWS && proc.getOsType() != OsType.MACOS) {\n            var temp = proc.getSystemTemporaryDirectory();\n            base = temp.join(AppNames.ofCurrent().getKebapName());\n            proc.view().mkdir(base);\n            // We have to make sure that also other users can create files here\n            // This command should work in all shells\n            proc.command(\"chmod 777 \" + proc.getShellDialect().fileArgument(base))\n                    .executeAndCheck();\n            var user = proc.view().user();\n            base = base.join(user);\n            // We have to make sure that also other users can create files here\n            // This command should work in all shells\n            proc.command(\"chmod 700 \" + proc.getShellDialect().fileArgument(base))\n                    .executeAndCheck();\n        } else {\n            var temp = proc.getSystemTemporaryDirectory();\n            base = temp.join(AppNames.ofCurrent().getKebapName());\n        }\n        return sub != null ? base.join(sub) : base;\n    }\n\n    public static void checkTempDirectory(ShellControl sc) throws Exception {\n        var d = sc.getShellDialect();\n        var systemTemp = sc.getSystemTemporaryDirectory();\n        var hasValidTemp = d.directoryExists(sc, systemTemp.toString()).executeAndCheck()\n                && checkDirectoryPermissions(sc, systemTemp.toString());\n\n        // We only really need temp for cmd\n        // On various containers temp might be not available, but we can make it work\n        if (!sc.isLocal() && sc.getShellDialect() == ShellDialects.CMD && !hasValidTemp) {\n            throw ErrorEventFactory.expected(\n                    new IOException(\"No permissions to access system temporary directory %s\".formatted(systemTemp)));\n        }\n\n        if (hasValidTemp) {\n            var sessionFile = systemTemp.join(\"xpipe-session-\"\n                    + AppProperties.get().getSessionId().toString().substring(0, 8));\n            var newSession = !sc.view().fileExists(sessionFile);\n            if (newSession) {\n                clearTemp(sc);\n                sc.view().touch(sessionFile);\n            }\n        }\n    }\n\n    public static void clearTemp(ShellControl sc) throws Exception {\n        var systemTemp = sc.getSystemTemporaryDirectory();\n\n        // The temp dir is a lot to clean on Windows potentially\n        // Also, the wildcard remove is very slow in PowerShell\n        var skipClear = OsType.ofLocal() == OsType.WINDOWS && sc.isLocal();\n        if (!skipClear) {\n            clearFiles(sc, systemTemp.join(\"xpipe-\"));\n        }\n    }\n\n    private static void clearFiles(ShellControl sc, FilePath prefix) throws Exception {\n        var d = sc.getShellDialect();\n        if (d == ShellDialects.CMD) {\n            sc.command(CommandBuilder.of().add(\"DEL\", \"/Q\", \"/F\").addQuoted(prefix.toString() + \"*\"))\n                    .executeAndCheck();\n        } else if (ShellDialects.isPowershell(d)) {\n            sc.command(CommandBuilder.of()\n                            .add(\"Get-ChildItem\")\n                            .addFile(prefix.getParent())\n                            .add(\n                                    \"|\",\n                                    \"Where-Object\",\n                                    \"{$_.Name.StartsWith(\\\"\" + prefix.getFileName() + \"\\\")}\",\n                                    \"|\",\n                                    \"Remove-Item\",\n                                    \"-Force\"))\n                    .executeAndCheck();\n        } else {\n            sc.command(CommandBuilder.of()\n                            .add(\"rm\", \"-f\")\n                            .add(\"\\\"\" + prefix.toString() + \"\\\"*\")\n                            .add(\"2>/dev/null\"))\n                    .executeAndCheck();\n        }\n    }\n\n    private static boolean checkDirectoryPermissions(ShellControl proc, String dir) throws Exception {\n        if (proc.getOsType() == OsType.WINDOWS) {\n            return true;\n        }\n\n        var d = proc.getShellDialect();\n        return proc.executeSimpleBooleanCommand(\"test -r %s && test -w %s && test -x %s\"\n                .formatted(d.fileArgument(dir), d.fileArgument(dir), d.fileArgument(dir)));\n    }\n\n    public static FilePath getSubDirectory(ShellControl proc, String... sub) throws Exception {\n        var base = proc.getSystemTemporaryDirectory();\n        var dir = base.join(sub);\n        // We assume that this directory does not exist yet and therefore don't perform any checks\n        proc.getShellDialect().prepareUserTempDirectory(proc, dir.toString()).execute();\n        return dir;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ShellTerminalInitCommand.java",
    "content": "package io.xpipe.app.process;\n\nimport java.util.Optional;\n\npublic interface ShellTerminalInitCommand {\n\n    Optional<String> terminalContent(ShellControl shellControl) throws Exception;\n\n    boolean canPotentiallyRunInDialect(ShellDialect dialect);\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ShellTtyState.java",
    "content": "package io.xpipe.app.process;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport lombok.Getter;\n\n@Getter\npublic enum ShellTtyState {\n    @JsonProperty(\"none\")\n    NONE(true, false, false, true, true),\n    @JsonProperty(\"merged\")\n    MERGED_STDERR(false, true, false, false, false),\n    @JsonProperty(\"pty\")\n    PTY_ALLOCATED(false, true, true, false, false);\n\n    private final boolean hasSeparateStreams;\n    private final boolean hasAnsiEscapes;\n    private final boolean echoesAllInput;\n    private final boolean supportsInput;\n    private final boolean preservesOutput;\n\n    ShellTtyState(\n            boolean hasSeparateStreams,\n            boolean hasAnsiEscapes,\n            boolean echoesAllInput,\n            boolean supportsInput,\n            boolean preservesOutput) {\n        this.hasSeparateStreams = hasSeparateStreams;\n        this.hasAnsiEscapes = hasAnsiEscapes;\n        this.echoesAllInput = echoesAllInput;\n        this.supportsInput = supportsInput;\n        this.preservesOutput = preservesOutput;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/ShellView.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.app.util.GroupFile;\nimport io.xpipe.app.util.PasswdFile;\nimport io.xpipe.core.FailableSupplier;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.*;\n\npublic class ShellView {\n\n    protected final ShellControl shellControl;\n    protected String user;\n    protected FilePath userHome;\n    protected Boolean root;\n    protected Boolean administrator;\n    protected PasswdFile passwdFile;\n    protected GroupFile groupFile;\n    protected final Map<String, Boolean> installedApplications = new HashMap<>();\n    protected final Map<String, Boolean> genericCache = new HashMap<>();\n\n    public ShellView(ShellControl shellControl) {\n        this.shellControl = shellControl;\n    }\n\n    protected ShellDialect getDialect() {\n        return shellControl.getShellDialect();\n    }\n\n    public synchronized void setCachedPredicate(String key, boolean value) {\n        genericCache.put(key, value);\n    }\n\n    public synchronized boolean getCachedPredicate(String key, FailableSupplier<Boolean> supplier) throws Exception {\n        var v = genericCache.get(key);\n        if (v != null) {\n            return v;\n        }\n\n        var supplied = supplier.get();\n        genericCache.put(key, supplied);\n        return supplied;\n    }\n\n    public synchronized PasswdFile getPasswdFile() throws Exception {\n        if (passwdFile == null) {\n            passwdFile = PasswdFile.parse(shellControl);\n        }\n\n        return passwdFile;\n    }\n\n    public synchronized GroupFile getGroupFile() throws Exception {\n        if (groupFile == null) {\n            groupFile = GroupFile.parse(shellControl);\n        }\n\n        return groupFile;\n    }\n\n    public FilePath writeTextFileDeterministic(FilePath base, String text) throws Exception {\n        var hash = Math.abs(Objects.hash(text, user()));\n        var ext = base.getExtension();\n        var target = FilePath.of(base.getBaseName().toString() + \"-\" + hash + (ext.isPresent() ? \".\" + ext.get() : \"\"));\n        if (fileExists(target)) {\n            return target;\n        }\n        writeTextFile(target, text);\n        return target;\n    }\n\n    public byte[] readRawFile(FilePath path) throws Exception {\n        var s = getDialect().getFileReadCommand(shellControl, path.toString()).readRawBytesOrThrow();\n        return s;\n    }\n\n    public String readTextFile(FilePath path) throws Exception {\n        var s = getDialect().getFileReadCommand(shellControl, path.toString()).readStdoutOrThrow();\n        return s;\n    }\n\n    public void writeRawFile(FilePath path, byte[] data) throws Exception {\n        writeStreamFile(path, new ByteArrayInputStream(data), data.length);\n    }\n\n    public void writeTextFile(FilePath path, String text) throws Exception {\n        var cc = getDialect().createTextFileWriteCommand(shellControl, text, path.toString());\n        cc.execute();\n    }\n\n    public void writeScriptFile(FilePath path, String text) throws Exception {\n        var cc = getDialect().createScriptTextFileWriteCommand(shellControl, text, path.toString());\n        cc.execute();\n    }\n\n    public FilePath writeScriptFileDeterministic(FilePath base, String text) throws Exception {\n        var hash = Math.abs(Objects.hash(text, user()));\n        var ext = base.getExtension();\n        var target = FilePath.of(base.getBaseName().toString() + \"-\" + hash + (ext.isPresent() ? \".\" + ext.get() : \"\"));\n        if (fileExists(target)) {\n            return target;\n        }\n        writeScriptFile(target, text);\n        return target;\n    }\n\n    public void writeStreamFile(FilePath path, InputStream inputStream, long size) throws Exception {\n        try (var out = getDialect()\n                .createStreamFileWriteCommand(shellControl, path.toString(), size)\n                .startExternalStdin()) {\n            inputStream.transferTo(out);\n        }\n    }\n\n    public FilePath userHome() throws Exception {\n        if (userHome == null) {\n            userHome = FilePath.of(OsFileSystem.of(shellControl.getOsType()).getUserHomeDirectory(shellControl));\n        }\n\n        return userHome;\n    }\n\n    public void moveFile(FilePath source, FilePath dest) throws Exception {\n        getDialect().getFileMoveCommand(shellControl, source.toString(), dest.toString()).execute();\n    }\n\n    public boolean fileExists(FilePath path) throws Exception {\n        return getDialect()\n                .createFileExistsCommand(shellControl, path.toString())\n                .executeAndCheck();\n    }\n\n    public void deleteDirectory(FilePath path) throws Exception {\n        getDialect().deleteFileOrDirectory(shellControl, path.toString()).execute();\n    }\n\n    public void deleteFile(FilePath path) throws Exception {\n        getDialect().getFileDeleteCommand(shellControl, path.toString()).execute();\n    }\n\n    public void deleteFileIfPossible(FilePath path) throws Exception {\n        getDialect().getFileDeleteCommand(shellControl, path.toString()).executeAndCheck();\n    }\n\n    public void mkdir(FilePath path) throws Exception {\n        shellControl.command(getDialect().getMkdirsCommand(path.toString())).execute();\n    }\n\n    public boolean directoryExists(FilePath path) throws Exception {\n        return getDialect().directoryExists(shellControl, path.toString()).executeAndCheck();\n    }\n\n    public String user() throws Exception {\n        if (user == null) {\n            user = getDialect().printUsernameCommand(shellControl).readStdoutOrThrow();\n        }\n\n        return user;\n    }\n\n    public boolean isRoot() throws Exception {\n        if (shellControl.getOsType() == OsType.WINDOWS) {\n            return false;\n        }\n\n        if (root != null) {\n            return root;\n        }\n\n        var isRoot = shellControl.executeSimpleBooleanCommand(\"test \\\"${EUID:-$(id -u)}\\\" -eq 0\");\n        return (root = isRoot);\n    }\n\n    public Optional<FilePath> findProgram(String name) throws Exception {\n        var out = shellControl\n                .command(shellControl.getShellDialect().whichCommand(shellControl, name))\n                .readStdoutIfPossible();\n        return out.flatMap(s -> s.lines().findFirst()).map(String::trim).map(s -> FilePath.of(s));\n    }\n\n    public void transferLocalFile(Path localPath, FilePath target) throws Exception {\n        try (var in = Files.newInputStream(localPath)) {\n            writeStreamFile(target, in, in.available());\n        }\n    }\n\n    public synchronized boolean isInPath(String executable, boolean cache) throws Exception {\n        if (cache) {\n            var r = installedApplications.get(executable);\n            if (r != null) {\n                return r;\n            }\n\n            var found = findProgram(executable).isPresent();\n            installedApplications.put(executable, found);\n            return found;\n        }\n\n        return findProgram(executable).isPresent();\n    }\n\n    public void cd(FilePath directory) throws Exception {\n        cd(directory.toString());\n    }\n\n    public FilePath pwd() throws Exception {\n        return FilePath.of(shellControl\n                .command(shellControl.getShellDialect().getPrintWorkingDirectoryCommand())\n                .readStdoutOrThrow());\n    }\n\n    public void touch(FilePath path) throws Exception {\n        var c = shellControl.getShellDialect().getFileTouchCommand(shellControl, path.toString());\n        c.execute();\n    }\n\n    public void cd(String directory) throws Exception {\n        var d = shellControl.getShellDialect();\n        var cmd = shellControl.command(d.getCdCommand(directory));\n        cmd.executeAndCheck();\n    }\n\n    public void unsetEnvironmentVariable(String name) throws Exception {\n        shellControl\n                .command(shellControl.getShellDialect().unsetEnvironmentVariableCommand(name))\n                .executeAndCheck();\n    }\n\n    public Optional<String> getEnvironmentVariable(String name) throws Exception {\n        var r = shellControl\n                .command(shellControl.getShellDialect().getPrintEnvironmentVariableCommand(name))\n                .readStdoutOrThrow();\n        if (r.isBlank() || r.equals(getDialect().environmentVariable(name))) {\n            return Optional.empty();\n        }\n\n        return Optional.of(r);\n    }\n\n    public String getEnvironmentVariableOrThrow(String name) throws Exception {\n        var r = getEnvironmentVariable(name);\n        return r.orElseThrow(\n                () -> new IllegalArgumentException(\"Required environment variable \" + name + \" not defined\"));\n    }\n\n    public void setEnvironmentVariable(String name, String value) throws Exception {\n        shellControl\n                .command(shellControl.getShellDialect().getSetEnvironmentVariableCommand(name, value))\n                .execute();\n    }\n\n    public void setSensitiveEnvironmentVariable(String name, String value) throws Exception {\n        var command =\n                shellControl.command(shellControl.getShellDialect().getSetEnvironmentVariableCommand(name, value));\n        command.sensitive();\n        command.execute();\n    }\n\n    public synchronized boolean isAdministrator() throws Exception {\n        if (shellControl.getOsType() != OsType.WINDOWS) {\n            return false;\n        }\n\n        if (administrator != null) {\n            return administrator;\n        }\n\n        if (shellControl.getShellDialect() == ShellDialects.CMD) {\n            administrator = shellControl.command(\"net.exe session 1>NUL 2>NUL\").executeAndCheck();\n        } else if (ShellDialects.isPowershell(shellControl)) {\n            administrator = shellControl\n                    .command(String.format(\n                            \"try {$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent());\"\n                                    + \"if (-not $($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))) {$host.ui\"\n                                    + \".WriteErrorLine(\\\"%s\\\"); throw \\\"error\\\"}} catch {}\",\n                            \"Not Administrator\"))\n                    .executeAndCheck();\n        } else {\n            administrator = false;\n        }\n\n        return administrator;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/StubShellControl.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.core.FailableConsumer;\n\nimport java.util.function.Consumer;\n\npublic class StubShellControl extends WrapperShellControl {\n\n    public StubShellControl(ShellControl parent) {\n        super(parent);\n    }\n\n    @Override\n    public ShellControl onInit(FailableConsumer<ShellControl, Exception> pc) {\n        super.onInit(pc);\n        return this;\n    }\n\n    @Override\n    public ShellControl onExit(Consumer<ShellControl> pc) {\n        super.onExit(pc);\n        return this;\n    }\n\n    @Override\n    public ShellControl onKill(Runnable pc) {\n        super.onKill(pc);\n        return this;\n    }\n\n    @Override\n    public ShellControl onStartupFail(Consumer<Throwable> t) {\n        super.onStartupFail(t);\n        return this;\n    }\n\n    @Override\n    public void close() {}\n\n    @Override\n    public ShellControl start() throws Exception {\n        super.start();\n        return this;\n    }\n\n    @Override\n    public boolean canHaveSubshells() {\n        return parent.canHaveSubshells();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/SudoCache.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.core.FilePath;\n\nimport java.util.Optional;\n\npublic interface SudoCache {\n\n    void setRequiresPassword();\n\n    boolean requiresPassword() throws Exception;\n\n    Optional<FilePath> getSudoExecutable() throws Exception;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/SystemState.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.core.OsType;\n\npublic interface SystemState {\n\n    OsType.Any getOsType();\n\n    String getOsName();\n\n    ShellDialect getShellDialect();\n\n    ShellTtyState getTtyState();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/TerminalInitFunction.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.core.FailableFunction;\n\npublic interface TerminalInitFunction {\n\n    static TerminalInitFunction of(FailableFunction<ShellControl, String, Exception> f) {\n        return new TerminalInitFunction() {\n            @Override\n            public boolean isSpecified() {\n                return true;\n            }\n\n            @Override\n            public String apply(ShellControl shellControl) throws Exception {\n                return f.apply(shellControl);\n            }\n        };\n    }\n\n    static TerminalInitFunction fixed(String s) {\n        return new TerminalInitFunction() {\n            @Override\n            public boolean isSpecified() {\n                return true;\n            }\n\n            @Override\n            public String apply(ShellControl shellControl) {\n                return s;\n            }\n        };\n    }\n\n    static TerminalInitFunction none() {\n        return new TerminalInitFunction() {\n            @Override\n            public boolean isSpecified() {\n                return false;\n            }\n\n            @Override\n            public String apply(ShellControl shellControl) {\n                return null;\n            }\n        };\n    }\n\n    boolean isSpecified();\n\n    String apply(ShellControl shellControl) throws Exception;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/TerminalInitScriptConfig.java",
    "content": "package io.xpipe.app.process;\n\nimport lombok.Value;\n\n@Value\npublic class TerminalInitScriptConfig {\n\n    String displayName;\n    boolean clearScreen;\n    TerminalInitFunction terminalSpecificCommands;\n\n    public static TerminalInitScriptConfig ofName(String name) {\n        return new TerminalInitScriptConfig(name, true, TerminalInitFunction.none());\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/WorkingDirectoryFunction.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.core.FailableFunction;\nimport io.xpipe.core.FilePath;\n\npublic interface WorkingDirectoryFunction {\n\n    static WorkingDirectoryFunction of(FailableFunction<ShellControl, FilePath, Exception> path) {\n        return new WorkingDirectoryFunction() {\n            @Override\n            public boolean isFixed() {\n                return false;\n            }\n\n            @Override\n            public boolean isSpecified() {\n                return true;\n            }\n\n            @Override\n            public FilePath apply(ShellControl shellControl) throws Exception {\n                return path.apply(shellControl);\n            }\n        };\n    }\n\n    static WorkingDirectoryFunction fixed(FilePath path) {\n        return new WorkingDirectoryFunction() {\n            @Override\n            public boolean isFixed() {\n                return true;\n            }\n\n            @Override\n            public boolean isSpecified() {\n                return true;\n            }\n\n            @Override\n            public FilePath apply(ShellControl shellControl) {\n                return path;\n            }\n        };\n    }\n\n    static WorkingDirectoryFunction none() {\n        return new WorkingDirectoryFunction() {\n            @Override\n            public boolean isFixed() {\n                return true;\n            }\n\n            @Override\n            public boolean isSpecified() {\n                return false;\n            }\n\n            @Override\n            public FilePath apply(ShellControl shellControl) {\n                return null;\n            }\n        };\n    }\n\n    boolean isFixed();\n\n    boolean isSpecified();\n\n    FilePath apply(ShellControl shellControl) throws Exception;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/process/WrapperShellControl.java",
    "content": "package io.xpipe.app.process;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.util.LicensedFeature;\nimport io.xpipe.core.FailableConsumer;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport lombok.Getter;\n\nimport java.io.IOException;\nimport java.nio.charset.Charset;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.UUID;\nimport java.util.concurrent.locks.ReentrantLock;\nimport java.util.function.Consumer;\n\n@Getter\npublic class WrapperShellControl implements ShellControl {\n\n    protected final ShellControl parent;\n\n    public WrapperShellControl(ShellControl parent) {\n        this.parent = parent;\n    }\n\n    @Override\n    public UUID getUuid() {\n        return parent.getUuid();\n    }\n\n    @Override\n    public String prepareTerminalOpen(TerminalInitScriptConfig config, WorkingDirectoryFunction workingDirectory)\n            throws Exception {\n        return parent.prepareTerminalOpen(config, workingDirectory);\n    }\n\n    @Override\n    public void refreshRunningState() {\n        parent.refreshRunningState();\n    }\n\n    @Override\n    public void closeStdin() throws IOException {\n        parent.closeStdin();\n    }\n\n    @Override\n    public boolean isAnyStreamClosed() {\n        return parent.isAnyStreamClosed();\n    }\n\n    @Override\n    public boolean isRunning(boolean refresh) {\n        return parent.isRunning(true);\n    }\n\n    @Override\n    public ShellDialect getShellDialect() {\n        return parent.getShellDialect();\n    }\n\n    @Override\n    public void close() throws Exception {\n        parent.close();\n    }\n\n    @Override\n    public void shutdown() throws Exception {\n        parent.shutdown();\n    }\n\n    @Override\n    public void kill() {\n        parent.kill();\n    }\n\n    @Override\n    public void killExternal() {\n        parent.killExternal();\n    }\n\n    @Override\n    public Charset getCharset() {\n        return parent.getCharset();\n    }\n\n    @Override\n    public void setExitTimeout(int timeout) {\n        parent.setExitTimeout(timeout);\n    }\n\n    @Override\n    public void setUser(String user) {\n        parent.setUser(user);\n    }\n\n    @Override\n    public boolean isExiting() {\n        return parent.isExiting();\n    }\n\n    @Override\n    public boolean isInitializing() {\n        return parent.isInitializing();\n    }\n\n    @Override\n    public void setDumbOpen(ShellOpenFunction openFunction) {\n        parent.setDumbOpen(openFunction);\n    }\n\n    @Override\n    public void setTerminalOpen(ShellOpenFunction openFunction) {\n        parent.setTerminalOpen(openFunction);\n    }\n\n    @Override\n    public void writeLine(String line) throws IOException {\n        parent.writeLine(line);\n    }\n\n    @Override\n    public void writeLine(String line, boolean log) throws IOException {\n        parent.writeLine(line, log);\n    }\n\n    @Override\n    public boolean isSubShellActive() {\n        return parent.isSubShellActive();\n    }\n\n    @Override\n    public void setSubShellActive(boolean active) {\n        parent.setSubShellActive(active);\n    }\n\n    @Override\n    public ShellView view() {\n        return parent.view();\n    }\n\n    @Override\n    public Optional<ShellControl> getParentControl() {\n        return parent.getParentControl();\n    }\n\n    @Override\n    public ShellTtyState getTtyState() {\n        return parent.getTtyState();\n    }\n\n    @Override\n    public void setNonInteractive() {\n        parent.setNonInteractive();\n    }\n\n    @Override\n    public boolean isInteractive() {\n        return parent.isInteractive();\n    }\n\n    @Override\n    public ElevationHandler getElevationHandler() {\n        return parent.getElevationHandler();\n    }\n\n    @Override\n    public void setElevationHandler(ElevationHandler ref) {\n        parent.setElevationHandler(ref);\n    }\n\n    @Override\n    public void closeStdout() throws IOException {\n        parent.closeStdout();\n    }\n\n    @Override\n    public List<UUID> getExitUuids() {\n        return parent.getExitUuids();\n    }\n\n    @Override\n    public void setWorkingDirectory(WorkingDirectoryFunction workingDirectory) {\n        parent.setWorkingDirectory(workingDirectory);\n    }\n\n    @Override\n    public Optional<DataStore> getSourceStore() {\n        return parent.getSourceStore();\n    }\n\n    @Override\n    public Optional<UUID> getSourceStoreId() {\n        return parent.getSourceStoreId();\n    }\n\n    @Override\n    public ShellControl withSourceStore(DataStore store) {\n        return parent.withSourceStore(store);\n    }\n\n    @Override\n    public ParentSystemAccess getParentSystemAccess() {\n        return parent.getParentSystemAccess();\n    }\n\n    @Override\n    public void setParentSystemAccess(ParentSystemAccess access) {\n        parent.setParentSystemAccess(access);\n    }\n\n    @Override\n    public ParentSystemAccess getLocalSystemAccess() {\n        return parent.getLocalSystemAccess();\n    }\n\n    @Override\n    public boolean isLocal() {\n        return parent.isLocal();\n    }\n\n    @Override\n    public String getOsName() {\n        return parent.getOsName();\n    }\n\n    @Override\n    public ReentrantLock getLock() {\n        return parent.getLock();\n    }\n\n    @Override\n    public void requireLicensedFeature(LicensedFeature id) {\n        parent.requireLicensedFeature(id);\n    }\n\n    @Override\n    public ShellDialect getOriginalShellDialect() {\n        return parent.getOriginalShellDialect();\n    }\n\n    @Override\n    public void setOriginalShellDialect(ShellDialect dialect) {\n        parent.setOriginalShellDialect(dialect);\n    }\n\n    @Override\n    public ShellControl onInit(FailableConsumer<ShellControl, Exception> pc) {\n        return parent.onInit(pc);\n    }\n\n    @Override\n    public ShellControl onExit(Consumer<ShellControl> pc) {\n        return parent.onExit(pc);\n    }\n\n    @Override\n    public ShellControl onKill(Runnable pc) {\n        return parent.onKill(pc);\n    }\n\n    @Override\n    public ShellControl onStartupFail(Consumer<Throwable> t) {\n        return parent.onStartupFail(t);\n    }\n\n    @Override\n    public ShellControl withExceptionConverter(ProcessExceptionConverter converter) {\n        return parent.withExceptionConverter(converter);\n    }\n\n    @Override\n    public ShellControl start() throws Exception {\n        return parent.start();\n    }\n\n    @Override\n    public LocalProcessInputStream getStdout() {\n        return parent.getStdout();\n    }\n\n    @Override\n    public LocalProcessOutputStream getStdin() {\n        return parent.getStdin();\n    }\n\n    @Override\n    public LocalProcessInputStream getStderr() {\n        return parent.getStderr();\n    }\n\n    @Override\n    public void checkLicenseOrThrow() {\n        parent.checkLicenseOrThrow();\n    }\n\n    @Override\n    public String prepareIntermediateTerminalOpen(\n            TerminalInitFunction content, TerminalInitScriptConfig config, WorkingDirectoryFunction workingDirectory)\n            throws Exception {\n        return parent.prepareIntermediateTerminalOpen(content, config, workingDirectory);\n    }\n\n    @Override\n    public FilePath getSystemTemporaryDirectory() {\n        return parent.getSystemTemporaryDirectory();\n    }\n\n    @Override\n    public ShellControl withSecurityPolicy(ShellSecurityPolicy policy) {\n        return parent.withSecurityPolicy(policy);\n    }\n\n    @Override\n    public ShellSecurityPolicy getEffectiveSecurityPolicy() {\n        return parent.getEffectiveSecurityPolicy();\n    }\n\n    @Override\n    public String buildElevatedCommand(\n            CommandConfiguration input, String prefix, UUID requestId, CountDown countDown, String user)\n            throws Exception {\n        return parent.buildElevatedCommand(input, prefix, requestId, countDown, user);\n    }\n\n    @Override\n    public void restart() throws Exception {\n        parent.restart();\n    }\n\n    @Override\n    public OsType.Any getOsType() {\n        return parent.getOsType();\n    }\n\n    @Override\n    public ShellControl elevated(ElevationFunction elevationFunction) {\n        return parent.elevated(elevationFunction);\n    }\n\n    @Override\n    public ShellControl withInitSnippet(ShellTerminalInitCommand snippet, boolean append) {\n        return parent.withInitSnippet(snippet, append);\n    }\n\n    @Override\n    public Optional<ShellControl> getActiveReplacementBackgroundSession() throws Exception {\n        return parent.getActiveReplacementBackgroundSession();\n    }\n\n    @Override\n    public ShellControl subShell() {\n        return parent.subShell();\n    }\n\n    @Override\n    public CommandControl command(CommandBuilder builder) {\n        return parent.command(builder);\n    }\n\n    @Override\n    public void exitAndWait() throws IOException {\n        parent.exitAndWait();\n    }\n\n    @Override\n    public SudoCache getSudoCache() {\n        return parent.getSudoCache();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/pwman/BitwardenPasswordManager.java",
    "content": "package io.xpipe.app.pwman;\n\nimport io.xpipe.app.comp.base.ButtonComp;\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.process.*;\nimport io.xpipe.app.terminal.TerminalLaunch;\nimport io.xpipe.app.util.*;\nimport io.xpipe.core.InPlaceSecretValue;\nimport io.xpipe.core.JacksonMapper;\nimport io.xpipe.core.OsType;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport javafx.application.Platform;\nimport javafx.beans.property.Property;\nimport javafx.geometry.Insets;\nimport javafx.scene.layout.Region;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.util.concurrent.atomic.AtomicReference;\n\n@JsonTypeName(\"bitwarden\")\npublic class BitwardenPasswordManager implements PasswordManager {\n\n    private static ShellControl SHELL;\n\n    private static synchronized ShellControl getOrStartShell() throws Exception {\n        if (SHELL == null) {\n            SHELL = ProcessControlProvider.get().createLocalProcessControl(true);\n            SHELL.start();\n\n            if (moveAppDir()) {\n                SHELL.view().unsetEnvironmentVariable(\"BW_SESSION\");\n                SHELL.view()\n                        .setEnvironmentVariable(\n                                \"BITWARDENCLI_APPDATA_DIR\",\n                                AppCache.getBasePath().toString());\n            }\n        }\n        SHELL.start();\n        return SHELL;\n    }\n\n    @SuppressWarnings(\"unused\")\n    public static OptionsBuilder createOptions(Property<BitwardenPasswordManager> p) {\n        AtomicReference<Region> button = new AtomicReference<>();\n        var testButton = new ButtonComp(AppI18n.observable(\"sync\"), new FontIcon(\"mdi2r-refresh\"), () -> {\n            button.get().setDisable(true);\n            ThreadHelper.runFailableAsync(() -> {\n                sync();\n                Platform.runLater(() -> {\n                    button.get().setDisable(false);\n                });\n            });\n        });\n        testButton.apply(struc -> button.set(struc));\n        testButton.padding(new Insets(6, 10, 6, 6));\n\n        return new OptionsBuilder()\n                .addComp(testButton);\n    }\n\n\n    private static boolean moveAppDir() throws Exception {\n        var path = SHELL.view().findProgram(\"bw\");\n        return OsType.ofLocal() != OsType.LINUX\n                || path.isEmpty()\n                || !path.get().toString().contains(\"snap\");\n    }\n\n    private static void sync() throws Exception {\n        // Copy existing file if possible to retain configuration. Only once per session\n        copyConfigIfNeeded();\n\n        if (!loginOrUnlock()) {\n            return;\n        }\n\n        getOrStartShell().command(CommandBuilder.of().add(\"bw\", \"sync\")).execute();\n    }\n\n    private static void copyConfigIfNeeded() {\n        var cacheDataFile = AppCache.getBasePath().resolve(\"data.json\");\n        var def = getDefaultConfigPath();\n        if (Files.exists(def)) {\n            try {\n                var defIsNewer = !Files.exists(cacheDataFile) || Files.getLastModifiedTime(def).compareTo(Files.getLastModifiedTime(cacheDataFile)) > 0;\n                if (defIsNewer) {\n                    Files.copy(def, cacheDataFile, StandardCopyOption.REPLACE_EXISTING);\n                }\n            } catch (IOException e) {\n                ErrorEventFactory.fromThrowable(e).handle();\n            }\n        }\n    }\n\n    private static boolean loginOrUnlock() throws Exception {\n        var sc = getOrStartShell();\n        var command = sc.command(CommandBuilder.of().add(\"bw\", \"get\", \"item\", \"xpipe-test\", \"--nointeraction\"));\n        var r = command.readStdoutAndStderr();\n        if (r[1].contains(\"You are not logged in\")) {\n            var script = ShellScript.lines(\n                    moveAppDir()\n                            ? LocalShell.getDialect()\n                            .getSetEnvironmentVariableCommand(\n                                    \"BITWARDENCLI_APPDATA_DIR\",\n                                    AppCache.getBasePath().toString())\n                            : null,\n                    sc.getShellDialect().getEchoCommand(\"Log in into your Bitwarden account from the CLI:\", false),\n                    \"bw login\");\n            TerminalLaunch.builder()\n                    .title(\"Bitwarden login\")\n                    .localScript(script)\n                    .logIfEnabled(false)\n                    .preferTabs(false)\n                    .pauseOnExit(true)\n                    .launch();\n            return false;\n        }\n\n        if (r[1].contains(\"Vault is locked\")) {\n            var pw = AskpassAlert.queryRaw(\"Unlock vault with your Bitwarden master password\", null, false);\n            if (pw.getSecret() == null) {\n                return false;\n            }\n            var cmd = sc.command(CommandBuilder.of()\n                    .add(\"bw\", \"unlock\", \"--raw\", \"--passwordenv\", \"BW_PASSWORD\")\n                    .fixedEnvironment(\"BW_PASSWORD\", pw.getSecret().getSecretValue()));\n            cmd.sensitive();\n            var out = cmd.readStdoutOrThrow();\n            sc.view().setSensitiveEnvironmentVariable(\"BW_SESSION\", out);\n        }\n\n        return true;\n    }\n\n    @Override\n    public synchronized CredentialResult retrieveCredentials(String key) {\n        try {\n            CommandSupport.isInLocalPathOrThrow(\"Bitwarden CLI\", \"bw\");\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e)\n                    .link(\"https://bitwarden.com/help/cli/#download-and-install\")\n                    .handle();\n            return null;\n        }\n\n        // Copy existing file if possible to retain configuration. Only once per session\n        copyConfigIfNeeded();\n\n        try {\n            if (!loginOrUnlock()) {\n                return null;\n            }\n\n            var sc = getOrStartShell();\n            var cmd =\n                    CommandBuilder.of().add(\"bw\", \"get\", \"item\").addLiteral(key).add(\"--nointeraction\");\n            var json = JacksonMapper.getDefault()\n                    .readTree(sc.command(cmd).sensitive().readStdoutOrThrow());\n            var login = json.get(\"login\");\n            if (login == null) {\n                throw ErrorEventFactory.expected(\n                        new IllegalArgumentException(\"No usable login found for item name \" + key));\n            }\n\n            var user = login.required(\"username\");\n            var password = login.required(\"password\");\n            return new CredentialResult(user.isNull() ? null : user.asText(), InPlaceSecretValue.of(password.asText()));\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).expected().handle();\n            return null;\n        }\n    }\n\n    private static Path getDefaultConfigPath() {\n        return switch (OsType.ofLocal()) {\n            case OsType.Linux ignored -> {\n                if (System.getenv(\"XDG_CONFIG_HOME\") != null) {\n                    yield Path.of(System.getenv(\"XDG_CONFIG_HOME\"), \"Bitwarden CLI\")\n                            .resolve(\"data.json\");\n                } else {\n                    yield AppSystemInfo.ofLinux()\n                            .getUserHome()\n                            .resolve(\".config\", \"Bitwarden CLI\")\n                            .resolve(\"data.json\");\n                }\n            }\n            case OsType.MacOs ignored ->\n                AppSystemInfo.ofMacOs()\n                        .getUserHome()\n                        .resolve(\"Library\", \"Application Support\", \"Bitwarden CLI\", \"data.json\");\n            case OsType.Windows ignored ->\n                AppSystemInfo.ofWindows()\n                        .getRoamingAppData()\n                        .resolve(\"Bitwarden CLI\")\n                        .resolve(\"data.json\");\n        };\n    }\n\n    @Override\n    public String getKeyPlaceholder() {\n        return \"Item name\";\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://bitwarden.com/\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/pwman/DashlanePasswordManager.java",
    "content": "package io.xpipe.app.pwman;\n\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.CommandSupport;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.app.terminal.TerminalLaunch;\nimport io.xpipe.core.InPlaceSecretValue;\nimport io.xpipe.core.JacksonMapper;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\n\n@JsonTypeName(\"dashlane\")\npublic class DashlanePasswordManager implements PasswordManager {\n\n    private static ShellControl SHELL;\n\n    private static synchronized ShellControl getOrStartShell() throws Exception {\n        if (SHELL == null) {\n            SHELL = ProcessControlProvider.get().createLocalProcessControl(true);\n        }\n        SHELL.start();\n        return SHELL;\n    }\n\n    @Override\n    public synchronized CredentialResult retrieveCredentials(String key) {\n        try {\n            CommandSupport.isInLocalPathOrThrow(\"Dashlane CLI\", \"dcli\");\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e)\n                    .link(\"https://cli.dashlane.com/install\")\n                    .handle();\n            return null;\n        }\n\n        try {\n            var sc = getOrStartShell();\n            var command = sc.command(sc.getShellDialect().nullStdin(\"dcli accounts whoami\"));\n            var r = command.readStdoutIfPossible();\n            if (r.isEmpty() || r.get().isEmpty()) {\n                var script = ShellScript.lines(\n                        sc.getShellDialect().getEchoCommand(\"Log in into your Dashlane account from the CLI:\", false),\n                        \"dcli accounts whoami\");\n                TerminalLaunch.builder()\n                        .title(\"Dashlane login\")\n                        .localScript(script)\n                        .logIfEnabled(false)\n                        .pauseOnExit(true)\n                        .launch();\n                return null;\n            }\n\n            var cmd = sc.command(CommandBuilder.of()\n                    .add(\"dcli\", \"password\", \"--output\", \"console\", \"-o\", \"json\")\n                    .addLiteral(key));\n            var out = cmd.sensitive().readStdoutOrThrow();\n            var tree = JacksonMapper.getDefault().readTree(out);\n            var login = tree.get(\"login\");\n            var password = tree.get(\"password\");\n            return new CredentialResult(\n                    login != null ? login.asText() : null,\n                    password != null ? InPlaceSecretValue.of(password.asText()) : null);\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).handle();\n            return null;\n        }\n    }\n\n    @Override\n    public String getKeyPlaceholder() {\n        return \"Item name\";\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://www.dashlane.com/\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/pwman/EnpassPasswordManager.java",
    "content": "package io.xpipe.app.pwman;\n\nimport io.xpipe.app.comp.base.ContextualFileReferenceChoiceComp;\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.CommandSupport;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.secret.SecretManager;\nimport io.xpipe.app.secret.SecretPromptStrategy;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.util.*;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.InPlaceSecretValue;\nimport io.xpipe.core.JacksonMapper;\n\nimport javafx.application.Platform;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.scene.control.TextField;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Getter;\nimport lombok.ToString;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.nio.file.Files;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\n\n@JsonTypeName(\"enpass\")\n@Getter\n@Builder\n@ToString\n@Jacksonized\npublic class EnpassPasswordManager implements PasswordManager {\n\n    private static final UUID MASTER_PASSWORD_UUID = UUID.randomUUID();\n    private static ShellControl SHELL;\n    private final FilePath vaultPath;\n\n    private static synchronized ShellControl getOrStartShell() throws Exception {\n        if (SHELL == null) {\n            SHELL = ProcessControlProvider.get().createLocalProcessControl(true);\n        }\n        SHELL.start();\n        return SHELL;\n    }\n\n    @SuppressWarnings(\"unused\")\n    public static OptionsBuilder createOptions(Property<EnpassPasswordManager> p) {\n        var prop = new SimpleObjectProperty<>(p.getValue().getVaultPath());\n        var comp = new ContextualFileReferenceChoiceComp(\n                new SimpleObjectProperty<>(DataStorage.get().local().ref()),\n                prop,\n                null,\n                List.of(),\n                e -> e.equals(DataStorage.get().local()),\n                true);\n        comp.apply(struc -> {\n            var text = (TextField) struc.getChildren().getFirst();\n            text.requestFocus();\n            text.setPromptText(AppSystemInfo.ofCurrent()\n                    .getUserHome()\n                    .resolve(\"Documents/Enpass/Vaults/primary/vault.json\")\n                    .toString());\n\n            // Show prompt text, remove focus\n            struc.focusedProperty().addListener((observable, oldValue, newValue) -> {\n                Platform.runLater(() -> {\n                    struc.getParent().requestFocus();\n                });\n            });\n        });\n        comp.maxWidth(600);\n        return new OptionsBuilder()\n                .nameAndDescription(\"enpassVaultFile\")\n                .addComp(comp, prop)\n                .bind(\n                        () -> {\n                            return EnpassPasswordManager.builder()\n                                    .vaultPath(prop.getValue())\n                                    .build();\n                        },\n                        p);\n    }\n\n    @Override\n    public synchronized CredentialResult retrieveCredentials(String key) {\n        try {\n            CommandSupport.isInLocalPathOrThrow(\"Enpass CLI\", \"enpass-cli\");\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e)\n                    .link(\"https://github.com/hazcod/enpass-cli\")\n                    .handle();\n            return null;\n        }\n\n        if (vaultPath == null) {\n            throw ErrorEventFactory.expected(new IllegalArgumentException(\"No vault path has been set\"));\n        }\n\n        var vaultDir = vaultPath.asLocalPath();\n        if (!Files.exists(vaultDir)) {\n            throw ErrorEventFactory.expected(\n                    new IllegalArgumentException(\"Vault path \" + vaultPath + \" does not exist\"));\n        }\n        if (Files.isRegularFile(vaultDir)) {\n            vaultDir = vaultDir.getParent();\n        }\n\n        var pass = SecretManager.retrieve(\n                new SecretPromptStrategy(), \"Enter Enpass vault master password\", MASTER_PASSWORD_UUID, 0, true);\n        if (pass == null) {\n            return null;\n        }\n\n        try {\n            var sc = getOrStartShell();\n            try (var command = sc.command(CommandBuilder.of()\n                            .add(\"enpass-cli\", \"-json\", \"-vault\")\n                            .addFile(vaultDir)\n                            .add(\"show\")\n                            .addQuoted(key))\n                    .sensitive()\n                    .start()) {\n                ThreadHelper.sleep(50);\n                sc.writeLine(pass.getSecretValue());\n                var out = command.readStdoutIfPossible();\n                if (out.isEmpty()) {\n                    return null;\n                }\n\n                var json = JacksonMapper.getDefault()\n                        .readTree(out.get().lines().skip(1).collect(Collectors.joining(\"\\n\")));\n                if (!json.isArray()) {\n                    return null;\n                }\n\n                if (json.size() == 0) {\n                    throw ErrorEventFactory.expected(\n                            new IllegalArgumentException(\"No items were found matching the title \" + key));\n                }\n\n                if (json.size() > 1) {\n                    var matches = new ArrayList<String>();\n                    json.iterator().forEachRemaining(item -> {\n                        var title = item.get(\"title\");\n                        if (title != null) {\n                            matches.add(title.asText());\n                        }\n                    });\n                    throw ErrorEventFactory.expected(new IllegalArgumentException(\n                            \"Ambiguous item name, multiple password entries match: \" + String.join(\", \", matches)));\n                }\n\n                var login = json.get(0).required(\"login\").asText();\n                var secret = json.get(0).required(\"password\").asText();\n                return new CredentialResult(\n                        !login.isEmpty() ? login : null, !secret.isEmpty() ? InPlaceSecretValue.of(secret) : null);\n            }\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).handle();\n            return null;\n        }\n    }\n\n    @Override\n    public String getKeyPlaceholder() {\n        return \"Item title\";\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://www.enpass.io/\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/pwman/KeePassXcAssociationComp.java",
    "content": "package io.xpipe.app.pwman;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.IconButtonComp;\n\nimport javafx.geometry.Insets;\nimport javafx.scene.control.Label;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Priority;\nimport javafx.scene.layout.Region;\n\nimport atlantafx.base.theme.Styles;\n\npublic class KeePassXcAssociationComp extends SimpleRegionBuilder {\n\n    private final KeePassXcAssociationKey associationKey;\n    private final Runnable onRemove;\n\n    public KeePassXcAssociationComp(KeePassXcAssociationKey associationKey, Runnable onRemove) {\n        this.associationKey = associationKey;\n        this.onRemove = onRemove;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var key = associationKey.getKey().getSecretValue();\n        var censoredKey = key.substring(0, 6) + \"*\".repeat(key.length() - 6);\n\n        var nameLabel = new Label(associationKey.getId());\n        nameLabel.getStyleClass().add(Styles.TEXT_BOLD);\n        nameLabel.setPrefWidth(150);\n        var keyLabel = new Label(censoredKey);\n        keyLabel.setMaxWidth(2000);\n        HBox.setHgrow(keyLabel, Priority.ALWAYS);\n        var delButton = new IconButtonComp(\"mdi2t-trash-can-outline\", onRemove).build();\n        var box = new HBox(nameLabel, keyLabel, delButton);\n        box.setSpacing(8);\n        box.setPadding(new Insets(5, 0, 5, 0));\n        return box;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/pwman/KeePassXcAssociationKey.java",
    "content": "package io.xpipe.app.pwman;\n\nimport io.xpipe.core.InPlaceSecretValue;\n\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\n@Value\n@Jacksonized\n@Builder\npublic class KeePassXcAssociationKey {\n    String id;\n    InPlaceSecretValue key;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/pwman/KeePassXcPasswordManager.java",
    "content": "package io.xpipe.app.pwman;\n\nimport io.xpipe.app.comp.base.ButtonComp;\nimport io.xpipe.app.comp.base.ListBoxViewComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.DerivedObservableList;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.util.*;\nimport io.xpipe.core.OsType;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\nimport javafx.collections.FXCollections;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Getter;\nimport lombok.ToString;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.regex.Pattern;\n\n@Getter\n@Builder(toBuilder = true)\n@ToString\n@JsonTypeName(\"keePassXc\")\npublic class KeePassXcPasswordManager implements PasswordManager {\n\n    private static KeePassXcProxyClient client;\n\n    private final List<KeePassXcAssociationKey> associationKeys;\n\n    @SuppressWarnings(\"unused\")\n    public static OptionsBuilder createOptions(Property<KeePassXcPasswordManager> p) {\n        var prop = FXCollections.<KeePassXcAssociationKey>observableArrayList();\n        p.subscribe(keePassXcManager -> {\n            DerivedObservableList.wrap(prop, true)\n                    .setContent(\n                            keePassXcManager != null && keePassXcManager.getAssociationKeys() != null\n                                    ? keePassXcManager.getAssociationKeys()\n                                    : List.of());\n        });\n\n        var associationsListComp =\n                new ListBoxViewComp<>(prop, prop, k -> new KeePassXcAssociationComp(k, () -> prop.remove(k)), false);\n\n        return new OptionsBuilder()\n                .nameAndDescription(\"keePassXcNotAssociated\")\n                .addComp(new ButtonComp(AppI18n.observable(\"keePassXcNotAssociatedButton\"), () -> {\n                    ThreadHelper.runFailableAsync(() -> {\n                        var r = associate();\n                        Platform.runLater(() -> {\n                            prop.add(r);\n                        });\n                    });\n                }))\n                .hide(Bindings.isNotEmpty(prop))\n                .nameAndDescription(\"keePassXcAssociated\")\n                .addComp(associationsListComp)\n                .hide(Bindings.isEmpty(prop))\n                .nameAndDescription(\"keePassXcAssociateMore\")\n                .addComp(new ButtonComp(AppI18n.observable(\"keePassXcNotAssociatedButton\"), () -> {\n                    ThreadHelper.runFailableAsync(() -> {\n                        var r = associate();\n                        Platform.runLater(() -> {\n                            prop.add(r);\n                        });\n                    });\n                }))\n                .hide(Bindings.isEmpty(prop))\n                .addProperty(prop)\n                .bind(\n                        () -> {\n                            return new KeePassXcPasswordManager(prop);\n                        },\n                        p);\n    }\n\n    private static KeePassXcAssociationKey associate() throws IOException {\n        var found = findKeePassProxy();\n        if (found.isEmpty()) {\n            throw ErrorEventFactory.expected(new UnsupportedOperationException(\"No KeePassXC installation was found\"));\n        }\n\n        var c = new KeePassXcProxyClient(found.get());\n        try {\n            c.connect();\n            c.exchangeKeys();\n            var key = c.associate();\n            c.testAssociation(key);\n            return key;\n        } finally {\n            c.disconnect();\n        }\n    }\n\n    public static void reset() {\n        if (client != null) {\n            client.disconnect();\n            client = null;\n        }\n    }\n\n    private synchronized KeePassXcProxyClient getOrCreateClient() throws Exception {\n        if (client == null) {\n            var found = findKeePassProxy();\n            if (found.isEmpty()) {\n                throw ErrorEventFactory.expected(\n                        new UnsupportedOperationException(\"No KeePassXC installation was found\"));\n            }\n\n            var c = new KeePassXcProxyClient(found.get());\n            c.connect();\n            c.exchangeKeys();\n            var pref = AppPrefs.get().passwordManager();\n\n            var available = pref.getValue() instanceof KeePassXcPasswordManager kpm ? kpm.getAssociationKeys() : null;\n            if (available == null) {\n                available = new ArrayList<>();\n            }\n\n            if (!available.isEmpty()) {\n                var valid = false;\n                Exception first = null;\n                for (KeePassXcAssociationKey key : new ArrayList<>(available)) {\n                    try {\n                        c.testAssociation(key);\n                        valid = true;\n                    } catch (Exception e) {\n                        if (first == null) {\n                            first = e;\n                        }\n                    }\n                }\n\n                // Only one association needs to work\n                if (!valid) {\n                    ErrorEventFactory.preconfigure(ErrorEventFactory.fromThrowable(first)\n                            .description(\"KeePassXC association for \"\n                                    + available.getFirst().getId() + \" failed\")\n                            .expected());\n                    throw first;\n                }\n            } else {\n                var key = c.associate();\n                c.testAssociation(key);\n                if (pref.getValue() instanceof KeePassXcPasswordManager kpm) {\n                    AppPrefs.get()\n                            .setFromExternal(\n                                    AppPrefs.get().passwordManager(),\n                                    kpm.toBuilder()\n                                            .associationKeys(List.of(key))\n                                            .build());\n                }\n            }\n\n            client = c;\n        }\n\n        return client;\n    }\n\n    private static Optional<Path> findKeePassProxy() throws IOException {\n        try (var sc = LocalShell.getShell().start()) {\n            var found = sc.view().findProgram(\"keepassxc-proxy\").map(filePath -> filePath.asLocalPath());\n            if (found.isPresent()) {\n                // Symlinks don't work with the proxy\n                return Optional.of(found.get().toRealPath());\n            }\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n        }\n\n        Optional<Path> found =\n                switch (OsType.ofLocal()) {\n                    case OsType.Linux ignored -> {\n                        var paths = List.of(\n                                Path.of(\"/usr/bin/keepassxc-proxy\"),\n                                Path.of(\"/usr/local/bin/keepassxc-proxy\"),\n                                Path.of(\"/snap/keepassxc/current/usr/bin/keepassxc-proxy\"));\n                        yield paths.stream().filter(path -> Files.exists(path)).findFirst();\n                    }\n                    case OsType.MacOs ignored -> {\n                        var paths = List.of(Path.of(\"/Applications/KeePassXC.app/Contents/MacOS/keepassxc-proxy\"));\n                        yield paths.stream().filter(path -> Files.exists(path)).findFirst();\n                    }\n                    case OsType.Windows ignored -> {\n                        try {\n                            var foundKey = WindowsRegistry.local()\n                                    .findKeyForEqualValueMatchRecursive(\n                                            WindowsRegistry.HKEY_LOCAL_MACHINE,\n                                            \"SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\",\n                                            \"https://keepassxc.org\");\n                            if (foundKey.isPresent()) {\n                                var installKey = WindowsRegistry.local()\n                                        .readStringValueIfPresent(\n                                                foundKey.get().getHkey(),\n                                                foundKey.get().getKey(),\n                                                \"InstallLocation\");\n                                if (installKey.isPresent()) {\n                                    yield installKey\n                                            .map(p -> p + \"\\\\keepassxc-proxy.exe\")\n                                            .map(Path::of);\n                                }\n                            }\n                        } catch (Exception e) {\n                            ErrorEventFactory.fromThrowable(e).handle();\n                        }\n                        yield Optional.empty();\n                    }\n                };\n        if (found.isEmpty()) {\n            return Optional.empty();\n        }\n\n        // Symlinks don't work with the proxy\n        var real = found.get().toRealPath();\n        return Optional.of(real);\n    }\n\n    @Override\n    public CredentialResult retrieveCredentials(String key) {\n        try {\n            var hasScheme = Pattern.compile(\"^\\\\w+://\").matcher(key).find();\n            var fixedKey = hasScheme ? key : \"https://\" + key;\n            var isPrefs = this == AppPrefs.get().passwordManager().getValue();\n            var client = getOrCreateClient();\n            // The prefs value might be updated during the client creation\n            var effectiveKeys = isPrefs\n                    ? ((KeePassXcPasswordManager)\n                                    AppPrefs.get().passwordManager().getValue())\n                            .getAssociationKeys()\n                    : associationKeys;\n            var credentials = client.getCredentials(effectiveKeys, fixedKey);\n            return credentials;\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n            return null;\n        }\n    }\n\n    @Override\n    public String getKeyPlaceholder() {\n        return AppI18n.get(\"keePassXcPlaceholder\");\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://keepassxc.org/\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/pwman/KeePassXcProxyClient.java",
    "content": "package io.xpipe.app.pwman;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.InPlaceSecretValue;\nimport io.xpipe.core.JacksonMapper;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.fasterxml.jackson.databind.type.TypeFactory;\nimport lombok.SneakyThrows;\n\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * Client for communicating with KeePassXC using the native messaging protocol.\n * This implementation communicates with the actual running KeePassXC-proxy process\n * via stdin and stdout.\n * <p>\n * Native messaging uses length-prefixed JSON messages over stdin/stdout.\n */\npublic class KeePassXcProxyClient {\n\n    // Default timeouts for different operations (milliseconds)\n    private static final long TIMEOUT_ASSOCIATE = 30000; // Associate needs user interaction\n    private static final long TIMEOUT_GET_LOGINS = 5000; // Getting logins is usually fast\n    private static final long TIMEOUT_TEST_ASSOCIATE = 2000; // Testing association is quick\n\n    private final Path proxyExecutable;\n    private Process process;\n    private String clientId;\n    private TweetNaClHelper.KeyPair keyPair;\n    private byte[] serverPublicKey;\n\n    public KeePassXcProxyClient(Path proxyExecutable) {\n        this.proxyExecutable = proxyExecutable;\n    }\n\n    /**\n     * Extracts the action from a JSON response.\n     *\n     * @param response The JSON response\n     * @return The action, or null if not found\n     */\n    private String extractAction(String response) {\n        Pattern pattern = Pattern.compile(\"\\\"action\\\":\\\"([^\\\"]+)\\\"\");\n        Matcher matcher = pattern.matcher(response);\n\n        if (matcher.find()) {\n            return matcher.group(1);\n        }\n        return null;\n    }\n\n    /**\n     * Connects to KeePassXC via the provided input and output streams.\n     * In a real application, these would be the streams connecting to KeePassXC.\n     *\n     * @throws IOException If there's an error connecting to KeePassXC\n     */\n    public void connect() throws IOException {\n        // Generate a random client ID (24 bytes base64 encoded)\n        this.clientId = TweetNaClHelper.encodeBase64(TweetNaClHelper.randomBytes(TweetNaClHelper.NONCE_SIZE));\n\n        // Generate actual cryptographic keys\n        this.keyPair = TweetNaClHelper.generateKeyPair();\n\n        var pb = new ProcessBuilder(List.of(proxyExecutable.toString()));\n        this.process = pb.start();\n    }\n\n    /**\n     * Performs a key exchange with KeePassXC.\n     *\n     * @throws IOException If there's an error communicating with KeePassXC\n     */\n    public void exchangeKeys() throws IOException {\n        if (process.isAlive()) {\n            // Generate a nonce\n            byte[] nonceBytes = TweetNaClHelper.randomBytes(TweetNaClHelper.NONCE_SIZE);\n            String nonce = TweetNaClHelper.encodeBase64(nonceBytes);\n\n            // Convert our public key to base64\n            String publicKeyB64 = TweetNaClHelper.encodeBase64(keyPair.getPublicKey());\n\n            // Build the key exchange message - NOTE: This is NOT encrypted\n            String requestId = UUID.randomUUID().toString();\n            Map<String, Object> messageMap = new HashMap<>();\n            messageMap.put(\"action\", \"change-public-keys\");\n            messageMap.put(\"publicKey\", publicKeyB64);\n            messageMap.put(\"nonce\", nonce);\n            messageMap.put(\"clientID\", clientId);\n            messageMap.put(\"requestId\", requestId);\n\n            // Convert to JSON string\n            String keyExchangeMessage = mapToJson(messageMap);\n\n            // Send the message directly\n            long startTime = System.currentTimeMillis();\n            try {\n                sendNativeMessage(keyExchangeMessage);\n            } catch (IOException e) {\n                var ex = new IllegalStateException(\n                        \"KeePassXC client did not respond. Is the browser integration enabled for your KeePassXC database?\",\n                        e);\n                ErrorEventFactory.preconfigure(\n                        ErrorEventFactory.fromThrowable(ex).expected().documentationLink(DocumentationLink.KEEPASSXC));\n                throw ex;\n            }\n\n            // Wait for a direct response rather than using CompletableFuture\n            // This is a special case because we can't use the encryption yet\n            long timeout = 3000; // 3 seconds for key exchange\n\n            while (System.currentTimeMillis() - startTime < timeout) {\n                if (process.getInputStream().available() > 0) {\n                    String response = receiveNativeMessage();\n                    if (response.contains(\"change-public-keys\")) {\n                        // Use regex to extract the public key to avoid any JSON parsing issues\n                        Pattern pattern = Pattern.compile(\"\\\"publicKey\\\":\\\"([^\\\"]+)\\\"\");\n                        Matcher matcher = pattern.matcher(response);\n\n                        if (matcher.find()) {\n                            String serverPubKeyB64 = matcher.group(1);\n\n                            // Store the server's public key\n                            this.serverPublicKey = TweetNaClHelper.decodeBase64(serverPubKeyB64);\n\n                            // Check for success in the response\n                            boolean success = response.contains(\"\\\"success\\\":\\\"true\\\"\");\n                            if (!success) {\n                                throw new IllegalStateException(\"Key exchange failed\");\n                            } else {\n                                return;\n                            }\n                        }\n                    }\n                }\n\n                // Small sleep to prevent CPU hogging\n                ThreadHelper.sleep(50);\n            }\n        }\n\n        var ex = new IllegalStateException(\n                \"KeePassXC client did not respond. Is the browser integration enabled for your KeePassXC database?\");\n        ErrorEventFactory.preconfigure(\n                ErrorEventFactory.fromThrowable(ex).expected().documentationLink(DocumentationLink.KEEPASSXC));\n        throw ex;\n    }\n\n    /**\n     * Tests the association with KeePassXC.\n     *\n     * @throws IOException If there's an error communicating with KeePassXC\n     */\n    public void testAssociation(KeePassXcAssociationKey associationKey) throws IOException {\n        if (associationKey == null) {\n            // We need to do an association first\n            throw ErrorEventFactory.expected(\n                    new IllegalStateException(\"KeePassXC association failed or was cancelled\"));\n        }\n\n        // Generate a nonce\n        String nonce = TweetNaClHelper.encodeBase64(TweetNaClHelper.randomBytes(TweetNaClHelper.NONCE_SIZE));\n\n        // Create the unencrypted message\n        Map<String, Object> messageData = new HashMap<>();\n        messageData.put(\"action\", \"test-associate\");\n        messageData.put(\"id\", associationKey.getId());\n        messageData.put(\"key\", associationKey.getKey().getSecretValue());\n\n        // Encrypt the message\n        String encryptedMessage = encrypt(messageData, nonce);\n        if (encryptedMessage == null) {\n            throw new IllegalStateException(\"Failed to encrypt test-associate message\");\n        }\n\n        // Build the request\n        Map<String, Object> request = new HashMap<>();\n        request.put(\"action\", \"test-associate\");\n        request.put(\"message\", encryptedMessage);\n        request.put(\"nonce\", nonce);\n        request.put(\"clientID\", clientId);\n\n        String requestJson = mapToJson(request);\n\n        // Send the request\n        String responseJson = sendRequest(\"test-associate\", requestJson, TIMEOUT_TEST_ASSOCIATE);\n\n        // Parse and decrypt the response\n        Map<String, Object> responseMap = jsonToMap(responseJson);\n\n        if (responseMap.containsKey(\"error\")) {\n            throw ErrorEventFactory.expected(\n                    new IllegalStateException(responseMap.get(\"error\").toString()));\n        }\n\n        if (responseMap.containsKey(\"message\") && responseMap.containsKey(\"nonce\")) {\n            String encryptedResponse = (String) responseMap.get(\"message\");\n            String responseNonce = (String) responseMap.get(\"nonce\");\n\n            String decryptedResponse = decrypt(encryptedResponse, responseNonce);\n            Map<String, Object> parsedResponse = jsonToMap(decryptedResponse);\n            boolean success = parsedResponse.containsKey(\"success\")\n                    && \"true\".equals(parsedResponse.get(\"success\").toString());\n            if (!success) {\n                throw new IllegalStateException(\"KeePassXC association failed\");\n            }\n        }\n    }\n\n    /**\n     * Retrieves credentials from KeePassXC.\n     *\n     * @param url The URL to get credentials for\n     * @return The response JSON, or null if failed\n     * @throws IOException If there's an error communicating with KeePassXC\n     */\n    private String getLoginsMessage(List<KeePassXcAssociationKey> associationKeys, String url) throws IOException {\n        // Generate a nonce\n        String nonce = TweetNaClHelper.encodeBase64(TweetNaClHelper.randomBytes(TweetNaClHelper.NONCE_SIZE));\n\n        // Create the unencrypted message\n        Map<String, Object> messageData = new HashMap<>();\n        messageData.put(\"action\", \"get-logins\");\n        messageData.put(\"url\", url);\n\n        // Add the keys\n        var keyArray = new Object[associationKeys.size()];\n        for (int i = 0; i < associationKeys.size(); i++) {\n            Map<String, Object> keyData = new HashMap<>();\n            keyData.put(\"id\", associationKeys.get(i).getId());\n            keyData.put(\"key\", associationKeys.get(i).getKey().getSecretValue());\n            keyArray[i] = keyData;\n        }\n        messageData.put(\"keys\", keyArray);\n\n        // Encrypt the message\n        String encryptedMessage = encrypt(messageData, nonce);\n\n        // Build the request\n        Map<String, Object> request = new HashMap<>();\n        request.put(\"action\", \"get-logins\");\n        request.put(\"message\", encryptedMessage);\n        request.put(\"nonce\", nonce);\n        request.put(\"clientID\", clientId);\n\n        String requestJson = mapToJson(request);\n\n        // Send the request\n        String responseJson = sendRequest(\"get-logins\", requestJson, TIMEOUT_GET_LOGINS);\n\n        Map<String, Object> responseMap = jsonToMap(responseJson);\n        if (responseMap.containsKey(\"error\")) {\n            throw ErrorEventFactory.expected(\n                    new IllegalStateException(responseMap.get(\"error\").toString()));\n        }\n\n        if (responseMap.containsKey(\"message\") && responseMap.containsKey(\"nonce\")) {\n            String encryptedResponse = (String) responseMap.get(\"message\");\n            String responseNonce = (String) responseMap.get(\"nonce\");\n            return decrypt(encryptedResponse, responseNonce);\n        }\n\n        throw new IllegalStateException(\"Login query failed for an unknown reason\");\n    }\n\n    public PasswordManager.CredentialResult getCredentials(List<KeePassXcAssociationKey> associationKeys, String key)\n            throws IOException {\n        var message = getLoginsMessage(associationKeys, key);\n        var tree = JacksonMapper.getDefault().readTree(message);\n        var count = tree.required(\"count\").asInt();\n        if (count == 0) {\n            throw ErrorEventFactory.expected(new IllegalArgumentException(\"No password was found for specified key\"));\n        }\n\n        if (count > 1) {\n            var entries = tree.required(\"entries\");\n            var names = new ArrayList<String>();\n            for (JsonNode entry : entries) {\n                names.add(entry.required(\"name\").textValue());\n            }\n            throw ErrorEventFactory.expected(new IllegalArgumentException(\n                    \"Password key is ambiguous and returned multiple entries: \" + String.join(\", \", names)));\n        }\n\n        var object = (ObjectNode) tree.required(\"entries\").get(0);\n        var usernameField = object.required(\"login\").asText();\n        var passwordField = object.required(\"password\").asText();\n        return new PasswordManager.CredentialResult(\n                usernameField.isEmpty() ? null : usernameField,\n                passwordField.isEmpty() ? null : InPlaceSecretValue.of(passwordField));\n    }\n\n    /**\n     * Disconnects from KeePassXC.\n     */\n    public void disconnect() {\n        if (process != null) {\n            process.destroy();\n        }\n        process = null;\n    }\n\n    /**\n     * Sends a request to KeePassXC and waits for a response.\n     *\n     * @param action  The action being performed (e.g., \"associate\", \"get-logins\")\n     * @param message The JSON message to send\n     * @param timeout The timeout in milliseconds\n     * @return The response JSON, or null if timed out\n     * @throws IOException If there's an error communicating with KeePassXC\n     */\n    private String sendRequest(String action, String message, long timeout) throws IOException {\n        // Send the message\n        sendNativeMessage(message);\n\n        long startTime = System.currentTimeMillis();\n        while (System.currentTimeMillis() - startTime < timeout) {\n            var response = receiveNativeMessage();\n            if (filterResponse(action, response)) {\n                continue;\n            }\n\n            return response;\n        }\n        throw new IllegalStateException(\"KeePassXC \" + action + \" request timed out\");\n    }\n\n    private boolean filterResponse(String action, String response) {\n        // Extract action\n        String extractedAction = extractAction(response);\n\n        // Special handling for action-specific responses\n        if (\"database-locked\".equals(extractedAction) || \"database-unlocked\".equals(extractedAction)) {\n            // Update state based on the action\n            if (\"database-locked\".equals(extractedAction)) {\n                return true;\n            }\n        }\n\n        if (action.equals(extractedAction)) {\n            return false;\n        } else {\n            return true;\n        }\n    }\n\n    /**\n     * Sends a message to KeePassXC using the native messaging protocol.\n     * The message is prefixed with a 32-bit length (little-endian).\n     *\n     * @param message The JSON message to send\n     * @throws IOException If there's an error writing to the stream\n     */\n    private void sendNativeMessage(String message) throws IOException {\n        byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);\n        int length = messageBytes.length;\n\n        // Create a ByteBuffer with length in little-endian format\n        ByteBuffer lengthBuffer = ByteBuffer.allocate(4);\n        lengthBuffer.order(ByteOrder.LITTLE_ENDIAN);\n        lengthBuffer.putInt(length);\n\n        process.getOutputStream().write(lengthBuffer.array());\n        process.getOutputStream().write(messageBytes);\n        process.getOutputStream().flush();\n    }\n\n    /**\n     * Receives a message from KeePassXC using the native messaging protocol.\n     * The message is prefixed with a 32-bit length (little-endian).\n     *\n     * @return The received JSON message as a string, or null if reading failed\n     * @throws IOException If there's an error reading from the stream\n     */\n    private String receiveNativeMessage() throws IOException {\n        // Read the length prefix (4 bytes, little-endian)\n        byte[] lengthBytes = new byte[4];\n        int bytesRead = process.getInputStream().read(lengthBytes);\n        if (bytesRead != 4) {\n            throw new IOException(\"Error reading received message\");\n        }\n\n        // Convert bytes to integer (little-endian)\n        ByteBuffer lengthBuffer = ByteBuffer.wrap(lengthBytes);\n        lengthBuffer.order(ByteOrder.LITTLE_ENDIAN);\n        int messageLength = lengthBuffer.getInt();\n\n        // Read the actual message\n        byte[] messageBytes = new byte[messageLength];\n        var read = process.getInputStream().read(messageBytes);\n        if (read != messageLength) {\n            throw new IOException(\"Received message with \" + read + \" bytes but expected \" + messageBytes.length);\n        }\n\n        return new String(messageBytes, StandardCharsets.UTF_8);\n    }\n\n    /**\n     * Encrypts a message for sending to KeePassXC.\n     *\n     * @param message The message to encrypt\n     * @param nonce   The nonce to use for encryption\n     * @return The encrypted message, or null if encryption failed\n     */\n    private String encrypt(Map<String, Object> message, String nonce) {\n        String messageJson = mapToJson(message);\n        byte[] messageBytes = messageJson.getBytes(StandardCharsets.UTF_8);\n        byte[] nonceBytes = TweetNaClHelper.decodeBase64(nonce);\n\n        byte[] encrypted = TweetNaClHelper.box(messageBytes, nonceBytes, serverPublicKey, keyPair.getSecretKey());\n\n        return TweetNaClHelper.encodeBase64(encrypted);\n    }\n\n    /**\n     * Decrypts a message received from KeePassXC.\n     *\n     * @param encryptedMessage The encrypted message\n     * @param nonce            The nonce used for encryption\n     * @return The decrypted message, or null if decryption failed\n     */\n    private String decrypt(String encryptedMessage, String nonce) {\n        byte[] messageBytes = TweetNaClHelper.decodeBase64(encryptedMessage);\n        byte[] nonceBytes = TweetNaClHelper.decodeBase64(nonce);\n\n        byte[] decrypted = TweetNaClHelper.boxOpen(messageBytes, nonceBytes, serverPublicKey, keyPair.getSecretKey());\n\n        if (decrypted == null) {\n            throw new IllegalArgumentException(\"Message decryption failed\");\n        }\n\n        return new String(decrypted, StandardCharsets.UTF_8);\n    }\n\n    /**\n     * Associate with KeePassXC.\n     *\n     * @throws IOException If there's an error communicating with KeePassXC\n     */\n    public KeePassXcAssociationKey associate() throws IOException {\n        // Generate a key pair for identification\n        TweetNaClHelper.KeyPair idKeyPair = TweetNaClHelper.generateKeyPair();\n\n        // Generate a nonce\n        String nonce = TweetNaClHelper.encodeBase64(TweetNaClHelper.randomBytes(TweetNaClHelper.NONCE_SIZE));\n\n        // Create the unencrypted message\n        Map<String, Object> messageData = new HashMap<>();\n        messageData.put(\"action\", \"associate\");\n        messageData.put(\"key\", TweetNaClHelper.encodeBase64(keyPair.getPublicKey()));\n        messageData.put(\"idKey\", TweetNaClHelper.encodeBase64(idKeyPair.getPublicKey()));\n\n        // Encrypt the message\n        String encryptedMessage = encrypt(messageData, nonce);\n\n        // Build the request\n        Map<String, Object> request = new HashMap<>();\n        request.put(\"action\", \"associate\");\n        request.put(\"message\", encryptedMessage);\n        request.put(\"nonce\", nonce);\n        request.put(\"clientID\", clientId);\n\n        String requestJson = mapToJson(request);\n        // Send the request using longer timeout as it requires user interaction\n        String responseJson = sendRequest(\"associate\", requestJson, TIMEOUT_ASSOCIATE);\n\n        Map<String, Object> responseMap = jsonToMap(responseJson);\n\n        if (responseMap.containsKey(\"error\")) {\n            throw ErrorEventFactory.expected(\n                    new IllegalStateException(responseMap.get(\"error\").toString()));\n        }\n\n        if (responseMap.containsKey(\"message\") && responseMap.containsKey(\"nonce\")) {\n            String encryptedResponse = (String) responseMap.get(\"message\");\n            String responseNonce = (String) responseMap.get(\"nonce\");\n\n            String decryptedResponse = decrypt(encryptedResponse, responseNonce);\n            Map<String, Object> parsedResponse = jsonToMap(decryptedResponse);\n            boolean success = parsedResponse.containsKey(\"success\")\n                    && \"true\".equals(parsedResponse.get(\"success\").toString());\n\n            if (success && parsedResponse.containsKey(\"id\") && parsedResponse.containsKey(\"hash\")) {\n                String id = (String) parsedResponse.get(\"id\");\n                var key = InPlaceSecretValue.of(TweetNaClHelper.encodeBase64(idKeyPair.getPublicKey()));\n                return new KeePassXcAssociationKey(id, key);\n            }\n        }\n\n        throw new IllegalStateException(\"KeePassXC association failed\");\n    }\n\n    /**\n     * Convert a map to a JSON string.\n     */\n    @SneakyThrows\n    private String mapToJson(Map<String, Object> map) {\n        var mapper = JacksonMapper.getDefault();\n        return mapper.writeValueAsString(map);\n    }\n\n    /**\n     * Convert a JSON string to a map.\n     */\n    @SneakyThrows\n    private Map<String, Object> jsonToMap(String json) {\n        var mapper = JacksonMapper.getDefault();\n        var type = TypeFactory.defaultInstance().constructType(new TypeReference<>() {});\n        Map<String, Object> map = mapper.readValue(json, type);\n        return map;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java",
    "content": "package io.xpipe.app.pwman;\n\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.platform.OptionsChoiceBuilder;\nimport io.xpipe.app.process.*;\nimport io.xpipe.app.secret.SecretManager;\nimport io.xpipe.app.secret.SecretPromptStrategy;\nimport io.xpipe.app.secret.SecretQueryState;\nimport io.xpipe.app.terminal.TerminalLaunch;\nimport io.xpipe.app.util.AskpassAlert;\nimport io.xpipe.core.*;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.property.SimpleStringProperty;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport lombok.Builder;\nimport lombok.Getter;\nimport lombok.ToString;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Random;\nimport java.util.UUID;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\n\n@JsonTypeName(\"keeper\")\n@Getter\n@Builder(toBuilder = true)\n@ToString\n@Jacksonized\npublic class KeeperPasswordManager implements PasswordManager {\n\n    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\n    public interface KeeperAuth {\n\n        static List<Class<?>> getClasses() {\n            var l = new ArrayList<Class<?>>();\n            l.add(None.class);\n            l.add(Sms.class);\n            l.add(AuthenticatorApp.class);\n            l.add(SecurityKey.class);\n            l.add(Other.class);\n            return l;\n        }\n\n\n        default List<String> getTotpDurationValues() {\n            var values = List.of(\"login\", \"12_hours\", \"24_hours\", \"30_days\", \"forever\");\n            return values;\n        }\n\n        String constructKeeperInput(KeeperPasswordManager passwordManager) throws Exception;\n\n        Duration getCacheDuration();\n\n        Duration getCommandTimeout();\n\n        String cleanMessage(String output);\n\n        @JsonTypeName(\"sms\")\n        @Value\n        @Jacksonized\n        @Builder\n        class Sms implements KeeperAuth {\n\n            @SuppressWarnings(\"unused\")\n            public static OptionsBuilder createOptions(Property<Sms> p) {\n                var duration = new SimpleStringProperty(p.getValue().getTotpDuration());\n                return new OptionsBuilder()\n                        .name(\"keeperTotpDuration\")\n                        .description(AppI18n.observable(\n                                \"keeperTotpDurationDescription\", \"login | 12_hours | 24_hours | 30_days | forever\"))\n                        .addString(duration)\n                        .bind(\n                                () -> {\n                                    return Sms.builder()\n                                            .totpDuration(duration.get())\n                                            .build();\n                                },\n                                p);\n            }\n\n            String totpDuration;\n\n            private int getTotpDurationIndex() {\n                var values = getTotpDurationValues();\n                var index = totpDuration != null ? values.indexOf(totpDuration) : -1;\n                return index;\n            }\n\n            private void sendInitialSms() throws Exception {\n                var sc = getOrStartShell();\n                var b = CommandBuilder.of()\n                        .add(getExecutable(), \"get\")\n                        .addLiteral(\"test\");\n                var file = sc.getSystemTemporaryDirectory().join(\"keeper\" + Math.abs(new Random().nextInt()) + \".txt\");\n                var input = \"\"\"\n                            \n                            1\n                            -\n                            q\n                            \"\"\";\n                sc.view().writeTextFile(file, input);\n\n                var fullCommand = CommandBuilder.of()\n                        .add(sc.getShellDialect() == ShellDialects.CMD ? \"type\" : \"cat\")\n                        .addFile(file)\n                        .add(\"|\")\n                        .add(b);\n                sc.command(fullCommand).sensitive().execute();\n            }\n\n            @Override\n            public String constructKeeperInput(KeeperPasswordManager passwordManager) throws Exception {\n                sendInitialSms();\n\n                var index = getTotpDurationIndex();\n                if (passwordManager.isHasCompletedRequestInSession() && index > 0) {\n                    var input = \"\"\"\n\n                          1\n\n                          \"\"\";\n                    return input;\n                } else {\n                    var totp = AskpassAlert.queryRaw(\"Enter Keeper Commander SMS Code\", null, true);\n                    if (totp.getState() != SecretQueryState.NORMAL) {\n                        return null;\n                    }\n\n                    var input = \"\"\"\n\n                                1%s\n                                %s\n\n                                \"\"\".formatted(\n                            index != -1 ? \"\\n\" + getTotpDurationValues().get(index) : \"\",\n                            totp.getSecret().getSecretValue());\n                    return input;\n                }\n            }\n\n            @Override\n            public Duration getCacheDuration() {\n                return getTotpDurationIndex() < 1 ? Duration.ofDays(1) : Duration.ofSeconds(30);\n            }\n\n            @Override\n            public Duration getCommandTimeout() {\n                return Duration.ofSeconds(25);\n            }\n\n            @Override\n            public String cleanMessage(String output) {\n                return output\n                        .replaceFirst(\"\"\"\n                             Select your 2FA method:\n                               1. Send SMS Code.+\n                               q. Cancel login\n                             \"\"\", \"\")\n                        .replace(\" Invalid entry, additional factors of authentication shown may be configured if not currently enabled.\", \"\")\n                        .replace(\"\"\"\n                                2FA Code Duration: Require Every Login.\n                                To change duration: 2fa_duration=login|12_hours|24_hours|30_days|forever\n                                \"\"\", \"\");\n            }\n        }\n\n        @JsonTypeName(\"authenticatorApp\")\n        @Value\n        @Jacksonized\n        @Builder\n        class AuthenticatorApp implements KeeperAuth {\n\n            @SuppressWarnings(\"unused\")\n            public static OptionsBuilder createOptions(Property<AuthenticatorApp> p) {\n                var duration = new SimpleStringProperty(p.getValue().getTotpDuration());\n                return new OptionsBuilder()\n                        .name(\"keeperTotpDuration\")\n                        .description(AppI18n.observable(\n                                \"keeperTotpDurationDescription\", \"login | 12_hours | 24_hours | 30_days | forever\"))\n                        .addString(duration)\n                        .bind(\n                                () -> {\n                                    return AuthenticatorApp.builder()\n                                            .totpDuration(duration.get())\n                                            .build();\n                                },\n                                p);\n            }\n\n            String totpDuration;\n\n            private int getTotpDurationIndex() {\n                var values = getTotpDurationValues();\n                var index = totpDuration != null ? values.indexOf(totpDuration) : -1;\n                return index;\n            }\n\n            @Override\n            public String constructKeeperInput(KeeperPasswordManager passwordManager) {\n                var index = getTotpDurationIndex();\n                if (passwordManager.isHasCompletedRequestInSession() && index > 0) {\n                    var input = \"\"\"\n\n                          1\n\n                          \"\"\";\n                    return input;\n                } else {\n                    var totp = AskpassAlert.queryRaw(\"Enter Keeper 2FA Code\", null, true);\n                    if (totp.getState() != SecretQueryState.NORMAL) {\n                        return null;\n                    }\n\n                    var input = \"\"\"\n\n                                1%s\n                                %s\n\n                                \"\"\".formatted(\n                            index != -1 ? \"\\n\" + getTotpDurationValues().get(index) : \"\",\n                            totp.getSecret().getSecretValue());\n                    return input;\n                }\n            }\n\n            @Override\n            public Duration getCacheDuration() {\n                return getTotpDurationIndex() < 1 ? Duration.ofDays(1) : Duration.ofSeconds(30);\n            }\n\n            @Override\n            public Duration getCommandTimeout() {\n                return Duration.ofSeconds(25);\n            }\n\n            @Override\n            public String cleanMessage(String output) {\n                return output.replace(\"\"\"\n                             Select your 2FA method:\n                               1. TOTP (Google and Microsoft Authenticator) \\s\n                               q. Cancel login\n                             \"\"\", \"\")\n                        .replace(\n                        \"\"\"\n                        Selection: Invalid entry, additional factors of authentication shown may be configured if not currently enabled.\n                        Selection:\\s\n                        2FA Code Duration: Require Every Login.\n                        To change duration: 2fa_duration=login|12_hours|24_hours|30_days|forever\n                        \"\"\", \"\")\n                        .replace(\n                        \"\"\"\n                        This account requires 2FA Authentication\n\n                          1. TOTP (Google and Microsoft Authenticator) \\s\n                          q. Quit login attempt and return to Commander prompt\n                        \"\"\", \"\");\n            }\n\n        }\n\n        @JsonTypeName(\"securityKey\")\n        @Value\n        @Jacksonized\n        @Builder\n        class SecurityKey implements KeeperAuth {\n\n            @Override\n            public String constructKeeperInput(KeeperPasswordManager passwordManager) {\n                var input = \"\"\"\n\n                          1\n\n                          \"\"\";\n                return input;\n            }\n\n            @Override\n            public Duration getCacheDuration() {\n                return Duration.ofDays(1);\n            }\n\n            @Override\n            public Duration getCommandTimeout() {\n                return null;\n            }\n\n            @Override\n            public String cleanMessage(String output) {\n                return output.replace(\"\"\"\n                               Select your 2FA method:\n                                 1. WebAuthN (FIDO2 Security Key) \\s\n                                 q. Cancel login\n                               \"\"\", \"\")\n                        .replace(\" Invalid entry, additional factors of authentication shown may be configured if not currently enabled.\", \"\");\n            }\n        }\n\n\n        @JsonTypeName(\"other\")\n        @Value\n        @Jacksonized\n        @Builder\n        class Other implements KeeperAuth {\n\n            @SuppressWarnings(\"unused\")\n            public static String getOptionsNameKey() {\n                return \"keeperOtherAuth\";\n            }\n\n            @Override\n            public Duration getCommandTimeout() {\n                return null;\n            }\n\n            @Override\n            public String cleanMessage(String output) {\n                return output;\n            }\n\n            @Override\n            public String constructKeeperInput(KeeperPasswordManager passwordManager) {\n                var input = \"\"\"\n\n                          1\n\n                          \"\"\";\n                return input;\n            }\n\n            @Override\n            public Duration getCacheDuration() {\n                return Duration.ofDays(1);\n            }\n        }\n\n        @JsonTypeName(\"none\")\n        @Value\n        @Jacksonized\n        @Builder\n        class None implements KeeperAuth {\n\n            @Override\n            public Duration getCommandTimeout() {\n                return Duration.ofSeconds(25);\n            }\n\n            @Override\n            public String cleanMessage(String output) {\n                return output;\n            }\n\n            @Override\n            public String constructKeeperInput(KeeperPasswordManager passwordManager) {\n                var input = \"\"\"\n\n                          1\n\n                          \"\"\";\n                return input;\n            }\n\n            @Override\n            public Duration getCacheDuration() {\n                return Duration.ofSeconds(30);\n            }\n        }\n    }\n\n    private static final UUID KEEPER_PASSWORD_ID = UUID.randomUUID();\n    private static ShellControl SHELL;\n    private final KeeperAuth twoFactorAuth;\n    @JsonIgnore\n    private boolean hasCompletedRequestInSession;\n\n    private static synchronized ShellControl getOrStartShell() throws Exception {\n        if (SHELL == null) {\n            SHELL = ProcessControlProvider.get().createLocalProcessControl(true);\n        }\n        SHELL.start();\n        return SHELL;\n    }\n\n    private static String getExecutable() {\n        return OsType.ofLocal() == OsType.WINDOWS ? \"keeper-commander\" : \"keeper\";\n    }\n\n    @SuppressWarnings(\"unused\")\n    public static OptionsBuilder createOptions(Property<KeeperPasswordManager> p) {\n        var mfa = new SimpleObjectProperty<>(p.getValue().getTwoFactorAuth() != null ? p.getValue().getTwoFactorAuth() : new KeeperAuth.None());\n\n        var choice = OptionsChoiceBuilder.builder()\n                .allowNull(false)\n                .available(KeeperAuth.getClasses())\n                .property(mfa)\n                .build();\n\n        return new OptionsBuilder()\n                .nameAndDescription(\"keeper2fa\")\n                .sub(choice.build(), mfa)\n                .bind(\n                        () -> {\n                            return KeeperPasswordManager.builder()\n                                    .twoFactorAuth(mfa.get())\n                                    .build();\n                        },\n                        p);\n    }\n\n    @Override\n    public synchronized CredentialResult retrieveCredentials(String key) {\n        // The copy UID button copies the whole URL in the Keeper UI. Why? ...\n        key = key.replaceFirst(\"https://\\\\w+\\\\.\\\\w+/vault/#detail/\", \"\");\n\n        try {\n            CommandSupport.isInLocalPathOrThrow(\"Keeper Commander CLI\", \"keeper-commander\");\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e)\n                    .link(\"https://docs.keeper.io/en/keeperpam/commander-cli/commander-installation-setup\")\n                    .handle();\n            return null;\n        }\n\n        try {\n            var sc = getOrStartShell();\n            var config = sc.view().userHome().join(\".keeper\", \"config.json\");\n            if (!sc.view().fileExists(config)) {\n                var script = ShellScript.lines(\n                        sc.getShellDialect().getEchoCommand(\"Log in into your Keeper account from the CLI:\", false),\n                        getExecutable() + \" login\");\n                TerminalLaunch.builder()\n                        .title(\"Keeper login\")\n                        .localScript(script)\n                        .logIfEnabled(false)\n                        .pauseOnExit(true)\n                        .launch();\n                return null;\n            }\n\n            var r = SecretManager.retrieve(\n                    new SecretPromptStrategy(),\n                    \"Enter your Keeper master password to unlock\",\n                    KEEPER_PASSWORD_ID,\n                    0,\n                    true);\n            if (r == null) {\n                return null;\n            }\n\n            if (r.getSecretValue().contains(\"\\\"\")) {\n                SecretManager.clearAll(KEEPER_PASSWORD_ID);\n                throw ErrorEventFactory.expected(\n                        new IllegalArgumentException(\n                                \"Keeper password contains double quote \\\" character, which is not supported by the Keeper Commander application\"));\n            }\n\n            var b = CommandBuilder.of()\n                    .add(getExecutable(), \"get\")\n                    .addLiteral(key)\n                    .add(\"--format\", \"json\", \"--unmask\")\n                    .add(\"--password\")\n                    .addLiteral(r.getSecretValue());\n            FilePath file = sc.getSystemTemporaryDirectory().join(\"keeper\" + Math.abs(new Random().nextInt()) + \".txt\");\n\n            var effectiveTwoFactor = twoFactorAuth != null ? twoFactorAuth : new KeeperAuth.None();\n            var input = effectiveTwoFactor.constructKeeperInput(this);\n            if (input == null) {\n                return null;\n            }\n            sc.view().writeTextFile(file, input);\n\n            var fullB = CommandBuilder.of()\n                    .add(sc.getShellDialect() == ShellDialects.CMD ? \"type\" : \"cat\")\n                    .addFile(file)\n                    .add(\"|\")\n                    .add(b);\n            var queryCommand = sc.command(fullB);\n            queryCommand.sensitive();\n\n            if (effectiveTwoFactor.getCommandTimeout() != null) {\n                var timeout = effectiveTwoFactor.getCommandTimeout().toMillis();\n                queryCommand.killOnTimeout(CountDown.of().start(timeout));\n            }\n\n            var result = queryCommand.readStdoutAndStderr();\n            var exitCode = queryCommand.getExitCode();\n\n            sc.view().deleteFileIfPossible(file);\n\n            var out = result[0]\n                    .replace(\"\\r\\n\", \"\\n\");\n            out = effectiveTwoFactor.cleanMessage(out);\n            out = out.replace(\"Selection:\", \"\")\n                    .strip();\n\n            var err = result[1]\n                    .replace(\"\\r\\n\", \"\\n\")\n                    .replace(\"EOF when reading a line\", \"\")\n                    .strip();\n\n            var jsonStart = out.indexOf(\"{\\n\");\n            var jsonEnd = out.indexOf(\"\\n}\");\n            if (jsonEnd != -1) {\n                jsonEnd += 2;\n            }\n\n            var outPrefix = jsonStart <= 0 ? out : out.substring(0, jsonStart);\n            outPrefix = outPrefix.lines().filter(s -> !s.isBlank()).map(s -> s.strip()).collect(Collectors.joining(\"\\n\"));\n\n            var outJson = jsonStart <= 0\n                    ? (jsonEnd != -1 ? out.substring(0, jsonEnd) : out)\n                    : (jsonEnd != -1 ? out.substring(jsonStart, jsonEnd) : out.substring(jsonStart));\n\n            if (exitCode != 0) {\n                // Another password prompt was made\n                var wrongPw =\n                        (outPrefix.contains(\"Enter password for\") || exitCode == CommandControl.EXIT_TIMEOUT_EXIT_CODE)\n                                && !hasCompletedRequestInSession;\n                if (wrongPw) {\n                    SecretManager.clearAll(KEEPER_PASSWORD_ID);\n                    ErrorEventFactory.fromMessage(\"Master password was not accepted by Keeper. Is it correct?\")\n                            .expected()\n                            .handle();\n                    return null;\n                }\n\n                var message = !err.isEmpty() ? outPrefix + \"\\n\" + err : outPrefix;\n                ErrorEventFactory.fromMessage(message).expected().handle();\n                return null;\n            }\n\n            JsonNode tree;\n            try {\n                tree = JacksonMapper.getDefault().readTree(outJson);\n            } catch (JsonProcessingException e) {\n                var message = !err.isEmpty() ? outPrefix + \"\\n\" + err : outPrefix;\n                ErrorEventFactory.fromMessage(message).expected().handle();\n                return null;\n            }\n\n            hasCompletedRequestInSession = true;\n\n            var fields = tree.get(\"fields\");\n            // There multiple schemas\n            if (fields == null || !fields.isArray()) {\n                String login = null;\n                String password = null;\n\n                var l = tree.get(\"login\");\n                if (l != null && l.isTextual()) {\n                    login = l.asText();\n                }\n\n                var p = tree.get(\"password\");\n                if (p != null && p.isTextual()) {\n                    password = p.asText();\n                }\n\n                if (login == null && password == null) {\n                    var message = !err.isEmpty() ? out + \"\\n\" + err : out;\n                    ErrorEventFactory.fromMessage(message)\n                            .description(\"Received invalid response\")\n                            .expected()\n                            .handle();\n                    return null;\n                }\n\n                return new CredentialResult(login, password != null ? InPlaceSecretValue.of(password) : null);\n            }\n\n            String login = null;\n            String password = null;\n            for (JsonNode field : fields) {\n                var type = field.required(\"type\").asText();\n                if (type.equals(\"login\")) {\n                    var v = field.required(\"value\");\n                    if (v.size() > 0) {\n                        login = v.get(0).asText();\n                    }\n                }\n                if (type.equals(\"password\")) {\n                    var v = field.required(\"value\");\n                    if (v.size() > 0) {\n                        password = v.get(0).asText();\n                    }\n                }\n            }\n\n            return new CredentialResult(login, password != null ? InPlaceSecretValue.of(password) : null);\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).handle();\n            return null;\n        }\n    }\n\n    @Override\n    public String getKeyPlaceholder() {\n        return \"Record UID\";\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://www.keepersecurity.com\";\n    }\n\n    @Override\n    public Duration getCacheDuration() {\n        var effectiveTwoFactor = twoFactorAuth != null ? twoFactorAuth : new KeeperAuth.None();\n        return effectiveTwoFactor.getCacheDuration();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/pwman/LastpassPasswordManager.java",
    "content": "package io.xpipe.app.pwman;\n\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.CommandSupport;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.app.terminal.TerminalLaunch;\nimport io.xpipe.app.util.*;\nimport io.xpipe.core.InPlaceSecretValue;\nimport io.xpipe.core.JacksonMapper;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\n\nimport java.util.ArrayList;\n\n@JsonTypeName(\"lastpass\")\npublic class LastpassPasswordManager implements PasswordManager {\n\n    private static ShellControl SHELL;\n\n    private static synchronized ShellControl getOrStartShell() throws Exception {\n        if (SHELL == null) {\n            SHELL = ProcessControlProvider.get().createLocalProcessControl(true);\n        }\n        SHELL.start();\n        return SHELL;\n    }\n\n    @Override\n    public synchronized CredentialResult retrieveCredentials(String key) {\n        try {\n            CommandSupport.isInLocalPathOrThrow(\"LastPass CLI\", \"lpass\");\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e)\n                    .link(\"https://github.com/LastPass/lastpass-cli\")\n                    .handle();\n            return null;\n        }\n\n        try {\n            var sc = getOrStartShell();\n            var loggedIn =\n                    sc.command(CommandBuilder.of().add(\"lpass\", \"status\")).readStdoutIfPossible();\n            if (loggedIn.isEmpty() || loggedIn.get().contains(\"Logged in as (null)\")) {\n                var email = AsktextAlert.query(\"Enter LastPass account email address to log in\", null);\n                if (email.isPresent()) {\n                    var script = ShellScript.lines(\n                            sc.getShellDialect()\n                                    .getEchoCommand(\"Log in into your LastPass account from the CLI:\", false),\n                            \"lpass login --trust \\\"\" + email.get() + \"\\\"\");\n                    TerminalLaunch.builder()\n                            .title(\"LastPass login\")\n                            .localScript(script)\n                            .logIfEnabled(false)\n                            .pauseOnExit(true)\n                            .launch();\n                }\n                return null;\n            }\n\n            var out = sc.command(CommandBuilder.of()\n                            .add(\"lpass\", \"show\")\n                            .add(\"--fixed-strings\", \"--json\")\n                            .addLiteral(key))\n                    .sensitive()\n                    .readStdoutOrThrow();\n            var tree = JacksonMapper.getDefault().readTree(out);\n\n            if (tree.size() > 1) {\n                var matches = new ArrayList<String>();\n                tree.iterator().forEachRemaining(item -> {\n                    var title = item.get(\"name\");\n                    if (title != null) {\n                        matches.add(title.asText());\n                    }\n                });\n                throw ErrorEventFactory.expected(new IllegalArgumentException(\n                        \"Ambiguous item name, multiple password entries match: \" + String.join(\", \", matches)));\n            }\n\n            var username = tree.get(0).required(\"username\").asText();\n            var password = tree.get(0).required(\"password\").asText();\n            return new CredentialResult(\n                    !username.isEmpty() ? username : null,\n                    !password.isEmpty() ? InPlaceSecretValue.of(password) : null);\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).handle();\n            return null;\n        }\n    }\n\n    @Override\n    public String getKeyPlaceholder() {\n        return \"Case-sensitive entry name\";\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://www.lastpass.com/\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/pwman/OnePasswordManager.java",
    "content": "package io.xpipe.app.pwman;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.CommandSupport;\nimport io.xpipe.app.process.ProcessOutputException;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.core.InPlaceSecretValue;\nimport io.xpipe.core.JacksonMapper;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\n\nimport java.util.regex.Pattern;\n\n@JsonTypeName(\"onePassword\")\npublic class OnePasswordManager implements PasswordManager {\n\n    private static ShellControl SHELL;\n\n    private static synchronized ShellControl getOrStartShell() throws Exception {\n        if (SHELL == null) {\n            SHELL = ProcessControlProvider.get().createLocalProcessControl(true);\n        }\n        SHELL.start();\n        return SHELL;\n    }\n\n    @Override\n    public synchronized CredentialResult retrieveCredentials(String key) {\n        try {\n            CommandSupport.isInLocalPathOrThrow(\"1Password CLI\", \"op\");\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e)\n                    .expected()\n                    .link(\"https://developer.1password.com/docs/cli/get-started/\")\n                    .handle();\n            return null;\n        }\n\n        String vault = null;\n        String name = key;\n\n        if (key.startsWith(\"op://\")) {\n            var match = Pattern.compile(\"op://([^/]+)/([^/]+)\").matcher(key);\n            if (match.find()) {\n                vault = match.group(1);\n                name = match.group(2);\n            }\n        }\n\n        try {\n            var b = CommandBuilder.of()\n                    .add(\"op\", \"item\", \"get\")\n                    .addLiteral(name)\n                    .add(\"--format\", \"json\", \"--fields\", \"username,password\");\n            if (vault != null) {\n                b.add(\"--vault\").addLiteral(vault);\n            }\n\n            var r = getOrStartShell().command(b).sensitive().readStdoutOrThrow();\n            var tree = JacksonMapper.getDefault().readTree(r);\n            if (!tree.isArray() || tree.size() != 2) {\n                return null;\n            }\n\n            var username = tree.get(0).get(\"value\");\n            var password = tree.get(1).get(\"value\");\n            return new CredentialResult(\n                    username != null ? username.asText() : null,\n                    password != null ? InPlaceSecretValue.of(password.asText()) : null);\n        } catch (Exception e) {\n            var event = ErrorEventFactory.fromThrowable(e);\n            if (!key.startsWith(\"op://\")\n                    && e instanceof ProcessOutputException pex\n                    && pex.getOutput().contains(\"Specify the item\")) {\n                event.documentationLink(DocumentationLink.ONE_PASSWORD_KEYS).expected();\n            }\n            event.handle();\n            return null;\n        }\n    }\n\n    @Override\n    public String getKeyPlaceholder() {\n        return AppI18n.get(\"onePasswordPlaceholder\");\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://1password.com/\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/pwman/PassboltPasswordManager.java",
    "content": "package io.xpipe.app.pwman;\n\nimport io.xpipe.app.comp.base.ContextualFileReferenceChoiceComp;\nimport io.xpipe.app.comp.base.SecretFieldComp;\nimport io.xpipe.app.comp.base.TextFieldComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.CommandSupport;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.InPlaceSecretValue;\nimport io.xpipe.core.JacksonMapper;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.property.*;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Getter;\nimport lombok.ToString;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.Optional;\n\n@Getter\n@Builder\n@ToString\n@Jacksonized\n@JsonTypeName(\"passbolt\")\npublic class PassboltPasswordManager implements PasswordManager {\n\n    private static ShellControl SHELL;\n    private final String serverUrl;\n    private final InPlaceSecretValue passphrase;\n    private final Path privateKey;\n\n    @SuppressWarnings(\"unused\")\n    public static OptionsBuilder createOptions(Property<PassboltPasswordManager> p) {\n        var serverUrl = new SimpleStringProperty(p.getValue().getServerUrl());\n        var passphrase = new SimpleObjectProperty<>(p.getValue().getPassphrase());\n        var privateKey = new SimpleObjectProperty<>(FilePath.of(p.getValue().getPrivateKey()));\n\n        ContextualFileReferenceChoiceComp chooser = new ContextualFileReferenceChoiceComp(\n                new ReadOnlyObjectWrapper<>(DataStorage.get().local().ref()),\n                privateKey,\n                null,\n                List.of(),\n                e -> e.equals(DataStorage.get().local()),\n                false);\n        chooser.setPrompt(new ReadOnlyObjectWrapper<>(FilePath.of(\"passbolt_private.asc\")));\n\n        return new OptionsBuilder()\n                .nameAndDescription(\"passboltServerUrl\")\n                .addComp(\n                        new TextFieldComp(serverUrl)\n                                .apply(struc -> {\n                                    struc.setPromptText(\"https://cloud.passbolt.com/myorg\");\n                                })\n                                .maxWidth(600),\n                        serverUrl)\n                .nonNull()\n                .nameAndDescription(\"passboltPassphrase\")\n                .addComp(new SecretFieldComp(passphrase, false).maxWidth(600), passphrase)\n                .nonNull()\n                .nameAndDescription(\"passboltPrivateKey\")\n                .addComp(chooser, privateKey)\n                .nonNull()\n                .bind(\n                        () -> {\n                            return PassboltPasswordManager.builder()\n                                    .passphrase(passphrase.get())\n                                    .privateKey(\n                                            privateKey.get() != null\n                                                    ? privateKey.get().asLocalPath()\n                                                    : null)\n                                    .serverUrl(serverUrl.get())\n                                    .build();\n                        },\n                        p);\n    }\n\n    private static synchronized ShellControl getOrStartShell() throws Exception {\n        if (SHELL == null) {\n            SHELL = ProcessControlProvider.get().createLocalProcessControl(true);\n        }\n        SHELL.start();\n        return SHELL;\n    }\n\n    private Optional<String> parseConfig() throws IOException {\n        var dir =\n                switch (OsType.ofLocal()) {\n                    case OsType.Windows ignored -> AppSystemInfo.ofWindows().getRoamingAppData();\n                    case OsType.Linux ignored ->\n                        AppSystemInfo.ofLinux().getUserHome().resolve(\".config\");\n                    case OsType.MacOs ignored ->\n                        AppSystemInfo.ofMacOs().getUserHome().resolve(\"Library\", \"Application Support\");\n                };\n        var path = dir.resolve(\"go-passbolt-cli\", \"go-passbolt-cli.toml\");\n        if (!Files.exists(path)) {\n            return Optional.empty();\n        }\n\n        var s = Files.readString(path);\n        return Optional.of(s);\n    }\n\n    @JsonIgnore\n    private Boolean validConfig;\n\n    @JsonIgnore\n    private boolean mfaTotpInteractiveConfigured;\n\n    @Override\n    public synchronized CredentialResult retrieveCredentials(String key) {\n        try {\n            CommandSupport.isInLocalPathOrThrow(\"Passbolt CLI\", \"passbolt\");\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e)\n                    .expected()\n                    .link(\"https://github.com/passbolt/go-passbolt-cli\")\n                    .handle();\n            return null;\n        }\n\n        if (validConfig == null) {\n            try {\n                var config = parseConfig();\n                if (config.isPresent()) {\n                    mfaTotpInteractiveConfigured =\n                            config.get().contains(\"totptoken\") && !config.get().contains(\"totptoken = ''\");\n                    var cmd = getOrStartShell().command(CommandBuilder.of().add(\"passbolt\", \"verify\"));\n                    var r = cmd.executeAndCheck();\n                    validConfig = r;\n                } else {\n                    validConfig = false;\n                }\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).handle();\n                validConfig = false;\n            }\n        }\n\n        var b = CommandBuilder.of().add(\"passbolt\");\n        if (!validConfig) {\n            if (serverUrl == null || passphrase == null || privateKey == null) {\n                return null;\n            }\n\n            b.addIf(AppPrefs.get().disableHttpsTlsCheck().getValue(), \"--tlsSkipVerify\")\n                    .add(\"--serverAddress\")\n                    .addLiteral(serverUrl)\n                    .add(\"--userPassword\")\n                    .addLiteral(passphrase.getSecretValue())\n                    .add(\"--userPrivateKeyFile\")\n                    .addFile(privateKey);\n        }\n        b.add(\"--mfaMode\", mfaTotpInteractiveConfigured ? \"noninteractive-totp\" : \"none\");\n        b.add(\"get\", \"resource\").add(\"--id\").addLiteral(key).add(\"--json\");\n\n        try {\n            var cmd = getOrStartShell().command(b).sensitive();\n            var r = JacksonMapper.getDefault().readTree(cmd.readStdoutOrThrow());\n            var username = r.required(\"username\").asText();\n            var password = r.required(\"password\").asText();\n            return new CredentialResult(\n                    username.isEmpty() ? null : username, password.isEmpty() ? null : InPlaceSecretValue.of(password));\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n            return null;\n        }\n    }\n\n    @Override\n    public String getKeyPlaceholder() {\n        return AppI18n.get(\"passboltPlaceholder\");\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://www.passbolt.com\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/pwman/PasswordManager.java",
    "content": "package io.xpipe.app.pwman;\n\nimport io.xpipe.core.OsType;\nimport io.xpipe.core.SecretValue;\n\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport lombok.Value;\n\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.List;\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\npublic interface PasswordManager {\n\n    static List<Class<?>> getClasses() {\n        var l = new ArrayList<Class<?>>();\n        l.add(OnePasswordManager.class);\n        l.add(KeePassXcPasswordManager.class);\n        l.add(BitwardenPasswordManager.class);\n        l.add(DashlanePasswordManager.class);\n        if (OsType.ofLocal() != OsType.WINDOWS) {\n            l.add(LastpassPasswordManager.class);\n            l.add(EnpassPasswordManager.class);\n        }\n        l.add(KeeperPasswordManager.class);\n        l.add(PsonoPasswordManager.class);\n        l.add(PassboltPasswordManager.class);\n        if (OsType.ofLocal() == OsType.WINDOWS) {\n            l.add(WindowsCredentialManager.class);\n        }\n        l.add(PasswordManagerCommand.class);\n        return l;\n    }\n\n    CredentialResult retrieveCredentials(String key);\n\n    String getKeyPlaceholder();\n\n    String getWebsite();\n\n    default Duration getCacheDuration() {\n        return Duration.ofSeconds(30);\n    }\n\n    @Value\n    class CredentialResult {\n\n        String username;\n        SecretValue password;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/pwman/PasswordManagerCommand.java",
    "content": "package io.xpipe.app.pwman;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.IntegratedTextAreaComp;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.platform.MenuHelper;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.prefs.ExternalApplicationHelper;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.core.InPlaceSecretValue;\nimport io.xpipe.core.SecretValue;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.scene.control.MenuItem;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\n@JsonTypeName(\"passwordManagerCommand\")\n@Value\n@Jacksonized\n@Builder\npublic class PasswordManagerCommand implements PasswordManager {\n\n    private static ShellControl SHELL;\n    ShellScript script;\n\n    @SuppressWarnings(\"unused\")\n    static OptionsBuilder createOptions(Property<PasswordManagerCommand> property) {\n        var template = new SimpleObjectProperty<PasswordManagerCommandTemplate>();\n        var script = new SimpleObjectProperty<>(\n                property.getValue() != null ? property.getValue().getScript() : null);\n\n        var templates = RegionBuilder.of(() -> {\n            var cb = MenuHelper.createMenuButton();\n            AppFontSizes.base(cb);\n            cb.textProperty().bind(BindingsHelper.flatMap(template, t -> {\n                return t != null ? AppI18n.observable(t.getId()) : AppI18n.observable(\"chooseTemplate\");\n            }));\n            PasswordManagerCommandTemplate.ALL.forEach(e -> {\n                var m = new MenuItem();\n                m.textProperty().bind(AppI18n.observable(e.getId()));\n                m.setOnAction(event -> {\n                    script.set(new ShellScript(e.getTemplate()));\n                    event.consume();\n                });\n                cb.getItems().add(m);\n            });\n            return cb;\n        });\n\n        var area = IntegratedTextAreaComp.script(\n                new SimpleObjectProperty<>(DataStorage.get().local().ref()), script);\n        area.maxWidth(600);\n\n        return new OptionsBuilder()\n                .nameAndDescription(\"passwordManagerCommand\")\n                .addComp(area, script)\n                .addComp(templates)\n                .bind(() -> new PasswordManagerCommand(script.get()), property);\n    }\n\n    private static synchronized ShellControl getOrStartShell() throws Exception {\n        if (SHELL == null) {\n            SHELL = ProcessControlProvider.get().createLocalProcessControl(true);\n        }\n        SHELL.start();\n        return SHELL;\n    }\n\n    public static SecretValue retrieveWithCommand(String cmd) {\n        try (var cc = getOrStartShell().command(cmd).start()) {\n            var out = cc.readStdoutOrThrow();\n\n            // Dashlane fixes\n            if (cmd.contains(\"dcli\")) {\n                out = out.lines()\n                        .findFirst()\n                        .map(s -> s.strip().replaceAll(\"\\\\s+$\", \"\"))\n                        .orElse(\"\");\n            }\n\n            return InPlaceSecretValue.of(out);\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(\"Unable to retrieve password with command \" + cmd, ex)\n                    .expected()\n                    .handle();\n            return null;\n        }\n    }\n\n    @Override\n    public CredentialResult retrieveCredentials(String key) {\n        if (script == null || script.getValue().isBlank()) {\n            return null;\n        }\n\n        var cmd = ExternalApplicationHelper.replaceVariableArgument(script.getValue(), \"KEY\", key);\n        var secret = retrieveWithCommand(cmd);\n        return new CredentialResult(null, secret);\n    }\n\n    @Override\n    public String getKeyPlaceholder() {\n        return \"$KEY\";\n    }\n\n    @Override\n    public String getWebsite() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/pwman/PasswordManagerCommandTemplate.java",
    "content": "package io.xpipe.app.pwman;\n\nimport io.xpipe.app.ext.PrefsChoiceValue;\nimport io.xpipe.core.OsType;\n\nimport java.util.List;\nimport java.util.stream.Stream;\n\n@SuppressWarnings(\"unused\")\npublic interface PasswordManagerCommandTemplate extends PrefsChoiceValue {\n\n    PasswordManagerCommandTemplate BITWARDEN = new PasswordManagerCommandTemplate() {\n        @Override\n        public String getTemplate() {\n            return \"bw get password $KEY --nointeraction --raw\";\n        }\n\n        @Override\n        public String getId() {\n            return \"bitwarden\";\n        }\n    };\n    PasswordManagerCommandTemplate ONEPASSWORD = new PasswordManagerCommandTemplate() {\n        @Override\n        public String getTemplate() {\n            return \"op read $KEY --force\";\n        }\n\n        @Override\n        public String getId() {\n            return \"1password\";\n        }\n    };\n    PasswordManagerCommandTemplate DASHLANE = new PasswordManagerCommandTemplate() {\n        @Override\n        public String getTemplate() {\n            return \"dcli password --output console $KEY\";\n        }\n\n        @Override\n        public String getId() {\n            return \"dashlane\";\n        }\n    };\n    PasswordManagerCommandTemplate LASTPASS = new PasswordManagerCommandTemplate() {\n        @Override\n        public String getTemplate() {\n            return \"lpass show --password $KEY\";\n        }\n\n        @Override\n        public String getId() {\n            return \"lastpass\";\n        }\n    };\n    PasswordManagerCommandTemplate KEEPER = new PasswordManagerCommandTemplate() {\n        @Override\n        public String getTemplate() {\n            var exec = OsType.ofLocal() == OsType.WINDOWS ? \"@keeper\" : \"keeper\";\n            return exec + \" get $KEY --format password --unmask\";\n        }\n\n        @Override\n        public String getId() {\n            return \"keeper\";\n        }\n    };\n    List<PasswordManagerCommandTemplate> ALL =\n            Stream.of(ONEPASSWORD, BITWARDEN, DASHLANE, LASTPASS, KEEPER).toList();\n\n    String getTemplate();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/pwman/PsonoPasswordManager.java",
    "content": "package io.xpipe.app.pwman;\n\nimport io.xpipe.app.comp.base.SecretFieldComp;\nimport io.xpipe.app.comp.base.TextFieldComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.CommandSupport;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.core.InPlaceSecretValue;\nimport io.xpipe.core.JacksonMapper;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.property.SimpleStringProperty;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Getter;\nimport lombok.ToString;\nimport lombok.extern.jackson.Jacksonized;\n\n@Getter\n@Builder\n@ToString\n@Jacksonized\n@JsonTypeName(\"psono\")\npublic class PsonoPasswordManager implements PasswordManager {\n\n    private static ShellControl SHELL;\n    private final InPlaceSecretValue apiKey;\n    private final InPlaceSecretValue apiSecretKey;\n    private final String serverUrl;\n\n    @SuppressWarnings(\"unused\")\n    public static OptionsBuilder createOptions(Property<PsonoPasswordManager> p) {\n        var apiKey = new SimpleObjectProperty<>(p.getValue().getApiKey());\n        var apiSecretKey = new SimpleObjectProperty<>(p.getValue().getApiSecretKey());\n        var serverUrl = new SimpleStringProperty(p.getValue().getServerUrl());\n        return new OptionsBuilder()\n                .nameAndDescription(\"psonoServerUrl\")\n                .addComp(\n                        new TextFieldComp(serverUrl)\n                                .apply(struc -> {\n                                    struc.setPromptText(\"https://www.psono.pw/server\");\n                                })\n                                .maxWidth(600),\n                        serverUrl)\n                .nameAndDescription(\"psonoApiKey\")\n                .addComp(new SecretFieldComp(apiKey, false).maxWidth(600), apiKey)\n                .nameAndDescription(\"psonoApiSecretKey\")\n                .addComp(new SecretFieldComp(apiSecretKey, false).maxWidth(600), apiSecretKey)\n                .bind(\n                        () -> {\n                            return PsonoPasswordManager.builder()\n                                    .apiKey(apiKey.get())\n                                    .apiSecretKey(apiSecretKey.get())\n                                    .serverUrl(serverUrl.get())\n                                    .build();\n                        },\n                        p);\n    }\n\n    private static synchronized ShellControl getOrStartShell() throws Exception {\n        if (SHELL == null) {\n            SHELL = ProcessControlProvider.get().createLocalProcessControl(true);\n        }\n        SHELL.start();\n        return SHELL;\n    }\n\n    @Override\n    public synchronized CredentialResult retrieveCredentials(String key) {\n        if (serverUrl == null || apiKey == null || apiSecretKey == null) {\n            return null;\n        }\n\n        try {\n            CommandSupport.isInLocalPathOrThrow(\"Psono CLI\", \"psonoci\");\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e)\n                    .expected()\n                    .link(\"https://doc.psono.com/user/psonoci/install.html\")\n                    .handle();\n            return null;\n        }\n\n        try {\n            getOrStartShell().view().setSensitiveEnvironmentVariable(\"PSONO_CI_API_KEY_ID\", apiKey.getSecretValue());\n            getOrStartShell()\n                    .view()\n                    .setSensitiveEnvironmentVariable(\"PSONO_CI_API_SECRET_KEY_HEX\", apiSecretKey.getSecretValue());\n            var cmd = getOrStartShell()\n                    .command(CommandBuilder.of()\n                            .add(\"psonoci\")\n                            .add(\"--server-url\")\n                            .addLiteral(serverUrl)\n                            .add(\"secret\", \"get\")\n                            .addLiteral(key)\n                            .add(\"json\"))\n                    .sensitive();\n            var r = JacksonMapper.getDefault().readTree(cmd.readStdoutOrThrow());\n            var username = r.required(\"username\");\n            var password = r.required(\"password\");\n            return new CredentialResult(\n                    username.isNull() ? null : username.asText(),\n                    password.isNull() ? null : InPlaceSecretValue.of(password.asText()));\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n            return null;\n        }\n    }\n\n    @Override\n    public String getKeyPlaceholder() {\n        return AppI18n.get(\"psonoPlaceholder\");\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://psono.com/\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/pwman/TweetNaClHelper.java",
    "content": "package io.xpipe.app.pwman;\n\nimport lombok.Getter;\nimport org.bouncycastle.crypto.AsymmetricCipherKeyPair;\nimport org.bouncycastle.crypto.KeyGenerationParameters;\nimport org.bouncycastle.crypto.agreement.X25519Agreement;\nimport org.bouncycastle.crypto.generators.X25519KeyPairGenerator;\nimport org.bouncycastle.crypto.params.X25519PrivateKeyParameters;\nimport org.bouncycastle.crypto.params.X25519PublicKeyParameters;\nimport org.bouncycastle.util.Arrays;\n\nimport java.security.SecureRandom;\nimport java.util.Base64;\n\n/**\n * Cryptographic helper for KeePassXC communication.\n * <p>\n * This implementation properly mimics TweetNaCl.js behavior using BouncyCastle,\n * implementing X25519 key exchange and XSalsa20-Poly1305 authenticated encryption\n * which is what KeePassXC expects.\n */\npublic class TweetNaClHelper {\n    public static final int NONCE_SIZE = 24;\n    private static final SecureRandom SECURE_RANDOM = new SecureRandom();\n    // Sigma constant (\"expand 32-byte k\")\n    private static final byte[] SIGMA = {101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98, 121, 116, 101, 32, 107};\n\n    /**\n     * Generate a new key pair.\n     */\n    public static KeyPair generateKeyPair() {\n        X25519KeyPairGenerator keyGen = new X25519KeyPairGenerator();\n        keyGen.init(new KeyGenerationParameters(SECURE_RANDOM, 0));\n        AsymmetricCipherKeyPair keyPair = keyGen.generateKeyPair();\n\n        X25519PrivateKeyParameters privateKey = (X25519PrivateKeyParameters) keyPair.getPrivate();\n        X25519PublicKeyParameters publicKey = (X25519PublicKeyParameters) keyPair.getPublic();\n\n        return new KeyPair(publicKey.getEncoded(), privateKey.getEncoded());\n    }\n\n    /**\n     * Generate random bytes.\n     */\n    public static byte[] randomBytes(int size) {\n        byte[] bytes = new byte[size];\n        SECURE_RANDOM.nextBytes(bytes);\n        return bytes;\n    }\n\n    /**\n     * Encrypt a message using NaCl box.\n     * <p>\n     * This uses X25519 for key exchange and XSalsa20-Poly1305 for authenticated encryption.\n     * Follows the TweetNaCl.js implementation exactly.\n     */\n    public static byte[] box(byte[] message, byte[] nonce, byte[] theirPublicKey, byte[] ourSecretKey) {\n        // Create a shared secret key for encryption - this is the 'beforenm' step\n        byte[] k = boxBeforeNm(theirPublicKey, ourSecretKey);\n\n        // Now use this key with secretbox (the 'afternm' step)\n        return secretbox(message, nonce, k);\n    }\n\n    /**\n     * Compute the shared key for box encryption (equivalent to nacl.box.before)\n     */\n    private static byte[] boxBeforeNm(byte[] theirPublicKey, byte[] ourSecretKey) {\n        // First compute the X25519 shared secret\n        byte[] sharedSecret = computeSharedSecret(theirPublicKey, ourSecretKey);\n\n        // Then use hsalsa20 to derive the key for XSalsa20\n        byte[] k = new byte[32];\n        hsalsa20(k, new byte[16], sharedSecret, SIGMA);\n\n        return k;\n    }\n\n    /**\n     * Decrypt a message using NaCl box open.\n     * Follows the TweetNaCl.js implementation exactly.\n     */\n    public static byte[] boxOpen(byte[] encryptedMessage, byte[] nonce, byte[] theirPublicKey, byte[] ourSecretKey) {\n        // Create a shared secret key for decryption - this is the 'beforenm' step\n        byte[] k = boxBeforeNm(theirPublicKey, ourSecretKey);\n\n        // Now use this key with secretbox_open (the 'afternm' step)\n        return secretboxOpen(encryptedMessage, nonce, k);\n    }\n\n    /**\n     * Compute a shared secret using X25519.\n     */\n    private static byte[] computeSharedSecret(byte[] publicKey, byte[] secretKey) {\n        try {\n            X25519PublicKeyParameters publicParams = new X25519PublicKeyParameters(publicKey, 0);\n            X25519PrivateKeyParameters privateParams = new X25519PrivateKeyParameters(secretKey, 0);\n\n            X25519Agreement agreement = new X25519Agreement();\n            agreement.init(privateParams);\n\n            byte[] sharedSecret = new byte[agreement.getAgreementSize()];\n            agreement.calculateAgreement(publicParams, sharedSecret, 0);\n\n            return sharedSecret;\n        } catch (Exception e) {\n            throw new RuntimeException(\"Error computing shared secret: \" + e.getMessage(), e);\n        }\n    }\n\n    /**\n     * Proper implementation of HSalsa20 function from NaCl, used to derive the subkey.\n     * This matches the TweetNaCl.js implementation.\n     */\n    private static void hsalsa20(byte[] out, byte[] nonce, byte[] key, byte[] constants) {\n        int[] x = new int[16]; // Working state\n\n        // Load constants (sigma)\n        x[0] = load32(constants, 0);\n        x[5] = load32(constants, 4);\n        x[10] = load32(constants, 8);\n        x[15] = load32(constants, 12);\n\n        // Load key\n        x[1] = load32(key, 0);\n        x[2] = load32(key, 4);\n        x[3] = load32(key, 8);\n        x[4] = load32(key, 12);\n        x[11] = load32(key, 16);\n        x[12] = load32(key, 20);\n        x[13] = load32(key, 24);\n        x[14] = load32(key, 28);\n\n        // Load nonce\n        x[6] = load32(nonce, 0);\n        x[7] = load32(nonce, 4);\n        x[8] = load32(nonce, 8);\n        x[9] = load32(nonce, 12);\n\n        // Perform 20 rounds of the Salsa20 core\n        for (int i = 0; i < 20; i += 2) {\n            // Column round\n            x[4] ^= rotl32(x[0] + x[12], 7);\n            x[8] ^= rotl32(x[4] + x[0], 9);\n            x[12] ^= rotl32(x[8] + x[4], 13);\n            x[0] ^= rotl32(x[12] + x[8], 18);\n\n            x[9] ^= rotl32(x[5] + x[1], 7);\n            x[13] ^= rotl32(x[9] + x[5], 9);\n            x[1] ^= rotl32(x[13] + x[9], 13);\n            x[5] ^= rotl32(x[1] + x[13], 18);\n\n            x[14] ^= rotl32(x[10] + x[6], 7);\n            x[2] ^= rotl32(x[14] + x[10], 9);\n            x[6] ^= rotl32(x[2] + x[14], 13);\n            x[10] ^= rotl32(x[6] + x[2], 18);\n\n            x[3] ^= rotl32(x[15] + x[11], 7);\n            x[7] ^= rotl32(x[3] + x[15], 9);\n            x[11] ^= rotl32(x[7] + x[3], 13);\n            x[15] ^= rotl32(x[11] + x[7], 18);\n\n            // Diagonal round\n            x[1] ^= rotl32(x[0] + x[3], 7);\n            x[2] ^= rotl32(x[1] + x[0], 9);\n            x[3] ^= rotl32(x[2] + x[1], 13);\n            x[0] ^= rotl32(x[3] + x[2], 18);\n\n            x[6] ^= rotl32(x[5] + x[4], 7);\n            x[7] ^= rotl32(x[6] + x[5], 9);\n            x[4] ^= rotl32(x[7] + x[6], 13);\n            x[5] ^= rotl32(x[4] + x[7], 18);\n\n            x[11] ^= rotl32(x[10] + x[9], 7);\n            x[8] ^= rotl32(x[11] + x[10], 9);\n            x[9] ^= rotl32(x[8] + x[11], 13);\n            x[10] ^= rotl32(x[9] + x[8], 18);\n\n            x[12] ^= rotl32(x[15] + x[14], 7);\n            x[13] ^= rotl32(x[12] + x[15], 9);\n            x[14] ^= rotl32(x[13] + x[12], 13);\n            x[15] ^= rotl32(x[14] + x[13], 18);\n        }\n\n        // Extract the output\n        store32(out, 0, x[0]);\n        store32(out, 4, x[5]);\n        store32(out, 8, x[10]);\n        store32(out, 12, x[15]);\n        store32(out, 16, x[6]);\n        store32(out, 20, x[7]);\n        store32(out, 24, x[8]);\n        store32(out, 28, x[9]);\n    }\n\n    /**\n     * Implementation of secretbox from NaCl.\n     */\n    private static byte[] secretbox(byte[] message, byte[] nonce, byte[] key) {\n        // For compatibility with TweetNaCl, we implement the same logic\n        // Our secretbox will combine XSalsa20 encryption with Poly1305 MAC\n\n        try {\n            // In TweetNaCl.js, secretbox adds 32 zero bytes before the message\n            byte[] paddedMessage = new byte[32 + message.length];\n            System.arraycopy(message, 0, paddedMessage, 32, message.length);\n\n            // Apply XSalsa20 encryption\n            byte[] c = new byte[paddedMessage.length];\n            streamXorXSalsa20(c, paddedMessage, paddedMessage.length, nonce, key);\n\n            // The first 16 bytes are used for the Poly1305 tag (MAC)\n            byte[] tag = new byte[16];\n            crypto_onetimeauth(tag, c, 32, c.length - 32, Arrays.copyOf(c, 32));\n\n            // Copy tag into the first 16 bytes of c\n            System.arraycopy(tag, 0, c, 16, 16);\n\n            // Clear the first 16 bytes (not used in the result)\n            for (int i = 0; i < 16; i++) {\n                c[i] = 0;\n            }\n\n            // Return result skipping the first 16 bytes (boxzerobytes)\n            return Arrays.copyOfRange(c, 16, c.length);\n        } catch (Exception e) {\n            throw new RuntimeException(\"Encryption failed: \" + e.getMessage(), e);\n        }\n    }\n\n    /**\n     * Implementation of secretbox_open from NaCl.\n     */\n    private static byte[] secretboxOpen(byte[] encryptedMessage, byte[] nonce, byte[] key) {\n        // Check if the message is long enough\n        if (encryptedMessage.length < 16) {\n            return null; // Not enough data\n        }\n\n        try {\n            // Reconstruct the ciphertext with boxzerobytes prefix\n            byte[] c = new byte[16 + encryptedMessage.length];\n            System.arraycopy(encryptedMessage, 0, c, 16, encryptedMessage.length);\n\n            // Verify the Poly1305 authentication tag\n            byte[] subkey = Arrays.copyOf(new byte[32], 32); // First 32 bytes of the keystream\n            streamXSalsa20(subkey, 32, nonce, key);\n\n            if (crypto_onetimeauth_verify(c, 16, c, 32, c.length - 32, subkey) != 0) {\n                return null; // MAC verification failed\n            }\n\n            // Decrypt the message\n            byte[] m = new byte[c.length];\n            streamXorXSalsa20(m, c, c.length, nonce, key);\n\n            // Return the actual message (skipping the 32 zero bytes prefix)\n            return Arrays.copyOfRange(m, 32, m.length);\n        } catch (Exception e) {\n            return null; // Return null on decryption failure (as in NaCl)\n        }\n    }\n\n    /**\n     * Core XSalsa20 stream cipher function.\n     */\n    private static void streamXSalsa20(byte[] out, int outLength, byte[] nonce, byte[] key) {\n        // First, derive a subkey using HSalsa20\n        byte[] subkey = new byte[32];\n        hsalsa20(subkey, Arrays.copyOf(nonce, 16), key, SIGMA);\n\n        // Then use the subkey with the remaining bytes of the nonce\n        streamSalsa20(out, outLength, Arrays.copyOfRange(nonce, 16, 24), subkey);\n    }\n\n    /**\n     * XSalsa20 stream XOR function\n     */\n    private static void streamXorXSalsa20(byte[] c, byte[] m, int mlen, byte[] nonce, byte[] key) {\n        // First, derive a subkey using HSalsa20\n        byte[] subkey = new byte[32];\n        hsalsa20(subkey, Arrays.copyOf(nonce, 16), key, SIGMA);\n\n        // Then use the subkey with the remaining bytes of the nonce\n        streamXorSalsa20(c, m, mlen, Arrays.copyOfRange(nonce, 16, 24), subkey);\n    }\n\n    /**\n     * Core Salsa20 stream cipher function.\n     */\n    private static void streamSalsa20(byte[] out, int outLength, byte[] nonce, byte[] key) {\n        byte[] zeros = new byte[outLength];\n        streamXorSalsa20(out, zeros, outLength, nonce, key);\n    }\n\n    /**\n     * Salsa20 stream XOR function\n     */\n    private static void streamXorSalsa20(byte[] c, byte[] m, int mlen, byte[] nonce, byte[] key) {\n        // Use BouncyCastle's Salsa20 implementation\n        org.bouncycastle.crypto.engines.Salsa20Engine salsa20 = new org.bouncycastle.crypto.engines.Salsa20Engine();\n        org.bouncycastle.crypto.params.ParametersWithIV params = new org.bouncycastle.crypto.params.ParametersWithIV(\n                new org.bouncycastle.crypto.params.KeyParameter(key), nonce);\n        salsa20.init(true, params);\n\n        salsa20.processBytes(m, 0, mlen, c, 0);\n    }\n\n    /**\n     * Poly1305 one-time authentication.\n     */\n    private static void crypto_onetimeauth(byte[] out, byte[] m, int mpos, int mlen, byte[] key) {\n        org.bouncycastle.crypto.macs.Poly1305 poly1305 = new org.bouncycastle.crypto.macs.Poly1305();\n        poly1305.init(new org.bouncycastle.crypto.params.KeyParameter(key));\n\n        poly1305.update(m, mpos, mlen);\n\n        poly1305.doFinal(out, 0);\n    }\n\n    /**\n     * Verify a Poly1305 one-time authentication tag.\n     */\n    private static int crypto_onetimeauth_verify(byte[] h, int hpos, byte[] m, int mpos, int mlen, byte[] key) {\n        byte[] correct = new byte[16];\n        crypto_onetimeauth(correct, m, mpos, mlen, key);\n        return crypto_verify_16(h, hpos, correct, 0);\n    }\n\n    /**\n     * Verify 16 bytes in constant time.\n     */\n    private static int crypto_verify_16(byte[] x, int xpos, byte[] y, int ypos) {\n        return constantTimeEquals(Arrays.copyOfRange(x, xpos, xpos + 16), Arrays.copyOfRange(y, ypos, ypos + 16))\n                ? 0\n                : -1;\n    }\n\n    /**\n     * Helper for loading 32-bit integers (little-endian).\n     */\n    private static int load32(byte[] src, int offset) {\n        int u = src[offset] & 0xff;\n        u |= (src[offset + 1] & 0xff) << 8;\n        u |= (src[offset + 2] & 0xff) << 16;\n        u |= (src[offset + 3] & 0xff) << 24;\n        return u;\n    }\n\n    /**\n     * Helper for storing 32-bit integers (little-endian).\n     */\n    private static void store32(byte[] dst, int offset, int u) {\n        dst[offset] = (byte) (u & 0xff);\n        dst[offset + 1] = (byte) ((u >>> 8) & 0xff);\n        dst[offset + 2] = (byte) ((u >>> 16) & 0xff);\n        dst[offset + 3] = (byte) ((u >>> 24) & 0xff);\n    }\n\n    /**\n     * Rotate a 32-bit integer left by the specified number of bits.\n     */\n    private static int rotl32(int x, int b) {\n        return ((x << b) | (x >>> (32 - b)));\n    }\n\n    /**\n     * Compare two byte arrays in constant time to prevent timing attacks.\n     */\n    private static boolean constantTimeEquals(byte[] a, byte[] b) {\n        if (a.length != b.length) {\n            return false;\n        }\n\n        int result = 0;\n        for (int i = 0; i < a.length; i++) {\n            result |= a[i] ^ b[i];\n        }\n        return result == 0;\n    }\n\n    /**\n     * Encode bytes as Base64.\n     */\n    public static String encodeBase64(byte[] data) {\n        return Base64.getEncoder().encodeToString(data);\n    }\n\n    /**\n     * Decode Base64 to bytes.\n     */\n    public static byte[] decodeBase64(String data) {\n        return Base64.getDecoder().decode(data);\n    }\n\n    @Getter\n    public static class KeyPair {\n        private final byte[] publicKey;\n        private final byte[] secretKey;\n\n        public KeyPair(byte[] publicKey, byte[] secretKey) {\n            this.publicKey = publicKey;\n            this.secretKey = secretKey;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/pwman/WindowsCredentialManager.java",
    "content": "package io.xpipe.app.pwman;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.core.InPlaceSecretValue;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Value;\n\n@JsonTypeName(\"windowsCredentialManager\")\n@Value\npublic class WindowsCredentialManager implements PasswordManager {\n\n    private static boolean loaded = false;\n\n    @Override\n    public synchronized CredentialResult retrieveCredentials(String key) {\n        try {\n            if (!loaded) {\n                loaded = true;\n\n                var shell = LocalShell.getLocalPowershell();\n                if (shell.isEmpty()) {\n                    return null;\n                }\n\n                var cmd = \"\"\"\n                          $code = @\"\n                          using System.Text;\n                          using System;\n                          using System.Runtime.InteropServices;\n\n                          namespace CredManager {\n                            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]\n                            public struct CredentialMem\n                            {\n                              public int flags;\n                              public int type;\n                              public string targetName;\n                              public string comment;\n                              public System.Runtime.InteropServices.ComTypes.FILETIME lastWritten;\n                              public int credentialBlobSize;\n                              public IntPtr credentialBlob;\n                              public int persist;\n                              public int attributeCount;\n                              public IntPtr credAttribute;\n                              public string targetAlias;\n                              public string userName;\n                            }\n\n                            public class Credential {\n                              [DllImport(\"advapi32.dll\", EntryPoint = \"CredReadW\", CharSet = CharSet.Unicode, SetLastError = true)]\n                              private static extern bool CredRead(string target, int type, int reservedFlag, out IntPtr credentialPtr);\n\n                              public static string GetUserName(string target)\n                              {\n                                CredentialMem credMem;\n                                IntPtr credPtr;\n\n                                if (CredRead(target, 1, 0, out credPtr))\n                                {\n                                  credMem = Marshal.PtrToStructure<CredentialMem>(credPtr);\n                                  return credMem.userName;\n                                } else {\n                                  throw new Exception(\"No credentials found for target: \" + target);\n                                }\n                              }\n\n                              public static string GetUserPassword(string target)\n                              {\n                                CredentialMem credMem;\n                                IntPtr credPtr;\n\n                                if (CredRead(target, 1, 0, out credPtr))\n                                {\n                                  credMem = Marshal.PtrToStructure<CredentialMem>(credPtr);\n                                  if (credMem.credentialBlobSize == 0)\n                                  {\n                                    return \"\";\n                                  }\n                                  byte[] passwordBytes = new byte[credMem.credentialBlobSize];\n                                  Marshal.Copy(credMem.credentialBlob, passwordBytes, 0, credMem.credentialBlobSize);\n                                  return Encoding.Unicode.GetString(passwordBytes);\n                                } else {\n                                  throw new Exception(\"No credentials found for target: \" + target);\n                                }\n                              }\n                            }\n                          }\n                          \"@\n                          Add-Type -TypeDefinition $code -Language CSharp\n                          \"\"\";\n                shell.get().command(cmd).execute();\n            }\n\n            var shell = LocalShell.getLocalPowershell();\n            if (shell.isEmpty()) {\n                return null;\n            }\n\n            var username = shell.get()\n                    .command(\"[CredManager.Credential]::GetUserName(\\\"\" + key.replaceAll(\"\\\"\", \"`\\\"\") + \"\\\")\")\n                    .sensitive()\n                    .readStdoutOrThrow();\n            var password = shell.get()\n                    .command(\"[CredManager.Credential]::GetUserPassword(\\\"\" + key.replaceAll(\"\\\"\", \"`\\\"\") + \"\\\")\")\n                    .sensitive()\n                    .readStdoutOrThrow();\n            return new CredentialResult(username, password.isEmpty() ? null : InPlaceSecretValue.of(password));\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).expected().handle();\n            return null;\n        }\n    }\n\n    @Override\n    public String getKeyPlaceholder() {\n        return \"Credential name\";\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://support.microsoft.com/en-us/windows/credential-manager-in-windows-1b5c916a-6a16-889f-8581-fc16e8165ac0\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/rdp/CustomRdpClient.java",
    "content": "package io.xpipe.app.rdp;\n\nimport io.xpipe.app.comp.base.TextFieldComp;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.prefs.ExternalApplicationHelper;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleObjectProperty;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.Locale;\n\n@JsonTypeName(\"custom\")\n@Value\n@Jacksonized\n@Builder\npublic class CustomRdpClient implements ExternalApplicationType, ExternalRdpClient {\n\n    @SuppressWarnings(\"unused\")\n    static OptionsBuilder createOptions(Property<CustomRdpClient> property) {\n        var command = new SimpleObjectProperty<>(property.getValue().getCommand());\n        return new OptionsBuilder()\n                .nameAndDescription(\"customRdpClientCommand\")\n                .addComp(\n                        new TextFieldComp(command, false)\n                                .apply(struc -> struc.setPromptText(\"myrdpclient -c $FILE\"))\n                                .maxWidth(600),\n                        command)\n                .bind(() -> CustomRdpClient.builder().command(command.get()).build(), property);\n    }\n\n    String command;\n\n    @Override\n    public void launch(RdpLaunchConfig configuration) throws Exception {\n        if (command == null || command.isBlank()) {\n            throw ErrorEventFactory.expected(new IllegalStateException(\"No custom RDP command specified\"));\n        }\n\n        var format = command.toLowerCase(Locale.ROOT).contains(\"$file\") ? command : command + \" $FILE\";\n        ExternalApplicationHelper.startAsync(CommandBuilder.of()\n                .add(ExternalApplicationHelper.replaceVariableArgument(\n                        format,\n                        \"FILE\",\n                        writeRdpConfigFile(configuration.getTitle(), configuration.getConfig())\n                                .toString())));\n    }\n\n    @Override\n    public boolean supportsPasswordPassing() {\n        return false;\n    }\n\n    @Override\n    public String getWebsite() {\n        return null;\n    }\n\n    @Override\n    public boolean isAvailable() {\n        return true;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/rdp/DevolutionsRdpClient.java",
    "content": "package io.xpipe.app.rdp;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.app.util.WindowsRegistry;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\nimport org.apache.commons.io.FileUtils;\n\nimport java.nio.file.Path;\nimport java.util.Optional;\n\n@JsonTypeName(\"devolutions\")\n@Value\n@Jacksonized\n@Builder\npublic class DevolutionsRdpClient implements ExternalApplicationType.WindowsType, ExternalRdpClient {\n\n    @Override\n    public boolean detach() {\n        return true;\n    }\n\n    @Override\n    public String getExecutable() {\n        return \"RemoteDesktopManager\";\n    }\n\n    @Override\n    public Optional<Path> determineInstallation() {\n        try {\n            var r = WindowsRegistry.local()\n                    .readStringValueIfPresent(\n                            WindowsRegistry.HKEY_LOCAL_MACHINE, \"SOFTWARE\\\\Classes\\\\rdm\\\\DefaultIcon\");\n            return r.map(Path::of);\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).omit().handle();\n            return Optional.empty();\n        }\n    }\n\n    @Override\n    public void launch(RdpLaunchConfig configuration) throws Exception {\n        var config = writeRdpConfigFile(configuration.getTitle(), configuration.getConfig());\n        launch(CommandBuilder.of().addFile(config));\n        ThreadHelper.runFailableAsync(() -> {\n            // Startup is slow\n            ThreadHelper.sleep(10000);\n            FileUtils.deleteQuietly(config.toFile());\n        });\n    }\n\n    @Override\n    public boolean supportsPasswordPassing() {\n        return false;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://devolutions.net/remote-desktop-manager/\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/rdp/ExternalRdpClient.java",
    "content": "package io.xpipe.app.rdp;\n\nimport io.xpipe.app.core.AppLocalTemp;\nimport io.xpipe.app.ext.PrefsValue;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.OsFileSystem;\nimport io.xpipe.app.util.*;\nimport io.xpipe.core.OsType;\n\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.*;\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\npublic interface ExternalRdpClient extends PrefsValue {\n\n    static List<Class<?>> getClasses() {\n        var l = new ArrayList<Class<?>>();\n        switch (OsType.ofLocal()) {\n            case OsType.Linux ignored -> {\n                l.add(RemminaRdpClient.class);\n                l.add(FreeRdpClient.class);\n            }\n            case OsType.MacOs ignored -> {\n                l.add(RemoteDesktopAppRdpClient.class);\n                l.add(WindowsAppRdpClient.class);\n                l.add(FreeRdpClient.class);\n            }\n            case OsType.Windows ignored -> {\n                l.add(MstscRdpClient.class);\n                l.add(DevolutionsRdpClient.class);\n            }\n        }\n        l.add(CustomRdpClient.class);\n        return l;\n    }\n\n    static ExternalRdpClient getApplicationLauncher() {\n        if (OsType.ofLocal() == OsType.WINDOWS) {\n            return MstscRdpClient.builder().smartSizing(false).build();\n        } else {\n            return AppPrefs.get().rdpClientType().getValue();\n        }\n    }\n\n    static ExternalRdpClient determineDefault(ExternalRdpClient existing) {\n        // Verify that our selection is still valid\n        if (existing != null && existing.isAvailable()) {\n            return existing;\n        }\n\n        return switch (OsType.ofLocal()) {\n            case OsType.Linux ignored -> {\n                var freeRdp = new FreeRdpClient();\n                var remmina = new RemminaRdpClient();\n                yield remmina.isAvailable() ? remmina : freeRdp.isAvailable() ? freeRdp : remmina;\n            }\n            case OsType.MacOs ignored -> {\n                var remoteDesktopApp = new RemoteDesktopAppRdpClient();\n                if (remoteDesktopApp.isAvailable()) {\n                    yield remoteDesktopApp;\n                }\n\n                var windowsApp = new WindowsAppRdpClient();\n                if (windowsApp.isAvailable()) {\n                    yield windowsApp;\n                }\n\n                var freeRdp = new FreeRdpClient();\n                if (freeRdp.isAvailable()) {\n                    yield freeRdp;\n                }\n\n                yield windowsApp;\n            }\n            case OsType.Windows ignored -> {\n                yield MstscRdpClient.builder().smartSizing(false).build();\n            }\n        };\n    }\n\n    void launch(RdpLaunchConfig configuration) throws Exception;\n\n    boolean supportsPasswordPassing();\n\n    String getWebsite();\n\n    default Path writeRdpConfigFile(String title, RdpConfig input) throws Exception {\n        var name = OsFileSystem.ofLocal().makeFileSystemCompatible(title);\n        var file = AppLocalTemp.getLocalTempDataDirectory(\"rdp\").resolve(name + \".rdp\");\n        var string = input.toString() + \"\\n\";\n        Files.createDirectories(file.getParent());\n        Files.writeString(file, string);\n        return file;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/rdp/FreeRdpClient.java",
    "content": "package io.xpipe.app.rdp;\n\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.CommandSupport;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.util.FlatpakCache;\nimport io.xpipe.core.OsType;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\n@JsonTypeName(\"freeRdp\")\n@Value\n@Jacksonized\n@Builder\npublic class FreeRdpClient implements ExternalRdpClient {\n\n    @Override\n    public void launch(RdpLaunchConfig configuration) throws Exception {\n        CommandBuilder exec;\n        var v3 = CommandSupport.isInLocalPath(\"xfreerdp3\");\n        if (!v3) {\n            var v2 = CommandSupport.isInLocalPath(\"xfreerdp\");\n            if (!v2 && OsType.ofLocal() == OsType.LINUX) {\n                var flatpak = FlatpakCache.getApp(\"com.freerdp.FreeRDP\");\n                if (flatpak.isPresent()) {\n                    exec = FlatpakCache.getRunCommand(\"com.freerdp.FreeRDP\");\n                    v3 = true;\n                } else {\n                    CommandSupport.isInPathOrThrow(LocalShell.getShell(), \"xfreerdp\");\n                    exec = CommandBuilder.of().add(\"xfreerdp\");\n                }\n            } else {\n                CommandSupport.isInPathOrThrow(LocalShell.getShell(), \"xfreerdp\");\n                exec = CommandBuilder.of().add(\"xfreerdp\");\n                // macOS uses xfreerdp3 by default\n                v3 = true;\n            }\n        } else {\n            exec = CommandBuilder.of().add(\"xfreerdp3\");\n        }\n\n        var file = writeRdpConfigFile(configuration.getTitle(), configuration.getConfig());\n        var b = CommandBuilder.of()\n                .add(exec)\n                .addFile(file.toString())\n                .add(v3 ? \"/cert:ignore\" : \"/cert-ignore\")\n                .add(\"/dynamic-resolution\")\n                .add(\"/network:auto\")\n                .add(\"/compression\")\n                .add(\"+clipboard\")\n                .add(\"-themes\")\n                .add(\"/size:100%\");\n\n        if (configuration.getPassword() != null) {\n            var escapedPw = configuration.getPassword().getSecretValue().replaceAll(\"'\", \"\\\\\\\\'\");\n            b.add(\"/p:'\" + escapedPw + \"'\");\n        }\n\n        try (var sc = LocalShell.getShell().start()) {\n            var cmd = sc.getShellDialect().launchAsync(b, true);\n            sc.command(cmd).sensitive().execute();\n        }\n    }\n\n    @Override\n    public boolean supportsPasswordPassing() {\n        return true;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://www.freerdp.com/\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/rdp/MstscRdpClient.java",
    "content": "package io.xpipe.app.rdp;\n\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.util.GlobalTimer;\nimport io.xpipe.app.util.RdpConfig;\nimport io.xpipe.app.util.WindowsRegistry;\nimport io.xpipe.core.SecretValue;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleObjectProperty;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\nimport org.apache.commons.io.FileUtils;\n\nimport java.time.Duration;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.UUID;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n@JsonTypeName(\"mstsc\")\n@Value\n@Jacksonized\n@Builder\npublic class MstscRdpClient implements ExternalApplicationType.PathApplication, ExternalRdpClient {\n\n    @Value\n    @Jacksonized\n    @Builder\n    public static class RegistryCache {\n        String usernameHint;\n        byte[] certHash;\n    }\n\n    private static int launchCounter = 0;\n\n    @SuppressWarnings(\"unused\")\n    static OptionsBuilder createOptions(Property<MstscRdpClient> property) {\n        var smartSizing = new SimpleObjectProperty<>(property.getValue().isSmartSizing());\n        return new OptionsBuilder()\n                .nameAndDescription(\"rdpSmartSizing\")\n                .addToggle(smartSizing)\n                .bind(\n                        () -> MstscRdpClient.builder()\n                                .smartSizing(smartSizing.get())\n                                .build(),\n                        property);\n    }\n\n    boolean smartSizing;\n\n    @Override\n    public void launch(RdpLaunchConfig configuration) throws Exception {\n        var adaptedRdpConfig = getAdaptedConfig(configuration);\n\n        var setCache = prepareLocalhostRegistryCache(configuration);\n\n        var file = writeRdpConfigFile(configuration.getTitle(), adaptedRdpConfig);\n        LocalShell.getShell()\n                .command(CommandBuilder.of().add(getExecutable()).addFile(file.toString()))\n                .execute();\n\n        GlobalTimer.delay(\n                () -> {\n                    FileUtils.deleteQuietly(file.toFile());\n                },\n                Duration.ofSeconds(1));\n\n        if (!setCache) {\n            var localhost = configuration\n                    .getConfig()\n                    .get(\"full address\")\n                    .orElseThrow()\n                    .getValue()\n                    .startsWith(\"localhost\");\n            if (localhost) {\n                saveLocalhostRegistryCache(configuration.getStoreId());\n            }\n        }\n    }\n\n    @Override\n    public boolean supportsPasswordPassing() {\n        return LocalShell.getLocalPowershell().isPresent();\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/mstsc\";\n    }\n\n    private RdpConfig getAdaptedConfig(RdpLaunchConfig configuration) throws Exception {\n        var input = configuration.getConfig();\n        var pass = configuration.getPassword();\n        if (input.get(\"password 51\").isPresent() || !supportsPasswordPassing() || pass == null) {\n            return input.overlay(Map.of(\"smart sizing\", new RdpConfig.TypedValue(\"i\", smartSizing ? \"1\" : \"0\")));\n        }\n\n        var adapted = input.overlay(Map.of(\n                \"password 51\",\n                new RdpConfig.TypedValue(\"b\", encrypt(pass)),\n                \"prompt for credentials\",\n                new RdpConfig.TypedValue(\"i\", \"0\"),\n                \"smart sizing\",\n                new RdpConfig.TypedValue(\"i\", smartSizing ? \"1\" : \"0\")));\n        return adapted;\n    }\n\n    private void saveLocalhostRegistryCache(UUID entry) {\n        var counter = ++launchCounter;\n        var attempts = new AtomicInteger();\n        GlobalTimer.scheduleUntil(Duration.ofSeconds(1), false, () -> {\n            if (counter != launchCounter || attempts.getAndIncrement() > 15) {\n                return true;\n            }\n\n            var ex = WindowsRegistry.local()\n                    .keyExists(\n                            WindowsRegistry.HKEY_CURRENT_USER,\n                            \"Software\\\\Microsoft\\\\Terminal Server Client\\\\Servers\\\\localhost\");\n            if (!ex) {\n                return false;\n            }\n\n            var user = WindowsRegistry.local()\n                    .readStringValueIfPresent(\n                            WindowsRegistry.HKEY_CURRENT_USER,\n                            \"Software\\\\Microsoft\\\\Terminal Server Client\\\\Servers\\\\localhost\",\n                            \"UsernameHint\")\n                    .orElse(null);\n            var cert = WindowsRegistry.local()\n                    .readBinaryValueIfPresent(\n                            WindowsRegistry.HKEY_CURRENT_USER,\n                            \"Software\\\\Microsoft\\\\Terminal Server Client\\\\Servers\\\\localhost\",\n                            \"CertHash\")\n                    .orElse(null);\n            if (user == null && cert == null) {\n                return true;\n            }\n\n            AppCache.update(\n                    \"rdp-\" + entry,\n                    RegistryCache.builder().usernameHint(user).certHash(cert).build());\n            return true;\n        });\n    }\n\n    private Optional<RegistryCache> getLocalhostRegistryCache(UUID entry) {\n        RegistryCache found = AppCache.getNonNull(\"rdp-\" + entry, RegistryCache.class, () -> null);\n        return Optional.ofNullable(found);\n    }\n\n    private boolean prepareLocalhostRegistryCache(RdpLaunchConfig configuration) {\n        WindowsRegistry.local()\n                .deleteKey(\n                        WindowsRegistry.HKEY_CURRENT_USER,\n                        \"Software\\\\Microsoft\\\\Terminal Server Client\\\\Servers\\\\localhost\");\n\n        var localhost = configuration\n                .getConfig()\n                .get(\"full address\")\n                .orElseThrow()\n                .getValue()\n                .startsWith(\"localhost\");\n        if (localhost) {\n            var found = getLocalhostRegistryCache(configuration.getStoreId());\n            if (found.isPresent()) {\n                var user = found.get().getUsernameHint();\n                if (user != null) {\n                    WindowsRegistry.local()\n                            .setStringValue(\n                                    WindowsRegistry.HKEY_CURRENT_USER,\n                                    \"Software\\\\Microsoft\\\\Terminal Server Client\\\\Servers\\\\localhost\",\n                                    \"UsernameHint\",\n                                    user);\n                }\n\n                var cert = found.get().getCertHash();\n                if (cert != null) {\n                    WindowsRegistry.local()\n                            .setBinaryValue(\n                                    WindowsRegistry.HKEY_CURRENT_USER,\n                                    \"Software\\\\Microsoft\\\\Terminal Server Client\\\\Servers\\\\localhost\",\n                                    \"CertHash\",\n                                    cert);\n                }\n\n                return user != null || cert != null;\n            }\n        }\n\n        return false;\n    }\n\n    private String encrypt(SecretValue password) throws Exception {\n        var ps = LocalShell.getLocalPowershell().orElseThrow();\n        var cmd = ps.command(CommandBuilder.of()\n                .add(sc -> \"(\" + sc.getShellDialect().literalArgument(password.getSecretValue())\n                        + \" | ConvertTo-SecureString -AsPlainText -Force) | ConvertFrom-SecureString\"));\n        cmd.sensitive();\n        return cmd.readStdoutOrThrow();\n    }\n\n    @Override\n    public String getExecutable() {\n        return \"mstsc.exe\";\n    }\n\n    @Override\n    public boolean detach() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/rdp/RdpLaunchConfig.java",
    "content": "package io.xpipe.app.rdp;\n\nimport io.xpipe.app.util.RdpConfig;\nimport io.xpipe.core.SecretValue;\n\nimport lombok.Value;\n\nimport java.util.UUID;\n\n@Value\npublic class RdpLaunchConfig {\n    String title;\n    RdpConfig config;\n    UUID storeId;\n    SecretValue password;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/rdp/RemminaRdpClient.java",
    "content": "package io.xpipe.app.rdp;\n\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.util.*;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.*;\n\n@JsonTypeName(\"remmina\")\n@Value\n@Jacksonized\n@Builder\npublic class RemminaRdpClient implements ExternalApplicationType.LinuxApplication, ExternalRdpClient {\n\n    private List<String> toStrip() {\n        return List.of(\"auto connect\", \"password 51\", \"prompt for credentials\", \"smart sizing\");\n    }\n\n    @Override\n    public void launch(RdpLaunchConfig configuration) throws Exception {\n        RdpConfig c = configuration.getConfig();\n\n        // Remmina does not support RemoteApps\n        if (c.get(\"remoteapplicationprogram\").isPresent()) {\n            var freerdp = new FreeRdpClient();\n            if (freerdp.isAvailable()) {\n                freerdp.launch(configuration);\n                return;\n            }\n        }\n\n        var l = new HashSet<>(c.getContent().keySet());\n        toStrip().forEach(l::remove);\n        if (l.size() == 2 && l.contains(\"username\") && l.contains(\"full address\")) {\n            var encrypted = RemminaHelper.encryptPassword(configuration.getPassword());\n            if (encrypted.isPresent()) {\n                var file = RemminaHelper.writeRemminaRdpConfigFile(configuration, encrypted.get());\n                launch(CommandBuilder.of().add(\"-c\").addFile(file.toString()));\n                return;\n            }\n        }\n\n        var file = writeRdpConfigFile(configuration.getTitle(), c);\n        launch(CommandBuilder.of().add(\"-c\").addFile(file.toString()));\n        LocalFileTracker.deleteOnExit(file);\n    }\n\n    @Override\n    public boolean supportsPasswordPassing() {\n        return false;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://remmina.org/\";\n    }\n\n    @Override\n    public String getExecutable() {\n        return \"remmina\";\n    }\n\n    @Override\n    public boolean detach() {\n        return true;\n    }\n\n    @Override\n    public String getFlatpakId() {\n        return \"org.remmina.Remmina\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/rdp/RemoteDesktopAppRdpClient.java",
    "content": "package io.xpipe.app.rdp;\n\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\n@JsonTypeName(\"microsoftRemoteDesktopApp\")\n@Value\n@Jacksonized\n@Builder\npublic class RemoteDesktopAppRdpClient implements ExternalApplicationType.MacApplication, ExternalRdpClient {\n\n    @Override\n    public void launch(RdpLaunchConfig configuration) throws Exception {\n        var file = writeRdpConfigFile(configuration.getTitle(), configuration.getConfig());\n        LocalShell.getShell()\n                .executeSimpleCommand(CommandBuilder.of()\n                        .add(\"open\", \"-a\")\n                        .addQuoted(\"Microsoft Remote Desktop.app\")\n                        .addFile(file.toString()));\n    }\n\n    @Override\n    public boolean supportsPasswordPassing() {\n        return false;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://learn.microsoft.com/en-us/previous-versions/remote-desktop-client/remote-desktop-macos\";\n    }\n\n    @Override\n    public String getApplicationName() {\n        return \"Microsoft Remote Desktop\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/rdp/WindowsAppRdpClient.java",
    "content": "package io.xpipe.app.rdp;\n\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\n@JsonTypeName(\"windowsApp\")\n@Value\n@Jacksonized\n@Builder\npublic class WindowsAppRdpClient implements ExternalApplicationType.MacApplication, ExternalRdpClient {\n\n    @Override\n    public void launch(RdpLaunchConfig configuration) throws Exception {\n        var file = writeRdpConfigFile(configuration.getTitle(), configuration.getConfig());\n        LocalShell.getShell()\n                .executeSimpleCommand(CommandBuilder.of()\n                        .add(\"open\", \"-a\")\n                        .addQuoted(\"Windows App.app\")\n                        .addFile(file.toString()));\n    }\n\n    @Override\n    public boolean supportsPasswordPassing() {\n        return false;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://learn.microsoft.com/en-us/windows-app/get-started-connect-devices-desktops-apps?tabs=windows-avd%2Cwindows-w365%2Cwindows\"\n                + \"-devbox%2Cmacos-rds%2Cmacos-pc&pivots=remote-pc\";\n    }\n\n    @Override\n    public String getApplicationName() {\n        return \"Windows App\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/secret/EncryptedValue.java",
    "content": "package io.xpipe.app.secret;\n\nimport io.xpipe.app.storage.DataStorageSecret;\nimport io.xpipe.app.storage.DataStorageUserHandler;\nimport io.xpipe.core.JacksonMapper;\n\nimport com.fasterxml.jackson.annotation.JsonSubTypes;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.*;\n\nimport java.util.Objects;\n\n@AllArgsConstructor\n@Getter\n@JsonTypeInfo(use = JsonTypeInfo.Id.NONE)\n@JsonSubTypes({\n    @JsonSubTypes.Type(value = EncryptedValue.VaultKey.class),\n    @JsonSubTypes.Type(value = EncryptedValue.CurrentKey.class),\n})\npublic abstract class EncryptedValue<T> {\n\n    @NonNull\n    private final T value;\n\n    @NonNull\n    private final DataStorageSecret secret;\n\n    @SneakyThrows\n    public static <T> EncryptedValue<T> of(T value) {\n        if (value == null) {\n            return null;\n        }\n\n        return CurrentKey.of(value);\n    }\n\n    public abstract boolean allowUserSecretKey();\n\n    public abstract EncryptedValue<T> withValue(T value);\n\n    @Override\n    public int hashCode() {\n        return Objects.hashCode(value);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (!(o instanceof EncryptedValue<?> that)) {\n            return false;\n        }\n        return Objects.equals(value, that.value);\n    }\n\n    @JsonTypeName(\"current\")\n    @EqualsAndHashCode(callSuper = true)\n    @ToString(callSuper = true)\n    public static class CurrentKey<T> extends EncryptedValue<T> {\n\n        public CurrentKey(T value, DataStorageSecret secret) {\n            super(value, secret);\n        }\n\n        @SneakyThrows\n        public static <T> CurrentKey<T> of(T value) {\n            if (value == null) {\n                return null;\n            }\n\n            var handler = DataStorageUserHandler.getInstance();\n            var s = JacksonMapper.getDefault().writeValueAsString(value);\n            var secret = new VaultKeySecretValue(s.toCharArray());\n            return new CurrentKey<>(\n                    value,\n                    DataStorageSecret.ofSecret(\n                            secret,\n                            handler.getActiveUser() != null ? EncryptionToken.ofUser() : EncryptionToken.ofVaultKey()));\n        }\n\n        @Override\n        public boolean allowUserSecretKey() {\n            return true;\n        }\n\n        @Override\n        public EncryptedValue.CurrentKey<T> withValue(T value) {\n            if (value == null) {\n                return null;\n            }\n\n            if (value == this.getValue()) {\n                return this;\n            }\n\n            return of(value);\n        }\n    }\n\n    @JsonTypeName(\"vault\")\n    @EqualsAndHashCode(callSuper = true)\n    @ToString(callSuper = true)\n    public static class VaultKey<T> extends EncryptedValue<T> {\n\n        public VaultKey(T value, DataStorageSecret secret) {\n            super(value, secret);\n        }\n\n        @SneakyThrows\n        public static <T> VaultKey<T> of(T value) {\n            if (value == null) {\n                return null;\n            }\n\n            var s = JacksonMapper.getDefault().writeValueAsString(value);\n            var secret = new VaultKeySecretValue(s.toCharArray());\n            return new VaultKey<>(value, DataStorageSecret.ofSecret(secret, EncryptionToken.ofVaultKey()));\n        }\n\n        @Override\n        public boolean allowUserSecretKey() {\n            return false;\n        }\n\n        @Override\n        public EncryptedValue.VaultKey<T> withValue(T value) {\n            if (value == null) {\n                return null;\n            }\n\n            if (value == this.getValue()) {\n                return this;\n            }\n\n            return of(value);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/secret/EncryptionKey.java",
    "content": "package io.xpipe.app.secret;\n\nimport lombok.SneakyThrows;\n\nimport java.security.spec.KeySpec;\nimport java.util.Random;\nimport javax.crypto.SecretKey;\nimport javax.crypto.SecretKeyFactory;\nimport javax.crypto.spec.PBEKeySpec;\nimport javax.crypto.spec.SecretKeySpec;\n\npublic class EncryptionKey {\n\n    @SneakyThrows\n    public static SecretKey getEncryptedKey(char[] password, byte[] salt) {\n        String algorithm = \"PBKDF2WithHmacSHA256\";\n        int derivedKeyLength = 256;\n        // https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2\n        int iterations = 600000;\n\n        KeySpec spec = new PBEKeySpec(password, salt, iterations, derivedKeyLength);\n        SecretKeyFactory f = SecretKeyFactory.getInstance(algorithm);\n        return new SecretKeySpec(f.generateSecret(spec).getEncoded(), \"AES\");\n    }\n\n    @SneakyThrows\n    public static SecretKey getLegacyEncryptedKey(char[] password) {\n        int iterations = 8192;\n        var salt = new byte[16];\n        new Random(128).nextBytes(salt);\n        KeySpec spec = new PBEKeySpec(password, salt, iterations, 128);\n        var factory = SecretKeyFactory.getInstance(\"PBKDF2WithHmacSHA256\");\n        return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), \"AES\");\n    }\n\n    @SneakyThrows\n    public static SecretKey getVaultSecretKey(String vaultId) {\n        int iterations = 8192;\n        var salt = new byte[16];\n        new Random(128).nextBytes(salt);\n        KeySpec spec = new PBEKeySpec(vaultId.toCharArray(), salt, iterations, 128);\n        var factory = SecretKeyFactory.getInstance(\"PBKDF2WithHmacSHA256\");\n        return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), \"AES\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/secret/EncryptionToken.java",
    "content": "package io.xpipe.app.secret;\n\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStorageUserHandler;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport lombok.Builder;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport lombok.extern.jackson.Jacksonized;\n\nimport javax.crypto.SecretKey;\n\n@EqualsAndHashCode\n@Builder\n@Jacksonized\n@ToString\npublic class EncryptionToken {\n\n    private static EncryptionToken vaultToken;\n    private static EncryptionToken groupToken;\n    private static EncryptionToken userToken;\n\n    private final String token;\n\n    @JsonIgnore\n    private Boolean isVault;\n\n    @JsonIgnore\n    private Boolean isUser;\n\n    @JsonIgnore\n    private EncryptionToken usedUserToken;\n\n    public static void invalidateUserToken() {\n        userToken = null;\n    }\n\n    private static EncryptionToken createUserToken() {\n        var userHandler = DataStorageUserHandler.getInstance();\n        var userSecretValue =\n                new PasswordLockSecretValue(userHandler.getActiveUser().toCharArray()) {\n                    @Override\n                    protected SecretKey getSecretKey() {\n                        return userHandler.getEncryptionKey();\n                    }\n                };\n        var userCrypt = userSecretValue.getEncryptedValue();\n        return EncryptionToken.builder().token(userCrypt).build();\n    }\n\n    private static EncryptionToken createVaultToken() {\n        var secretValue = new VaultKeySecretValue(new char[] {'x', 'p', 'i', 'p', 'e'});\n        var crypt = secretValue.getEncryptedValue();\n        return EncryptionToken.builder().token(crypt).build();\n    }\n\n    public static EncryptionToken ofUser() {\n        if (userToken == null) {\n            var userHandler = DataStorageUserHandler.getInstance();\n            if (userHandler.getActiveUser() == null) {\n                throw new IllegalStateException(\"No active user available\");\n            }\n\n            userToken = createUserToken();\n        }\n        return userToken;\n    }\n\n    public static EncryptionToken ofVaultKey() {\n        if (vaultToken == null) {\n            vaultToken = createVaultToken();\n        }\n        return vaultToken;\n    }\n\n    public boolean canDecrypt() {\n        return isVault() || isUser();\n    }\n\n    public String decode(SecretKey secretKey) {\n        var secretValue = new PasswordLockSecretValue(token) {\n            @Override\n            protected SecretKey getSecretKey() {\n                return secretKey;\n            }\n        };\n        return secretValue.getSecretValue();\n    }\n\n    public boolean isUser() {\n        var userHandler = DataStorageUserHandler.getInstance();\n        if (userHandler.getActiveUser() == null) {\n            return false;\n        }\n\n        if (userToken == EncryptionToken.ofUser() && isUser != null) {\n            return isUser;\n        }\n\n        usedUserToken = ofUser();\n        isUser = userHandler.getActiveUser().equals(decode(userHandler.getEncryptionKey()));\n        return isUser;\n    }\n\n    public boolean isVault() {\n        if (isVault != null) {\n            return isVault;\n        }\n\n        var key = DataStorage.get().getVaultKey();\n        var s = decode(key);\n        return (isVault = s.equals(\"xpipe\"));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/secret/PasswordLockSecretValue.java",
    "content": "package io.xpipe.app.secret;\n\nimport io.xpipe.app.storage.DataStorageUserHandler;\nimport io.xpipe.core.AesSecretValue;\nimport io.xpipe.core.InPlaceSecretValue;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.EqualsAndHashCode;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport javax.crypto.SecretKey;\n\n@JsonTypeName(\"locked\")\n@SuperBuilder\n@Jacksonized\n@EqualsAndHashCode(callSuper = true)\npublic class PasswordLockSecretValue extends AesSecretValue {\n\n    public PasswordLockSecretValue(String encryptedValue) {\n        super(encryptedValue);\n    }\n\n    public PasswordLockSecretValue(char[] secret) {\n        super(secret);\n    }\n\n    @Override\n    protected SecretKey getSecretKey() {\n        var handler = DataStorageUserHandler.getInstance();\n        return handler != null ? handler.getEncryptionKey() : null;\n    }\n\n    @Override\n    public InPlaceSecretValue inPlace() {\n        return new InPlaceSecretValue(getSecret());\n    }\n\n    @Override\n    public String toString() {\n        return \"<password lock secret>\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/secret/SecretCustomCommandStrategy.java",
    "content": "package io.xpipe.app.secret;\n\nimport io.xpipe.app.comp.base.TextFieldComp;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.ext.ValidationException;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.util.Validators;\nimport io.xpipe.core.InPlaceSecretValue;\n\nimport javafx.beans.property.Property;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.time.Duration;\n\n@JsonTypeName(\"customCommand\")\n@Builder\n@Jacksonized\n@Value\npublic class SecretCustomCommandStrategy implements SecretRetrievalStrategy {\n\n    @SuppressWarnings(\"unused\")\n    public static OptionsBuilder createOptions(\n            Property<SecretCustomCommandStrategy> p, SecretStrategyChoiceConfig config) {\n        var options = new OptionsBuilder();\n        var cmdProperty = options.map(p, SecretCustomCommandStrategy::getCommand);\n        return options.addComp(new TextFieldComp(cmdProperty), cmdProperty)\n                .nonNull()\n                .bind(\n                        () -> {\n                            return new SecretCustomCommandStrategy(cmdProperty.getValue());\n                        },\n                        p);\n    }\n\n    String command;\n\n    @Override\n    public void checkComplete() throws ValidationException {\n        Validators.nonNull(command);\n    }\n\n    @Override\n    public SecretQuery query() {\n        return new SecretQuery() {\n            @Override\n            public SecretQueryResult query(String prompt) {\n                if (command == null || command.isBlank()) {\n                    throw ErrorEventFactory.expected(new IllegalStateException(\"No custom command specified\"));\n                }\n\n                try (var cc = ProcessControlProvider.get()\n                        .createLocalProcessControl(true)\n                        .command(command)\n                        .start()) {\n                    return new SecretQueryResult(\n                            InPlaceSecretValue.of(cc.readStdoutOrThrow()), SecretQueryState.NORMAL);\n                } catch (Exception ex) {\n                    ErrorEventFactory.fromThrowable(\"Unable to retrieve password with command \" + command, ex)\n                            .handle();\n                    return new SecretQueryResult(null, SecretQueryState.RETRIEVAL_FAILURE);\n                }\n            }\n\n            @Override\n            public Duration cacheDuration() {\n                return Duration.ZERO;\n            }\n\n            @Override\n            public boolean retryOnFail() {\n                return false;\n            }\n\n            @Override\n            public boolean requiresUserInteraction() {\n                return false;\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/secret/SecretInPlaceStrategy.java",
    "content": "package io.xpipe.app.secret;\n\nimport io.xpipe.app.comp.base.SecretFieldComp;\nimport io.xpipe.app.ext.ValidationException;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.util.Validators;\nimport io.xpipe.core.InPlaceSecretValue;\n\nimport javafx.beans.property.Property;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\n\nimport java.time.Duration;\nimport java.util.Arrays;\n\n@JsonTypeName(\"inPlace\")\n@Builder\n@Value\npublic class SecretInPlaceStrategy implements SecretRetrievalStrategy {\n\n    @SuppressWarnings(\"unused\")\n    public static String getOptionsNameKey(SecretStrategyChoiceConfig config) {\n        return config != null && config.getPasswordKey() != null ? config.getPasswordKey() : \"password\";\n    }\n\n    @SuppressWarnings(\"unused\")\n    public static OptionsBuilder createOptions(Property<SecretInPlaceStrategy> p, SecretStrategyChoiceConfig config) {\n        var options = new OptionsBuilder();\n        var original = p.getValue() != null ? p.getValue().getValue() : null;\n        var secretProperty = options.map(p, SecretInPlaceStrategy::getValue);\n        return new OptionsBuilder()\n                .addComp(new SecretFieldComp(secretProperty, true), secretProperty)\n                .nonNull()\n                .bind(\n                        () -> {\n                            var newSecret = secretProperty.get();\n                            var changed = !Arrays.equals(\n                                    newSecret != null ? newSecret.getSecret() : new char[0],\n                                    original != null ? original.getSecret() : new char[0]);\n                            var val = changed ? secretProperty.getValue() : original;\n                            return new SecretInPlaceStrategy(val);\n                        },\n                        p);\n    }\n\n    InPlaceSecretValue value;\n\n    public SecretInPlaceStrategy(InPlaceSecretValue value) {\n        this.value = value;\n    }\n\n    @Override\n    public void checkComplete() throws ValidationException {\n        Validators.nonNull(value);\n    }\n\n    @Override\n    public SecretQuery query() {\n        return new SecretQuery() {\n            @Override\n            public SecretQueryResult query(String prompt) {\n                return value != null ? new SecretQueryResult(value, SecretQueryState.NORMAL) : new SecretQueryResult(null, SecretQueryState.RETRIEVAL_FAILURE);\n            }\n\n            @Override\n            public Duration cacheDuration() {\n                return Duration.ZERO;\n            }\n\n            @Override\n            public boolean retryOnFail() {\n                return false;\n            }\n\n            @Override\n            public boolean requiresUserInteraction() {\n                return false;\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/secret/SecretManager.java",
    "content": "package io.xpipe.app.secret;\n\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.process.CountDown;\nimport io.xpipe.app.process.SecretReference;\nimport io.xpipe.app.util.GlobalTimer;\nimport io.xpipe.core.SecretValue;\n\nimport java.time.Duration;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic class SecretManager {\n\n    private static final Map<SecretReference, SecretValue> secrets = new HashMap<>();\n    private static final Set<SecretQueryProgress> progress = new HashSet<>();\n\n    public static synchronized Optional<SecretQueryProgress> getProgress(UUID requestId, UUID storeId) {\n        return progress.stream()\n                .filter(secretQueryProgress ->\n                        secretQueryProgress.getRequestId().equals(requestId)\n                                && secretQueryProgress.getStoreId().equals(storeId))\n                .findFirst();\n    }\n\n    public static synchronized Optional<SecretQueryProgress> getProgress(UUID requestId) {\n        return progress.stream()\n                .filter(secretQueryProgress ->\n                        secretQueryProgress.getRequestId().equals(requestId))\n                .findFirst();\n    }\n\n    public static synchronized SecretQueryProgress expectAskpass(\n            UUID request,\n            UUID storeId,\n            List<SecretQuery> suppliers,\n            SecretQuery fallback,\n            List<SecretQueryFilter> filters,\n            List<SecretQueryFormatter> formatters,\n            CountDown countDown,\n            boolean interactive) {\n        var p = new SecretQueryProgress(\n                request, storeId, suppliers, fallback, filters, formatters, countDown, interactive);\n        // Clear old ones in case we restarted a session\n        clearSecretProgress(request);\n        progress.add(p);\n        return p;\n    }\n\n    public static synchronized void clearSecretProgress(UUID request) {\n        progress.removeIf(\n                secretQueryProgress -> secretQueryProgress.getRequestId().equals(request));\n    }\n\n    public static boolean disableCachingForPrompt(String prompt) {\n        var l = prompt.toLowerCase(Locale.ROOT);\n        // 2FA\n        if (l.contains(\"passcode\") || l.contains(\"verification code\")) {\n            return true;\n        }\n\n        // SSH host key trust prompt\n        if (l.contains(\"authenticity of host\") || l.contains(\"please type 'yes', 'no' or the fingerprint\")) {\n            return true;\n        }\n\n        return false;\n    }\n\n    public static SecretValue retrieve(\n            SecretRetrievalStrategy strategy, String prompt, UUID secretId, int sub, boolean interactive) {\n        if (!strategy.expectsQuery()) {\n            return null;\n        }\n\n        var uuid = UUID.randomUUID();\n        var p = expectAskpass(\n                uuid,\n                secretId,\n                List.of(strategy.query()),\n                SecretQuery.prompt(false),\n                List.of(),\n                List.of(),\n                CountDown.of(),\n                interactive);\n        p.preAdvance(sub);\n        var r = p.process(prompt);\n        completeRequest(uuid);\n        return r;\n    }\n\n    public static synchronized List<SecretQueryProgress> completeRequest(UUID request) {\n        var found = progress.stream()\n                .filter(secretQueryProgress ->\n                        secretQueryProgress.getRequestId().equals(request))\n                .toList();\n\n        if (progress.removeAll(found)) {\n            TrackEvent.withTrace(\"Completed secret request\")\n                    .tag(\"uuid\", request)\n                    .handle();\n            return found;\n        } else {\n            return List.of();\n        }\n    }\n\n    public static synchronized void clearAll(UUID id) {\n        secrets.entrySet()\n                .removeIf(secretReferenceSecretValueEntry ->\n                        secretReferenceSecretValueEntry.getKey().getSecretId().equals(id));\n    }\n\n    public static synchronized void moveReferences(UUID oldId, UUID newId) {\n        var oldSecrets = secrets.entrySet().stream()\n                .filter(e -> e.getKey().getSecretId().equals(oldId))\n                .collect(Collectors.toSet());\n        secrets.entrySet().removeAll(oldSecrets);\n\n        for (Map.Entry<SecretReference, SecretValue> e : oldSecrets) {\n            var newRef = new SecretReference(newId, e.getKey().getSubId());\n            secrets.put(newRef, e.getValue());\n        }\n    }\n\n    public static synchronized void clear(SecretReference ref) {\n        secrets.remove(ref);\n    }\n\n    public static synchronized void cache(SecretReference ref, SecretValue value, Duration duration) {\n        secrets.put(ref, value);\n        if (duration != null && duration.isPositive()) {\n            GlobalTimer.delay(\n                    () -> {\n                        synchronized (SecretManager.class) {\n                            secrets.remove(ref);\n                        }\n                    },\n                    duration);\n        }\n    }\n\n    public static synchronized Optional<SecretValue> get(SecretReference ref) {\n        return Optional.ofNullable(secrets.get(ref));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/secret/SecretNoneStrategy.java",
    "content": "package io.xpipe.app.secret;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Value;\n\n@JsonTypeName(\"none\")\n@Value\npublic class SecretNoneStrategy implements SecretRetrievalStrategy {\n\n    @Override\n    public SecretQuery query() {\n        return null;\n    }\n\n    public boolean expectsQuery() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/secret/SecretPasswordManagerStrategy.java",
    "content": "package io.xpipe.app.secret;\n\nimport io.xpipe.app.comp.base.ButtonComp;\nimport io.xpipe.app.comp.base.InputGroupComp;\nimport io.xpipe.app.comp.base.TextFieldComp;\nimport io.xpipe.app.core.App;\nimport io.xpipe.app.ext.ValidationException;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.util.Validators;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.nio.CharBuffer;\nimport java.time.Duration;\nimport java.util.List;\n\n@JsonTypeName(\"passwordManager\")\n@Builder\n@Jacksonized\n@Value\npublic class SecretPasswordManagerStrategy implements SecretRetrievalStrategy {\n\n    @SuppressWarnings(\"unused\")\n    public static OptionsBuilder createOptions(\n            Property<SecretPasswordManagerStrategy> p, SecretStrategyChoiceConfig config) {\n        var options = new OptionsBuilder();\n        var prefs = AppPrefs.get();\n        var keyProperty = options.map(p, SecretPasswordManagerStrategy::getKey);\n        var field = new TextFieldComp(keyProperty).apply(struc -> struc.promptTextProperty()\n                .bind(Bindings.createStringBinding(\n                        () -> {\n                            return prefs.passwordManager().getValue() != null\n                                    ? prefs.passwordManager().getValue().getKeyPlaceholder()\n                                    : \"?\";\n                        },\n                        prefs.passwordManager())));\n        var button = new ButtonComp(null, new FontIcon(\"mdomz-settings\"), () -> {\n            AppPrefs.get().selectCategory(\"passwordManager\");\n            App.getApp().getStage().requestFocus();\n        });\n        var content = new InputGroupComp(List.of(field, button));\n        content.setMainReference(field);\n        return options.nameAndDescription(\"passwordManagerKey\")\n                .addComp(content, keyProperty)\n                .nonNull()\n                .bind(\n                        () -> {\n                            return new SecretPasswordManagerStrategy(keyProperty.getValue());\n                        },\n                        p);\n    }\n\n    String key;\n\n    @Override\n    public void checkComplete() throws ValidationException {\n        Validators.nonNull(key);\n    }\n\n    @Override\n    public SecretQuery query() {\n        return new SecretQuery() {\n            @Override\n            public SecretQueryResult query(String prompt) {\n                var pm = AppPrefs.get().passwordManager().getValue();\n                if (pm == null) {\n                    ErrorEventFactory.fromMessage(\n                                    \"A password manager was requested but no password manager has been set in the settings menu\")\n                            .expected()\n                            .handle();\n                    return new SecretQueryResult(null, SecretQueryState.RETRIEVAL_FAILURE);\n                }\n\n                var r = pm.retrieveCredentials(key);\n                if (r == null || r.getPassword() == null) {\n                    return new SecretQueryResult(null, SecretQueryState.RETRIEVAL_FAILURE);\n                }\n\n                r.getPassword().withSecretValue(chars -> {\n                    var seq = CharBuffer.wrap(chars);\n                    var newline = seq.chars().anyMatch(value -> value == 10);\n                    if (seq.length() == 0 || newline) {\n                        throw ErrorEventFactory.expected(\n                                new IllegalArgumentException(\"Received not exactly one output line:\\n\" + r\n                                        + \"\\n\\n\"\n                                        + \"XPipe requires your password manager command to output only the raw password.\"\n                                        + \" If the output includes any formatting, messages, or your password key either matched multiple entries or \"\n                                        + \"none,\"\n                                        + \" you will have to change the command and/or password key.\"));\n                    }\n                });\n                return new SecretQueryResult(r.getPassword(), SecretQueryState.NORMAL);\n            }\n\n            @Override\n            public Duration cacheDuration() {\n                // To reduce password manager access, cache it\n                var pm = AppPrefs.get().passwordManager().getValue();\n                return pm != null ? pm.getCacheDuration() : Duration.ofSeconds(15);\n            }\n\n            @Override\n            public boolean retryOnFail() {\n                return false;\n            }\n\n            @Override\n            public boolean requiresUserInteraction() {\n                return false;\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/secret/SecretPromptStrategy.java",
    "content": "package io.xpipe.app.secret;\n\nimport io.xpipe.app.util.AskpassAlert;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Value;\n\nimport java.time.Duration;\n\n@JsonTypeName(\"prompt\")\n@Value\npublic class SecretPromptStrategy implements SecretRetrievalStrategy {\n\n    @Override\n    public SecretQuery query() {\n        return new SecretQuery() {\n            @Override\n            public SecretQueryResult query(String prompt) {\n                return AskpassAlert.queryRaw(prompt, null, true);\n            }\n\n            @Override\n            public Duration cacheDuration() {\n                return null;\n            }\n\n            @Override\n            public boolean retryOnFail() {\n                return true;\n            }\n\n            @Override\n            public boolean requiresUserInteraction() {\n                return true;\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/secret/SecretQuery.java",
    "content": "package io.xpipe.app.secret;\n\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.SecretReference;\nimport io.xpipe.app.util.AskpassAlert;\n\nimport java.time.Duration;\nimport java.util.Optional;\n\npublic interface SecretQuery {\n\n    static SecretQuery confirmElevationIfNeeded(SecretQuery original, boolean needed) {\n        if (!needed) {\n            return original;\n        }\n\n        return confirmElevation(original);\n    }\n\n    static SecretQuery confirmElevation(SecretQuery original) {\n        return new SecretQuery() {\n\n            @Override\n            public Optional<SecretQueryResult> retrieveCache(String prompt, SecretReference reference) {\n                var found = SecretQuery.super.retrieveCache(prompt, reference);\n                if (found.isEmpty()) {\n                    return Optional.empty();\n                }\n\n                var ask = AppPrefs.get().alwaysConfirmElevation().getValue();\n                if (!ask) {\n                    return found;\n                }\n\n                var inPlace = found.get().getSecret().inPlace();\n                var r = AskpassAlert.queryRaw(prompt, inPlace, true);\n                return r.getState() != SecretQueryState.NORMAL ? Optional.of(r) : found;\n            }\n\n            @Override\n            public SecretQueryResult query(String prompt) {\n                var r = original.query(prompt);\n                if (r.getState() != SecretQueryState.NORMAL) {\n                    return r;\n                }\n\n                // Don't confirm if we already had user interaction\n                var ask = AppPrefs.get().alwaysConfirmElevation().getValue() && !original.requiresUserInteraction();\n                if (!ask) {\n                    return r;\n                }\n\n                var inPlace = r.getSecret().inPlace();\n                return AskpassAlert.queryRaw(prompt, inPlace, true);\n            }\n\n            @Override\n            public Duration cacheDuration() {\n                return null;\n            }\n\n            @Override\n            public boolean retryOnFail() {\n                return true;\n            }\n\n            @Override\n            public boolean requiresUserInteraction() {\n                return original.requiresUserInteraction()\n                        || AppPrefs.get().alwaysConfirmElevation().getValue();\n            }\n        };\n    }\n\n    static SecretQuery prompt(boolean cache) {\n        return new SecretQuery() {\n            @Override\n            public SecretQueryResult query(String prompt) {\n                return AskpassAlert.queryRaw(prompt, null, true);\n            }\n\n            @Override\n            public Duration cacheDuration() {\n                return cache ? null : Duration.ZERO;\n            }\n\n            @Override\n            public boolean retryOnFail() {\n                return true;\n            }\n\n            @Override\n            public boolean requiresUserInteraction() {\n                return true;\n            }\n        };\n    }\n\n    default Optional<SecretQueryResult> retrieveCache(String prompt, SecretReference reference) {\n        var r = SecretManager.get(reference);\n        return r.map(secretValue -> new SecretQueryResult(secretValue, SecretQueryState.NORMAL));\n    }\n\n    SecretQueryResult query(String prompt);\n\n    Duration cacheDuration();\n\n    boolean retryOnFail();\n\n    boolean requiresUserInteraction();\n\n    default boolean respectDontCacheSetting() {\n        return true;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/secret/SecretQueryFilter.java",
    "content": "package io.xpipe.app.secret;\n\nimport io.xpipe.core.SecretValue;\n\nimport java.util.Optional;\n\npublic interface SecretQueryFilter {\n\n    Optional<SecretValue> filter(SecretQueryProgress progress, String prompt);\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/secret/SecretQueryFormatter.java",
    "content": "package io.xpipe.app.secret;\n\nimport java.util.Optional;\n\npublic interface SecretQueryFormatter {\n\n    Optional<String> format(String prompt);\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/secret/SecretQueryProgress.java",
    "content": "package io.xpipe.app.secret;\n\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.CountDown;\nimport io.xpipe.app.process.SecretReference;\nimport io.xpipe.core.SecretValue;\n\nimport lombok.Getter;\nimport lombok.NonNull;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\n\n@Getter\npublic class SecretQueryProgress {\n\n    private final UUID requestId;\n    private final UUID storeId;\n    private final List<SecretQuery> suppliers;\n    private final SecretQuery fallback;\n    private final List<SecretQueryFilter> filters;\n    private final List<SecretQueryFormatter> formatters;\n    private final List<String> seenPrompts;\n    private final CountDown countDown;\n    private final boolean interactive;\n    private SecretQueryState state = SecretQueryState.NORMAL;\n\n    public SecretQueryProgress(\n            @NonNull UUID requestId,\n            @NonNull UUID storeId,\n            @NonNull List<SecretQuery> suppliers,\n            @NonNull SecretQuery fallback,\n            @NonNull List<SecretQueryFilter> filters,\n            List<SecretQueryFormatter> formatters,\n            @NonNull CountDown countDown,\n            boolean interactive) {\n        this.requestId = requestId;\n        this.storeId = storeId;\n        this.suppliers = new ArrayList<>(suppliers);\n        this.fallback = fallback;\n        this.filters = filters;\n        this.formatters = formatters;\n        this.countDown = countDown;\n        this.interactive = interactive;\n        this.seenPrompts = new ArrayList<>();\n    }\n\n    public void preAdvance(int count) {\n        for (int i = 0; i < count; i++) {\n            seenPrompts.addFirst(null);\n            suppliers.addFirst(SecretQuery.prompt(false));\n        }\n    }\n\n    public SecretValue process(String prompt) {\n        // Cancel early\n        if (state != SecretQueryState.NORMAL) {\n            return null;\n        }\n\n        for (SecretQueryFilter filter : filters) {\n            var o = filter.filter(this, prompt);\n            if (o.isPresent()) {\n                return o.get();\n            }\n        }\n\n        for (var formatter : formatters) {\n            var r = formatter.format(prompt);\n            if (r.isPresent()) {\n                prompt = r.get();\n            }\n        }\n\n        var seenBefore = seenPrompts.contains(prompt);\n        if (!seenBefore) {\n            seenPrompts.add(prompt);\n        }\n\n        var firstSeenIndex = seenPrompts.indexOf(prompt);\n        if (firstSeenIndex >= suppliers.size()) {\n            // Check whether we can have user inputs\n            if (!interactive && fallback.requiresUserInteraction()) {\n                state = SecretQueryState.NON_INTERACTIVE;\n                return null;\n            }\n\n            countDown.pause();\n            var r = fallback.query(prompt);\n            countDown.resume();\n\n            if (r.getState() != SecretQueryState.NORMAL) {\n                state = r.getState();\n                return null;\n            }\n            return r.getSecret();\n        }\n\n        var ref = new SecretReference(storeId, firstSeenIndex);\n        var sup = suppliers.get(firstSeenIndex);\n        var shouldCache = shouldCache(sup, prompt);\n        var wasLastPrompt = firstSeenIndex == seenPrompts.size() - 1;\n\n        // Check whether we can have user inputs\n        if (!interactive && sup.requiresUserInteraction()) {\n            state = SecretQueryState.NON_INTERACTIVE;\n            return null;\n        }\n\n        // Clear cache if secret was wrong/queried again\n        // Check whether this is actually the last prompt seen as it might happen that\n        // previous prompts get rolled back again when one further down is wrong\n        if (seenBefore && shouldCache && wasLastPrompt) {\n            SecretManager.clear(ref);\n        }\n\n        // If we supplied a wrong secret and cannot retry, cancel the entire request\n        if (seenBefore && wasLastPrompt && !sup.retryOnFail()) {\n            state = SecretQueryState.FIXED_SECRET_WRONG;\n            return null;\n        }\n\n        if (shouldCache) {\n            countDown.pause();\n            var cached = sup.retrieveCache(prompt, ref);\n            countDown.resume();\n            if (cached.isPresent()) {\n                if (cached.get().getState() != SecretQueryState.NORMAL) {\n                    state = cached.get().getState();\n                    return null;\n                }\n\n                return cached.get().getSecret();\n            }\n        }\n\n        countDown.pause();\n        var r = sup.query(prompt);\n        countDown.resume();\n\n        if (r.getState() != SecretQueryState.NORMAL) {\n            state = r.getState();\n            return null;\n        }\n\n        if (shouldCache) {\n            SecretManager.cache(ref, r.getSecret(), sup.cacheDuration());\n        }\n        return r.getSecret();\n    }\n\n    private boolean shouldCache(SecretQuery query, String prompt) {\n        var hasDuration = query.cacheDuration() == null || query.cacheDuration().isPositive();\n        var shouldCache = hasDuration\n                && !SecretManager.disableCachingForPrompt(prompt)\n                && (!query.respectDontCacheSetting()\n                        || !AppPrefs.get().dontCachePasswords().get());\n        return shouldCache;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/secret/SecretQueryResult.java",
    "content": "package io.xpipe.app.secret;\n\nimport io.xpipe.core.SecretValue;\n\nimport lombok.Value;\n\n@Value\npublic class SecretQueryResult {\n\n    SecretValue secret;\n    SecretQueryState state;\n\n    public SecretQueryResult(SecretValue secret, SecretQueryState state) {\n        this.secret = secret;\n        this.state = state;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/secret/SecretQueryState.java",
    "content": "package io.xpipe.app.secret;\n\npublic enum SecretQueryState {\n    NORMAL,\n    CANCELLED,\n    NON_INTERACTIVE,\n    FIXED_SECRET_WRONG,\n    RETRIEVAL_FAILURE;\n\n    public static String toErrorMessage(SecretQueryState s) {\n        if (s == null) {\n            return \"None\";\n        }\n\n        return switch (s) {\n            case NORMAL -> {\n                yield \"None\";\n            }\n            case CANCELLED -> {\n                yield \"Authentication operation was cancelled\";\n            }\n            case NON_INTERACTIVE -> {\n                yield \"Session is not interactive but required user input for authentication\";\n            }\n            case FIXED_SECRET_WRONG -> {\n                yield \"Authentication failed: Provided authentication secret was not accepted by the server, probably because it is incorrect\";\n            }\n            case RETRIEVAL_FAILURE -> {\n                yield \"Failed to retrieve secret for authentication\";\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/secret/SecretRetrievalStrategy.java",
    "content": "package io.xpipe.app.secret;\n\nimport io.xpipe.app.ext.ValidationException;\n\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\npublic interface SecretRetrievalStrategy {\n\n    static List<Class<?>> getClasses() {\n        var l = new ArrayList<Class<?>>();\n        l.add(SecretNoneStrategy.class);\n        l.add(SecretInPlaceStrategy.class);\n        l.add(SecretPromptStrategy.class);\n        l.add(SecretPasswordManagerStrategy.class);\n        l.add(SecretCustomCommandStrategy.class);\n        return l;\n    }\n\n    default void checkComplete() throws ValidationException {}\n\n    SecretQuery query();\n\n    default boolean expectsQuery() {\n        return true;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/secret/SecretStrategyChoiceConfig.java",
    "content": "package io.xpipe.app.secret;\n\nimport lombok.Builder;\nimport lombok.Value;\n\n@Value\n@Builder\npublic class SecretStrategyChoiceConfig {\n\n    boolean allowNone;\n    String passwordKey;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/secret/VaultKeySecretValue.java",
    "content": "package io.xpipe.app.secret;\n\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.core.AesSecretValue;\nimport io.xpipe.core.InPlaceSecretValue;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.EqualsAndHashCode;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport javax.crypto.SecretKey;\n\n@JsonTypeName(\"vault\")\n@SuperBuilder\n@Jacksonized\n@EqualsAndHashCode(callSuper = true)\npublic class VaultKeySecretValue extends AesSecretValue {\n\n    public VaultKeySecretValue(String encryptedValue) {\n        super(encryptedValue);\n    }\n\n    public VaultKeySecretValue(char[] secret) {\n        super(secret);\n    }\n\n    @Override\n    protected SecretKey getSecretKey() {\n        return DataStorage.get() != null ? DataStorage.get().getVaultKey() : null;\n    }\n\n    @Override\n    public InPlaceSecretValue inPlace() {\n        return new InPlaceSecretValue(getSecret());\n    }\n\n    @Override\n    public String toString() {\n        return \"<vault secret>\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/spice/CustomSpiceClient.java",
    "content": "package io.xpipe.app.spice;\n\nimport io.xpipe.app.comp.base.TextFieldComp;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.prefs.ExternalApplicationHelper;\nimport io.xpipe.app.process.CommandBuilder;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleObjectProperty;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.Locale;\n\n@JsonTypeName(\"custom\")\n@Value\n@Jacksonized\n@Builder\npublic class CustomSpiceClient implements ExternalSpiceClient {\n\n    String command;\n\n    @SuppressWarnings(\"unused\")\n    static OptionsBuilder createOptions(Property<CustomSpiceClient> property) {\n        var command = new SimpleObjectProperty<>(property.getValue().getCommand());\n        return new OptionsBuilder()\n                .nameAndDescription(\"customSpiceCommand\")\n                .addComp(\n                        new TextFieldComp(command, false)\n                                .apply(struc -> struc.setPromptText(\"myspiceClient $FILE\"))\n                                .maxWidth(600),\n                        command)\n                .bind(() -> CustomSpiceClient.builder().command(command.get()).build(), property);\n    }\n\n    @Override\n    public void launch(SpiceLaunchConfig configuration) throws Exception {\n        if (command == null) {\n            return;\n        }\n\n        var format = command.toLowerCase(Locale.ROOT).contains(\"$file\") ? command : command + \" $FILE\";\n        var toExecute = ExternalApplicationHelper.replaceVariableArgument(\n                format, \"ADDRESS\", configuration.getFile().toString());\n        ExternalApplicationHelper.startAsync(CommandBuilder.of().add(toExecute));\n    }\n\n    @Override\n    public String getWebsite() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/spice/ExternalSpiceClient.java",
    "content": "package io.xpipe.app.spice;\n\nimport io.xpipe.app.ext.PrefsValue;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.core.OsType;\n\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\npublic interface ExternalSpiceClient extends PrefsValue {\n\n    static ExternalSpiceClient determineDefault(ExternalSpiceClient existing) {\n        // Verify that our selection is still valid\n        if (existing != null && existing.isAvailable()) {\n            return existing;\n        }\n\n        return switch (OsType.ofLocal()) {\n            case OsType.Linux ignored -> {\n                yield new RemoteViewerSpiceClient.Linux();\n            }\n            case OsType.MacOs ignored -> {\n                yield new RemoteViewerSpiceClient.MacOs();\n            }\n            case OsType.Windows ignored -> {\n                yield new RemoteViewerSpiceClient.Windows();\n            }\n        };\n    }\n\n    static void launchClient(SpiceLaunchConfig configuration) throws Exception {\n        var client = AppPrefs.get().spiceClient.getValue();\n        if (client == null) {\n            return;\n        }\n\n        client.launch(configuration);\n    }\n\n    static List<Class<?>> getClasses() {\n        var l = new ArrayList<Class<?>>();\n        switch (OsType.ofLocal()) {\n            case OsType.Linux ignored -> {\n                l.add(RemoteViewerSpiceClient.Linux.class);\n            }\n            case OsType.MacOs ignored -> {\n                l.add(RemoteViewerSpiceClient.MacOs.class);\n            }\n            case OsType.Windows ignored -> {\n                l.add(RemoteViewerSpiceClient.Windows.class);\n            }\n        }\n        l.add(CustomSpiceClient.class);\n        return l;\n    }\n\n    void launch(SpiceLaunchConfig configuration) throws Exception;\n\n    String getWebsite();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/spice/RemoteViewerSpiceClient.java",
    "content": "package io.xpipe.app.spice;\n\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Optional;\n\npublic abstract class RemoteViewerSpiceClient implements ExternalSpiceClient {\n\n    protected CommandBuilder createBuilder(SpiceLaunchConfig configuration) {\n        var builder = CommandBuilder.of().addFile(configuration.getFile());\n        return builder;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://virt-manager.org\";\n    }\n\n    @Builder\n    @Jacksonized\n    @JsonTypeName(\"remoteViewer\")\n    public static class Windows extends RemoteViewerSpiceClient implements ExternalApplicationType.WindowsType {\n\n        @Override\n        public boolean detach() {\n            return false;\n        }\n\n        @Override\n        public String getExecutable() {\n            return \"remote-viewer.exe\";\n        }\n\n        @Override\n        public Optional<Path> determineInstallation() {\n            try (var stream = Files.list(AppSystemInfo.ofWindows().getProgramFiles())) {\n                var l = stream.toList();\n                var found = l.stream()\n                        .filter(path -> path.toString().contains(\"VirtViewer\"))\n                        .findFirst();\n                if (found.isEmpty()) {\n                    return Optional.empty();\n                }\n\n                return Optional.ofNullable(found.get().resolve(\"bin\", \"remote-viewer.exe\"));\n            } catch (IOException e) {\n                ErrorEventFactory.fromThrowable(e).handle();\n                return Optional.empty();\n            }\n        }\n\n        @Override\n        public void launch(SpiceLaunchConfig configuration) throws Exception {\n            var builder = createBuilder(configuration);\n            launch(builder);\n        }\n    }\n\n    @Builder\n    @Jacksonized\n    @JsonTypeName(\"remoteViewer\")\n    public static class Linux extends RemoteViewerSpiceClient implements ExternalApplicationType.LinuxApplication {\n\n        @Override\n        public void launch(SpiceLaunchConfig configuration) throws Exception {\n            var builder = createBuilder(configuration);\n            launch(builder);\n        }\n\n        @Override\n        public String getExecutable() {\n            return \"remote-viewer\";\n        }\n\n        @Override\n        public boolean detach() {\n            return true;\n        }\n\n        @Override\n        public String getFlatpakId() {\n            return \"org.virt_manager.virt-viewer\";\n        }\n    }\n\n    @Builder\n    @Jacksonized\n    @JsonTypeName(\"remoteViewer\")\n    public static class MacOs extends RemoteViewerSpiceClient implements ExternalApplicationType.PathApplication {\n\n        @Override\n        public void launch(SpiceLaunchConfig configuration) throws Exception {\n            var builder = createBuilder(configuration);\n            launch(builder);\n        }\n\n        @Override\n        public String getExecutable() {\n            return \"remote-viewer\";\n        }\n\n        @Override\n        public boolean detach() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/spice/SpiceLaunchConfig.java",
    "content": "package io.xpipe.app.spice;\n\nimport io.xpipe.app.storage.DataStoreEntry;\n\nimport lombok.Value;\n\nimport java.nio.file.Path;\n\n@Value\npublic class SpiceLaunchConfig {\n\n    DataStoreEntry entry;\n    Path file;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/storage/ContextualFileReference.java",
    "content": "package io.xpipe.app.storage;\n\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.process.OsFileSystem;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.core.FilePath;\n\nimport lombok.AccessLevel;\nimport lombok.AllArgsConstructor;\nimport lombok.NonNull;\nimport lombok.Value;\n\nimport java.util.regex.Matcher;\n\n@AllArgsConstructor(access = AccessLevel.PRIVATE)\n@Value\npublic class ContextualFileReference {\n\n    private static FilePath lastDataDir;\n\n    @NonNull\n    String path;\n\n    private static FilePath getDataDir() {\n        if (DataStorage.get() == null) {\n            return lastDataDir != null\n                    ? lastDataDir\n                    : FilePath.of(AppProperties.get()\n                                    .getDataDir()\n                                    .resolve(\"storage\")\n                                    .resolve(\"data\"))\n                            .toUnix();\n        }\n\n        return lastDataDir = FilePath.of(DataStorage.get().getDataDir()).toUnix();\n    }\n\n    public static ContextualFileReference of(FilePath p) {\n        if (p == null) {\n            return null;\n        }\n\n        var ns = p.normalize().toUnix();\n        var home =\n                FilePath.of(AppSystemInfo.ofCurrent().getUserHome()).normalize().toUnix();\n\n        String replaced;\n        var withHomeResolved = ns.toString().replace(\"~\", home.toString());\n        // Only replace ~ if it is part of data dir, otherwise keep it raw\n        if (withHomeResolved.startsWith(getDataDir().toString())) {\n            replaced = withHomeResolved.replace(\"<DATA>\", getDataDir().toString());\n        } else {\n            replaced = ns.toString().replace(\"<DATA>\", getDataDir().toString());\n        }\n        return new ContextualFileReference(replaced);\n    }\n\n    public static ContextualFileReference of(String s) {\n        return of(s != null ? FilePath.of(s) : null);\n    }\n\n    public FilePath toAbsoluteFilePath(ShellControl sc) {\n        return FilePath.of(path.replaceAll(\n                \"/\",\n                Matcher.quoteReplacement(\n                        sc != null ? OsFileSystem.of(sc.getOsType()).getFileSystemSeparator() : \"/\")));\n    }\n\n    public FilePath toLocalAbsoluteFilePath() {\n        return FilePath.of(path.replaceAll(\n                \"/\", Matcher.quoteReplacement(OsFileSystem.ofLocal().getFileSystemSeparator())));\n    }\n\n    public boolean isInDataDirectory() {\n        return serialize().contains(\"<DATA>\");\n    }\n\n    public String serialize() {\n        var start = getDataDir();\n        var normalizedPath = FilePath.of(path).normalize().toUnix();\n        if (normalizedPath.startsWith(start) && !normalizedPath.equals(start)) {\n            return \"<DATA>\" + \"/\" + normalizedPath.relativize(start);\n        }\n        return path;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/storage/DataStateHandler.java",
    "content": "package io.xpipe.app.storage;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.DataStoreState;\nimport io.xpipe.app.ext.StatefulDataStore;\n\nimport java.util.function.Supplier;\n\npublic class DataStateHandler {\n\n    private static final DataStateHandler INSTANCE = new DataStateHandler();\n\n    public static DataStateHandler get() {\n        return INSTANCE;\n    }\n\n    public void setState(DataStore store, DataStoreState value) {\n        if (DataStorage.get() == null) {\n            return;\n        }\n\n        var entry = DataStorage.get().getStoreEntryIfPresent(store, true).or(() -> DataStorage.get()\n                .getStoreEntryInProgressIfPresent(store));\n        if (entry.isEmpty()) {\n            return;\n        }\n\n        entry.get().setStorePersistentState(value);\n    }\n\n    public <T extends DataStoreState> T getState(DataStore store, Supplier<T> def) {\n        if (DataStorage.get() == null) {\n            return def.get();\n        }\n\n        var entry = DataStorage.get().getStoreEntryIfPresent(store, true).or(() -> DataStorage.get()\n                .getStoreEntryInProgressIfPresent(store));\n        if (entry.isEmpty()) {\n            return def.get();\n        }\n\n        if (!(store instanceof StatefulDataStore<?>)) {\n            return def.get();\n        }\n\n        var found = entry.get().getStorePersistentState();\n        if (found == null) {\n            entry.get().setStorePersistentState(def.get());\n        }\n        T r = entry.get().getStorePersistentState();\n        return r != null ? r : def.get();\n    }\n\n    public void putCache(DataStore store, String key, Object value) {\n        if (DataStorage.get() == null) {\n            return;\n        }\n\n        var entry = DataStorage.get().getStoreEntryIfPresent(store, true).or(() -> DataStorage.get()\n                .getStoreEntryInProgressIfPresent(store));\n        if (entry.isEmpty()) {\n            return;\n        }\n\n        entry.get().setStoreCache(key, value);\n    }\n\n    public <T> T getCache(DataStore store, String key, Class<T> c, Supplier<T> def) {\n        if (DataStorage.get() == null) {\n            return def.get();\n        }\n\n        var entry = DataStorage.get().getStoreEntryIfPresent(store, true).or(() -> DataStorage.get()\n                .getStoreEntryInProgressIfPresent(store));\n        if (entry.isEmpty()) {\n            return def.get();\n        }\n\n        var r = entry.get().getStoreCache().get(key);\n        if (r == null) {\n            r = def.get();\n            entry.get().setStoreCache(key, r);\n        }\n        return c.cast(r);\n    }\n\n    public boolean canCacheToStorage(DataStore store) {\n        if (DataStorage.get() == null) {\n            return false;\n        }\n\n        var entry = DataStorage.get().getStoreEntryIfPresent(store, true).or(() -> DataStorage.get()\n                .getStoreEntryInProgressIfPresent(store));\n        return entry.isPresent();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/storage/DataStorage.java",
    "content": "package io.xpipe.app.storage;\n\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.secret.SecretManager;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.StorePath;\n\nimport javafx.util.Pair;\n\nimport lombok.Getter;\nimport lombok.NonNull;\nimport lombok.Setter;\nimport lombok.SneakyThrows;\n\nimport java.nio.file.Path;\nimport java.time.Instant;\nimport java.util.*;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport javax.crypto.SecretKey;\n\npublic abstract class DataStorage {\n\n    public static final UUID ALL_CONNECTIONS_CATEGORY_UUID = UUID.fromString(\"bfb0b51a-e7a3-4ce4-8878-8d4cb5828d6c\");\n    public static final UUID ALL_SCRIPTS_CATEGORY_UUID = UUID.fromString(\"19024cf9-d192-41a9-88a6-a22694cf716a\");\n    public static final UUID PREDEFINED_SCRIPTS_CATEGORY_UUID = UUID.fromString(\"5faf1d71-0efc-4293-8b70-299406396973\");\n    public static final UUID CUSTOM_SCRIPTS_CATEGORY_UUID = UUID.fromString(\"d3496db5-b709-41f9-abc0-ee0a660fbab9\");\n    public static final UUID SCRIPT_SOURCES_CATEGORY_UUID = UUID.fromString(\"4b766928-372b-4ac4-9d10-260ce65288cd\");\n    public static final UUID DEFAULT_CATEGORY_UUID = UUID.fromString(\"97458c07-75c0-4f9d-a06e-92d8cdf67c40\");\n    public static final UUID LOCAL_ID = UUID.fromString(\"f0ec68aa-63f5-405c-b178-9a4454556d6b\");\n    public static final UUID ALL_IDENTITIES_CATEGORY_UUID = UUID.fromString(\"23a5565d-b343-4ab2-abf4-48a5d12dda22\");\n    public static final UUID ALL_MACROS_CATEGORY_UUID = UUID.fromString(\"f65b769a-cec9-4f30-ad58-95fe68d79c2c\");\n    public static final UUID LOCAL_IDENTITIES_CATEGORY_UUID = UUID.fromString(\"e784de4e-abea-4cb8-a839-fc557cd23097\");\n    public static final UUID SYNCED_IDENTITIES_CATEGORY_UUID = UUID.fromString(\"69aa5040-28dc-451e-b4ff-1192ce5e1e3c\");\n    private static DataStorage INSTANCE;\n    protected final Path dir;\n\n    @Getter\n    protected final List<DataStoreCategory> storeCategories;\n\n    protected final Map<DataStoreEntry, DataStoreEntry> storeEntries;\n\n    @Getter\n    protected final Set<DataStoreEntry> storeEntriesSet;\n\n    @Getter\n    private final List<StorageListener> listeners = new CopyOnWriteArrayList<>();\n\n    private final Map<DataStoreEntry, DataStoreEntry> storeEntriesInProgress = new ConcurrentHashMap<>();\n    private final Map<DataStore, DataStoreEntry> identityStoreEntryMapCache = new IdentityHashMap<>();\n    private final Map<DataStore, DataStoreEntry> storeEntryMapCache = new HashMap<>();\n    private final Map<DataStore, DataStore> storeMoveCache = new IdentityHashMap<>();\n\n    @Getter\n    protected boolean entriesAvailable;\n\n    @Getter\n    @Setter\n    protected DataStoreCategory selectedCategory;\n\n    public DataStorage() {\n        this.dir = getStorageDirectory();\n        this.storeEntries = new ConcurrentHashMap<>();\n        this.storeEntriesSet = storeEntries.keySet();\n        this.storeCategories = new CopyOnWriteArrayList<>();\n    }\n\n    public static Path getStorageDirectory() {\n        var dir = AppProperties.get().getDataDir().resolve(\"storage\");\n        return dir;\n    }\n\n    public static void init() {\n        if (INSTANCE != null) {\n            return;\n        }\n\n        INSTANCE = AppProperties.get().isPersistData() ? new StandardStorage() : new ImpersistentStorage();\n        INSTANCE.load();\n    }\n\n    public static void reset() {\n        if (INSTANCE == null) {\n            return;\n        }\n\n        INSTANCE.dispose();\n\n        // We want to keep the storage for all dependent instances\n        // that might still refer to it after the reset\n        // INSTANCE = null;\n    }\n\n    public static DataStorage get() {\n        return INSTANCE;\n    }\n\n    public void generateCaches() {\n        for (DataStoreEntry storeEntry : getStoreEntries()) {\n            getStoreChildren(storeEntry);\n        }\n    }\n\n    public abstract void pullManually();\n\n    public abstract void pushManually();\n\n    public abstract void reloadContent();\n\n    public abstract SecretKey getVaultKey();\n\n    public DataStoreCategory getDefaultConnectionsCategory() {\n        return getStoreCategoryIfPresent(DEFAULT_CATEGORY_UUID).orElseThrow();\n    }\n\n    public DataStoreCategory getAllConnectionsCategory() {\n        return getStoreCategoryIfPresent(ALL_CONNECTIONS_CATEGORY_UUID).orElseThrow();\n    }\n\n    public DataStoreCategory getAllScriptsCategory() {\n        return getStoreCategoryIfPresent(ALL_SCRIPTS_CATEGORY_UUID).orElseThrow();\n    }\n\n    public DataStoreCategory getAllIdentitiesCategory() {\n        return getStoreCategoryIfPresent(ALL_IDENTITIES_CATEGORY_UUID).orElseThrow();\n    }\n\n    @SuppressWarnings(\"unused\")\n    public DataStoreCategory getAllMacrosCategory() {\n        return getStoreCategoryIfPresent(ALL_MACROS_CATEGORY_UUID).orElseThrow();\n    }\n\n    public void forceRewrite() {\n        TrackEvent.info(\"Starting forced storage rewrite\");\n        getStoreEntries().forEach(dataStoreEntry -> {\n            dataStoreEntry.reassignStoreNode();\n        });\n        TrackEvent.info(\"Finished forced storage rewrite\");\n    }\n\n    private void dispose() {\n        save(true);\n        var finalizing = false;\n        for (DataStoreEntry entry : getStoreEntries()) {\n            // Prevent blocking of shutdown\n            if (entry.finalizeEntryAsync()) {\n                finalizing = true;\n            }\n        }\n        if (finalizing) {\n            ThreadHelper.sleep(1000);\n        }\n    }\n\n    protected void setupBuiltinCategories() {\n        var categoriesDir = getCategoriesDir();\n        var allConnections = getStoreCategoryIfPresent(ALL_CONNECTIONS_CATEGORY_UUID);\n        if (allConnections.isEmpty()) {\n            var cat = DataStoreCategory.createNew(null, ALL_CONNECTIONS_CATEGORY_UUID, \"All connections\");\n            cat.setDirectory(categoriesDir.resolve(ALL_CONNECTIONS_CATEGORY_UUID.toString()));\n            storeCategories.add(cat);\n        } else {\n            allConnections.get().setParentCategory(null);\n        }\n\n        var allScripts = getStoreCategoryIfPresent(ALL_SCRIPTS_CATEGORY_UUID);\n        if (allScripts.isEmpty()) {\n            var cat = DataStoreCategory.createNew(null, ALL_SCRIPTS_CATEGORY_UUID, \"All scripts\");\n            cat.setDirectory(categoriesDir.resolve(ALL_SCRIPTS_CATEGORY_UUID.toString()));\n            storeCategories.add(cat);\n        } else {\n            allScripts.get().setParentCategory(null);\n        }\n\n        if (getStoreCategoryIfPresent(PREDEFINED_SCRIPTS_CATEGORY_UUID).isEmpty()) {\n            var cat =\n                    DataStoreCategory.createNew(ALL_SCRIPTS_CATEGORY_UUID, PREDEFINED_SCRIPTS_CATEGORY_UUID, \"Samples\");\n            cat.setDirectory(categoriesDir.resolve(PREDEFINED_SCRIPTS_CATEGORY_UUID.toString()));\n            storeCategories.add(cat);\n        }\n\n        if (getStoreCategoryIfPresent(CUSTOM_SCRIPTS_CATEGORY_UUID).isEmpty()) {\n            var cat = DataStoreCategory.createNew(ALL_SCRIPTS_CATEGORY_UUID, CUSTOM_SCRIPTS_CATEGORY_UUID, \"Custom\");\n            cat.setDirectory(categoriesDir.resolve(CUSTOM_SCRIPTS_CATEGORY_UUID.toString()));\n            storeCategories.add(cat);\n        }\n\n        if (getStoreCategoryIfPresent(SCRIPT_SOURCES_CATEGORY_UUID).isEmpty()) {\n            var cat = DataStoreCategory.createNew(ALL_SCRIPTS_CATEGORY_UUID, SCRIPT_SOURCES_CATEGORY_UUID, \"Sources\");\n            cat.setDirectory(categoriesDir.resolve(SCRIPT_SOURCES_CATEGORY_UUID.toString()));\n            storeCategories.add(cat);\n        }\n\n        var allIdentities = getStoreCategoryIfPresent(ALL_IDENTITIES_CATEGORY_UUID);\n        if (allIdentities.isEmpty()) {\n            var cat = DataStoreCategory.createNew(null, ALL_IDENTITIES_CATEGORY_UUID, \"All identities\");\n            cat.setDirectory(categoriesDir.resolve(ALL_IDENTITIES_CATEGORY_UUID.toString()));\n            storeCategories.add(cat);\n        } else {\n            allIdentities.get().setParentCategory(null);\n        }\n\n        var localIdentities = getStoreCategoryIfPresent(LOCAL_IDENTITIES_CATEGORY_UUID);\n        if (localIdentities.isEmpty()) {\n            var cat =\n                    DataStoreCategory.createNew(ALL_IDENTITIES_CATEGORY_UUID, LOCAL_IDENTITIES_CATEGORY_UUID, \"Local\");\n            cat.setDirectory(categoriesDir.resolve(LOCAL_IDENTITIES_CATEGORY_UUID.toString()));\n            storeCategories.add(cat);\n        } else {\n            localIdentities.get().setParentCategory(ALL_IDENTITIES_CATEGORY_UUID);\n        }\n\n        if (supportsSync()) {\n            var sharedIdentities = getStoreCategoryIfPresent(SYNCED_IDENTITIES_CATEGORY_UUID);\n            if (sharedIdentities.isEmpty()) {\n                var cat = DataStoreCategory.createNew(\n                        ALL_IDENTITIES_CATEGORY_UUID, SYNCED_IDENTITIES_CATEGORY_UUID, \"Synced\");\n                cat.setDirectory(categoriesDir.resolve(SYNCED_IDENTITIES_CATEGORY_UUID.toString()));\n                cat.setConfig(cat.getConfig().withSync(true));\n                storeCategories.add(cat);\n            } else {\n                sharedIdentities.get().setParentCategory(ALL_IDENTITIES_CATEGORY_UUID);\n            }\n        }\n\n        var def = getStoreCategoryIfPresent(DEFAULT_CATEGORY_UUID);\n        if (def.isEmpty()) {\n            DataStoreCategory cat = new DataStoreCategory(\n                    categoriesDir.resolve(DEFAULT_CATEGORY_UUID.toString()),\n                    DEFAULT_CATEGORY_UUID,\n                    \"Default\",\n                    Instant.now(),\n                    Instant.now(),\n                    true,\n                    ALL_CONNECTIONS_CATEGORY_UUID,\n                    true,\n                    DataStoreCategoryConfig.empty());\n            storeCategories.add(cat);\n        } else {\n            def.get().setParentCategory(ALL_CONNECTIONS_CATEGORY_UUID);\n        }\n\n        storeCategories.forEach(dataStoreCategory -> {\n            if (dataStoreCategory.getParentCategory() != null\n                    && getStoreCategoryIfPresent(dataStoreCategory.getParentCategory())\n                            .isEmpty()) {\n                dataStoreCategory.setParentCategory(ALL_CONNECTIONS_CATEGORY_UUID);\n            } else if (dataStoreCategory.getParentCategory() == null\n                    && !dataStoreCategory.getUuid().equals(ALL_CONNECTIONS_CATEGORY_UUID)\n                    && !dataStoreCategory.getUuid().equals(ALL_SCRIPTS_CATEGORY_UUID)\n                    && !dataStoreCategory.getUuid().equals(ALL_IDENTITIES_CATEGORY_UUID)) {\n                dataStoreCategory.setParentCategory(ALL_CONNECTIONS_CATEGORY_UUID);\n            }\n        });\n    }\n\n    public Path getStorageDir() {\n        return dir;\n    }\n\n    protected Path getStoresDir() {\n        return dir.resolve(\"stores\");\n    }\n\n    public Path getDataDir() {\n        return dir.resolve(\"data\");\n    }\n\n    public Path getIconsDir() {\n        return dir.resolve(\"icons\");\n    }\n\n    protected Path getCategoriesDir() {\n        return dir.resolve(\"categories\");\n    }\n\n    public void addListener(StorageListener l) {\n        this.listeners.add(l);\n    }\n\n    public abstract void load();\n\n    public abstract void saveAsync();\n\n    public abstract void save(boolean dispose);\n\n    public abstract boolean supportsSync();\n\n    public boolean shouldSync(DataStoreCategory category) {\n        // Don't sync lone identities category\n        if (category.getUuid().equals(SYNCED_IDENTITIES_CATEGORY_UUID)\n                && getStoreEntries().stream().noneMatch(e -> {\n                    var hierarchy = getCategoryParentHierarchy(getStoreCategory(e));\n                    return hierarchy.stream().anyMatch(h -> h.getUuid().equals(SYNCED_IDENTITIES_CATEGORY_UUID));\n                })) {\n            return false;\n        }\n\n        if (!category.canShare()) {\n            return false;\n        }\n\n        var config = getEffectiveCategoryConfig(category);\n        return Boolean.TRUE.equals(config.getSync());\n    }\n\n    public boolean shouldSync(DataStoreEntry entry) {\n        if (!shouldSync(DataStorage.get()\n                .getStoreCategoryIfPresent(entry.getCategoryUuid())\n                .orElseThrow())) {\n            return false;\n        }\n\n        DataStoreEntry c = entry;\n        do {\n            // We can't check for sharing of failed entries\n            if (c.getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {\n                return false;\n            }\n\n            if (c.getStore() instanceof LocalStore && entry.getProvider().isSyncableFromLocalMachine()) {\n                return true;\n            }\n\n            try {\n                if (!c.getProvider().isSyncable(c)) {\n                    return false;\n                }\n            } catch (Exception e) {\n                return false;\n            }\n        } while ((c = DataStorage.get().getDefaultDisplayParent(c).orElse(null)) != null);\n        return true;\n    }\n\n    protected void refreshEntries() {\n        storeEntries.keySet().forEach(dataStoreEntry -> {\n            dataStoreEntry.refreshStore();\n        });\n    }\n\n    public void updateEntry(DataStoreEntry entry, DataStoreEntry newEntry) {\n        var state = entry.getStorePersistentState();\n        var nState = newEntry.getStorePersistentState();\n        if (state != null && nState != null) {\n            var updatedState = state.mergeCopy(nState);\n            newEntry.setStorePersistentState(updatedState);\n        }\n\n        var icon = entry.getIcon();\n        if (icon != null && newEntry.getIcon() == null) {\n            newEntry.setIcon(icon, true);\n        }\n\n        var oldParent = DataStorage.get().getDefaultDisplayParent(entry);\n        var newParent = DataStorage.get().getDefaultDisplayParent(newEntry);\n        var sameParent = Objects.equals(oldParent, newParent);\n\n        finalizeWithChildren(entry);\n\n        var children = getDeepStoreChildren(entry);\n        if (!sameParent) {\n            var toRemove = Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new);\n            listeners.forEach(storageListener -> storageListener.onStoreRemove(toRemove));\n        }\n\n        if (entry.getStore() != null) {\n            synchronized (identityStoreEntryMapCache) {\n                identityStoreEntryMapCache.remove(entry.getStore());\n            }\n            synchronized (storeEntryMapCache) {\n                storeEntryMapCache.remove(entry.getStore());\n            }\n        }\n\n        var categoryChanged = !entry.getCategoryUuid().equals(newEntry.getCategoryUuid());\n\n        if (entry.getStore() != null\n                && newEntry.getStore() != null\n                && !entry.getStore().equals(newEntry.getStore())) {\n            synchronized (storeMoveCache) {\n                storeMoveCache.put(entry.getStore(), newEntry.getStore());\n            }\n        }\n        entry.applyChanges(newEntry);\n\n        if (!sameParent) {\n            if (oldParent.isPresent()) {\n                oldParent.get().setChildrenCache(null);\n            }\n            if (newParent.isPresent()) {\n                newParent.get().setChildrenCache(null);\n                newParent.get().setExpanded(true);\n            }\n            var toAdd = Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new);\n            listeners.forEach(storageListener -> storageListener.onStoreAdd(toAdd));\n        }\n\n        if (categoryChanged) {\n            listeners.forEach(storageListener -> storageListener.onEntryCategoryChange());\n            listeners.forEach(storageListener -> storageListener.onStoreListUpdate());\n        }\n\n        SecretManager.moveReferences(newEntry.getUuid(), entry.getUuid());\n\n        refreshEntries();\n        saveAsync();\n    }\n\n    private void finalizeWithChildren(DataStoreEntry entry) {\n        var c = getDeepStoreChildren(entry);\n        var l = new ArrayList<>(c);\n        l.addFirst(entry);\n        for (int i = l.size() - 1; i >= 0; i--) {\n            l.get(i).finalizeEntry();\n        }\n    }\n\n    public void updateEntryStore(DataStoreEntry entry, DataStore store) {\n        if (entry.getStore() == store) {\n            return;\n        }\n\n        finalizeWithChildren(entry);\n        if (entry.getStore() != null && store != null && !entry.getStore().equals(store)) {\n            synchronized (storeMoveCache) {\n                storeMoveCache.put(entry.getStore(), store);\n            }\n        }\n        entry.setStoreInternal(store, false);\n\n        saveAsync();\n    }\n\n    public void updateCategory(DataStoreCategory category, DataStoreCategory newCategory) {\n        category.setName(newCategory.getName());\n        category.setParentCategory(newCategory.getParentCategory());\n        updateCategoryConfig(category, newCategory.getConfig());\n        saveAsync();\n    }\n\n    public void updateCategoryConfig(DataStoreCategory category, DataStoreCategoryConfig config) {\n        if (category.setConfig(config)) {\n            // Update git remote if needed\n            DataStorage.get().saveAsync();\n        }\n    }\n\n    public DataStoreCategory breakOutCategory(DataStoreEntry entry) {\n        if (!(entry.getStore() instanceof FixedHierarchyStore) && !(entry.getStore() instanceof GroupStore<?>)) {\n            return null;\n        }\n\n        var cat = getStoreCategory(entry);\n        var breakOut = new DataStoreCategory(\n                null,\n                UUID.randomUUID(),\n                entry.getName(),\n                Instant.now(),\n                Instant.now(),\n                true,\n                cat.getUuid(),\n                true,\n                DataStoreCategoryConfig.empty());\n        addStoreCategory(breakOut);\n        entry.setBreakOutCategory(breakOut);\n        entry.setExpanded(true);\n\n        var children = getDeepStoreChildren(entry);\n        var childrenToKeep = new HashSet<DataStoreEntry>();\n        children.forEach(c -> {\n            if (c.getBreakOutCategory() != null) {\n                childrenToKeep.addAll(getDeepStoreChildren(c));\n                childrenToKeep.add(c);\n            }\n        });\n        children.forEach(child -> {\n            if (!childrenToKeep.contains(child)) {\n                child.setCategoryUuid(breakOut.getUuid());\n            }\n        });\n        entry.setCategoryUuid(breakOut.getUuid());\n\n        var categoriesToMove = new ArrayList<DataStoreCategory>();\n        children.forEach(child -> {\n            if (child.getBreakOutCategory() != null) {\n                var childBreakOut = getStoreCategoryIfPresent(child.getBreakOutCategory());\n                if (childBreakOut.isPresent()\n                        && childBreakOut.get().getParentCategory().equals(cat.getUuid())) {\n                    categoriesToMove.add(childBreakOut.get());\n                }\n            }\n        });\n        categoriesToMove.forEach(toMove -> {\n            toMove.setParentCategory(breakOut.getUuid());\n            // The update mechanism does not support moves, so readd them\n            listeners.forEach(storageListener -> storageListener.onCategoryRemove(toMove));\n            listeners.forEach(storageListener -> storageListener.onCategoryAdd(toMove));\n        });\n\n        listeners.forEach(storageListener -> storageListener.onEntryCategoryChange());\n        listeners.forEach(storageListener -> storageListener.onStoreListUpdate());\n\n        saveAsync();\n        return breakOut;\n    }\n\n    public void mergeBreakOutCategory(DataStoreEntry entry) {\n        if (entry.getBreakOutCategory() == null) {\n            return;\n        }\n\n        var breakOut = getStoreCategoryIfPresent(entry.getBreakOutCategory());\n        if (breakOut.isEmpty()) {\n            entry.setBreakOutCategory(null);\n            return;\n        }\n\n        var parent = getDefaultDisplayParent(entry).or(() -> getSyntheticParent(entry));\n        if (parent.isEmpty()) {\n            deleteStoreCategory(breakOut.get(), false, false);\n            return;\n        }\n\n        var moveCategories = new ArrayList<DataStoreCategory>();\n        var children = getDeepStoreChildren(entry);\n\n        var childrenToKeep = new HashSet<DataStoreEntry>();\n        children.forEach(c -> {\n            if (c.getBreakOutCategory() != null) {\n                childrenToKeep.addAll(getDeepStoreChildren(c));\n                childrenToKeep.add(c);\n            }\n        });\n\n        children.forEach(child -> {\n            if (childrenToKeep.contains(child)) {\n                var cbo = getStoreCategoryIfPresent(child.getBreakOutCategory());\n                if (cbo.isPresent()\n                        && cbo.get().getParentCategory().equals(breakOut.get().getUuid())) {\n                    moveCategories.add(cbo.get());\n                }\n                return;\n            }\n\n            child.setCategoryUuid(parent.get().getCategoryUuid());\n        });\n        moveCategories.forEach(toMove -> {\n            toMove.setParentCategory(parent.get().getCategoryUuid());\n            // The update mechanism does not support moves, so readd them\n            listeners.forEach(storageListener -> storageListener.onCategoryRemove(toMove));\n            listeners.forEach(storageListener -> storageListener.onCategoryAdd(toMove));\n        });\n        entry.setCategoryUuid(parent.get().getCategoryUuid());\n\n        listeners.forEach(storageListener -> storageListener.onEntryCategoryChange());\n        deleteStoreCategory(breakOut.get(), false, false);\n        entry.setBreakOutCategory(null);\n        listeners.forEach(storageListener -> storageListener.onStoreListUpdate());\n        saveAsync();\n    }\n\n    public void moveEntryToCategory(DataStoreEntry entry, DataStoreCategory newCategory) {\n        if (newCategory.getUuid().equals(entry.getCategoryUuid())) {\n            return;\n        }\n\n        var oldCat = getStoreCategoryIfPresent(entry.getCategoryUuid()).orElse(getDefaultConnectionsCategory());\n        entry.setCategoryUuid(newCategory.getUuid());\n        var children = getDeepStoreChildren(entry);\n        children.forEach(child -> {\n            if (!child.getCategoryUuid().equals(oldCat.getUuid())) {\n                return;\n            }\n\n            child.setCategoryUuid(newCategory.getUuid());\n        });\n        listeners.forEach(storageListener -> storageListener.onEntryCategoryChange());\n        listeners.forEach(storageListener -> storageListener.onStoreListUpdate());\n        saveAsync();\n    }\n\n    public void moveCategoryToParent(DataStoreCategory cat, DataStoreCategory newParent) {\n        if (newParent.getUuid().equals(cat.getUuid()) || newParent.getUuid().equals(cat.getParentCategory())) {\n            return;\n        }\n\n        if (cat.getParentCategory() == null) {\n            return;\n        }\n\n        cat.setParentCategory(newParent.getUuid());\n        listeners.forEach(storageListener -> storageListener.onCategoryRemove(cat));\n        listeners.forEach(storageListener -> storageListener.onCategoryAdd(cat));\n        listeners.forEach(storageListener -> storageListener.onEntryCategoryChange());\n        listeners.forEach(storageListener -> storageListener.onStoreListUpdate());\n        saveAsync();\n    }\n\n    public void setOrderIndex(DataStoreEntry entry, int index) {\n        entry.setOrderIndex(index);\n        listeners.forEach(storageListener -> storageListener.onStoreListUpdate());\n        saveAsync();\n    }\n\n    @SneakyThrows\n    public boolean refreshChildren(DataStoreEntry e) {\n        return refreshChildren(e, false);\n    }\n\n    public boolean refreshChildrenOrThrow(DataStoreEntry e) throws Exception {\n        return refreshChildren(e, true);\n    }\n\n    public boolean refreshChildren(DataStoreEntry e, boolean throwOnFail) throws Exception {\n        if (!(e.getStore() instanceof FixedHierarchyStore h)) {\n            return false;\n        }\n\n        e.incrementBusyCounter();\n        List<? extends DataStoreEntryRef<? extends FixedChildStore>> newChildren;\n        try {\n            List<? extends DataStoreEntryRef<? extends FixedChildStore>> l = h.listChildren();\n            if (l != null) {\n                newChildren = l.stream()\n                        .filter(dataStoreEntryRef -> dataStoreEntryRef != null && dataStoreEntryRef.get() != null)\n                        .toList();\n                e.getProvider().onChildrenRefresh(e);\n            } else {\n                newChildren = null;\n            }\n        } catch (Exception ex) {\n            if (throwOnFail) {\n                throw ex;\n            } else {\n                ErrorEventFactory.fromThrowable(ex).handle();\n                return false;\n            }\n        } finally {\n            e.decrementBusyCounter();\n        }\n\n        if (newChildren == null) {\n            return false;\n        }\n\n        var oldChildren = getStoreChildren(e);\n        var toRemove = oldChildren.stream()\n                .filter(oc -> {\n                    var oid = getFixedChildId(oc);\n                    if (oid.isEmpty()) {\n                        return false;\n                    }\n\n                    return newChildren.stream()\n                            .filter(nc -> getFixedChildId(nc.get()).isPresent())\n                            .noneMatch(nc -> {\n                                return getFixedChildId(nc.get()).getAsInt() == oid.getAsInt();\n                            });\n                })\n                .toList();\n        var toAdd = newChildren.stream()\n                .filter(nc -> {\n                    var nid = getFixedChildId(nc.get());\n                    // These can't be automatically generated\n                    if (nid.isEmpty()) {\n                        return false;\n                    }\n\n                    return oldChildren.stream()\n                            .filter(oc -> oc.getStore() instanceof FixedChildStore)\n                            .filter(oc -> getFixedChildId(oc).isPresent())\n                            .noneMatch(oc -> {\n                                return getFixedChildId(oc).getAsInt() == nid.getAsInt();\n                            });\n                })\n                .toList();\n        var toUpdate = new ArrayList<>(oldChildren.stream()\n                .map(oc -> {\n                    var oid = getFixedChildId(oc);\n                    if (oid.isEmpty()) {\n                        return new Pair<DataStoreEntry, DataStoreEntryRef<? extends FixedChildStore>>(oc, null);\n                    }\n\n                    var found = newChildren.stream()\n                            .filter(nc -> getFixedChildId(nc.get()).isPresent())\n                            .filter(nc -> getFixedChildId(nc.get()).getAsInt() == oid.getAsInt())\n                            .findFirst()\n                            .orElse(null);\n                    return new Pair<DataStoreEntry, DataStoreEntryRef<? extends FixedChildStore>>(oc, found);\n                })\n                .filter(en -> en.getValue() != null)\n                .toList());\n\n        toUpdate.removeIf(pair -> {\n            // Children classes might not be the same, the same goes for state classes\n            // This can happen when there are multiple child classes and the ids got switched around\n            var storeClassMatch = pair.getKey()\n                    .getStore()\n                    .getClass()\n                    .equals(pair.getValue().get().getStore().getClass());\n            if (!storeClassMatch) {\n                return true;\n            }\n\n            DataStore merged = ((FixedChildStore) pair.getKey().getStore())\n                    .merge(pair.getValue().getStore().asNeeded());\n            var mergedStoreChanged = pair.getKey().getStore() != merged;\n\n            var nameChanged =\n                    shouldUpdateChildrenStoreName(pair.getKey(), pair.getValue().get());\n\n            if (pair.getKey().getStorePersistentState() == null\n                    || pair.getValue().get().getStorePersistentState() == null) {\n                return !mergedStoreChanged && !nameChanged;\n            }\n\n            var stateClassMatch = pair.getKey()\n                    .getStorePersistentState()\n                    .getClass()\n                    .equals(pair.getValue().get().getStorePersistentState().getClass());\n            if (!stateClassMatch) {\n                return true;\n            }\n\n            var stateChange = !pair.getKey()\n                    .getStorePersistentState()\n                    .equals(pair.getValue().get().getStorePersistentState());\n            return !mergedStoreChanged && !stateChange && !nameChanged;\n        });\n\n        if (toRemove.isEmpty() && toAdd.isEmpty() && toUpdate.isEmpty()) {\n            oldChildren.forEach(oe -> oe.getProvider().onParentRefresh(oe));\n            return false;\n        }\n\n        if (!newChildren.isEmpty()) {\n            e.setExpanded(true);\n        }\n        toAdd.reversed().forEach(nc -> {\n            ThreadHelper.sleep(1);\n            // Update after parent entry\n            nc.get().notifyUpdate(false, true);\n        });\n\n        if (h.removeLeftovers()) {\n            deleteWithChildren(toRemove.toArray(DataStoreEntry[]::new));\n        }\n        if (e.getBreakOutCategory() != null) {\n            toAdd.forEach(nc -> nc.get().setCategoryUuid(e.getBreakOutCategory()));\n        }\n        addStoreEntriesIfNotPresent(toAdd.stream().map(DataStoreEntryRef::get).toArray(DataStoreEntry[]::new));\n        toUpdate.forEach(pair -> {\n            if (shouldUpdateChildrenStoreName(pair.getKey(), pair.getValue().get())) {\n                pair.getKey()\n                        .setName(getNameableStoreName(pair.getValue().get())\n                                .orElse(pair.getKey().getName()));\n            }\n\n            DataStore merged = ((FixedChildStore) pair.getKey().getStore())\n                    .merge(pair.getValue().getStore().asNeeded());\n            if (merged != null && !merged.equals(pair.getKey().getStore())) {\n                pair.getKey().setStoreInternal(merged, false);\n            }\n\n            var s = pair.getKey().getStorePersistentState();\n            // We might not be a stateful store\n            if (s != null) {\n                var mergedState = s.mergeCopy(pair.getValue().get().getStorePersistentState());\n                pair.getKey().setStorePersistentState(mergedState);\n            }\n\n            if (pair.getKey().getOrderIndex() == 0 && pair.getValue().get().getOrderIndex() != 0) {\n                pair.getKey().setOrderIndex(pair.getValue().get().getOrderIndex());\n            }\n        });\n        refreshEntries();\n        saveAsync();\n        toAdd.forEach(\n                dataStoreEntryRef -> dataStoreEntryRef.get().getProvider().onParentRefresh(dataStoreEntryRef.get()));\n        toUpdate.forEach(dataStoreEntryRef ->\n                dataStoreEntryRef.getKey().getProvider().onParentRefresh(dataStoreEntryRef.getKey()));\n        return !newChildren.isEmpty();\n    }\n\n    private boolean shouldUpdateChildrenStoreName(DataStoreEntry o, DataStoreEntry n) {\n        var oldName = getNameableStoreName(o);\n        if (oldName.isEmpty()) {\n            return false;\n        }\n\n        var isCustom = !o.getName().equals(oldName.get());\n        if (isCustom) {\n            return false;\n        }\n\n        var newName = getNameableStoreName(n);\n        if (newName.isEmpty()) {\n            return false;\n        }\n        return !o.getName().equals(newName.get());\n    }\n\n    private Optional<String> getNameableStoreName(DataStoreEntry o) {\n        if (!(o.getStore() instanceof NameableStore nameable)) {\n            return Optional.empty();\n        }\n\n        try {\n            return Optional.ofNullable(nameable.getName());\n        } catch (Exception e) {\n            return Optional.empty();\n        }\n    }\n\n    private OptionalInt getFixedChildId(DataStoreEntry entry) {\n        if (!(entry.getStore() instanceof FixedChildStore f)) {\n            return OptionalInt.empty();\n        }\n\n        try {\n            return f.getFixedId();\n        } catch (Throwable t) {\n            return OptionalInt.empty();\n        }\n    }\n\n    public void deleteWithChildren(DataStoreEntry... entries) {\n        List<DataStoreEntry> toDelete = Arrays.stream(entries)\n                .flatMap(entry -> {\n                    var c = getDeepStoreChildren(entry);\n                    c.add(entry);\n                    return c.stream();\n                })\n                .toList();\n        if (toDelete.isEmpty()) {\n            return;\n        }\n\n        for (var td : toDelete) {\n            td.finalizeEntry();\n            this.storeEntriesSet.remove(td);\n            synchronized (identityStoreEntryMapCache) {\n                identityStoreEntryMapCache.remove(td.getStore());\n            }\n            synchronized (storeEntryMapCache) {\n                storeEntryMapCache.remove(td.getStore());\n            }\n            var parent = getDefaultDisplayParent(td);\n            parent.ifPresent(p -> p.setChildrenCache(null));\n        }\n\n        this.listeners.forEach(l -> l.onStoreRemove(toDelete.toArray(DataStoreEntry[]::new)));\n        refreshEntries();\n        saveAsync();\n    }\n\n    public void addStoreCategory(@NonNull DataStoreCategory cat) {\n        cat.setDirectory(getCategoriesDir().resolve(cat.getUuid().toString()));\n        this.storeCategories.add(cat);\n        saveAsync();\n\n        this.listeners.forEach(l -> l.onCategoryAdd(cat));\n    }\n\n    public void addStoreEntryInProgress(@NonNull DataStoreEntry e) {\n        this.storeEntriesInProgress.put(e, e);\n    }\n\n    public void removeStoreEntryInProgress(@NonNull DataStoreEntry e) {\n        this.storeEntriesInProgress.remove(e);\n    }\n\n    public DataStoreEntry addStoreEntryIfNotPresent(@NonNull DataStoreEntry e) {\n        var found = storeEntries.get(e);\n        if (found != null) {\n            return found;\n        }\n\n        var byId = getStoreEntryIfPresent(e.getUuid()).orElse(null);\n        if (byId != null) {\n            return byId;\n        }\n\n        if (getStoreCategoryIfPresent(e.getCategoryUuid()).isEmpty()) {\n            e.setCategoryUuid(DEFAULT_CATEGORY_UUID);\n        }\n\n        var syntheticParent = getSyntheticParent(e);\n        if (syntheticParent.isPresent()) {\n            addStoreEntryIfNotPresent(syntheticParent.get());\n        }\n\n        var displayParent = syntheticParent.or(() -> getDefaultDisplayParent(e));\n        if (displayParent.isPresent()) {\n            displayParent.get().setExpanded(true);\n            e.setCategoryUuid(displayParent.get().getCategoryUuid());\n        }\n\n        e.setDirectory(getStoresDir().resolve(e.getUuid().toString()));\n        this.storeEntries.put(e, e);\n        displayParent.ifPresent(p -> {\n            p.setChildrenCache(null);\n        });\n        saveAsync();\n\n        this.listeners.forEach(l -> l.onStoreAdd(e));\n        e.refreshStore();\n        return e;\n    }\n\n    public void addStoreEntriesIfNotPresent(@NonNull DataStoreEntry... es) {\n        if (es.length == 0) {\n            return;\n        }\n\n        var toAdd = Arrays.stream(es)\n                .filter(e -> {\n                    if (storeEntriesSet.contains(e)\n                            || getStoreEntryIfPresent(e.getStore(), false).isPresent()) {\n                        return false;\n                    }\n                    return true;\n                })\n                .toList();\n        for (DataStoreEntry e : toAdd) {\n            var syntheticParent = getSyntheticParent(e);\n            if (syntheticParent.isPresent()) {\n                addStoreEntryIfNotPresent(syntheticParent.get());\n            }\n\n            var displayParent = syntheticParent.or(() -> getDefaultDisplayParent(e));\n            if (displayParent.isPresent()\n                    && (displayParent.get().getBreakOutCategory() == null\n                            || getStoreCategoryIfPresent(displayParent.get().getBreakOutCategory())\n                                    .isEmpty())) {\n                displayParent.get().setExpanded(true);\n                e.setCategoryUuid(displayParent.get().getCategoryUuid());\n            }\n\n            e.setDirectory(getStoresDir().resolve(e.getUuid().toString()));\n            this.storeEntries.put(e, e);\n            displayParent.ifPresent(p -> {\n                p.setChildrenCache(null);\n            });\n        }\n        for (DataStoreEntry e : toAdd) {\n            e.refreshStore();\n        }\n\n        // Retain ordering\n        toAdd.reversed().forEach(e -> {\n            ThreadHelper.sleep(1);\n            e.notifyUpdate(false, true);\n        });\n\n        this.listeners.forEach(l -> l.onStoreAdd(toAdd.toArray(DataStoreEntry[]::new)));\n        saveAsync();\n    }\n\n    public DataStoreEntry addStoreIfNotPresent(@NonNull String name, DataStore store) {\n        return addStoreIfNotPresent(null, name, store);\n    }\n\n    public DataStoreEntry addStoreIfNotPresent(DataStoreEntry related, @NonNull String name, DataStore store) {\n        var f = getStoreEntryIfPresent(store, false);\n        if (f.isPresent()) {\n            return f.get();\n        }\n\n        var categoryId = related != null ? related.getCategoryUuid() : selectedCategory.getUuid();\n        var provider = DataStoreProviders.byStore(store);\n        if (provider != null) {\n            categoryId = provider.getTargetCategory(store, categoryId);\n        }\n\n        var e = DataStoreEntry.createNew(UUID.randomUUID(), categoryId, name, store);\n        addStoreEntryIfNotPresent(e);\n        return e;\n    }\n\n    public void deleteStoreEntry(@NonNull DataStoreEntry entry) {\n        finalizeWithChildren(entry);\n        this.storeEntries.remove(entry);\n        synchronized (identityStoreEntryMapCache) {\n            identityStoreEntryMapCache.remove(entry.getStore());\n        }\n        synchronized (storeEntryMapCache) {\n            storeEntryMapCache.remove(entry.getStore());\n        }\n        getDefaultDisplayParent(entry).ifPresent(p -> p.setChildrenCache(null));\n        this.listeners.forEach(l -> l.onStoreRemove(entry));\n        refreshEntries();\n        saveAsync();\n    }\n\n    public boolean canDeleteStoreCategory(@NonNull DataStoreCategory cat) {\n        if (cat.getParentCategory() == null) {\n            return false;\n        }\n\n        if (cat.getUuid().equals(DEFAULT_CATEGORY_UUID)\n                || cat.getUuid().equals(PREDEFINED_SCRIPTS_CATEGORY_UUID)\n                || cat.getUuid().equals(LOCAL_IDENTITIES_CATEGORY_UUID)\n                || cat.getUuid().equals(CUSTOM_SCRIPTS_CATEGORY_UUID)\n                || cat.getUuid().equals(SYNCED_IDENTITIES_CATEGORY_UUID)\n                || cat.getUuid().equals(SCRIPT_SOURCES_CATEGORY_UUID)) {\n            return false;\n        }\n\n        return true;\n    }\n\n    public boolean canMoveStoreCategory(@NonNull DataStoreCategory cat) {\n        return canDeleteStoreCategory(cat);\n    }\n\n    public void deleteStoreCategory(@NonNull DataStoreCategory cat, boolean deleteChildren, boolean deleteEntries) {\n        if (!canDeleteStoreCategory(cat)) {\n            return;\n        }\n\n        var toDelete = new ArrayList<DataStoreCategory>();\n        if (deleteChildren) {\n            for (DataStoreCategory other : getStoreCategories()) {\n                var hierarchy = getCategoryParentHierarchy(other);\n                if (hierarchy.contains(cat)) {\n                    toDelete.add(other);\n                }\n            }\n        } else {\n            toDelete.add(cat);\n        }\n\n        for (DataStoreCategory delCat : toDelete) {\n            if (deleteEntries) {\n                var toDeleteEntries = new ArrayList<DataStoreEntry>();\n                for (DataStoreEntry entry : storeEntriesSet) {\n                    if (getStoreCategory(entry).equals(delCat)) {\n                        toDeleteEntries.add(entry);\n                    }\n                }\n                deleteWithChildren(toDeleteEntries.toArray(DataStoreEntry[]::new));\n            } else {\n                storeEntriesSet.forEach(entry -> {\n                    if (entry.getCategoryUuid().equals(delCat.getUuid())) {\n                        entry.setCategoryUuid(getFallbackCategory(delCat).getUuid());\n                    }\n                });\n            }\n\n            storeCategories.remove(delCat);\n            this.listeners.forEach(l -> l.onCategoryRemove(delCat));\n        }\n\n        saveAsync();\n    }\n\n    private DataStoreCategory getFallbackCategory(DataStoreCategory cat) {\n        var parent = getStoreCategoryIfPresent(cat.getParentCategory()).orElseThrow();\n        if (parent.getParentCategory() != null) {\n            return parent;\n        }\n\n        return getStoreCategoryIfPresent(DEFAULT_CATEGORY_UUID).orElseThrow();\n    }\n\n    // Get operations\n\n    public boolean isRootEntry(DataStoreEntry entry, DataStoreCategory current) {\n        if (entry.isPinToTop()) {\n            return true;\n        }\n\n        var parent = getDefaultDisplayParent(entry);\n        var noParent = parent.isEmpty();\n        if (noParent) {\n            return true;\n        }\n\n        var parentCat = getStoreCategoryIfPresent(parent.get().getCategoryUuid());\n        if (parentCat.isEmpty()) {\n            return true;\n        }\n\n        var parentCatHierarchy = getCategoryParentHierarchy(parentCat.get());\n        var cat = getStoreCategoryIfPresent(entry.getCategoryUuid());\n        if (cat.isEmpty()) {\n            return true;\n        }\n\n        var catHierarchy = getCategoryParentHierarchy(cat.get());\n\n        var currentContainsBoth = catHierarchy.contains(current) && parentCatHierarchy.contains(current);\n        if (currentContainsBoth) {\n            return false;\n        }\n\n        var diffParentCategoryHierarchy = !catHierarchy.contains(parentCat.get());\n        if (diffParentCategoryHierarchy) {\n            return true;\n        }\n\n        var subParent = catHierarchy.indexOf(current) > catHierarchy.indexOf(parentCat.get());\n        if (subParent) {\n            return true;\n        }\n\n        var loop = isParentLoop(entry);\n        return loop;\n    }\n\n    private boolean isParentLoop(DataStoreEntry entry) {\n        var es = new HashSet<DataStoreEntry>();\n\n        DataStoreEntry current = entry;\n        while ((current = getDefaultDisplayParent(current).orElse(null)) != null) {\n            if (es.contains(current)) {\n                return true;\n            }\n\n            es.add(current);\n        }\n\n        return false;\n    }\n\n    public boolean getEffectiveReadOnlyState(DataStoreEntry entry) {\n        var cat = getStoreCategoryIfPresent(entry.getCategoryUuid());\n        if (cat.isEmpty()) {\n            return false;\n        }\n\n        var catConfig = getEffectiveCategoryConfig(cat.get());\n        return catConfig.getFreezeConfigurations() != null ? catConfig.getFreezeConfigurations() : entry.isFreeze();\n    }\n\n    public DataStoreColor getEffectiveColor(DataStoreEntry entry) {\n        var cat = getStoreCategoryIfPresent(entry.getCategoryUuid()).orElseThrow();\n        var root = getRootForEntry(entry, cat);\n        if (root.getColor() != null) {\n            return root.getColor();\n        }\n\n        var catConfig = getEffectiveCategoryConfig(cat);\n        return catConfig.getColor();\n    }\n\n    public DataStoreEntry getRootForEntry(DataStoreEntry entry, DataStoreCategory cat) {\n        if (entry == null) {\n            return null;\n        }\n\n        if (isRootEntry(entry, cat)) {\n            return entry;\n        }\n\n        var current = entry;\n        Optional<DataStoreEntry> parent;\n        while ((parent = getDefaultDisplayParent(current)).isPresent()) {\n            current = parent.get();\n            if (isRootEntry(current, cat)) {\n                break;\n            }\n        }\n\n        return current;\n    }\n\n    public Optional<DataStoreEntry> getSyntheticParent(DataStoreEntry entry) {\n        if (entry.getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {\n            return Optional.empty();\n        }\n\n        try {\n            var provider = entry.getProvider();\n            return Optional.ofNullable(provider.getSyntheticParent(entry));\n        } catch (Exception ex) {\n            return Optional.empty();\n        }\n    }\n\n    public Optional<DataStoreEntry> getDefaultDisplayParent(DataStoreEntry entry) {\n        if (entry.getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {\n            return Optional.empty();\n        }\n\n        try {\n            var provider = entry.getProvider();\n            return Optional.ofNullable(provider.getDisplayParent(entry))\n                    .filter(dataStoreEntry -> storeEntries.get(dataStoreEntry) != null);\n        } catch (Exception ex) {\n            return Optional.empty();\n        }\n    }\n\n    public SequencedSet<DataStoreEntry> getDeepStoreChildren(DataStoreEntry entry) {\n        var set = new LinkedHashSet<DataStoreEntry>();\n        getDeepStoreChildren(entry, set);\n        return set;\n    }\n\n    private void getDeepStoreChildren(DataStoreEntry entry, Set<DataStoreEntry> current) {\n        getStoreChildren(entry).forEach(c -> {\n            var added = current.add(c);\n            // Guard against loop\n            if (added) {\n                getDeepStoreChildren(c, current);\n            }\n        });\n    }\n\n    public Set<DataStoreEntry> getStoreChildren(DataStoreEntry entry) {\n        if (entry.getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {\n            return Set.of();\n        }\n\n        if (entry.getChildrenCache() != null) {\n            return entry.getChildrenCache();\n        }\n\n        var entries = getStoreEntries();\n        if (!entries.contains(entry)) {\n            return Set.of();\n        }\n\n        var children = entries.stream()\n                .filter(other -> {\n                    if (other.getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {\n                        return false;\n                    }\n\n                    var parent = getDefaultDisplayParent(other);\n                    return parent.isPresent() && parent.get().equals(entry) && !isParentLoop(entry);\n                })\n                .collect(Collectors.toSet());\n\n        // Don't build caches too early to prevent wrong caches\n        if (entriesAvailable) {\n            entry.setChildrenCache(children);\n        }\n\n        return children;\n    }\n\n    public List<DataStoreCategory> getCategoryParentHierarchy(DataStoreCategory cat) {\n        var es = new ArrayList<DataStoreCategory>();\n        es.add(cat);\n\n        DataStoreCategory current = cat;\n        while ((current = getStoreCategoryIfPresent(current.getParentCategory()).orElse(null)) != null) {\n            if (es.contains(current)) {\n                break;\n            }\n\n            es.addFirst(current);\n        }\n\n        return es;\n    }\n\n    public List<DataStoreEntry> getStoreParentHierarchy(DataStoreEntry entry) {\n        var es = new ArrayList<DataStoreEntry>();\n        es.add(entry);\n\n        DataStoreEntry current = entry;\n        while ((current = getDefaultDisplayParent(current).orElse(null)) != null) {\n            if (es.contains(current)) {\n                break;\n            }\n\n            es.addFirst(current);\n        }\n\n        return es;\n    }\n\n    public StorePath getStorePath(DataStoreEntry entry) {\n        return StorePath.create(getStoreParentHierarchy(entry).stream()\n                .map(e -> e.getName().toLowerCase().replaceAll(\"/\", \"_\"))\n                .toArray(String[]::new));\n    }\n\n    public StorePath getStorePath(DataStoreCategory entry) {\n        return StorePath.create(getCategoryParentHierarchy(entry).stream()\n                .map(e -> e.getName().toLowerCase().replaceAll(\"/\", \"_\"))\n                .toArray(String[]::new));\n    }\n\n    public Optional<DataStoreEntry> getStoreEntryInProgressIfPresent(@NonNull DataStore store) {\n        var found = storeEntriesInProgress.keySet().stream()\n                .filter(n -> n.getStore() == store)\n                .findFirst();\n        if (found.isPresent()) {\n            return found;\n        }\n\n        DataStore moved;\n        synchronized (storeMoveCache) {\n            moved = storeMoveCache.get(store);\n        }\n        if (moved != null) {\n            return getStoreEntryInProgressIfPresent(moved);\n        }\n\n        return Optional.empty();\n    }\n\n    public Optional<DataStoreEntry> getStoreEntryIfPresent(@NonNull DataStore store, boolean identityOnly) {\n        if (identityOnly) {\n            synchronized (identityStoreEntryMapCache) {\n                var found = identityStoreEntryMapCache.get(store);\n                if (found != null) {\n                    return Optional.of(found);\n                }\n            }\n        } else {\n            synchronized (storeEntryMapCache) {\n                var found = storeEntryMapCache.get(store);\n                if (found != null) {\n                    return Optional.of(found);\n                }\n            }\n        }\n\n        DataStore moved;\n        synchronized (storeMoveCache) {\n            moved = storeMoveCache.get(store);\n        }\n        if (moved != null) {\n            return getStoreEntryIfPresent(moved, identityOnly);\n        }\n\n        var found = storeEntriesSet.stream()\n                .filter(n -> n.getStore() == store\n                        || (!identityOnly\n                                && (n.getStore() != null\n                                        && Objects.equals(\n                                                store.getClass(), n.getStore().getClass())\n                                        && store.equals(n.getStore()))))\n                .findFirst();\n        if (found.isPresent()) {\n            if (identityOnly) {\n                synchronized (identityStoreEntryMapCache) {\n                    identityStoreEntryMapCache.put(store, found.get());\n                }\n            } else {\n                synchronized (storeEntryMapCache) {\n                    storeEntryMapCache.put(store, found.get());\n                }\n            }\n        }\n        return found;\n    }\n\n    public DataStoreCategory getRootCategory(DataStoreCategory category) {\n        DataStoreCategory last = category;\n        DataStoreCategory p = category;\n        while ((p = DataStorage.get()\n                        .getStoreCategoryIfPresent(p.getParentCategory())\n                        .orElse(null))\n                != null) {\n            last = p;\n        }\n        return last;\n    }\n\n    public DataStoreCategory getStoreCategory(DataStoreEntry entry) {\n        return getStoreCategoryIfPresent(entry.getCategoryUuid()).orElseThrow();\n    }\n\n    public Optional<DataStoreCategory> getStoreCategoryIfPresent(UUID uuid) {\n        if (uuid == null) {\n            return Optional.empty();\n        }\n\n        return storeCategories.stream()\n                .filter(n -> {\n                    return Objects.equals(n.getUuid(), uuid);\n                })\n                .findFirst();\n    }\n\n    public Optional<DataStoreEntry> getStoreEntryIfPresent(@NonNull String name) {\n        return storeEntriesSet.stream()\n                .filter(n -> n.getName().equalsIgnoreCase(name))\n                .findFirst();\n    }\n\n    public String getStoreEntryDisplayName(DataStoreEntry entry) {\n        if (entry == null) {\n            return \"Unknown\";\n        }\n\n        if (!entry.getValidity().isUsable()) {\n            return entry.getName();\n        }\n\n        return entry.getProvider().displayName(entry);\n    }\n\n    public DataStoreCategoryConfig getEffectiveCategoryConfig(DataStoreEntry entry) {\n        var category = getStoreCategoryIfPresent(entry.getCategoryUuid());\n        if (category.isEmpty()) {\n            return DataStoreCategoryConfig.empty();\n        }\n\n        return getEffectiveCategoryConfig(category.get());\n    }\n\n    public DataStoreCategoryConfig getEffectiveCategoryConfig(DataStoreCategory category) {\n        var hierarchy = getCategoryParentHierarchy(category);\n        return DataStoreCategoryConfig.merge(hierarchy.stream()\n                .map(dataStoreCategory -> dataStoreCategory.getConfig())\n                .toList());\n    }\n\n    public Optional<DataStoreEntry> getStoreEntryIfPresent(UUID id) {\n        return storeEntriesSet.stream().filter(e -> e.getUuid().equals(id)).findAny();\n    }\n\n    public Set<DataStoreEntry> getStoreEntries() {\n        return storeEntriesSet;\n    }\n\n    public DataStoreEntry getOrCreateNewSyntheticEntry(DataStoreEntry parent, String name, DataStore store) {\n        var forStore = getStoreEntryIfPresent(store, false);\n        if (forStore.isPresent()) {\n            return forStore.get();\n        }\n\n        return DataStoreEntry.createNew(UUID.randomUUID(), parent.getCategoryUuid(), name, store);\n    }\n\n    public DataStoreEntry getStoreEntry(UUID id) {\n        return getStoreEntryIfPresent(id).orElseThrow();\n    }\n\n    public DataStoreEntry local() {\n        return getStoreEntryIfPresent(LOCAL_ID)\n                .orElseThrow(() ->\n                        new IllegalStateException(\"Missing local machine connection, restart is required to fix this\"));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/storage/DataStorageGroupStrategy.java",
    "content": "package io.xpipe.app.storage;\n\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.App;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.ext.ValidationException;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.CountDown;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.app.secret.SecretPasswordManagerStrategy;\nimport io.xpipe.app.secret.SecretQueryState;\nimport io.xpipe.app.util.HttpHelper;\nimport io.xpipe.app.util.Validators;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.ReadOnlyObjectWrapper;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.property.SimpleStringProperty;\n\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.http.HttpResponse;\nimport java.nio.file.Files;\nimport java.util.ArrayList;\nimport java.util.List;\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\npublic interface DataStorageGroupStrategy {\n\n    static List<Class<?>> getClasses() {\n        var l = new ArrayList<Class<?>>();\n        l.add(PasswordManager.class);\n        l.add(File.class);\n        l.add(Command.class);\n        l.add(HttpRequest.class);\n        return l;\n    }\n\n    default void checkComplete() throws ValidationException {}\n\n    String queryEncryptionSecret() throws Exception;\n\n    @JsonTypeName(\"passwordManager\")\n    @Builder\n    @Jacksonized\n    @Value\n    class PasswordManager implements DataStorageGroupStrategy {\n\n        @SuppressWarnings(\"unused\")\n        public static String getOptionsNameKey() {\n            return \"passwordManager\";\n        }\n\n        @SuppressWarnings(\"unused\")\n        public static OptionsBuilder createOptions(Property<PasswordManager> p) {\n            var key = new SimpleStringProperty(p.getValue().getKey());\n\n            var prefs = AppPrefs.get();\n            var field = new TextFieldComp(key).apply(struc -> struc.promptTextProperty()\n                    .bind(Bindings.createStringBinding(\n                            () -> {\n                                return prefs.passwordManager().getValue() != null\n                                        ? prefs.passwordManager().getValue().getKeyPlaceholder()\n                                        : \"?\";\n                            },\n                            prefs.passwordManager())));\n            var button = new ButtonComp(null, new FontIcon(\"mdomz-settings\"), () -> {\n                AppPrefs.get().selectCategory(\"passwordManager\");\n                App.getApp().getStage().requestFocus();\n            });\n            var content = new InputGroupComp(List.of(field, button));\n            content.setMainReference(field);\n\n            return new OptionsBuilder()\n                    .nameAndDescription(\"passwordManagerKey\")\n                    .addComp(content, key)\n                    .nonNull()\n                    .bind(\n                            () -> {\n                                return PasswordManager.builder().key(key.get()).build();\n                            },\n                            p);\n        }\n\n        String key;\n\n        @Override\n        public void checkComplete() throws ValidationException {\n            Validators.nonNull(key);\n        }\n\n        @Override\n        public String queryEncryptionSecret() {\n            var r = SecretPasswordManagerStrategy.builder()\n                    .key(key)\n                    .build()\n                    .query()\n                    .query(\"Group secret\");\n            return r.getState() == SecretQueryState.NORMAL ? r.getSecret().getSecretValue() : null;\n        }\n    }\n\n    @JsonTypeName(\"file\")\n    @Builder\n    @Jacksonized\n    @Value\n    class File implements DataStorageGroupStrategy {\n\n        @SuppressWarnings(\"unused\")\n        public static String getOptionsNameKey() {\n            return \"fileSecret\";\n        }\n\n        @SuppressWarnings(\"unused\")\n        public static OptionsBuilder createOptions(Property<File> p) {\n            var file = new SimpleObjectProperty<>(\n                    p.getValue().getFile() != null ? p.getValue().getFile().toLocalAbsoluteFilePath() : null);\n            return new OptionsBuilder()\n                    .nameAndDescription(\"fileSecretChoice\")\n                    .addComp(\n                            new ContextualFileReferenceChoiceComp(\n                                    new ReadOnlyObjectWrapper<>(\n                                            DataStorage.get().local().ref()),\n                                    file,\n                                    null,\n                                    List.of(),\n                                    e -> e.equals(DataStorage.get().local()),\n                                    false),\n                            file)\n                    .nonNull()\n                    .bind(\n                            () -> {\n                                return File.builder()\n                                        .file(ContextualFileReference.of(file.get()))\n                                        .build();\n                            },\n                            p);\n        }\n\n        ContextualFileReference file;\n\n        @Override\n        public void checkComplete() throws ValidationException {\n            Validators.nonNull(file);\n        }\n\n        @Override\n        public String queryEncryptionSecret() throws Exception {\n            var abs = file.toLocalAbsoluteFilePath().asLocalPath();\n            if (!Files.exists(abs)) {\n                throw ErrorEventFactory.expected(\n                        new IllegalArgumentException(\"Group key file \" + abs + \" does not exist\"));\n            }\n\n            var read = Files.readString(abs);\n            if (read.length() == 0) {\n                throw ErrorEventFactory.expected(new IllegalArgumentException(\"Group key file \" + abs + \" is empty\"));\n            }\n\n            return read;\n        }\n    }\n\n    @JsonTypeName(\"command\")\n    @Builder\n    @Jacksonized\n    @Value\n    class Command implements DataStorageGroupStrategy {\n\n        @SuppressWarnings(\"unused\")\n        public static String getOptionsNameKey() {\n            return \"commandSecret\";\n        }\n\n        @SuppressWarnings(\"unused\")\n        public static OptionsBuilder createOptions(Property<Command> p) {\n            var command = new SimpleObjectProperty<>(p.getValue().getCommand());\n            return new OptionsBuilder()\n                    .nameAndDescription(\"commandSecretField\")\n                    .addComp(\n                            IntegratedTextAreaComp.script(\n                                    new ReadOnlyObjectWrapper<>(\n                                            DataStorage.get().local().ref()),\n                                    command),\n                            command)\n                    .nonNull()\n                    .bind(\n                            () -> {\n                                return Command.builder().command(command.get()).build();\n                            },\n                            p);\n        }\n\n        ShellScript command;\n\n        @Override\n        public void checkComplete() throws ValidationException {\n            Validators.nonNull(command);\n        }\n\n        @Override\n        public String queryEncryptionSecret() throws Exception {\n            try (var sc =\n                    ProcessControlProvider.get().createLocalProcessControl(true).start()) {\n                try (var cc = sc.command(command).sensitive().start()) {\n                    cc.killOnTimeout(CountDown.of().start(10_000));\n                    var out = cc.readStdoutOrThrow();\n                    if (out.length() == 0) {\n                        throw ErrorEventFactory.expected(\n                                new IllegalArgumentException(\"Command did not return any output\"));\n                    }\n                    return out;\n                }\n            }\n        }\n    }\n\n    @JsonTypeName(\"httpRequest\")\n    @Builder\n    @Jacksonized\n    @Value\n    class HttpRequest implements DataStorageGroupStrategy {\n\n        @SuppressWarnings(\"unused\")\n        public static String getOptionsNameKey() {\n            return \"httpRequestSecret\";\n        }\n\n        @SuppressWarnings(\"unused\")\n        public static OptionsBuilder createOptions(Property<HttpRequest> p) {\n            var uri = new SimpleStringProperty(p.getValue().getUri());\n            return new OptionsBuilder()\n                    .nameAndDescription(\"httpRequestSecretField\")\n                    .addString(uri)\n                    .nonNull()\n                    .bind(\n                            () -> {\n                                return HttpRequest.builder().uri(uri.get()).build();\n                            },\n                            p);\n        }\n\n        String uri;\n\n        @Override\n        public void checkComplete() throws ValidationException {\n            Validators.nonNull(uri);\n        }\n\n        @Override\n        public String queryEncryptionSecret() throws Exception {\n            var uri = URI.create(getUri());\n            var request = java.net.http.HttpRequest.newBuilder()\n                    .uri(uri)\n                    .POST(java.net.http.HttpRequest.BodyPublishers.noBody())\n                    .build();\n            var result = HttpHelper.client().send(request, HttpResponse.BodyHandlers.ofString());\n            if (result.statusCode() >= 400) {\n                throw ErrorEventFactory.expected(new IOException(result.body()));\n            }\n            var body = result.body();\n            if (body.length() == 0) {\n                throw ErrorEventFactory.expected(new IllegalArgumentException(\"Http response body is empty\"));\n            }\n            return body;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/storage/DataStorageNode.java",
    "content": "package io.xpipe.app.storage;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.UserScopeStore;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.secret.EncryptionToken;\nimport io.xpipe.core.InPlaceSecretValue;\nimport io.xpipe.core.JacksonMapper;\n\nimport com.fasterxml.jackson.core.JsonFactory;\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.core.util.DefaultPrettyPrinter;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.JsonNodeFactory;\nimport lombok.Value;\n\nimport java.io.CharArrayReader;\nimport java.io.CharArrayWriter;\nimport java.io.IOException;\n\n@Value\npublic class DataStorageNode {\n\n    JsonNode contentNode;\n    boolean perUser;\n    boolean readableForUser;\n    boolean encrypted;\n\n    private static boolean encryptPerUser(DataStore store) {\n        if (DataStorageUserHandler.getInstance().getActiveUser() == null) {\n            return false;\n        }\n\n        var perUser = false;\n        try {\n            perUser = store instanceof UserScopeStore s && s.isPerUser();\n        } catch (Exception ignored) {\n        }\n\n        if (perUser) {\n            return true;\n        }\n\n        var all = AppPrefs.get() != null && AppPrefs.get().encryptAllVaultData().get();\n        var useUserKey = DataStorageUserHandler.getInstance().getUserCount() == 1\n                && DataStorageUserHandler.getInstance().getActiveUser() != null;\n        return all && useUserKey;\n    }\n\n    private static boolean encrypt(DataStore store) {\n        if (AppPrefs.get() != null && AppPrefs.get().encryptAllVaultData().get()) {\n            return true;\n        }\n\n        if (DataStorageUserHandler.getInstance().getActiveUser() == null) {\n            return false;\n        }\n\n        var perUser = false;\n        try {\n            perUser = store instanceof UserScopeStore s && s.isPerUser();\n        } catch (Exception ignored) {\n        }\n        return perUser;\n    }\n\n    public static DataStorageNode ofNewStore(DataStore store) {\n        return new DataStorageNode(\n                JacksonMapper.getDefault().valueToTree(store), encryptPerUser(store), true, encrypt(store));\n    }\n\n    public static DataStorageNode fail() {\n        return new DataStorageNode(null, false, false, false);\n    }\n\n    public static DataStorageNode readPossiblyEncryptedNode(JsonNode node) {\n        if (!node.isObject()) {\n            return fail();\n        }\n\n        try {\n            var secret = DataStorageSecret.deserialize(node);\n            if (secret == null) {\n                return new DataStorageNode(node, false, true, false);\n            }\n\n            if (secret.getInternalSecret() == null) {\n                return fail();\n            }\n\n            if (!secret.getEncryptedToken().canDecrypt()) {\n                return new DataStorageNode(node, true, false, true);\n            }\n\n            var read = secret.getInternalSecret().mapSecretValueFailable(chars -> {\n                if (chars.length == 0) {\n                    return JsonNodeFactory.instance.missingNode();\n                }\n\n                return JacksonMapper.getDefault().readTree(new CharArrayReader(chars));\n            });\n            var currentUser = secret.getEncryptedToken().isUser();\n            return new DataStorageNode(read, currentUser, true, true);\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).build().handle();\n            return fail();\n        }\n    }\n\n    public static JsonNode encryptNodeIfNeeded(DataStorageNode node) {\n        if (!node.isEncrypted()) {\n            return node.getContentNode();\n        }\n\n        var writer = new CharArrayWriter();\n        JsonFactory f = new JsonFactory();\n        try (JsonGenerator g = f.createGenerator(writer).setPrettyPrinter(new DefaultPrettyPrinter())) {\n            JacksonMapper.getDefault().writeTree(g, node.getContentNode());\n        } catch (IOException e) {\n            ErrorEventFactory.fromThrowable(e).build().handle();\n            return node.getContentNode();\n        }\n\n        var newContent = writer.toCharArray();\n        var token = node.isPerUser() ? EncryptionToken.ofUser() : EncryptionToken.ofVaultKey();\n        var secret = DataStorageSecret.ofSecret(InPlaceSecretValue.of(newContent), token);\n        return secret.serialize(node.isPerUser());\n    }\n\n    public DataStore parseStore() throws JsonProcessingException {\n        if (contentNode == null) {\n            return null;\n        }\n\n        return JacksonMapper.getDefault().treeToValue(getContentNode(), DataStore.class);\n    }\n\n    public boolean hasAccess() {\n        // In this case the loading failed\n        // We have access to it, we just can't read it\n        if (!perUser && !readableForUser) {\n            return true;\n        }\n\n        return !perUser || readableForUser;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/storage/DataStorageQuery.java",
    "content": "package io.xpipe.app.storage;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.regex.Pattern;\n\npublic class DataStorageQuery {\n\n    public static List<DataStoreEntry> queryUserInput(String input) {\n        var found = queryEntry(\"**\", \"**\" + input + \"**\", \"*\");\n        if (found.size() > 1) {\n            var narrowPath = found.stream()\n                    .filter(dataStoreEntry -> DataStorage.get().getStorePath(dataStoreEntry).toString().equalsIgnoreCase(input))\n                    .toList();\n            if (narrowPath.size() >= 1) {\n                return narrowPath;\n            }\n\n            var narrowName = found.stream()\n                    .filter(dataStoreEntry -> dataStoreEntry.getName().equalsIgnoreCase(input))\n                    .toList();\n            if (narrowName.size() >= 1) {\n                return narrowName;\n            }\n        }\n        return found;\n    }\n\n    public static List<DataStoreCategory> queryCategory(String categoryFilter) {\n        if (DataStorage.get() == null) {\n            return List.of();\n        }\n\n        var catMatcher = Pattern.compile(toRegex(categoryFilter.toLowerCase()));\n\n        List<DataStoreCategory> found = new ArrayList<>();\n        for (DataStoreCategory cat : DataStorage.get().getStoreCategories()) {\n            var c = DataStorage.get().getStorePath(cat).toString();\n            if (!catMatcher.matcher(c).matches()) {\n                continue;\n            }\n\n            found.add(cat);\n        }\n        return found;\n    }\n\n    public static List<DataStoreEntry> queryEntry(String categoryFilter, String connectionFilter, String typeFilter) {\n        if (DataStorage.get() == null) {\n            return List.of();\n        }\n\n        var catMatcher = Pattern.compile(toRegex(categoryFilter.toLowerCase()));\n        var conMatcher = Pattern.compile(toRegex(connectionFilter.toLowerCase()));\n        var typeMatcher = Pattern.compile(toRegex(typeFilter.toLowerCase()));\n\n        List<DataStoreEntry> found = new ArrayList<>();\n        for (DataStoreEntry storeEntry : DataStorage.get().getStoreEntries()) {\n            if (!storeEntry.getValidity().isUsable()) {\n                continue;\n            }\n\n            var name = DataStorage.get().getStorePath(storeEntry).toString();\n            if (!conMatcher.matcher(name).matches()) {\n                continue;\n            }\n\n            var cat = DataStorage.get()\n                    .getStoreCategoryIfPresent(storeEntry.getCategoryUuid())\n                    .orElse(null);\n            if (cat == null) {\n                continue;\n            }\n\n            var c = DataStorage.get().getStorePath(cat).toString();\n            if (!catMatcher.matcher(c).matches()) {\n                continue;\n            }\n\n            if (!typeMatcher\n                    .matcher(storeEntry.getProvider().getId().toLowerCase())\n                    .matches()) {\n                continue;\n            }\n\n            found.add(storeEntry);\n        }\n        return found;\n    }\n\n    public static String toRegex(String pattern) {\n        pattern = pattern.replaceAll(\"\\\\*\\\\*\", \"#\");\n        // https://stackoverflow.com/a/17369948/6477761\n        StringBuilder sb = new StringBuilder(pattern.length());\n        int inGroup = 0;\n        int inClass = 0;\n        int firstIndexInClass = -1;\n        char[] arr = pattern.toCharArray();\n        for (int i = 0; i < arr.length; i++) {\n            char ch = arr[i];\n            switch (ch) {\n                case '\\\\':\n                    if (++i >= arr.length) {\n                        sb.append('\\\\');\n                    } else {\n                        char next = arr[i];\n                        switch (next) {\n                            case ',':\n                                // escape not needed\n                                break;\n                            case 'Q':\n                            case 'E':\n                                // extra escape needed\n                                sb.append('\\\\');\n                            default:\n                                sb.append('\\\\');\n                        }\n                        sb.append(next);\n                    }\n                    break;\n                case '*':\n                    if (inClass == 0) {\n                        sb.append(\"[^/]*\");\n                    } else {\n                        sb.append('*');\n                    }\n                    break;\n                case '#':\n                    if (inClass == 0) {\n                        sb.append(\".*\");\n                    } else {\n                        sb.append('*');\n                    }\n                    break;\n                case '?':\n                    if (inClass == 0) {\n                        sb.append('.');\n                    } else {\n                        sb.append('?');\n                    }\n                    break;\n                case '[':\n                    inClass++;\n                    firstIndexInClass = i + 1;\n                    sb.append('[');\n                    break;\n                case ']':\n                    inClass--;\n                    sb.append(']');\n                    break;\n                case '.':\n                case '(':\n                case ')':\n                case '+':\n                case '|':\n                case '^':\n                case '$':\n                case '@':\n                case '%':\n                    if (inClass == 0 || (firstIndexInClass == i && ch == '^')) {\n                        sb.append('\\\\');\n                    }\n                    sb.append(ch);\n                    break;\n                case '!':\n                    if (firstIndexInClass == i) {\n                        sb.append('^');\n                    } else {\n                        sb.append('!');\n                    }\n                    break;\n                case '{':\n                    inGroup++;\n                    sb.append('(');\n                    break;\n                case '}':\n                    inGroup--;\n                    sb.append(')');\n                    break;\n                case ',':\n                    if (inGroup > 0) {\n                        sb.append('|');\n                    } else {\n                        sb.append(',');\n                    }\n                    break;\n                default:\n                    sb.append(ch);\n            }\n        }\n        return sb.toString();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/storage/DataStorageSecret.java",
    "content": "package io.xpipe.app.storage;\n\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.secret.EncryptionToken;\nimport io.xpipe.app.secret.PasswordLockSecretValue;\nimport io.xpipe.app.secret.VaultKeySecretValue;\nimport io.xpipe.core.EncryptedSecretValue;\nimport io.xpipe.core.InPlaceSecretValue;\nimport io.xpipe.core.JacksonMapper;\nimport io.xpipe.core.SecretValue;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.JsonNodeFactory;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.ToString;\n\nimport java.io.IOException;\n\n@EqualsAndHashCode\n@ToString\npublic class DataStorageSecret {\n\n    private final InPlaceSecretValue secret;\n\n    @Getter\n    private JsonNode originalNode;\n\n    @Getter\n    private EncryptionToken encryptedToken;\n\n    public DataStorageSecret(EncryptionToken encryptedToken, JsonNode originalNode, InPlaceSecretValue secret) {\n        this.encryptedToken = encryptedToken;\n        this.originalNode = originalNode;\n        this.secret = secret;\n    }\n\n    public static DataStorageSecret deserialize(JsonNode tree) throws IOException {\n        if (!tree.isObject()) {\n            return null;\n        }\n\n        var legacy = JacksonMapper.getDefault().treeToValue(tree, EncryptedSecretValue.class);\n        if (legacy != null) {\n            // Don't cache legacy node\n            return new DataStorageSecret(EncryptionToken.ofVaultKey(), null, legacy.inPlace());\n        }\n\n        var obj = (ObjectNode) tree;\n        if (!obj.has(\"secret\")) {\n            return null;\n        }\n\n        var secretTree = obj.required(\"secret\");\n        var secret = JacksonMapper.getDefault().treeToValue(secretTree, SecretValue.class);\n        if (secret == null) {\n            return null;\n        }\n\n        var hadLock = AppPrefs.get().getLockCrypt().get() != null\n                && !AppPrefs.get().getLockCrypt().get().isEmpty();\n        var tokenNode = obj.get(\"encryptedToken\");\n        var token = tokenNode != null ? JacksonMapper.getDefault().treeToValue(tokenNode, EncryptionToken.class) : null;\n        if (token == null) {\n            var userToken = hadLock;\n            if (userToken && DataStorageUserHandler.getInstance().getActiveUser() == null) {\n                return null;\n            }\n            token = userToken ? EncryptionToken.ofUser() : EncryptionToken.ofVaultKey();\n        }\n\n        return new DataStorageSecret(token, secretTree, secret.inPlace());\n    }\n\n    public static DataStorageSecret ofCurrentSecret(SecretValue internalSecret) {\n        var handler = DataStorageUserHandler.getInstance();\n        return new DataStorageSecret(\n                handler.getActiveUser() != null ? EncryptionToken.ofUser() : EncryptionToken.ofVaultKey(),\n                null,\n                internalSecret.inPlace());\n    }\n\n    public static DataStorageSecret ofSecret(SecretValue internalSecret, EncryptionToken token) {\n        return new DataStorageSecret(token, null, internalSecret.inPlace());\n    }\n\n    public boolean requiresRewrite(boolean allowUserSecretKey) {\n        var isVault = encryptedToken.isVault();\n        var isUser = encryptedToken.isUser();\n        var userHandler = DataStorageUserHandler.getInstance();\n\n        // User key must have changed\n        if (!isUser && !isVault) {\n            // We have loaded a secret with a user key that does no longer exist\n            // This means that the user was deleted in this session\n            // Replace it with a vault key\n            if (userHandler.getActiveUser() == null) {\n                return true;\n            }\n\n            // Password was changed\n            return true;\n        }\n\n        var hasUserKey = userHandler.getActiveUser() != null;\n        // Switch from vault to user\n        if (hasUserKey && isVault && allowUserSecretKey) {\n            return true;\n        }\n        // Switch from user to vault\n        if (!hasUserKey && isUser) {\n            return true;\n        }\n\n        return false;\n    }\n\n    private void rewrite(boolean allowUserSecretKey) {\n        var handler = DataStorageUserHandler.getInstance();\n        if (handler != null && handler.getActiveUser() != null && allowUserSecretKey) {\n            var val = new PasswordLockSecretValue(getSecret());\n            originalNode = JacksonMapper.getDefault().valueToTree(val);\n            encryptedToken = EncryptionToken.ofUser();\n            return;\n        }\n\n        var val = new VaultKeySecretValue(getSecret());\n        originalNode = JacksonMapper.getDefault().valueToTree(val);\n        encryptedToken = EncryptionToken.ofVaultKey();\n    }\n\n    public JsonNode serialize(boolean allowUserSecretKey) {\n        if (secret == null) {\n            return null;\n        }\n\n        var mapper = JacksonMapper.getDefault();\n        var tree = JsonNodeFactory.instance.objectNode();\n\n        // Preserve same output if not changed\n        if (getOriginalNode() != null && !requiresRewrite(allowUserSecretKey)) {\n            tree.set(\"secret\", getOriginalNode());\n            tree.set(\"encryptedToken\", mapper.valueToTree(getEncryptedToken()));\n            return tree;\n        }\n\n        // Reencrypt\n        rewrite(allowUserSecretKey);\n        tree.set(\"secret\", getOriginalNode());\n        tree.set(\"encryptedToken\", mapper.valueToTree(getEncryptedToken()));\n        return tree;\n    }\n\n    public char[] getSecret() {\n        return secret != null ? secret.getSecret() : new char[0];\n    }\n\n    public InPlaceSecretValue getInternalSecret() {\n        return secret;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/storage/DataStorageSyncHandler.java",
    "content": "package io.xpipe.app.storage;\n\nimport io.xpipe.app.ext.ProcessControlProvider;\n\nimport java.nio.file.Path;\nimport java.util.List;\n\npublic interface DataStorageSyncHandler {\n\n    static DataStorageSyncHandler getInstance() {\n        return (DataStorageSyncHandler) ProcessControlProvider.get().getStorageSyncHandler();\n    }\n\n    void pullManually();\n\n    void pushManually();\n\n    void reset() throws Exception;\n\n    boolean validateConnection();\n\n    boolean supportsSync();\n\n    boolean hasExternalStoredCredentials();\n\n    void init();\n\n    void prepareGpgIfNeeded();\n\n    void retrieveSyncedData();\n\n    void refreshRemoteData();\n\n    void afterStorageLoad();\n\n    void beforeStorageSave();\n\n    void afterStorageSave(boolean pushIfNeeded, boolean dispose);\n\n    void handleEntry(DataStoreEntry entry, boolean exists, boolean dirty);\n\n    void handleCategory(DataStoreCategory category, boolean exists, boolean dirty);\n\n    void handleDeletion(Path target, String name);\n\n    Path getDirectory();\n\n    List<Path> getSavedDataFiles();\n\n    Path getDataFile(Path rel);\n\n    Path addDataFile(Path file, Path target, boolean perUser);\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/storage/DataStorageUserHandler.java",
    "content": "package io.xpipe.app.storage;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.prefs.VaultAuthentication;\n\nimport javafx.beans.property.ObjectProperty;\n\nimport java.io.IOException;\nimport javax.crypto.SecretKey;\n\npublic interface DataStorageUserHandler {\n\n    static DataStorageUserHandler getInstance() {\n        return (DataStorageUserHandler) ProcessControlProvider.get().getStorageUserHandler();\n    }\n\n    int getUserCount();\n\n    void init() throws IOException;\n\n    void save();\n\n    void login();\n\n    SecretKey getEncryptionKey();\n\n    BaseRegionBuilder<?, ?> createOverview();\n\n    OptionsBuilder createGroupStrategyOptions(ObjectProperty<DataStorageGroupStrategy> groupStrategy);\n\n    String getActiveUser();\n\n    VaultAuthentication getVaultAuthenticationType();\n\n    DataStorageGroupStrategy getGroupStrategy(String user);\n\n    void setCurrentGroupStrategy(DataStorageGroupStrategy groupStrategy);\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/storage/DataStoreCategory.java",
    "content": "package io.xpipe.app.storage;\n\nimport io.xpipe.core.JacksonMapper;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.JsonNodeFactory;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport lombok.EqualsAndHashCode;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.experimental.NonFinal;\nimport org.apache.commons.io.FileUtils;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Instant;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.UUID;\n\n@EqualsAndHashCode(callSuper = true)\n@Value\npublic class DataStoreCategory extends StorageElement {\n\n    @NonFinal\n    UUID parentCategory;\n\n    @NonFinal\n    DataStoreCategoryConfig config;\n\n    public DataStoreCategory(\n            Path directory,\n            UUID uuid,\n            String name,\n            Instant lastUsed,\n            Instant lastModified,\n            boolean dirty,\n            UUID parentCategory,\n            boolean expanded,\n            DataStoreCategoryConfig config) {\n        super(directory, uuid, name, lastUsed, lastModified, expanded, dirty);\n        this.parentCategory = parentCategory;\n        this.config = config;\n    }\n\n    public static DataStoreCategory createNew(UUID parentCategory, @NonNull String name) {\n        return new DataStoreCategory(\n                null,\n                UUID.randomUUID(),\n                name,\n                Instant.now(),\n                Instant.now(),\n                true,\n                parentCategory,\n                true,\n                DataStoreCategoryConfig.empty());\n    }\n\n    public static DataStoreCategory createNew(UUID parentCategory, @NonNull UUID uuid, @NonNull String name) {\n        return new DataStoreCategory(\n                null,\n                uuid,\n                name,\n                Instant.now(),\n                Instant.now(),\n                true,\n                parentCategory,\n                true,\n                DataStoreCategoryConfig.empty());\n    }\n\n    public static Optional<DataStoreCategory> fromDirectory(Path dir) throws IOException {\n        ObjectMapper mapper = JacksonMapper.getDefault();\n\n        var entryFile = dir.resolve(\"category.json\");\n        var stateFile = dir.resolve(\"state.json\");\n        if (!Files.exists(entryFile)) {\n            return Optional.empty();\n        }\n\n        var stateJson =\n                Files.exists(stateFile) ? mapper.readTree(stateFile.toFile()) : JsonNodeFactory.instance.objectNode();\n        var json = mapper.readTree(entryFile.toFile());\n\n        var uuid = UUID.fromString(json.required(\"uuid\").textValue());\n        var parentUuid = Optional.ofNullable(json.get(\"parentUuid\"))\n                .filter(jsonNode -> !jsonNode.isNull())\n                .map(jsonNode -> UUID.fromString(jsonNode.textValue()))\n                .orElse(null);\n        var name = json.required(\"name\").textValue();\n\n        var lastUsed = Optional.ofNullable(stateJson.get(\"lastUsed\"))\n                .map(jsonNode -> jsonNode.textValue())\n                .map(Instant::parse)\n                .orElse(Instant.now());\n        var lastModified = Optional.ofNullable(stateJson.get(\"lastModified\"))\n                .map(jsonNode -> jsonNode.textValue())\n                .map(Instant::parse)\n                .orElse(Instant.now());\n        var expanded = Optional.ofNullable(stateJson.get(\"expanded\"))\n                .map(jsonNode -> jsonNode.booleanValue())\n                .orElse(true);\n        var config = Optional.ofNullable(json.get(\"config\"))\n                .map(jsonNode -> {\n                    try {\n                        return JacksonMapper.getDefault().treeToValue(jsonNode, DataStoreCategoryConfig.class);\n                    } catch (JsonProcessingException e) {\n                        return DataStoreCategoryConfig.empty();\n                    }\n                })\n                .orElse(DataStoreCategoryConfig.empty());\n\n        var share =\n                Optional.ofNullable(json.get(\"share\")).map(JsonNode::asBoolean).orElse(null);\n        if (share != null) {\n            config = config.withSync(share);\n        }\n        var color = Optional.ofNullable(json.get(\"color\"))\n                .map(node -> {\n                    try {\n                        return mapper.treeToValue(node, DataStoreColor.class);\n                    } catch (JsonProcessingException e) {\n                        return null;\n                    }\n                })\n                .orElse(null);\n        if (color != null) {\n            config = config.withColor(color);\n        }\n\n        return Optional.of(\n                new DataStoreCategory(dir, uuid, name, lastUsed, lastModified, false, parentUuid, expanded, config));\n    }\n\n    public boolean setConfig(DataStoreCategoryConfig config) {\n        var changed = !this.config.equals(config);\n        if (changed) {\n            this.config = config;\n            notifyUpdate(false, true);\n            return true;\n        }\n        return false;\n    }\n\n    public boolean isChangedForReload(DataStoreCategory other) {\n        return !Objects.equals(getName(), other.getName())\n                || !Objects.equals(getConfig(), other.getConfig())\n                || !Objects.equals(getParentCategory(), other.getParentCategory());\n    }\n\n    public void setParentCategory(UUID parentCategory) {\n        var changed = !Objects.equals(this.parentCategory, parentCategory);\n        this.parentCategory = parentCategory;\n        if (changed) {\n            notifyUpdate(false, true);\n        }\n    }\n\n    public boolean canShare() {\n        if (parentCategory == null) {\n            return false;\n        }\n\n        if (getUuid().equals(DataStorage.PREDEFINED_SCRIPTS_CATEGORY_UUID)) {\n            return false;\n        }\n\n        if (getUuid().equals(DataStorage.LOCAL_IDENTITIES_CATEGORY_UUID)) {\n            return false;\n        }\n\n        return true;\n    }\n\n    @Override\n    public boolean isInStorage() {\n        return DataStorage.get().getStoreCategories().contains(this);\n    }\n\n    @Override\n    public Path[] getShareableFiles() {\n        return new Path[] {directory.resolve(\"category.json\")};\n    }\n\n    public void writeDataToDisk() throws Exception {\n        if (!dirty) {\n            return;\n        }\n\n        // Reset the dirty state early\n        // That way, if any other changes are made during this save operation,\n        // the dirty bit can be set to true again\n        dirty = false;\n\n        ObjectMapper mapper = JacksonMapper.getDefault();\n        ObjectNode obj = JsonNodeFactory.instance.objectNode();\n        ObjectNode stateObj = JsonNodeFactory.instance.objectNode();\n        obj.put(\"uuid\", uuid.toString());\n        obj.put(\"name\", name);\n        stateObj.put(\"lastUsed\", lastUsed.toString());\n        stateObj.put(\"lastModified\", lastModified.toString());\n        stateObj.put(\"expanded\", expanded);\n        obj.put(\"parentUuid\", parentCategory != null ? parentCategory.toString() : null);\n        obj.set(\"config\", JacksonMapper.getDefault().valueToTree(config));\n\n        var entryString = mapper.writeValueAsString(obj);\n        var stateString = mapper.writeValueAsString(stateObj);\n        FileUtils.forceMkdir(directory.toFile());\n        Files.writeString(directory.resolve(\"category.json\"), entryString);\n        Files.writeString(directory.resolve(\"state.json\"), stateString);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/storage/DataStoreCategoryConfig.java",
    "content": "package io.xpipe.app.storage;\n\nimport lombok.*;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\nimport java.util.UUID;\n\n@Value\n@Builder\n@Jacksonized\n@AllArgsConstructor\npublic class DataStoreCategoryConfig {\n\n    @With\n    DataStoreColor color;\n\n    Boolean dontAllowScripts;\n    Boolean confirmAllModifications;\n\n    @With\n    Boolean sync;\n\n    Boolean freezeConfigurations;\n    UUID defaultIdentityStore;\n\n    public static DataStoreCategoryConfig empty() {\n        return new DataStoreCategoryConfig(null, null, null, null, null, null);\n    }\n\n    public static DataStoreCategoryConfig merge(List<DataStoreCategoryConfig> configs) {\n        DataStoreColor color = null;\n        Boolean dontAllowScripts = null;\n        Boolean warnOnAllModifications = null;\n        Boolean sync = null;\n        Boolean readOnly = null;\n        UUID defaultIdentityStore = null;\n        for (int i = configs.size() - 1; i >= 0; i--) {\n            var config = configs.get(i);\n            if (color == null) {\n                color = config.color;\n            }\n            if (dontAllowScripts == null) {\n                dontAllowScripts = config.dontAllowScripts;\n            }\n            if (warnOnAllModifications == null) {\n                warnOnAllModifications = config.confirmAllModifications;\n            }\n            if (defaultIdentityStore == null) {\n                defaultIdentityStore = config.defaultIdentityStore;\n            }\n            if (sync == null) {\n                sync = config.sync;\n            }\n            if (readOnly == null) {\n                readOnly = config.freezeConfigurations;\n            }\n        }\n        return new DataStoreCategoryConfig(\n                color, dontAllowScripts, warnOnAllModifications, sync, readOnly, defaultIdentityStore);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/storage/DataStoreColor.java",
    "content": "package io.xpipe.app.storage;\n\nimport javafx.scene.Node;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\nimport javafx.scene.paint.Color;\nimport javafx.scene.shape.Rectangle;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport lombok.Getter;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\n\n@Getter\npublic enum DataStoreColor {\n    @JsonProperty(\"red\")\n    RED(\"red\", \"\\uD83D\\uDD34\", Color.DARKRED),\n\n    @JsonProperty(\"yellow\")\n    YELLOW(\"yellow\", \"\\uD83D\\uDFE1\", Color.web(\"#999922\")),\n\n    @JsonProperty(\"green\")\n    GREEN(\"green\", \"\\uD83D\\uDFE2\", Color.DARKGREEN),\n\n    @JsonProperty(\"cyan\")\n    CYAN(\"cyan\", \"\\uD83D\\uDFE9\", Color.CYAN),\n\n    @JsonProperty(\"blue\")\n    BLUE(\"blue\", \"\\uD83D\\uDD35\", Color.DARKBLUE),\n\n    @JsonProperty(\"purple\")\n    VIOLET(\"purple\", \"\\uD83D\\uDFE3\", Color.VIOLET);\n\n    private final String id;\n    private final String emoji;\n    private final Color terminalColor;\n\n    DataStoreColor(String id, String emoji, Color terminalColor) {\n        this.id = id;\n        this.emoji = emoji;\n        this.terminalColor = terminalColor;\n    }\n\n    public static Region createDisplayGraphic(DataStoreColor color) {\n        var b = new Rectangle(8, 8);\n        b.setArcWidth(4);\n        b.setArcHeight(4);\n        b.getStyleClass().add(\"dot\");\n        b.getStyleClass().add(\"color-box\");\n        b.getStyleClass().add(color != null ? color.getId() : \"gray\");\n\n        var d = new Rectangle(10, 10);\n        d.setArcWidth(4 + 2);\n        d.setArcHeight(4 + 2);\n        d.getStyleClass().add(\"dot\");\n        d.getStyleClass().add(\"color-box\");\n        d.getStyleClass().add(color != null ? color.getId() : \"gray\");\n\n        var s = new StackPane(d, b);\n        return s;\n    }\n\n    public static void applyStyleClasses(DataStoreColor color, Node node) {\n        var newList = new ArrayList<>(node.getStyleClass());\n        newList.removeIf(s -> Arrays.stream(DataStoreColor.values())\n                .anyMatch(dataStoreColor -> dataStoreColor.getId().equals(s)));\n        newList.remove(\"gray\");\n        if (color != null) {\n            newList.add(color.getId());\n        } else {\n            newList.add(\"gray\");\n        }\n        node.getStyleClass().setAll(newList);\n    }\n\n    private String format(double val) {\n        String in = Integer.toHexString((int) Math.round(val * 255));\n        return in.length() == 1 ? \"0\" + in : in;\n    }\n\n    public String toHexString() {\n        var value = terminalColor;\n        return \"#\" + (format(value.getRed()) + format(value.getGreen()) + format(value.getBlue())).toUpperCase();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java",
    "content": "package io.xpipe.app.storage;\n\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.icon.SystemIconManager;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.JacksonMapper;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.core.JacksonException;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.JsonNodeFactory;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport lombok.*;\nimport lombok.experimental.NonFinal;\nimport org.apache.commons.io.FileUtils;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Instant;\nimport java.util.*;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.stream.Stream;\n\n@Value\npublic class DataStoreEntry extends StorageElement {\n\n    Map<String, Object> storeCache = Collections.synchronizedMap(new HashMap<>());\n    AtomicInteger busyCounter = new AtomicInteger();\n\n    @NonFinal\n    Validity validity;\n\n    @NonFinal\n    @Setter\n    DataStorageNode storeNode;\n\n    @Getter\n    @NonFinal\n    DataStore store;\n\n    @Getter\n    @NonFinal\n    DataStoreProvider provider;\n\n    @NonFinal\n    UUID categoryUuid;\n\n    @NonFinal\n    DataStoreState storePersistentState;\n\n    @NonFinal\n    JsonNode storePersistentStateNode;\n\n    @NonFinal\n    @Setter\n    Set<DataStoreEntry> childrenCache = null;\n\n    @NonFinal\n    String notes;\n\n    @NonFinal\n    String lastWrittenNotes;\n\n    @NonFinal\n    String icon;\n\n    @NonFinal\n    @Getter\n    DataStoreColor color;\n\n    @NonFinal\n    @Getter\n    boolean freeze;\n\n    @NonFinal\n    @Getter\n    boolean pinToTop;\n\n    @Getter\n    @NonFinal\n    int orderIndex;\n\n    @Getter\n    @NonFinal\n    UUID breakOutCategory;\n\n    List<String> tags;\n\n    private DataStoreEntry(\n            Path directory,\n            UUID uuid,\n            UUID categoryUuid,\n            String name,\n            Instant lastUsed,\n            Instant lastModified,\n            DataStore store,\n            DataStorageNode storeNode,\n            boolean dirty,\n            Validity validity,\n            JsonNode storePersistentState,\n            boolean expanded,\n            DataStoreColor color,\n            String notes,\n            String icon,\n            boolean freeze,\n            boolean pinToTop,\n            int orderIndex,\n            UUID breakOutCategory,\n            List<String> tags) {\n        super(directory, uuid, name, lastUsed, lastModified, expanded, dirty);\n        this.color = color;\n        this.categoryUuid = categoryUuid;\n        this.store = store;\n        this.storeNode = storeNode;\n        this.provider =\n                store != null ? DataStoreProviders.byStoreIfPresent(store).orElse(null) : null;\n        this.validity = this.provider != null ? validity : Validity.LOAD_FAILED;\n        this.storePersistentStateNode = storePersistentState;\n        this.notes = notes;\n        this.icon = icon;\n        this.freeze = freeze;\n        this.pinToTop = pinToTop;\n        this.orderIndex = orderIndex;\n        this.breakOutCategory = breakOutCategory;\n        this.tags = tags;\n    }\n\n    public static DataStoreEntry createTempWrapper(@NonNull DataStore store) {\n        var storage = DataStorage.get();\n        var cat = storage != null ? storage.getSelectedCategory().getUuid() : UUID.randomUUID();\n        return new DataStoreEntry(\n                null,\n                UUID.randomUUID(),\n                cat,\n                UUID.randomUUID().toString(),\n                Instant.now(),\n                Instant.now(),\n                store,\n                DataStorageNode.fail(),\n                false,\n                Validity.COMPLETE,\n                null,\n                false,\n                null,\n                null,\n                null,\n                false,\n                false,\n                0,\n                null,\n                new ArrayList<>());\n    }\n\n    public static DataStoreEntry createNew(@NonNull NameableStore store) {\n        return createNew(\n                UUID.randomUUID(), DataStorage.get().getSelectedCategory().getUuid(), store.getName(), store);\n    }\n\n    public static DataStoreEntry createNew(@NonNull String name, @NonNull DataStore store) {\n        return createNew(\n                UUID.randomUUID(), DataStorage.get().getSelectedCategory().getUuid(), name, store);\n    }\n\n    @SneakyThrows\n    public static DataStoreEntry createNew(\n            @NonNull UUID uuid, @NonNull UUID categoryUuid, @NonNull String name, @NonNull DataStore store) {\n        if (name.isBlank()) {\n            throw new IllegalArgumentException(\"Name is empty\");\n        }\n\n        var storeNode = DataStorageNode.ofNewStore(store);\n        var storeFromNode = storeNode.parseStore();\n        var validity = storeFromNode == null\n                ? Validity.LOAD_FAILED\n                : store.isComplete() ? Validity.COMPLETE : Validity.INCOMPLETE;\n        var entry = new DataStoreEntry(\n                null,\n                uuid,\n                categoryUuid,\n                name.strip(),\n                Instant.now(),\n                Instant.now(),\n                storeFromNode,\n                storeNode,\n                true,\n                validity,\n                null,\n                false,\n                null,\n                null,\n                null,\n                false,\n                false,\n                0,\n                null,\n                new ArrayList<>());\n        return entry;\n    }\n\n    public static Optional<DataStoreEntry> fromDirectory(Path dir) throws IOException {\n        ObjectMapper mapper = JacksonMapper.getDefault();\n\n        var entryFile = dir.resolve(\"entry.json\");\n        var storeFile = dir.resolve(\"store.json\");\n        var stateFile = dir.resolve(\"state.json\");\n        var normalNotesFile = dir.resolve(\"notes.md\");\n        var encryptedNotesFile = dir.resolve(\"notes.json\");\n        if (!Files.exists(entryFile) || !Files.exists(storeFile)) {\n            return Optional.empty();\n        }\n\n        if (!Files.exists(stateFile)) {\n            stateFile = entryFile;\n        }\n\n        var json = mapper.readTree(entryFile.toFile());\n        var stateJson = mapper.readTree(stateFile.toFile());\n        var uuid = UUID.fromString(json.required(\"uuid\").textValue());\n        var categoryUuid = Optional.ofNullable(json.get(\"categoryUuid\"))\n                .map(jsonNode -> UUID.fromString(jsonNode.textValue()))\n                .orElse(DataStorage.DEFAULT_CATEGORY_UUID);\n        var breakOutCategory = Optional.ofNullable(json.get(\"breakOutCategoryUuid\"))\n                .filter(jsonNode -> !jsonNode.isNull())\n                .map(jsonNode -> UUID.fromString(jsonNode.asText()))\n                .orElse(null);\n        var name = json.required(\"name\").textValue().strip();\n\n        // Fix for legacy issue where entries could have empty names\n        if (name.isBlank()) {\n            return Optional.empty();\n        }\n\n        var color = Optional.ofNullable(json.get(\"color\"))\n                .map(node -> {\n                    try {\n                        return mapper.treeToValue(node, DataStoreColor.class);\n                    } catch (JsonProcessingException e) {\n                        return null;\n                    }\n                })\n                .orElse(null);\n        var freeze = Optional.ofNullable(json.get(\"freeze\"))\n                .map(jsonNode -> jsonNode.booleanValue())\n                .orElse(false);\n        var pinToTop = Optional.ofNullable(json.get(\"pinToTop\"))\n                .map(jsonNode -> jsonNode.booleanValue())\n                .orElse(false);\n        var tags = Optional.ofNullable(json.get(\"tags\"))\n                .map(jsonNode -> {\n                    List<String> l = new ArrayList<>();\n                    for (JsonNode node : jsonNode) {\n                        var tag = node.asText();\n                        if (!tag.isBlank()) {\n                            l.add(tag);\n                        }\n                    }\n                    return l;\n                })\n                .orElse(new ArrayList<>());\n\n        var iconNode = json.get(\"icon\");\n        String icon = iconNode != null && !iconNode.isNull() ? iconNode.asText() : null;\n\n        // Legacy compat for old icons\n        if (icon != null && !icon.contains(\"/\")) {\n            icon = \"selfhst/\" + icon;\n        }\n\n        var persistentState = stateJson.get(\"persistentState\");\n        var orderIndex = Optional.ofNullable(json.get(\"orderIndex\"))\n                .map(jsonNode -> jsonNode.intValue())\n                .orElse(0);\n        var lastUsed = Optional.ofNullable(stateJson.get(\"lastUsed\"))\n                .map(jsonNode -> jsonNode.textValue())\n                .map(Instant::parse)\n                .orElse(Instant.EPOCH);\n        var lastModified = Optional.ofNullable(stateJson.get(\"lastModified\"))\n                .map(jsonNode -> jsonNode.textValue())\n                .map(Instant::parse)\n                .orElse(Instant.EPOCH);\n        var expanded = Optional.ofNullable(stateJson.get(\"expanded\"))\n                .map(jsonNode -> jsonNode.booleanValue())\n                .orElse(true);\n\n        if (color == null) {\n            color = Optional.ofNullable(stateJson.get(\"color\"))\n                    .map(node -> {\n                        try {\n                            return mapper.treeToValue(node, DataStoreColor.class);\n                        } catch (JsonProcessingException e) {\n                            return null;\n                        }\n                    })\n                    .orElse(null);\n        }\n\n        String notes = null;\n        if (Files.exists(normalNotesFile)) {\n            notes = Files.readString(normalNotesFile);\n        }\n        if (Files.exists(encryptedNotesFile)) {\n            var node = DataStorageNode.readPossiblyEncryptedNode(mapper.readTree(encryptedNotesFile.toFile()));\n            var mdNode = node.getContentNode().get(\"markdown\");\n            notes = mdNode != null ? mdNode.asText() : null;\n        }\n        if (notes != null && notes.isBlank()) {\n            notes = null;\n        }\n\n        DataStorageNode node;\n        try {\n            var fileNode = mapper.readTree(storeFile.toFile());\n            node = DataStorageNode.readPossiblyEncryptedNode(fileNode);\n        } catch (JacksonException ex) {\n            ErrorEventFactory.fromThrowable(ex).omit().expected().handle();\n            node = DataStorageNode.fail();\n        }\n\n        var store = node.parseStore();\n        return Optional.of(new DataStoreEntry(\n                dir,\n                uuid,\n                categoryUuid,\n                name,\n                lastUsed,\n                lastModified,\n                store,\n                node,\n                false,\n                store == null ? Validity.LOAD_FAILED : Validity.INCOMPLETE,\n                persistentState,\n                expanded,\n                color,\n                notes,\n                icon,\n                freeze,\n                pinToTop,\n                orderIndex,\n                breakOutCategory,\n                tags));\n    }\n\n    public String getEffectiveIconFile() {\n        if (getValidity() == Validity.LOAD_FAILED) {\n            return \"error.png\";\n        }\n\n        if (icon == null) {\n            return getProvider().getDisplayIconFileName(getStore());\n        }\n\n        var found = SystemIconManager.getIcon(icon);\n        if (found.isPresent()) {\n            return SystemIconManager.getAndLoadIconFile(found.get());\n        } else {\n            return \"error.png\";\n        }\n    }\n\n    public void setColor(DataStoreColor newColor) {\n        var changed = !Objects.equals(color, newColor);\n        this.color = newColor;\n        if (changed) {\n            notifyUpdate(false, true);\n        }\n    }\n\n    @Override\n    public int hashCode() {\n        return getUuid().hashCode();\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        return o == this || (o instanceof DataStoreEntry e && e.getUuid().equals(getUuid()));\n    }\n\n    @Override\n    public String toString() {\n        return getName();\n    }\n\n    public boolean isChangedForReload(DataStoreEntry other) {\n        return !Objects.equals(getStore(), other.getStore())\n                || !Objects.equals(getName(), other.getName())\n                || !Objects.equals(getNotes(), other.getNotes())\n                || !Objects.equals(getColor(), other.getColor())\n                || !Objects.equals(getCategoryUuid(), other.getCategoryUuid())\n                || !Objects.equals(getOrderIndex(), other.getOrderIndex())\n                || !Objects.equals(getEffectiveIconFile(), other.getEffectiveIconFile());\n    }\n\n    public boolean isPerUserStore() {\n        var perUser = false;\n        try {\n            perUser = store instanceof UserScopeStore s && s.isPerUser();\n        } catch (Exception ignored) {\n        }\n        return perUser;\n    }\n\n    public void incrementBusyCounter() {\n        var r = busyCounter.incrementAndGet() == 1;\n        if (r) {\n            notifyUpdate(false, false);\n        }\n    }\n\n    public boolean decrementBusyCounter() {\n        var r = busyCounter.decrementAndGet() == 0;\n        if (r) {\n            notifyUpdate(false, false);\n        }\n        return r;\n    }\n\n    public <T extends DataStore> DataStoreEntryRef<T> ref() {\n        return new DataStoreEntryRef<>(this);\n    }\n\n    public void setStoreCache(String key, Object value) {\n        if (!Objects.equals(storeCache.put(key, value), value)) {\n            notifyUpdate(false, false);\n        }\n    }\n\n    @SneakyThrows\n    @SuppressWarnings(\"unchecked\")\n    public <T extends DataStoreState> T getStorePersistentState() {\n        if (!(store instanceof StatefulDataStore<?> sds)) {\n            return null;\n        }\n\n        if (storePersistentStateNode != null && storePersistentStateNode.isNull()) {\n            storePersistentStateNode = null;\n        }\n\n        if (storePersistentStateNode == null && storePersistentState == null) {\n            storePersistentState = sds.createDefaultState();\n            storePersistentStateNode = JacksonMapper.getDefault().valueToTree(storePersistentState);\n        } else if (storePersistentState == null) {\n            storePersistentState =\n                    JacksonMapper.getDefault().treeToValue(storePersistentStateNode, sds.getStateClass());\n            if (storePersistentState == null) {\n                storePersistentState = sds.createDefaultState();\n                storePersistentStateNode = JacksonMapper.getDefault().valueToTree(storePersistentState);\n            }\n        }\n        return (T) storePersistentState;\n    }\n\n    public void setStorePersistentState(DataStoreState value) {\n        var changed = !Objects.equals(storePersistentState, value);\n        this.storePersistentState = value;\n        this.storePersistentStateNode = JacksonMapper.getDefault().valueToTree(value);\n        if (changed) {\n            notifyUpdate(false, true);\n        }\n    }\n\n    public void setIcon(String icon, boolean force) {\n        if (this.icon != null && !force) {\n            return;\n        }\n\n        var changed = !Objects.equals(this.icon, icon);\n        this.icon = icon;\n        if (changed) {\n            notifyUpdate(false, true);\n        }\n    }\n\n    public void setBreakOutCategory(DataStoreCategory category) {\n        var changed = !Objects.equals(breakOutCategory, category != null ? category.getUuid() : null);\n        this.breakOutCategory = category != null ? category.getUuid() : null;\n        if (changed) {\n            notifyUpdate(false, true);\n        }\n    }\n\n    public void setOrderIndex(int orderIndex) {\n        var changed = this.orderIndex != orderIndex;\n        this.orderIndex = orderIndex;\n        if (changed) {\n            notifyUpdate(false, true);\n        }\n    }\n\n    public void addTag(String tag) {\n        if (tags == null || tag.isBlank()) {\n            return;\n        }\n\n        tag = tag.strip();\n\n        if (tags.contains(tag)) {\n            return;\n        }\n\n        tags.add(tag);\n        notifyUpdate(false, true);\n    }\n\n    public void removeTag(String tag) {\n        if (tags == null || tag.isBlank()) {\n            return;\n        }\n\n        tag = tag.strip();\n\n        if (tags.remove(tag)) {\n            notifyUpdate(false, true);\n        }\n    }\n\n    public void setCategoryUuid(UUID categoryUuid) {\n        var changed = !Objects.equals(this.categoryUuid, categoryUuid);\n        this.categoryUuid = categoryUuid;\n        if (changed) {\n            notifyUpdate(false, true);\n        }\n    }\n\n    @Override\n    public boolean isInStorage() {\n        return DataStorage.get().getStoreEntries().contains(this);\n    }\n\n    @Override\n    public Path[] getShareableFiles() {\n        var notes = directory.resolve(\"notes.md\");\n        var list = List.of(directory.resolve(\"store.json\"), directory.resolve(\"entry.json\"));\n        return Stream.concat(list.stream(), Files.exists(notes) ? Stream.of(notes) : Stream.of())\n                .toArray(Path[]::new);\n    }\n\n    public void writeDataToDisk() throws Exception {\n        if (!dirty) {\n            return;\n        }\n\n        // Reset the dirty state early\n        // That way, if any other changes are made during this save operation,\n        // the dirty bit can be set to true again\n        dirty = false;\n\n        ObjectMapper mapper = JacksonMapper.getDefault();\n\n        ObjectNode obj = JsonNodeFactory.instance.objectNode();\n        obj.put(\"uuid\", uuid.toString());\n        obj.put(\"name\", name);\n        obj.put(\"categoryUuid\", categoryUuid.toString());\n        obj.put(\"breakOutCategoryUuid\", breakOutCategory != null ? breakOutCategory.toString() : null);\n        obj.set(\"color\", mapper.valueToTree(color));\n        obj.set(\"icon\", mapper.valueToTree(icon));\n        obj.put(\"freeze\", freeze);\n        obj.put(\"pinToTop\", pinToTop);\n        obj.put(\"orderIndex\", orderIndex);\n\n        var tagsArray = obj.putArray(\"tags\");\n        for (String tag : tags) {\n            tagsArray.add(tag);\n        }\n\n        ObjectNode stateObj = JsonNodeFactory.instance.objectNode();\n        stateObj.put(\"lastUsed\", lastUsed.toString());\n        stateObj.put(\"lastModified\", lastModified.toString());\n        stateObj.set(\"persistentState\", storePersistentStateNode);\n        stateObj.put(\"expanded\", expanded);\n\n        var entryString = mapper.writeValueAsString(obj);\n        var stateString = mapper.writeValueAsString(stateObj);\n        var storeString = mapper.writeValueAsString(DataStorageNode.encryptNodeIfNeeded(storeNode));\n\n        FileUtils.forceMkdir(directory.toFile());\n        Files.writeString(directory.resolve(\"state.json\"), stateString);\n        Files.writeString(directory.resolve(\"entry.json\"), entryString);\n        Files.writeString(directory.resolve(\"store.json\"), storeString);\n\n        var encryptNotes = storeNode.isEncrypted();\n        var normalNotesFile = directory.resolve(\"notes.md\");\n        var encryptedNotesFile = directory.resolve(\"notes.json\");\n        if (Files.exists(normalNotesFile) && (notes == null || encryptNotes)) {\n            Files.delete(normalNotesFile);\n        }\n        if (Files.exists(encryptedNotesFile) && (notes == null || !encryptNotes)) {\n            Files.delete(encryptedNotesFile);\n        }\n        if (notes != null && encryptNotes) {\n            var notesNode = JsonNodeFactory.instance.objectNode();\n            notesNode.put(\"markdown\", notes);\n            var storageNode = DataStorageNode.encryptNodeIfNeeded(new DataStorageNode(\n                    notesNode, storeNode.isPerUser(), storeNode.isReadableForUser(), storeNode.isEncrypted()));\n            var string = mapper.writeValueAsString(storageNode);\n            Files.writeString(encryptedNotesFile, string);\n        } else if (notes != null) {\n            Files.writeString(normalNotesFile, notes);\n        }\n        lastWrittenNotes = notes;\n    }\n\n    public void setNotes(String newNotes) {\n        var changed = !Objects.equals(notes, newNotes);\n        this.notes = newNotes;\n        if (changed) {\n            notifyUpdate(false, true);\n        }\n    }\n\n    public void setFreeze(boolean newValue) {\n        var changed = freeze != newValue;\n        this.freeze = newValue;\n        if (changed) {\n            notifyUpdate(false, true);\n        }\n    }\n\n    public void setPinToTop(boolean newValue) {\n        var changed = pinToTop != newValue;\n        this.pinToTop = newValue;\n        if (changed) {\n            notifyUpdate(false, false);\n            dirty = true;\n        }\n    }\n\n    public boolean isDisabled() {\n        return validity == Validity.LOAD_FAILED;\n    }\n\n    public void applyChanges(DataStoreEntry e) {\n        name = e.getName();\n        storeNode = e.storeNode;\n        store = e.store;\n        validity = e.validity;\n        provider = e.provider;\n        childrenCache = null;\n        storeCache.clear();\n        storeCache.putAll(e.storeCache);\n        validity = store == null ? Validity.LOAD_FAILED : store.isComplete() ? Validity.COMPLETE : Validity.INCOMPLETE;\n        storePersistentState = e.storePersistentState;\n        storePersistentStateNode = e.storePersistentStateNode;\n        icon = e.icon;\n        categoryUuid = e.categoryUuid;\n        notifyUpdate(false, true);\n    }\n\n    void setStoreInternal(DataStore store, boolean updateTime) {\n        var changed = !Objects.equals(this.store, store);\n        if (!changed) {\n            return;\n        }\n\n        if (!storeNode.hasAccess()) {\n            return;\n        }\n\n        this.store = store;\n        this.storeNode = DataStorageNode.ofNewStore(store);\n        this.provider = DataStoreProviders.byStore(store);\n        this.validity = provider != null\n                ? (store.isComplete() ? Validity.COMPLETE : Validity.INCOMPLETE)\n                : Validity.LOAD_FAILED;\n        if (updateTime) {\n            lastModified = Instant.now();\n        }\n        childrenCache = null;\n        dirty = true;\n        notifyUpdate(false, updateTime);\n    }\n\n    public void reassignStoreNode() {\n        if (!storeNode.hasAccess()) {\n            return;\n        }\n\n        DataStorageNode newNode = DataStorageNode.ofNewStore(store);\n        var changed = !Objects.equals(this.storeNode.getContentNode(), newNode.getContentNode());\n        if (changed) {\n            this.storeNode = newNode;\n            dirty = true;\n        }\n    }\n\n    public void validate() {\n        try {\n            validateOrThrow();\n        } catch (Throwable ex) {\n            ErrorEventFactory.fromThrowable(ex).handle();\n        }\n    }\n\n    public void validateOrThrow() throws Throwable {\n        if (store == null) {\n            return;\n        }\n\n        if (!(store instanceof ValidatableStore l)) {\n            return;\n        }\n\n        try {\n            store.checkComplete();\n            incrementBusyCounter();\n            l.validate();\n        } finally {\n            decrementBusyCounter();\n        }\n    }\n\n    public void refreshStore() {\n        if (validity == Validity.LOAD_FAILED) {\n            return;\n        }\n\n        DataStore newStore;\n        try {\n            newStore = storeNode.parseStore();\n            // Check whether we have a provider as well\n            DataStoreProviders.byStore(newStore);\n        } catch (Throwable e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n            newStore = null;\n        }\n\n        if (newStore == null) {\n            var changed = store != null;\n            store = null;\n            provider = null;\n            validity = Validity.LOAD_FAILED;\n            if (changed) {\n                notifyUpdate(false, false);\n            }\n            return;\n        }\n\n        try {\n            var newComplete = newStore.isComplete();\n            if (!newComplete) {\n                var changed = !Objects.equals(store, newStore) || validity != Validity.INCOMPLETE;\n                validity = Validity.INCOMPLETE;\n                store = newStore;\n                provider = DataStoreProviders.byStore(store);\n                if (changed) {\n                    notifyUpdate(false, false);\n                }\n                return;\n            }\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).omit().handle();\n            return;\n        }\n\n        var newPerUser = false;\n        try {\n            newPerUser = newStore instanceof UserScopeStore u && u.isPerUser();\n        } catch (Exception ignored) {\n        }\n        var storeChanged = !Objects.equals(store, newStore);\n        if (storeChanged) {\n            store = newStore;\n            provider = DataStoreProviders.byStore(store);\n        }\n        var changed = storeChanged || validity != Validity.COMPLETE || isPerUserStore() != newPerUser;\n        validity = Validity.COMPLETE;\n        if (changed) {\n            notifyUpdate(false, false);\n        }\n    }\n\n    public void finalizeEntry() {\n        if (store instanceof ExpandedLifecycleStore lifecycleStore) {\n            try {\n                incrementBusyCounter();\n                notifyUpdate(false, false);\n                lifecycleStore.finalizeStore();\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).handle();\n            } finally {\n                decrementBusyCounter();\n                notifyUpdate(false, false);\n            }\n        }\n    }\n\n    public boolean finalizeEntryAsync() {\n        if (store instanceof ExpandedLifecycleStore) {\n            ThreadHelper.runAsync(() -> {\n                finalizeEntry();\n            });\n            return true;\n        } else {\n            return false;\n        }\n    }\n\n    public boolean shouldSave() {\n        return getStore() != null;\n    }\n\n    @Getter\n    public enum Validity {\n        @JsonProperty(\"loadFailed\")\n        LOAD_FAILED(false),\n        @JsonProperty(\"incomplete\")\n        INCOMPLETE(false),\n        @JsonProperty(\"complete\")\n        COMPLETE(true);\n\n        private final boolean isUsable;\n\n        Validity(boolean isUsable) {\n            this.isUsable = isUsable;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/storage/DataStoreEntryRef.java",
    "content": "package io.xpipe.app.storage;\n\nimport io.xpipe.app.ext.DataStore;\n\nimport lombok.NonNull;\n\nimport java.util.Objects;\n\npublic class DataStoreEntryRef<T extends DataStore> {\n\n    @NonNull\n    private final DataStoreEntry entry;\n\n    public DataStoreEntryRef(@NonNull DataStoreEntry entry) {\n        this.entry = entry;\n    }\n\n    @Override\n    public int hashCode() {\n        return entry.getUuid().hashCode();\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (!(o instanceof DataStoreEntryRef<?> that)) {\n            return false;\n        }\n        return Objects.equals(entry.getUuid(), that.entry.getUuid());\n    }\n\n    @Override\n    public String toString() {\n        return entry.getUuid().toString() + \" / \" + entry.getName();\n    }\n\n    public void checkComplete() throws Throwable {\n        var store = getStore();\n        if (store != null) {\n            getStore().checkComplete();\n        }\n    }\n\n    public DataStoreEntry get() {\n        return entry;\n    }\n\n    public T getStore() {\n        return entry.getStore() != null ? entry.getStore().asNeeded() : null;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public <S extends DataStore> DataStoreEntryRef<S> asNeeded() {\n        return (DataStoreEntryRef<S>) this;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/storage/ImpersistentStorage.java",
    "content": "package io.xpipe.app.storage;\n\nimport io.xpipe.app.ext.LocalStore;\nimport io.xpipe.app.secret.EncryptionKey;\n\nimport java.time.Instant;\nimport javax.crypto.SecretKey;\n\npublic class ImpersistentStorage extends DataStorage {\n\n    @Override\n    public void pullManually() {}\n\n    @Override\n    public void pushManually() {}\n\n    @Override\n    public void reloadContent() {}\n\n    @Override\n    public SecretKey getVaultKey() {\n        return EncryptionKey.getVaultSecretKey(\"\");\n    }\n\n    @Override\n    public void load() {\n        {\n            var cat = DataStoreCategory.createNew(null, ALL_CONNECTIONS_CATEGORY_UUID, \"All connections\");\n            storeCategories.add(cat);\n        }\n        {\n            var cat = DataStoreCategory.createNew(null, ALL_SCRIPTS_CATEGORY_UUID, \"All scripts\");\n            storeCategories.add(cat);\n        }\n        {\n            var cat = DataStoreCategory.createNew(null, ALL_IDENTITIES_CATEGORY_UUID, \"All identities\");\n            storeCategories.add(cat);\n        }\n        {\n            var cat = DataStoreCategory.createNew(null, ALL_MACROS_CATEGORY_UUID, \"All macros\");\n            storeCategories.add(cat);\n        }\n        {\n            var cat = new DataStoreCategory(\n                    null,\n                    DEFAULT_CATEGORY_UUID,\n                    \"Default\",\n                    Instant.now(),\n                    Instant.now(),\n                    true,\n                    ALL_CONNECTIONS_CATEGORY_UUID,\n                    true,\n                    DataStoreCategoryConfig.empty());\n            storeCategories.add(cat);\n            selectedCategory = getStoreCategoryIfPresent(DEFAULT_CATEGORY_UUID).orElseThrow();\n        }\n\n        var e = DataStoreEntry.createNew(\n                LOCAL_ID, DataStorage.DEFAULT_CATEGORY_UUID, \"Local Machine\", new LocalStore());\n        storeEntries.put(e, e);\n        e.validate();\n\n        entriesAvailable = true;\n    }\n\n    @Override\n    public void saveAsync() {}\n\n    @Override\n    public synchronized void save(boolean dispose) {}\n\n    @Override\n    public boolean supportsSync() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/storage/StandardStorage.java",
    "content": "package io.xpipe.app.storage;\n\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.core.window.AppMainWindow;\nimport io.xpipe.app.ext.DataStorageExtensionProvider;\nimport io.xpipe.app.ext.LocalStore;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.secret.EncryptionKey;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.app.util.GlobalTimer;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.OsType;\n\nimport lombok.Getter;\nimport org.apache.commons.io.FileUtils;\n\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Duration;\nimport java.util.*;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.concurrent.locks.ReentrantLock;\nimport javax.crypto.SecretKey;\n\npublic class StandardStorage extends DataStorage {\n\n    private final Set<Path> directoriesToKeep = new HashSet<>();\n\n    @Getter\n    private final DataStorageSyncHandler dataStorageSyncHandler;\n\n    @Getter\n    private final DataStorageUserHandler dataStorageUserHandler;\n\n    private final ReentrantLock busyIo = new ReentrantLock();\n    private SecretKey vaultKey;\n\n    @Getter\n    private boolean disposed;\n\n    private boolean saveQueued;\n\n    StandardStorage() {\n        this.dataStorageSyncHandler = DataStorageSyncHandler.getInstance();\n        this.dataStorageUserHandler = DataStorageUserHandler.getInstance();\n    }\n\n    public void pullManually() {\n        if (!busyIo.tryLock()) {\n            return;\n        }\n        dataStorageSyncHandler.pullManually();\n        busyIo.unlock();\n    }\n\n    @Override\n    public void pushManually() {\n        if (!busyIo.tryLock()) {\n            return;\n        }\n        dataStorageSyncHandler.pushManually();\n        busyIo.unlock();\n    }\n\n    private void startSyncWatcher() {\n        GlobalTimer.scheduleUntil(Duration.ofSeconds(20), false, () -> {\n            ThreadHelper.runAsync(() -> {\n                if (!busyIo.tryLock()) {\n                    return;\n                }\n                dataStorageSyncHandler.refreshRemoteData();\n                busyIo.unlock();\n            });\n            return false;\n        });\n    }\n\n    public void reloadContent() {\n        if (AppOperationMode.isInShutdown()) {\n            return;\n        }\n\n        busyIo.lock();\n\n        var initialLoad = getStoreEntries().size() == 0;\n        var storesDir = getStoresDir();\n        var categoriesDir = getCategoriesDir();\n        var dataDir = getDataDir();\n        var iconsDir = getIconsDir();\n\n        try {\n            FileUtils.forceMkdir(storesDir.toFile());\n            FileUtils.forceMkdir(categoriesDir.toFile());\n            FileUtils.forceMkdir(dataDir.toFile());\n            FileUtils.forceMkdir(iconsDir.toFile());\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(\"Unable to create vault directory\", e)\n                    .terminal(true)\n                    .build()\n                    .handle();\n        }\n\n        for (DataStoreCategory cat : new ArrayList<>(storeCategories)) {\n            if (Arrays.stream(cat.getShareableFiles()).noneMatch(Files::exists)) {\n                deleteStoreCategory(cat, false, false);\n            }\n        }\n\n        var laterAddedEntries = new HashSet<DataStoreEntry>();\n        try {\n            var exception = new AtomicReference<Exception>();\n            try (var cats = Files.list(categoriesDir)) {\n                cats.filter(Files::isDirectory).forEach(path -> {\n                    try {\n                        var c = DataStoreCategory.fromDirectory(path);\n                        if (c.isEmpty()) {\n                            return;\n                        }\n\n                        if (initialLoad) {\n                            storeCategories.add(c.get());\n                            return;\n                        }\n\n                        var existing = getStoreCategoryIfPresent(c.get().getUuid());\n                        if (existing.isPresent()) {\n                            if (existing.get().isChangedForReload(c.get())) {\n                                updateCategory(existing.get(), c.get());\n                            }\n                            return;\n                        }\n\n                        addStoreCategory(c.get());\n                    } // IO exceptions are not expected\n                    catch (Exception ex) {\n                        // Data corruption and schema changes are expected\n                        ErrorEventFactory.fromThrowable(ex)\n                                .expected()\n                                .omit()\n                                .build()\n                                .handle();\n                    }\n                });\n            }\n\n            // Show one exception\n            if (exception.get() != null) {\n                ErrorEventFactory.fromThrowable(exception.get()).handle();\n            }\n\n            setupBuiltinCategories();\n            selectedCategory = getStoreCategoryIfPresent(DEFAULT_CATEGORY_UUID).orElseThrow();\n\n            for (DataStoreEntry entry : new ArrayList<>(getStoreEntries())) {\n                if (Arrays.stream(entry.getShareableFiles()).noneMatch(Files::exists)) {\n                    deleteStoreEntry(entry);\n                }\n            }\n\n            try (var dirs = Files.list(storesDir)) {\n                dirs.filter(Files::isDirectory).forEach(path -> {\n                    try {\n                        var entry = DataStoreEntry.fromDirectory(path);\n                        if (entry.isEmpty()) {\n                            return;\n                        }\n\n                        if (initialLoad) {\n                            var foundCat = getStoreCategoryIfPresent(entry.get().getCategoryUuid());\n                            if (foundCat.isEmpty()) {\n                                entry.get().setCategoryUuid(null);\n                            }\n\n                            storeEntries.put(entry.get(), entry.get());\n                            return;\n                        }\n\n                        var existing = getStoreEntryIfPresent(entry.get().getUuid());\n                        if (existing.isPresent()) {\n                            if (existing.get().isChangedForReload(entry.get())) {\n                                updateEntry(existing.get(), entry.get());\n                            }\n                            return;\n                        }\n\n                        laterAddedEntries.add(entry.get());\n                        storeEntries.put(entry.get(), entry.get());\n                    } // IO exceptions are not expected\n                    catch (Exception ex) {\n                        // Data corruption and schema changes are expected\n\n                        // We only keep invalid entries in developer mode as there's no point in keeping them in\n                        // production.\n                        if (AppProperties.get().isDevelopmentEnvironment()) {\n                            directoriesToKeep.add(path);\n                        }\n\n                        ErrorEventFactory.fromThrowable(ex)\n                                .expected()\n                                .omit()\n                                .build()\n                                .handle();\n                    }\n                });\n\n                // Show one exception\n                if (exception.get() != null) {\n                    ErrorEventFactory.fromThrowable(exception.get()).expected().handle();\n                }\n\n                storeEntriesSet.forEach(e -> {\n                    if (e.getCategoryUuid() == null\n                            || getStoreCategoryIfPresent(e.getCategoryUuid()).isEmpty()) {\n                        e.setCategoryUuid(DEFAULT_CATEGORY_UUID);\n                    }\n\n                    if (e.getCategoryUuid() != null && e.getCategoryUuid().equals(ALL_CONNECTIONS_CATEGORY_UUID)) {\n                        e.setCategoryUuid(DEFAULT_CATEGORY_UUID);\n                    }\n                });\n            }\n        } catch (IOException ex) {\n            ErrorEventFactory.fromThrowable(ex).terminal(true).build().handle();\n        }\n\n        var hasFixedLocal = storeEntriesSet.stream()\n                .anyMatch(dataStoreEntry -> dataStoreEntry.getUuid().equals(LOCAL_ID));\n        if (hasFixedLocal) {\n            var local = getStoreEntry(LOCAL_ID);\n            if (local.getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {\n                try {\n                    storeEntries.remove(local);\n                    local.deleteFromDisk();\n                    hasFixedLocal = false;\n                } catch (IOException ex) {\n                    ErrorEventFactory.fromThrowable(ex)\n                            .terminal(true)\n                            .expected()\n                            .build()\n                            .handle();\n                }\n            }\n        }\n\n        if (!hasFixedLocal) {\n            var e = DataStoreEntry.createNew(\n                    LOCAL_ID, DataStorage.DEFAULT_CATEGORY_UUID, \"Local Machine\", new LocalStore());\n            e.setDirectory(getStoresDir().resolve(LOCAL_ID.toString()));\n            storeEntries.put(e, e);\n            e.validate();\n        }\n\n        var local = DataStorage.get().getStoreEntry(LOCAL_ID);\n        if (storeEntriesSet.stream().noneMatch(entry -> entry.getColor() != null)) {\n            local.setColor(DataStoreColor.BLUE);\n        }\n\n        // Reload stores, this time with all entry refs present\n        // These do however not have a completed validity yet\n        refreshEntries();\n        // Bring entries into completed validity if possible\n        // Needed for chained stores\n        refreshEntries();\n        if (initialLoad) {\n            // Let providers work on complete stores\n            callProviders();\n        }\n        // Update validities after any possible changes\n        refreshEntries();\n        // Add any possible missing synthetic parents\n        storeEntriesSet.forEach(entry -> {\n            var syntheticParent = getSyntheticParent(entry);\n            syntheticParent.ifPresent(entry1 -> {\n                addStoreEntryIfNotPresent(entry1);\n            });\n        });\n        entriesAvailable = true;\n        // Update validities from synthetic parent changes and entries available flag changes\n        refreshEntries();\n        // Remove user inaccessible entries only when everything is valid, so we can check the parent hierarchies\n        filterPerUserEntries(storeEntries.keySet());\n\n        // Only add new stores if really necessary\n        laterAddedEntries.stream()\n                .filter(dataStoreEntry -> storeEntries.containsKey(dataStoreEntry))\n                .forEach(e -> {\n                    storeEntries.remove(e);\n                    addStoreEntryIfNotPresent(e);\n                });\n\n        deleteLeftovers();\n\n        this.dataStorageSyncHandler.afterStorageLoad();\n\n        busyIo.unlock();\n    }\n\n    @Override\n    public SecretKey getVaultKey() {\n        return vaultKey;\n    }\n\n    public void load() {\n        if (!busyIo.tryLock()) {\n            return;\n        }\n\n        try {\n            FileUtils.forceMkdir(dir.toFile());\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(\"Unable to create vault directory\", e)\n                    .terminal(true)\n                    .build()\n                    .handle();\n        }\n\n        try {\n            initSystemInfo();\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(\"Unable to load vault system info\", e)\n                    .build()\n                    .handle();\n        }\n\n        initVaultKey();\n\n        try {\n            dataStorageUserHandler.init();\n        } catch (IOException e) {\n            ErrorEventFactory.fromThrowable(\"Unable to load vault users\", e)\n                    .terminal(true)\n                    .build()\n                    .handle();\n        }\n\n        if (dataStorageUserHandler.getUserCount() > 0) {\n            AppMainWindow.loadingText(\"unlockingVault\");\n        }\n\n        dataStorageUserHandler.login();\n\n        reloadContent();\n\n        busyIo.unlock();\n\n        startSyncWatcher();\n\n        // Full save on initial load\n        saveAsync();\n    }\n\n    public void saveAsync() {\n        // If we are already loading or saving, don't queue up another operation.\n        // This could otherwise lead to thread starvation with virtual threads\n\n        // Technically the load and save operations also return instantly if locked, but let's not even create new\n        // threads here\n\n        // Technically we would have to synchronize the saveQueued update to avoid a rare lost update\n        // but in practice it doesn't really matter as the save queueing is optional\n        // The last dispose save will save everything anyway, it's about optimizing before that\n        if (busyIo.isLocked()) {\n            saveQueued = true;\n            return;\n        }\n\n        ThreadHelper.runAsync(() -> {\n            save(false);\n        });\n    }\n\n    public void save(boolean dispose) {\n        try {\n            // If another save operation is in progress, we have to wait on dispose\n            // Otherwise the application may quit and kill the daemon thread that is performing the other save operation\n            if (dispose && !busyIo.tryLock(1, TimeUnit.MINUTES)) {\n                disposed = true;\n                return;\n            }\n        } catch (InterruptedException e) {\n            return;\n        }\n\n        // We don't need to wait on normal saves though\n        if (!dispose && !busyIo.tryLock()) {\n            saveQueued = true;\n            return;\n        }\n\n        if (disposed) {\n            busyIo.unlock();\n            return;\n        }\n\n        this.saveQueued = false;\n\n        this.dataStorageSyncHandler.beforeStorageSave();\n\n        try {\n            FileUtils.forceMkdir(getStoresDir().toFile());\n            FileUtils.forceMkdir(getCategoriesDir().toFile());\n            FileUtils.forceMkdir(getDataDir().toFile());\n            FileUtils.forceMkdir(getIconsDir().toFile());\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e)\n                    .description(\"Unable to create storage directory \" + getStoresDir())\n                    .terminal(true)\n                    .build()\n                    .handle();\n        }\n\n        var exception = new AtomicReference<Exception>();\n\n        storeCategories.forEach(e -> {\n            try {\n                var exists = Files.exists(e.getDirectory());\n                var dirty = e.isDirty();\n                e.writeDataToDisk();\n                dataStorageSyncHandler.handleCategory(e, exists, dirty);\n            } catch (IOException ex) {\n                // IO exceptions are not expected\n                exception.set(ex);\n            } catch (Exception ex) {\n                // Data corruption and schema changes are expected\n                ErrorEventFactory.fromThrowable(ex).expected().omit().build().handle();\n            }\n        });\n\n        storeEntriesSet.stream()\n                .filter(dataStoreEntry -> dataStoreEntry.shouldSave())\n                .forEach(e -> {\n                    try {\n                        var exists = Files.exists(e.getDirectory());\n                        var dirty = e.isDirty();\n                        e.writeDataToDisk();\n                        dataStorageSyncHandler.handleEntry(e, exists, dirty);\n                    } catch (Exception ex) {\n                        // Data corruption and schema changes are expected\n                        exception.set(ex);\n                        ErrorEventFactory.fromThrowable(ex)\n                                .expected()\n                                .omit()\n                                .build()\n                                .handle();\n                    }\n                });\n\n        // Show one exception\n        if (exception.get() != null) {\n            ErrorEventFactory.fromThrowable(exception.get()).expected().handle();\n        }\n\n        deleteLeftovers();\n        dataStorageUserHandler.save();\n        dataStorageSyncHandler.afterStorageSave(true, dispose);\n        if (dispose) {\n            disposed = true;\n        }\n\n        busyIo.unlock();\n        if (!dispose && saveQueued) {\n            // Avoid stack overflow by doing it async\n            saveAsync();\n        }\n    }\n\n    @Override\n    public boolean supportsSync() {\n        return dataStorageSyncHandler.supportsSync();\n    }\n\n    private void filterPerUserEntries(Collection<DataStoreEntry> entries) {\n        var toRemove = getStoreEntries().stream()\n                .filter(dataStoreEntry -> shouldRemoveOtherUserEntry(dataStoreEntry))\n                .toList();\n        directoriesToKeep.addAll(toRemove.stream()\n                .map(dataStoreEntry -> dataStoreEntry.getDirectory())\n                .toList());\n        toRemove.forEach(entries::remove);\n    }\n\n    private boolean shouldRemoveOtherUserEntry(DataStoreEntry entry) {\n        var current = entry;\n        while (true) {\n            if (!current.getStoreNode().hasAccess()) {\n                return true;\n            }\n\n            var parent = getDefaultDisplayParent(current);\n            if (parent.isEmpty()) {\n                return false;\n            } else {\n                current = parent.get();\n            }\n        }\n    }\n\n    private void callProviders() {\n        DataStorageExtensionProvider.getAll().forEach(p -> {\n            try {\n                p.storageInit();\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).omit().handle();\n            }\n        });\n    }\n\n    private void deleteLeftovers() {\n        var storesDir = getStoresDir();\n        var categoriesDir = getCategoriesDir();\n\n        // Delete leftover directories in entries dir\n        try (var s = Files.list(storesDir)) {\n            s.forEach(file -> {\n                if (directoriesToKeep.contains(file)) {\n                    return;\n                }\n\n                var name = file.getFileName().toString();\n                try {\n                    UUID uuid;\n                    try {\n                        uuid = UUID.fromString(name);\n                    } catch (Exception ex) {\n                        FileUtils.forceDelete(file.toFile());\n                        return;\n                    }\n\n                    var entry = getStoreEntryIfPresent(uuid);\n                    if (entry.isEmpty()) {\n                        TrackEvent.withTrace(\"Deleting leftover store directory\")\n                                .tag(\"uuid\", uuid)\n                                .handle();\n                        FileUtils.forceDelete(file.toFile());\n                        dataStorageSyncHandler.handleDeletion(file, uuid.toString());\n                    }\n                } catch (Exception ex) {\n                    ErrorEventFactory.fromThrowable(ex)\n                            .expected()\n                            .omit()\n                            .build()\n                            .handle();\n                }\n            });\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).terminal(true).build().handle();\n        }\n\n        // Delete leftover directories in categories dir\n        try (var s = Files.list(categoriesDir)) {\n            s.forEach(file -> {\n                if (directoriesToKeep.contains(file)) {\n                    return;\n                }\n\n                var name = file.getFileName().toString();\n                try {\n                    UUID uuid;\n                    try {\n                        uuid = UUID.fromString(name);\n                    } catch (Exception ex) {\n                        FileUtils.forceDelete(file.toFile());\n                        return;\n                    }\n\n                    var entry = getStoreCategoryIfPresent(uuid);\n                    if (entry.isEmpty()) {\n                        TrackEvent.withTrace(\"Deleting leftover category directory\")\n                                .tag(\"uuid\", uuid)\n                                .handle();\n                        FileUtils.forceDelete(file.toFile());\n                        dataStorageSyncHandler.handleDeletion(file, uuid.toString());\n                    }\n                } catch (Exception ex) {\n                    ErrorEventFactory.fromThrowable(ex)\n                            .expected()\n                            .omit()\n                            .build()\n                            .handle();\n                }\n            });\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).terminal(true).build().handle();\n        }\n    }\n\n    private void initVaultKey() {\n        var file = dir.resolve(\"vaultkey\");\n        try {\n            if (Files.exists(file)) {\n                var s = Files.readString(file);\n                var id = new String(Base64.getDecoder().decode(s), StandardCharsets.UTF_8);\n                vaultKey = EncryptionKey.getVaultSecretKey(id);\n            } else {\n                FileUtils.forceMkdir(dir.toFile());\n                var id = UUID.randomUUID().toString();\n                Files.writeString(file, Base64.getEncoder().encodeToString(id.getBytes(StandardCharsets.UTF_8)));\n                vaultKey = EncryptionKey.getVaultSecretKey(id);\n            }\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(\n                            \"Unable to load vault key file \" + file + \" to decrypt vault contents. Is it corrupted?\", e)\n                    .terminal(true)\n                    .build()\n                    .handle();\n        }\n    }\n\n    private void initSystemInfo() throws IOException {\n        var file = dir.resolve(\"systeminfo\");\n        if (Files.exists(file)) {\n            var read = Files.readString(file);\n            if (!OsType.ofLocal().getName().equals(read)) {\n                ErrorEventFactory.fromMessage(\n                                \"This vault was originally created on a different system running \" + read\n                                        + \". Sharing the same data directory between systems directly will cause some problems.\"\n                                        + \" If you want to properly synchronize connection information across many systems, you can take a look into the git vault synchronization functionality in the settings. It also supports local directory git repositories.\")\n                        .documentationLink(DocumentationLink.SYNC_LOCAL)\n                        .expected()\n                        .handle();\n                var s = OsType.ofLocal().getName();\n                Files.writeString(file, s);\n            }\n        } else {\n            FileUtils.forceMkdir(dir.toFile());\n            var s = OsType.ofLocal().getName();\n            Files.writeString(file, s);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/storage/StorageElement.java",
    "content": "package io.xpipe.app.storage;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\n\nimport lombok.Getter;\nimport lombok.Setter;\nimport lombok.experimental.NonFinal;\nimport org.apache.commons.io.FileUtils;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\n\npublic abstract class StorageElement {\n\n    @Getter\n    protected final UUID uuid;\n\n    protected final List<Listener> listeners = new ArrayList<>();\n\n    @Getter\n    protected boolean dirty;\n\n    @Setter\n    @Getter\n    protected Path directory;\n\n    @Getter\n    protected String name;\n\n    @Getter\n    protected Instant lastUsed;\n\n    @Getter\n    protected Instant lastModified;\n\n    @NonFinal\n    @Getter\n    protected boolean expanded;\n\n    public StorageElement(\n            Path directory,\n            UUID uuid,\n            String name,\n            Instant lastUsed,\n            Instant lastModified,\n            boolean expanded,\n            boolean dirty) {\n        this.directory = directory;\n        this.uuid = uuid;\n        this.name = name;\n        this.lastUsed = lastUsed;\n        this.lastModified = lastModified;\n        this.expanded = expanded;\n        this.dirty = dirty;\n    }\n\n    public Instant getStorageCreationDate() {\n        if (!Files.exists(directory)) {\n            return Instant.now();\n        }\n\n        try {\n            return Files.getLastModifiedTime(directory).toInstant();\n        } catch (IOException e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n            return Instant.now();\n        }\n    }\n\n    public void setExpanded(boolean expanded) {\n        var changed = expanded != this.expanded;\n        if (!changed) {\n            return;\n        }\n\n        this.expanded = expanded;\n\n        // Update state but don't register updated time for expanded change\n        this.dirty = true;\n        synchronized (listeners) {\n            listeners.forEach(l -> l.onUpdate());\n        }\n\n        // Save changes instantly\n        if (isInStorage()) {\n            DataStorage.get().saveAsync();\n        }\n    }\n\n    public abstract boolean isInStorage();\n\n    public abstract Path[] getShareableFiles();\n\n    public void notifyUpdate(boolean used, boolean modified) {\n        if (used) {\n            lastUsed = Instant.now();\n            dirty = true;\n        }\n        if (modified) {\n            lastModified = Instant.now();\n            dirty = true;\n        }\n        synchronized (listeners) {\n            listeners.forEach(l -> l.onUpdate());\n        }\n\n        // Save changes instantly\n        if (modified && isInStorage()) {\n            DataStorage.get().saveAsync();\n        }\n    }\n\n    public void addListener(Listener l) {\n        synchronized (listeners) {\n            this.listeners.add(l);\n        }\n    }\n\n    public final void deleteFromDisk() throws IOException {\n        FileUtils.deleteDirectory(directory.toFile());\n    }\n\n    public abstract void writeDataToDisk() throws Exception;\n\n    public synchronized Instant getLastAccess() {\n        if (getLastUsed() == null) {\n            return getLastModified();\n        }\n\n        return getLastUsed().isAfter(getLastModified()) ? getLastUsed() : getLastModified();\n    }\n\n    public void setName(String name) {\n        if (name.equals(this.name)) {\n            return;\n        }\n\n        if (name.isBlank()) {\n            return;\n        }\n\n        this.name = name;\n        notifyUpdate(false, true);\n    }\n\n    public void setLastModified(Instant lastModified) {\n        if (lastModified.equals(this.lastModified)) {\n            return;\n        }\n\n        notifyUpdate(false, true);\n    }\n\n    public void setLastUsed(Instant lastUsed) {\n        if (lastUsed.equals(this.lastUsed)) {\n            return;\n        }\n\n        notifyUpdate(true, false);\n    }\n\n    public interface Listener {\n        void onUpdate();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/storage/StorageListener.java",
    "content": "package io.xpipe.app.storage;\n\npublic interface StorageListener {\n\n    void onStoreListUpdate();\n\n    void onStoreAdd(DataStoreEntry... entry);\n\n    void onStoreRemove(DataStoreEntry... entry);\n\n    void onCategoryAdd(DataStoreCategory category);\n\n    void onCategoryRemove(DataStoreCategory category);\n\n    void onEntryCategoryChange();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/AlacrittyTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.CommandSupport;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ShellControl;\n\npublic interface AlacrittyTerminalType extends ExternalTerminalType, TrackableTerminalType {\n\n    ExternalTerminalType ALACRITTY_WINDOWS = new Windows();\n    ExternalTerminalType ALACRITTY_LINUX = new Linux();\n    ExternalTerminalType ALACRITTY_MAC_OS = new MacOs();\n\n    @Override\n    default TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.NEW_WINDOW;\n    }\n\n    @Override\n    default String getWebsite() {\n        return \"https://github.com/alacritty/alacritty\";\n    }\n\n    @Override\n    default boolean isRecommended() {\n        return AppPrefs.get().terminalMultiplexer().getValue() != null;\n    }\n\n    @Override\n    default boolean useColoredTitle() {\n        return false;\n    }\n\n    class Windows implements ExternalApplicationType.PathApplication, ExternalTerminalType, AlacrittyTerminalType {\n\n        @Override\n        public TerminalDockMode getDockMode() {\n            return TerminalDockMode.WITH_BORDER;\n        }\n\n        @Override\n        public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n            // Alacritty is bugged and will not accept arguments with spaces even if they are correctly passed/escaped\n            // So this will not work when the script file has spaces\n            var spaces = configuration\n                    .getPanes()\n                    .getFirst()\n                    .getScriptFile()\n                    .toString()\n                    .contains(\" \");\n            var scriptFile = spaces\n                    ? configuration.single().getScriptFile().getFileName()\n                    : configuration.single().getScriptFile().toString();\n            var scriptOpenCommand = configuration.single().getScriptDialect().getOpenScriptCommand(scriptFile);\n            var b = CommandBuilder.of()\n                    .add(\"alacritty\")\n                    .add(\"-t\")\n                    .addQuoted(configuration.getCleanTitle())\n                    .add(\"-e\")\n                    .add(scriptOpenCommand);\n\n            try (ShellControl sc = LocalShell.getShell()) {\n                CommandSupport.isInPathOrThrow(sc, \"alacritty\");\n                var command = sc.command(b);\n                if (spaces) {\n                    command.withWorkingDirectory(\n                            configuration.single().getScriptFile().getParent());\n                }\n                command.execute();\n            }\n        }\n\n        @Override\n        public String getExecutable() {\n            return \"alacritty\";\n        }\n\n        @Override\n        public boolean detach() {\n            return false;\n        }\n\n        @Override\n        public String getId() {\n            return \"app.alacritty\";\n        }\n    }\n\n    class Linux implements ExternalApplicationType.PathApplication, AlacrittyTerminalType {\n\n        @Override\n        public String getExecutable() {\n            return \"alacritty\";\n        }\n\n        @Override\n        public boolean detach() {\n            return true;\n        }\n\n        @Override\n        public String getId() {\n            return \"app.alacritty\";\n        }\n\n        @Override\n        public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n            var b = CommandBuilder.of()\n                    .add(\"-t\")\n                    .addQuoted(configuration.getCleanTitle())\n                    .add(\"-e\")\n                    .addFile(configuration.single().getScriptFile());\n            launch(b);\n        }\n    }\n\n    class MacOs implements ExternalApplicationType.MacApplication, ExternalTerminalType, TrackableTerminalType {\n\n        @Override\n        public TerminalOpenFormat getOpenFormat() {\n            return null;\n        }\n\n        @Override\n        public boolean isRecommended() {\n            return false;\n        }\n\n        @Override\n        public boolean useColoredTitle() {\n            return false;\n        }\n\n        @Override\n        public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n            LocalShell.getShell()\n                    .executeSimpleCommand(CommandBuilder.of()\n                            .add(\"open\", \"-a\")\n                            .addQuoted(\"Alacritty.app\")\n                            .add(\"-n\", \"--args\", \"-t\")\n                            .addQuoted(configuration.getCleanTitle())\n                            .add(\"-e\")\n                            .addFile(configuration.single().getScriptFile()));\n        }\n\n        @Override\n        public String getId() {\n            return \"app.alacritty\";\n        }\n\n        @Override\n        public String getApplicationName() {\n            return \"Alacritty\";\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/ClinkHelper.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.ShellTemp;\nimport io.xpipe.app.util.GithubReleaseDownloader;\nimport io.xpipe.core.FilePath;\n\nimport java.nio.file.FileSystems;\n\npublic class ClinkHelper {\n\n    public static FilePath getTargetDir(ShellControl sc) throws Exception {\n        var targetDir = ShellTemp.createUserSpecificTempDataDirectory(sc, null).join(\"bin\", \"clink\");\n        return targetDir;\n    }\n\n    public static boolean checkIfInstalled(ShellControl sc) throws Exception {\n        if (sc.view().findProgram(\"clink\").isPresent()) {\n            return true;\n        }\n\n        var targetDir = getTargetDir(sc);\n        return sc.view().fileExists(targetDir.join(\"clink_x64.exe\"));\n    }\n\n    public static void install(ShellControl sc) throws Exception {\n        var targetDir = getTargetDir(sc);\n        sc.view().mkdir(targetDir);\n        var temp = GithubReleaseDownloader.getDownloadTempFile(\n                \"chrisant996/clink\", \"clink.zip\", name -> name.endsWith(\".zip\") && !name.endsWith(\"symbols.zip\"));\n        try (var fs = FileSystems.newFileSystem(temp)) {\n            var exeFile = fs.getPath(\"clink_x64.exe\");\n            sc.view().transferLocalFile(exeFile, targetDir.join(\"clink_x64.exe\"));\n\n            var batFile = fs.getPath(\"clink.bat\");\n            sc.view().transferLocalFile(batFile, targetDir.join(\"clink.bat\"));\n\n            var dllFile = fs.getPath(\"clink_dll_x64.dll\");\n            sc.view().transferLocalFile(dllFile, targetDir.join(\"clink_dll_x64.dll\"));\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/CmdTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ShellDialects;\n\npublic class CmdTerminalType\n        implements ExternalApplicationType.PathApplication, ExternalTerminalType, TrackableTerminalType {\n\n    @Override\n    public TerminalDockMode getDockMode() {\n        return TerminalDockMode.WITH_BORDER;\n    }\n\n    @Override\n    public TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.NEW_WINDOW;\n    }\n\n    @Override\n    public boolean isRecommended() {\n        return false;\n    }\n\n    @Override\n    public boolean useColoredTitle() {\n        return false;\n    }\n\n    @Override\n    public boolean supportsEscapes() {\n        return false;\n    }\n\n    @Override\n    public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n        var args = toCommand(configuration);\n        launch(args);\n    }\n\n    @Override\n    public int getProcessHierarchyOffset() {\n        var powershell = ShellDialects.isPowershell(LocalShell.getDialect());\n        return powershell ? 0 : -1;\n    }\n\n    private CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {\n        var pane = configuration.single();\n        if (pane.getScriptDialect() == ShellDialects.CMD) {\n            return CommandBuilder.of().add(\"/c\").addFile(pane.getScriptFile());\n        }\n\n        return CommandBuilder.of().add(\"/c\").add(pane.getDialectLaunchCommand());\n    }\n\n    @Override\n    public String getExecutable() {\n        return \"cmd.exe\";\n    }\n\n    @Override\n    public boolean detach() {\n        return true;\n    }\n\n    @Override\n    public String getId() {\n        return \"app.cmd\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/ConfigFileTerminalPrompt.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.comp.base.IntegratedTextAreaComp;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.ShellDialect;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.app.process.ShellTerminalInitCommand;\nimport io.xpipe.core.FilePath;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.property.SimpleStringProperty;\n\nimport lombok.experimental.SuperBuilder;\n\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.function.Function;\n\n@SuperBuilder\npublic abstract class ConfigFileTerminalPrompt implements TerminalPrompt {\n\n    protected String configuration;\n\n    protected static <T extends ConfigFileTerminalPrompt> OptionsBuilder createOptions(\n            Property<T> p, Function<String, T> creator) {\n        var prop = new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().configuration : null);\n        return new OptionsBuilder()\n                .nameAndDescription(\"terminalPromptConfig\")\n                .addComp(\n                        new IntegratedTextAreaComp(\n                                        prop,\n                                        false,\n                                        p.getValue() != null ? p.getValue().getId() : \"config\",\n                                        new SimpleStringProperty(\n                                                p.getValue() != null\n                                                        ? p.getValue().getConfigFileExtension()\n                                                        : null))\n                                .prefHeight(400),\n                        prop)\n                .bind(\n                        () -> {\n                            return creator.apply(prop.getValue());\n                        },\n                        p);\n    }\n\n    @Override\n    public ShellTerminalInitCommand terminalCommand() throws Exception {\n        return new ShellTerminalInitCommand() {\n            @Override\n            public Optional<String> terminalContent(ShellControl shellControl) throws Exception {\n                if (!installIfNeeded(shellControl)) {\n                    return Optional.empty();\n                }\n\n                FilePath configFile = null;\n                if (configuration != null && !configuration.isBlank()) {\n                    configFile = shellControl\n                            .view()\n                            .writeTextFileDeterministic(getTargetConfigFile(shellControl), configuration);\n                }\n\n                var s = shellControl\n                        .getShellDialect()\n                        .addToPathVariableCommand(\n                                List.of(getBinaryDirectory(shellControl).toString()), false);\n                return Optional.of(s + \"\\n\"\n                        + setupTerminalCommand(shellControl, configFile).toString());\n            }\n\n            @Override\n            public boolean canPotentiallyRunInDialect(ShellDialect dialect) {\n                return getSupportedDialects().contains(dialect);\n            }\n        };\n    }\n\n    protected FilePath getTargetConfigFile(ShellControl shellControl) throws Exception {\n        FilePath configFile = getConfigurationDirectory(shellControl).join(getId() + \".\" + getConfigFileExtension());\n        return configFile;\n    }\n\n    protected abstract String getConfigFileExtension();\n\n    protected abstract ShellScript setupTerminalCommand(ShellControl shellControl, FilePath config) throws Exception;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/ControllableTerminalSession.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.util.Rect;\n\nimport lombok.Getter;\n\n@Getter\npublic abstract class ControllableTerminalSession extends TerminalView.TerminalSession {\n\n    protected Rect lastBounds;\n    protected boolean customBounds;\n\n    protected ControllableTerminalSession(ProcessHandle terminalProcess, ExternalTerminalType terminalType) {\n        super(terminalProcess, terminalType);\n    }\n\n    public abstract void own();\n\n    public abstract void disown();\n\n    public abstract void removeIcon();\n\n    public abstract void restoreIcon();\n\n    public abstract void removeStyle();\n\n    public abstract void restoreStyle();\n\n    public abstract void show();\n\n    public abstract void minimize();\n\n    public abstract void frontOfMainWindow();\n\n    public abstract void backOfMainWindow();\n\n    public abstract void focus();\n\n    public abstract void updatePosition(Rect bounds);\n\n    public abstract void close();\n\n    public abstract boolean isActive();\n\n    public abstract Rect queryBounds();\n\n    public void updateBoundsState() {\n        if (!isActive()) {\n            return;\n        }\n\n        var bounds = queryBounds();\n        if (lastBounds != null && !lastBounds.equals(bounds)) {\n            customBounds = true;\n        }\n        lastBounds = bounds;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/CustomTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.prefs.ExternalApplicationHelper;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.core.OsType;\n\nimport java.util.Locale;\n\npublic class CustomTerminalType implements ExternalApplicationType, ExternalTerminalType {\n\n    @Override\n    public TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.NEW_WINDOW;\n    }\n\n    @Override\n    public boolean isRecommended() {\n        return true;\n    }\n\n    @Override\n    public boolean useColoredTitle() {\n        return true;\n    }\n\n    @Override\n    public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n        var custom = AppPrefs.get().customTerminalCommand().getValue();\n        if (custom == null || custom.isBlank()) {\n            throw ErrorEventFactory.expected(new IllegalStateException(\"No custom terminal command specified\"));\n        }\n\n        var format = custom.toLowerCase(Locale.ROOT).contains(\"$cmd\") ? custom : custom + \" $CMD\";\n        try (var pc = LocalShell.getShell()) {\n            var toExecute = ExternalApplicationHelper.replaceVariableArgument(\n                    format, \"CMD\", configuration.single().getScriptFile().toString());\n            // We can't be sure whether the command is blocking or not, so always make it not blocking\n            if (pc.getOsType() == OsType.WINDOWS) {\n                toExecute = \"start \\\"\" + configuration.getCleanTitle() + \"\\\" \" + toExecute;\n            } else {\n                toExecute = \"nohup \" + toExecute + \" </dev/null &>/dev/null & disown\";\n            }\n            pc.executeSimpleCommand(toExecute);\n        }\n    }\n\n    @Override\n    public boolean isAvailable() {\n        return true;\n    }\n\n    @Override\n    public String getId() {\n        return \"app.custom\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.ext.PrefsChoiceValue;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.*;\nimport io.xpipe.app.update.AppDistributionType;\nimport io.xpipe.core.OsType;\n\nimport lombok.Getter;\n\nimport java.util.*;\n\npublic interface ExternalTerminalType extends PrefsChoiceValue {\n\n    //    ExternalTerminalType PUTTY = new WindowsType(\"app.putty\",\"putty\") {\n    //\n    //        @Override\n    //        public Optional<Path> determineInstallation() {\n    //            try {\n    //                var r = WindowsRegistry.local().readValue(WindowsRegistry.HKEY_LOCAL_MACHINE,\n    //                        \"SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\App Paths\\\\Xshell.exe\");\n    //                return r.map(Path::of);\n    //            }  catch (Exception e) {\n    //                ErrorEvent.fromThrowable(e).omit().handle();\n    //                return Optional.empty();\n    //            }\n    //        }\n    //\n    //        @Override\n    //        public boolean supportsTabs() {\n    //            return true;\n    //        }\n    //\n    //        @Override\n    //        public boolean isRecommended() {\n    //            return false;\n    //        }\n    //\n    //        @Override\n    //        public boolean supportsColoredTitle() {\n    //            return false;\n    //        }\n    //\n    //        @Override\n    //        protected void execute(Path file, LaunchConfiguration configuration) throws Exception {\n    //            try (var sc = LocalShell.getShell()) {\n    //                SshLocalBridge.init();\n    //                var b = SshLocalBridge.get();\n    //                var command = CommandBuilder.of().addFile(file.toString()).add(\"-ssh\", \"localhost\",\n    // \"-l\").addQuoted(b.getUser())\n    //                        .add(\"-i\").addFile(b.getIdentityKey().toString()).add(\"-P\", \"\" +\n    // b.getPort()).add(\"-hostkey\").addFile(b.getPubHostKey().toString());\n    //                sc.executeSimpleCommand(command);\n    //            }\n    //        }\n    //    };\n\n    ExternalTerminalType XSHELL = new XShellTerminalType();\n    ExternalTerminalType SECURECRT = new SecureCrtTerminalType();\n    ExternalTerminalType MOBAXTERM = new MobaXTermTerminalType();\n    ExternalTerminalType TERMIUS = new TermiusTerminalType();\n    ExternalTerminalType CMD = new CmdTerminalType();\n    ExternalTerminalType POWERSHELL = new PowerShellTerminalType();\n    ExternalTerminalType PWSH = new PwshTerminalType();\n    ExternalTerminalType GNOME_TERMINAL = new GnomeTerminalType();\n    ExternalTerminalType GNOME_CONSOLE = new GnomeConsoleType();\n    ExternalTerminalType PTYXIS = new PtyxisTerminalType();\n    ExternalTerminalType YAKUAKE = new YakuakeTerminalType();\n    ExternalTerminalType KONSOLE = new KonsoleTerminalType();\n\n    ExternalTerminalType COOL_RETRO_TERM = new SimplePathType(\"app.coolRetroTerm\", \"cool-retro-term\", true) {\n\n        @Override\n        public TerminalOpenFormat getOpenFormat() {\n            return TerminalOpenFormat.NEW_WINDOW;\n        }\n\n        @Override\n        public boolean isRecommended() {\n            return false;\n        }\n\n        @Override\n        public boolean useColoredTitle() {\n            return false;\n        }\n\n        @Override\n        protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {\n            return CommandBuilder.of()\n                    .add(\"-T\")\n                    .addQuoted(configuration.getCleanTitle())\n                    .add(\"-e\")\n                    .add(configuration.single().getDialectLaunchCommand());\n        }\n    };\n\n    ExternalTerminalType XFCE = new SimplePathType(\"app.xfce\", \"xfce4-terminal\", true) {\n        @Override\n        public TerminalOpenFormat getOpenFormat() {\n            return TerminalOpenFormat.NEW_WINDOW_OR_TABBED;\n        }\n\n        @Override\n        public String getWebsite() {\n            return \"https://docs.xfce.org/apps/terminal/start\";\n        }\n\n        @Override\n        public boolean isRecommended() {\n            return true;\n        }\n\n        @Override\n        public boolean useColoredTitle() {\n            return true;\n        }\n\n        @Override\n        protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {\n            return CommandBuilder.of()\n                    .addIf(configuration.isPreferTabs(), \"--tab\")\n                    .add(\"--title\")\n                    .addQuoted(configuration.getColoredTitle())\n                    .add(\"--command\")\n                    .addFile(configuration.single().getScriptFile());\n        }\n    };\n    ExternalTerminalType LXTERMINAL = new SimplePathType(\"app.lxterminal\", \"lxterminal\", true) {\n        @Override\n        public TerminalOpenFormat getOpenFormat() {\n            return TerminalOpenFormat.NEW_WINDOW;\n        }\n\n        @Override\n        public String getWebsite() {\n            return \"https://github.com/lxde/lxterminal\";\n        }\n\n        @Override\n        public boolean isRecommended() {\n            return false;\n        }\n\n        @Override\n        public boolean useColoredTitle() {\n            return false;\n        }\n\n        @Override\n        protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {\n            return CommandBuilder.of()\n                    .add(\"-t\")\n                    .addQuoted(configuration.getColoredTitle())\n                    .add(\"-e\")\n                    .addFile(configuration.single().getScriptFile());\n        }\n    };\n    ExternalTerminalType FOOT = new FootTerminalType();\n    ExternalTerminalType ELEMENTARY = new SimplePathType(\"app.elementaryTerminal\", \"io.elementary.terminal\", true) {\n\n        @Override\n        public TerminalOpenFormat getOpenFormat() {\n            return TerminalOpenFormat.NEW_WINDOW_OR_TABBED;\n        }\n\n        @Override\n        public String getWebsite() {\n            return \"https://github.com/elementary/terminal\";\n        }\n\n        @Override\n        public boolean isRecommended() {\n            return true;\n        }\n\n        @Override\n        public boolean useColoredTitle() {\n            return true;\n        }\n\n        @Override\n        protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {\n            return CommandBuilder.of()\n                    .addIf(configuration.isPreferTabs(), \"--new-tab\")\n                    .add(\"-e\")\n                    .addFile(configuration.single().getScriptFile());\n        }\n    };\n    ExternalTerminalType TILIX = new SimplePathType(\"app.tilix\", \"tilix\", true) {\n        @Override\n        public TerminalOpenFormat getOpenFormat() {\n            return TerminalOpenFormat.NEW_WINDOW;\n        }\n\n        @Override\n        public String getWebsite() {\n            return \"https://gnunn1.github.io/tilix-web/\";\n        }\n\n        @Override\n        public boolean isRecommended() {\n            return false;\n        }\n\n        @Override\n        public boolean useColoredTitle() {\n            return true;\n        }\n\n        @Override\n        protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {\n            return CommandBuilder.of()\n                    .add(\"-t\")\n                    .addQuoted(configuration.getColoredTitle())\n                    .add(\"-e\")\n                    .addFile(configuration.single().getScriptFile());\n        }\n    };\n    ExternalTerminalType TERMINATOR = new SimplePathType(\"app.terminator\", \"terminator\", true) {\n        @Override\n        public TerminalOpenFormat getOpenFormat() {\n            return TerminalOpenFormat.NEW_WINDOW_OR_TABBED;\n        }\n\n        @Override\n        public String getWebsite() {\n            return \"https://gnome-terminator.org/\";\n        }\n\n        @Override\n        public boolean isRecommended() {\n            return true;\n        }\n\n        @Override\n        public boolean useColoredTitle() {\n            return true;\n        }\n\n        @Override\n        protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {\n            return CommandBuilder.of()\n                    .add(\"-e\")\n                    .addFile(configuration.single().getScriptFile())\n                    .add(\"-T\")\n                    .addQuoted(configuration.getColoredTitle())\n                    .addIf(configuration.isPreferTabs(), \"--new-tab\");\n        }\n    };\n    ExternalTerminalType TERMINOLOGY = new SimplePathType(\"app.terminology\", \"terminology\", true) {\n        @Override\n        public TerminalOpenFormat getOpenFormat() {\n            return TerminalOpenFormat.NEW_WINDOW_OR_TABBED;\n        }\n\n        @Override\n        public String getWebsite() {\n            return \"https://github.com/borisfaure/terminology\";\n        }\n\n        @Override\n        public boolean isRecommended() {\n            return true;\n        }\n\n        @Override\n        public boolean useColoredTitle() {\n            return true;\n        }\n\n        @Override\n        protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {\n            return CommandBuilder.of()\n                    .addIf(!configuration.isPreferTabs(), \"-s\")\n                    .add(\"-T\")\n                    .addQuoted(configuration.getColoredTitle())\n                    .add(\"-2\")\n                    .add(\"-e\")\n                    .addFile(configuration.single().getScriptFile());\n        }\n    };\n    ExternalTerminalType GUAKE = new SimplePathType(\"app.guake\", \"guake\", true) {\n\n        @Override\n        public int getProcessHierarchyOffset() {\n            return 1;\n        }\n\n        @Override\n        public TerminalOpenFormat getOpenFormat() {\n            return TerminalOpenFormat.TABBED;\n        }\n\n        @Override\n        public String getWebsite() {\n            return \"https://github.com/Guake/guake\";\n        }\n\n        @Override\n        public boolean isRecommended() {\n            return true;\n        }\n\n        @Override\n        public boolean useColoredTitle() {\n            return true;\n        }\n\n        @Override\n        protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {\n            return CommandBuilder.of()\n                    .add(\"-n\", \"~\")\n                    .add(\"-r\")\n                    .addQuoted(configuration.getColoredTitle())\n                    .add(\"-e\")\n                    .addFile(configuration.single().getScriptFile());\n        }\n    };\n    ExternalTerminalType TILDA = new SimplePathType(\"app.tilda\", \"tilda\", true) {\n        @Override\n        public TerminalOpenFormat getOpenFormat() {\n            return TerminalOpenFormat.TABBED;\n        }\n\n        @Override\n        public String getWebsite() {\n            return \"https://github.com/lanoxx/tilda\";\n        }\n\n        @Override\n        public boolean isRecommended() {\n            return true;\n        }\n\n        @Override\n        public boolean useColoredTitle() {\n            return true;\n        }\n\n        @Override\n        protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {\n            return CommandBuilder.of().add(\"-c\").addFile(configuration.single().getScriptFile());\n        }\n    };\n    ExternalTerminalType COSMIC_TERM = new SimplePathType(\"app.cosmicTerm\", \"cosmic-term\", true) {\n        @Override\n        public TerminalOpenFormat getOpenFormat() {\n            return TerminalOpenFormat.NEW_WINDOW;\n        }\n\n        @Override\n        public String getWebsite() {\n            return \"https://github.com/pop-os/cosmic-term\";\n        }\n\n        @Override\n        public boolean isRecommended() {\n            return AppPrefs.get().terminalMultiplexer().getValue() != null;\n        }\n\n        @Override\n        public boolean useColoredTitle() {\n            return true;\n        }\n\n        @Override\n        protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {\n            return CommandBuilder.of().add(\"-e\").addFile(configuration.single().getScriptFile());\n        }\n    };\n    ExternalTerminalType XTERM = new MultiPathType(\"app.xterm\", true, List.of(\"uxterm\", \"xterm\")) {\n        @Override\n        public TerminalOpenFormat getOpenFormat() {\n            return TerminalOpenFormat.NEW_WINDOW;\n        }\n\n        @Override\n        public String getWebsite() {\n            return \"https://invisible-island.net/xterm/\";\n        }\n\n        @Override\n        public boolean isRecommended() {\n            return false;\n        }\n\n        @Override\n        public boolean useColoredTitle() {\n            return false;\n        }\n\n        @Override\n        public boolean supportsUnicode() {\n            return false;\n        }\n\n        @Override\n        protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {\n            return CommandBuilder.of()\n                    .add(\"-title\")\n                    .addQuoted(configuration.getColoredTitle())\n                    .add(\"-e\")\n                    .addFile(configuration.single().getScriptFile());\n        }\n    };\n    ExternalTerminalType DEEPIN_TERMINAL = new SimplePathType(\"app.deepinTerminal\", \"deepin-terminal\", true) {\n\n        @Override\n        public int getProcessHierarchyOffset() {\n            return 1;\n        }\n\n        @Override\n        public TerminalOpenFormat getOpenFormat() {\n            return TerminalOpenFormat.NEW_WINDOW;\n        }\n\n        @Override\n        public String getWebsite() {\n            return \"https://www.deepin.org/en/original/deepin-terminal/\";\n        }\n\n        @Override\n        public boolean isRecommended() {\n            return AppPrefs.get().terminalMultiplexer().getValue() != null;\n        }\n\n        @Override\n        public boolean useColoredTitle() {\n            return true;\n        }\n\n        @Override\n        protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {\n            return CommandBuilder.of().add(\"-C\").addFile(configuration.single().getScriptFile());\n        }\n    };\n    ExternalTerminalType Q_TERMINAL = new SimplePathType(\"app.qTerminal\", \"qterminal\", true) {\n\n        @Override\n        public int getProcessHierarchyOffset() {\n            return LocalShell.getDialect() == ShellDialects.BASH ? 0 : 1;\n        }\n\n        @Override\n        public TerminalOpenFormat getOpenFormat() {\n            return TerminalOpenFormat.NEW_WINDOW;\n        }\n\n        @Override\n        public String getWebsite() {\n            return \"https://github.com/lxqt/qterminal\";\n        }\n\n        @Override\n        public boolean isRecommended() {\n            return AppPrefs.get().terminalMultiplexer().getValue() != null;\n        }\n\n        @Override\n        public boolean useColoredTitle() {\n            return true;\n        }\n\n        @Override\n        protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {\n            return CommandBuilder.of().add(\"-e\").add(configuration.single().getDialectLaunchCommand());\n        }\n    };\n    ExternalTerminalType MACOS_TERMINAL = new MacOsTerminalType();\n    ExternalTerminalType ITERM2 = new ITerm2TerminalType();\n    ExternalTerminalType CUSTOM = new CustomTerminalType();\n    List<ExternalTerminalType> WINDOWS_TERMINALS = List.of(\n            WindowsTerminalType.WINDOWS_TERMINAL_CANARY,\n            WindowsTerminalType.WINDOWS_TERMINAL_PREVIEW,\n            WindowsTerminalType.WINDOWS_TERMINAL,\n            AlacrittyTerminalType.ALACRITTY_WINDOWS,\n            WezTerminalType.WEZTERM_WINDOWS,\n            WarpTerminalType.WINDOWS,\n            CMD,\n            PWSH,\n            POWERSHELL,\n            MOBAXTERM,\n            SECURECRT,\n            TERMIUS,\n            XSHELL,\n            TabbyTerminalType.TABBY_WINDOWS,\n            WaveTerminalType.WAVE_WINDOWS);\n    List<ExternalTerminalType> LINUX_TERMINALS = List.of(\n            AlacrittyTerminalType.ALACRITTY_LINUX,\n            WezTerminalType.WEZTERM_LINUX,\n            KittyTerminalType.KITTY_LINUX,\n            GNOME_CONSOLE,\n            PTYXIS,\n            TERMINATOR,\n            TERMINOLOGY,\n            XFCE,\n            ELEMENTARY,\n            KONSOLE,\n            GNOME_TERMINAL,\n            GhosttyTerminalType.GHOSTTY_LINUX,\n            YAKUAKE,\n            TILIX,\n            GUAKE,\n            TILDA,\n            COSMIC_TERM,\n            XTERM,\n            DEEPIN_TERMINAL,\n            FOOT,\n            LXTERMINAL,\n            Q_TERMINAL,\n            WarpTerminalType.LINUX,\n            COOL_RETRO_TERM,\n            TERMIUS,\n            WaveTerminalType.WAVE_LINUX);\n    List<ExternalTerminalType> MACOS_TERMINALS = List.of(\n            ITERM2,\n            KittyTerminalType.KITTY_MACOS,\n            TabbyTerminalType.TABBY_MAC_OS,\n            AlacrittyTerminalType.ALACRITTY_MAC_OS,\n            WezTerminalType.WEZTERM_MAC_OS,\n            GhosttyTerminalType.GHOSTTY_MACOS,\n            WarpTerminalType.MACOS,\n            MACOS_TERMINAL,\n            TERMIUS,\n            WaveTerminalType.WAVE_MAC_OS);\n    List<ExternalTerminalType> ALL = getTypes(OsType.ofLocal(), true);\n    List<ExternalTerminalType> ALL_ON_ALL_PLATFORMS = getTypes(null, true);\n\n    static ExternalTerminalType determineFallbackTerminalToOpen(ExternalTerminalType type) {\n        if (type != null\n                && type != XSHELL\n                && type != MOBAXTERM\n                && type != SECURECRT\n                && type != TERMIUS\n                && !(type instanceof WaveTerminalType)) {\n            return type;\n        }\n\n        // Fallback to an available default\n        switch (OsType.ofLocal()) {\n            case OsType.Linux ignored -> {\n                // This should not be termius or wave as all others take precedence\n                var def = determineDefault(null);\n                // If there's no other terminal available, use a fallback which won't work\n                return def != TERMIUS && def != WaveTerminalType.WAVE_LINUX ? def : XTERM;\n            }\n            case OsType.MacOs ignored -> {\n                return MACOS_TERMINAL;\n            }\n            case OsType.Windows ignored -> {\n                return LocalShell.getDialect() == ShellDialects.CMD ? CMD : POWERSHELL;\n            }\n        }\n    }\n\n    static List<ExternalTerminalType> getTypes(OsType.Local osType, boolean custom) {\n        var all = new ArrayList<ExternalTerminalType>();\n        if (osType == null || osType == OsType.WINDOWS) {\n            all.addAll(WINDOWS_TERMINALS);\n        }\n        if (osType == null || osType == OsType.LINUX) {\n            all.addAll(LINUX_TERMINALS);\n        }\n        if (osType == null || osType == OsType.MACOS) {\n            all.addAll(MACOS_TERMINALS);\n        }\n        // Prefer recommended\n        all.sort(Comparator.comparingInt(o -> (o.isRecommended() ? -1 : 0)));\n        if (custom) {\n            all.add(CUSTOM);\n        }\n        return all;\n    }\n\n    static ExternalTerminalType determineDefault(ExternalTerminalType existing) {\n        // Check for incompatibility with fallback shell\n        if (ExternalTerminalType.CMD.equals(existing) && LocalShell.getDialect() != ShellDialects.CMD) {\n            return ExternalTerminalType.POWERSHELL;\n        }\n\n        // Verify that our selection is still valid\n        if (existing != null && existing.isAvailable()) {\n            return existing;\n        }\n\n        if (existing == null && AppDistributionType.get() == AppDistributionType.WEBTOP) {\n            return ExternalTerminalType.KONSOLE;\n        }\n\n        var r = ALL.stream()\n                .filter(externalTerminalType -> !externalTerminalType.equals(CUSTOM))\n                .filter(terminalType -> terminalType.isAvailable())\n                .findFirst()\n                .orElse(null);\n\n        // Check if detection failed for some reason\n        if (r == null) {\n            var def = OsType.ofLocal() == OsType.WINDOWS\n                    ? (LocalShell.getDialect() == ShellDialects.CMD\n                            ? ExternalTerminalType.CMD\n                            : ExternalTerminalType.POWERSHELL)\n                    : OsType.ofLocal() == OsType.MACOS ? ExternalTerminalType.MACOS_TERMINAL : null;\n            r = def;\n        }\n\n        return r;\n    }\n\n    default TerminalInitFunction additionalInitCommands() {\n        return TerminalInitFunction.none();\n    }\n\n    TerminalOpenFormat getOpenFormat();\n\n    default String getWebsite() {\n        return null;\n    }\n\n    default boolean supportsSplitView() {\n        return false;\n    }\n\n    boolean isRecommended();\n\n    boolean useColoredTitle();\n\n    default boolean supportsEscapes() {\n        return true;\n    }\n\n    default boolean supportsUnicode() {\n        return true;\n    }\n\n    default boolean shouldClear() {\n        return true;\n    }\n\n    void launch(TerminalLaunchConfiguration configuration) throws Exception;\n\n    abstract class SimplePathType implements ExternalApplicationType.PathApplication, TrackableTerminalType {\n\n        @Getter\n        private final String id;\n\n        @Getter\n        private final String executable;\n\n        private final boolean async;\n\n        public SimplePathType(String id, String executable, boolean async) {\n            this.id = id;\n            this.executable = executable;\n            this.async = async;\n        }\n\n        @Override\n        public boolean detach() {\n            return async;\n        }\n\n        @Override\n        public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n            var args = toCommand(configuration);\n            launch(args);\n        }\n\n        protected abstract CommandBuilder toCommand(TerminalLaunchConfiguration configuration);\n    }\n\n    abstract class MultiPathType implements ExternalApplicationType.PathApplication, TrackableTerminalType {\n\n        @Getter\n        private final String id;\n\n        @Getter\n        private final List<String> executables;\n\n        private final boolean async;\n\n        private String executable;\n\n        public MultiPathType(String id, boolean async, List<String> executables) {\n            this.id = id;\n            this.executables = executables;\n            this.async = async;\n        }\n\n        @Override\n        public boolean detach() {\n            return async;\n        }\n\n        @Override\n        public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n            var args = toCommand(configuration);\n            launch(args);\n        }\n\n        @Override\n        public String getExecutable() {\n            if (executable != null) {\n                return executable;\n            }\n\n            for (String executable : executables) {\n                try (ShellControl pc = LocalShell.getShell()) {\n                    if (pc.view().findProgram(executable).isPresent()) {\n                        return (this.executable = executable);\n                    }\n                } catch (Exception e) {\n                    ErrorEventFactory.fromThrowable(e).omit().handle();\n                }\n            }\n\n            return (executable = executables.getFirst());\n        }\n\n        protected abstract CommandBuilder toCommand(TerminalLaunchConfiguration configuration);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/FootTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\n\npublic class FootTerminalType implements ExternalTerminalType, ExternalApplicationType.LinuxApplication {\n\n    @Override\n    public TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.NEW_WINDOW;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://codeberg.org/dnkl/foot\";\n    }\n\n    @Override\n    public boolean isRecommended() {\n        return AppPrefs.get().terminalMultiplexer().getValue() != null;\n    }\n\n    @Override\n    public boolean useColoredTitle() {\n        return false;\n    }\n\n    @Override\n    public boolean supportsEscapes() {\n        return false;\n    }\n\n    @Override\n    public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n        var toExecute = CommandBuilder.of()\n                .add(\"--title\")\n                .addQuoted(configuration.getColoredTitle())\n                .addFile(configuration.single().getScriptFile());\n        launch(toExecute);\n    }\n\n    @Override\n    public String getFlatpakId() {\n        return \"page.codeberg.dnkl.foot\";\n    }\n\n    @Override\n    public String getExecutable() {\n        return \"foot\";\n    }\n\n    @Override\n    public boolean detach() {\n        return true;\n    }\n\n    @Override\n    public String getId() {\n        return \"app.foot\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/GhosttyTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\n\npublic interface GhosttyTerminalType extends ExternalTerminalType, TrackableTerminalType {\n\n    ExternalTerminalType GHOSTTY_LINUX = new Linux();\n    ExternalTerminalType GHOSTTY_MACOS = new MacOs();\n\n    @Override\n    default TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.NEW_WINDOW;\n    }\n\n    @Override\n    default String getWebsite() {\n        return \"https://ghostty.org\";\n    }\n\n    @Override\n    default boolean isRecommended() {\n        return AppPrefs.get().terminalMultiplexer().getValue() != null;\n    }\n\n    @Override\n    default boolean useColoredTitle() {\n        return true;\n    }\n\n    class Linux implements GhosttyTerminalType, ExternalApplicationType.PathApplication {\n\n        @Override\n        public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n            var builder =\n                    CommandBuilder.of().add(\"-e\").addFile(configuration.single().getScriptFile());\n            launch(builder);\n        }\n\n        @Override\n        public String getId() {\n            return \"app.ghostty\";\n        }\n\n        @Override\n        public String getExecutable() {\n            return \"ghostty\";\n        }\n\n        @Override\n        public boolean detach() {\n            return true;\n        }\n    }\n\n    class MacOs implements ExternalApplicationType.MacApplication, GhosttyTerminalType {\n\n        @Override\n        public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n            LocalShell.getShell()\n                    .executeSimpleCommand(CommandBuilder.of()\n                            .add(\"open\", \"-n\", \"-a\")\n                            .addQuoted(getApplicationName())\n                            .add(\"--args\", \"-e\")\n                            .add(configuration.single().getDialectLaunchCommand()));\n        }\n\n        @Override\n        public String getApplicationName() {\n            return \"Ghostty\";\n        }\n\n        @Override\n        public String getId() {\n            return \"app.ghostty\";\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/GnomeConsoleType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\n\npublic class GnomeConsoleType implements ExternalApplicationType.PathApplication, TrackableTerminalType {\n\n    @Override\n    public TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.NEW_WINDOW_OR_TABBED;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://apps.gnome.org/en-GB/Console/\";\n    }\n\n    @Override\n    public boolean isRecommended() {\n        return true;\n    }\n\n    @Override\n    public boolean useColoredTitle() {\n        return true;\n    }\n\n    @Override\n    public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n        var toExecute = CommandBuilder.of()\n                .addIf(configuration.isPreferTabs(), \"--tab\")\n                .add(\"--\")\n                .add(configuration.single().getDialectLaunchCommand());\n        launch(toExecute);\n    }\n\n    @Override\n    public String getExecutable() {\n        return \"kgx\";\n    }\n\n    @Override\n    public boolean detach() {\n        return true;\n    }\n\n    @Override\n    public String getId() {\n        return \"app.gnomeConsole\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/GnomeTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.CommandSupport;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ShellControl;\n\npublic class GnomeTerminalType implements ExternalApplicationType.PathApplication, TrackableTerminalType {\n\n    @Override\n    public TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.NEW_WINDOW;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://help.gnome.org/users/gnome-terminal/stable/\";\n    }\n\n    @Override\n    public boolean isRecommended() {\n        return AppPrefs.get().terminalMultiplexer().getValue() != null;\n    }\n\n    @Override\n    public boolean useColoredTitle() {\n        return false;\n    }\n\n    @Override\n    public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n        try (ShellControl pc = LocalShell.getShell()) {\n            CommandSupport.isInPathOrThrow(\n                    pc, getExecutable(), toTranslatedString().getValue(), null);\n\n            var toExecute = CommandBuilder.of()\n                    .add(getExecutable(), \"-v\", \"--title\")\n                    .addQuoted(configuration.getColoredTitle())\n                    .add(\"--\")\n                    .addFile(configuration.single().getScriptFile())\n                    // In order to fix this bug which also affects us:\n                    // https://askubuntu.com/questions/1148475/launching-gnome-terminal-from-vscode\n                    .environment(\"GNOME_TERMINAL_SCREEN\", sc -> \"\");\n            pc.executeSimpleCommand(toExecute);\n        }\n    }\n\n    @Override\n    public String getExecutable() {\n        return \"gnome-terminal\";\n    }\n\n    @Override\n    public boolean detach() {\n        return true;\n    }\n\n    @Override\n    public String getId() {\n        return \"app.gnomeTerminal\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/ITerm2TerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\n\npublic class ITerm2TerminalType implements ExternalApplicationType.MacApplication, TrackableTerminalType {\n\n    @Override\n    public TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.TABBED;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://iterm2.com/\";\n    }\n\n    @Override\n    public boolean isRecommended() {\n        return true;\n    }\n\n    @Override\n    public boolean useColoredTitle() {\n        return true;\n    }\n\n    @Override\n    public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n        LocalShell.getShell()\n                .executeSimpleCommand(CommandBuilder.of()\n                        .add(\"open\", \"-a\")\n                        .addQuoted(\"iTerm.app\")\n                        .addFile(configuration.single().getScriptFile()));\n    }\n\n    @Override\n    public int getProcessHierarchyOffset() {\n        return 3;\n    }\n\n    @Override\n    public String getApplicationName() {\n        return \"iTerm\";\n    }\n\n    @Override\n    public String getId() {\n        return \"app.iterm2\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/KittyTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.core.AppNames;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.CommandSupport;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.ShellDialects;\nimport io.xpipe.app.process.ShellTemp;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.FilePath;\n\nimport com.fasterxml.jackson.databind.node.JsonNodeFactory;\n\nimport java.io.IOException;\nimport java.net.StandardProtocolFamily;\nimport java.net.UnixDomainSocketAddress;\nimport java.nio.channels.SocketChannel;\n\npublic interface KittyTerminalType extends ExternalTerminalType, TrackableTerminalType {\n\n    ExternalTerminalType KITTY_LINUX = new Linux();\n    ExternalTerminalType KITTY_MACOS = new MacOs();\n\n    @Override\n    default boolean supportsSplitView() {\n        return true;\n    }\n\n    private static FilePath getSocket() throws Exception {\n        try (var sc = LocalShell.getShell().start()) {\n            var temp = ShellTemp.createUserSpecificTempDataDirectory(sc, null);\n            return temp.join(AppNames.ofCurrent().getSnakeName() + \"_kitty\");\n        }\n    }\n\n    private static void open(TerminalLaunchConfiguration configuration, CommandBuilder socketWrite, boolean preferTab)\n            throws Exception {\n        try (var sc = LocalShell.getShell().start()) {\n            for (int i = 0; i < configuration.getPanes().size(); i++) {\n                var payload = JsonNodeFactory.instance.objectNode();\n                var args = configuration\n                        .getPanes()\n                        .get(i)\n                        .getDialectLaunchCommand()\n                        .buildBaseParts(sc);\n                var argsArray = payload.putArray(\"args\");\n                args.forEach(argsArray::add);\n                payload.put(\"tab_title\", configuration.getColoredTitle());\n\n                var type = i == 0 ? (preferTab ? \"tab\" : \"os-window\") : \"window\";\n                payload.put(\"type\", type);\n\n                var json = JsonNodeFactory.instance.objectNode();\n                json.put(\"cmd\", \"launch\");\n                json.set(\"payload\", payload);\n                json.putArray(\"version\").add(0).add(14).add(2);\n                var jsonString = json.toString();\n                var echoString = \"'\\\\eP@kitty-cmd\" + jsonString + \"\\\\e\\\\\\\\'\";\n\n                sc.command(CommandBuilder.of()\n                                .add(\"printf\", echoString, \"|\")\n                                .add(socketWrite)\n                                .addFile(getSocket()))\n                        .execute();\n\n                if (i == 0) {\n                    setLayout(socketWrite);\n                }\n            }\n        }\n    }\n\n    private static void setLayout(CommandBuilder socketWrite) throws Exception {\n        var layout =\n                switch (AppPrefs.get().terminalSplitStrategy().getValue()) {\n                    case HORIZONTAL -> \"horizontal\";\n                    case VERTICAL -> \"vertical\";\n                    case BALANCED -> \"grid\";\n                };\n\n        try (var sc = LocalShell.getShell().start()) {\n            var payload = JsonNodeFactory.instance.objectNode();\n            var layoutArray = JsonNodeFactory.instance.arrayNode();\n            layoutArray.add(layout);\n            payload.set(\"layouts\", layoutArray);\n            payload.put(\"configured\", true);\n\n            var json = JsonNodeFactory.instance.objectNode();\n            json.put(\"cmd\", \"set-enabled-layouts\");\n            json.set(\"payload\", payload);\n            json.putArray(\"version\").add(0).add(14).add(2);\n\n            var jsonString = json.toString();\n            var echoString = \"'\\\\eP@kitty-cmd\" + jsonString + \"\\\\e\\\\\\\\'\";\n\n            sc.command(CommandBuilder.of()\n                            .add(\"printf\", echoString, \"|\")\n                            .add(socketWrite)\n                            .addFile(getSocket()))\n                    .execute();\n        }\n    }\n\n    private static void closeInitial(CommandBuilder socketWrite) throws Exception {\n        try (var sc = LocalShell.getShell().start()) {\n            var payload = JsonNodeFactory.instance.objectNode();\n            payload.put(\"match\", \"not recent:0\");\n\n            var json = JsonNodeFactory.instance.objectNode();\n            json.put(\"cmd\", \"close-tab\");\n            json.set(\"payload\", payload);\n            json.putArray(\"version\").add(0).add(14).add(2);\n            var jsonString = json.toString();\n            var echoString = \"'\\\\eP@kitty-cmd\" + jsonString + \"\\\\e\\\\\\\\'\";\n\n            sc.command(CommandBuilder.of()\n                            .add(\"printf\", echoString, \"|\")\n                            .add(socketWrite)\n                            .addFile(getSocket()))\n                    .execute();\n        }\n    }\n\n    @Override\n    default TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.NEW_WINDOW_OR_TABBED;\n    }\n\n    @Override\n    default String getWebsite() {\n        return \"https://github.com/kovidgoyal/kitty\";\n    }\n\n    @Override\n    default boolean isRecommended() {\n        return true;\n    }\n\n    @Override\n    default boolean useColoredTitle() {\n        return true;\n    }\n\n    class Linux implements KittyTerminalType {\n\n        @Override\n        public int getProcessHierarchyOffset() {\n            return LocalShell.getDialect() == ShellDialects.BASH ? 0 : 1;\n        }\n\n        public boolean isAvailable() {\n            try (ShellControl pc = LocalShell.getShell()) {\n                return pc.view().findProgram(\"kitty\").isPresent();\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).omit().handle();\n                return false;\n            }\n        }\n\n        @Override\n        public String getId() {\n            return \"app.kitty\";\n        }\n\n        @Override\n        public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n            try (var sc = LocalShell.getShell().start()) {\n                CommandSupport.isInPathOrThrow(sc, \"kitty\", \"Kitty\", null);\n                CommandSupport.isInPathOrThrow(sc, \"socat\", \"socat\", null);\n            }\n\n            var toClose = prepare();\n            var socketWrite = CommandBuilder.of().add(\"socat\", \"-\");\n            open(configuration, socketWrite, configuration.isPreferTabs());\n            if (toClose) {\n                closeInitial(socketWrite);\n            }\n        }\n\n        private boolean prepare() throws Exception {\n            var socket = getSocket();\n            try (var sc = LocalShell.getShell().start()) {\n                if (sc.executeSimpleBooleanCommand(\n                        \"test -w \" + sc.getShellDialect().fileArgument(socket))) {\n                    return false;\n                }\n\n                sc.command(CommandBuilder.of()\n                                .add(\"kitty\")\n                                .add(\n                                        \"-o\",\n                                        \"allow_remote_control=socket-only\",\n                                        \"--listen-on\",\n                                        \"unix:\" + getSocket(),\n                                        \"--detach\",\n                                        \"--start-as\",\n                                        \"minimized\"))\n                        .execute();\n\n                for (int i = 0; i < 50; i++) {\n                    ThreadHelper.sleep(100);\n                    try (SocketChannel channel = SocketChannel.open(StandardProtocolFamily.UNIX)) {\n                        if (channel.connect(UnixDomainSocketAddress.of(socket.asLocalPath()))) {\n                            break;\n                        }\n                    } catch (IOException ignored) {\n                    }\n                }\n\n                return true;\n            }\n        }\n    }\n\n    class MacOs implements ExternalApplicationType.MacApplication, KittyTerminalType {\n\n        @Override\n        public int getProcessHierarchyOffset() {\n            return LocalShell.getDialect() == ShellDialects.ZSH ? 1 : 0;\n        }\n\n        @Override\n        public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n            // We use the absolute path to force the usage of macOS netcat\n            // Homebrew versions have different option formats\n            try (var sc = LocalShell.getShell().start()) {\n                CommandSupport.isInPathOrThrow(sc, \"/usr/bin/nc\", \"Netcat\", null);\n            }\n\n            var toClose = prepare();\n            var socketWrite = CommandBuilder.of().add(\"/usr/bin/nc\", \"-U\");\n            open(configuration, socketWrite, configuration.isPreferTabs());\n            if (toClose) {\n                closeInitial(socketWrite);\n            }\n        }\n\n        private boolean prepare() throws Exception {\n            var socket = getSocket();\n            try (var sc = LocalShell.getShell().start()) {\n                if (sc.executeSimpleBooleanCommand(\n                        \"test -w \" + sc.getShellDialect().fileArgument(socket))) {\n                    return false;\n                }\n\n                sc.command(CommandBuilder.of()\n                                .add(\"open\", \"-n\", \"-a\", \"kitty.app\", \"--args\")\n                                .add(\"-o\", \"allow_remote_control=socket-only\", \"--listen-on\", \"unix:\" + getSocket()))\n                        .execute();\n\n                for (int i = 0; i < 50; i++) {\n                    ThreadHelper.sleep(100);\n                    try (SocketChannel channel = SocketChannel.open(StandardProtocolFamily.UNIX)) {\n                        if (channel.connect(UnixDomainSocketAddress.of(socket.asLocalPath()))) {\n                            break;\n                        }\n                    } catch (IOException ignored) {\n                    }\n                }\n\n                return true;\n            }\n        }\n\n        @Override\n        public String getApplicationName() {\n            return \"kitty\";\n        }\n\n        @Override\n        public String getId() {\n            return \"app.kitty\";\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/KonsoleTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\n\npublic class KonsoleTerminalType implements ExternalTerminalType, ExternalApplicationType.LinuxApplication {\n\n    @Override\n    public TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.NEW_WINDOW_OR_TABBED;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://konsole.kde.org/download.html\";\n    }\n\n    @Override\n    public boolean isRecommended() {\n        // We can manually fix the single process option in konsole\n        return true;\n    }\n\n    @Override\n    public boolean useColoredTitle() {\n        return false;\n    }\n\n    @Override\n    public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n        configureSingleInstanceMode();\n        // Note for later: When debugging konsole launches, it will always open as a child process of\n        // IntelliJ/XPipe even though we try to detach it.\n        // This is not the case for production where it works as expected\n        var toExecute = CommandBuilder.of()\n                .add(\"-p\")\n                .add(\"tabtitle='\" + configuration.single().getTitle() + \"'\")\n                .addIf(configuration.isPreferTabs(), \"--new-tab\")\n                .add(\"-e\")\n                .addFile(configuration.single().getScriptFile());\n        launch(toExecute);\n    }\n\n    private synchronized void configureSingleInstanceMode() {\n        var cache = AppCache.getBoolean(\"konsoleInstanceOptionSet\", false);\n        if (cache) {\n            return;\n        }\n\n        var config = AppSystemInfo.ofCurrent().getUserHome().resolve(\".config\", \"konsolerc\");\n        if (!Files.exists(config)) {\n            return;\n        }\n\n        try {\n            var content = Files.readString(config);\n            var contains = content.contains(\"UseSingleInstance=true\");\n            if (!contains) {\n                var index = content.indexOf(\"[KonsoleWindow]\");\n                var augmented = index != -1\n                        ? content.replace(\"[KonsoleWindow]\", \"[KonsoleWindow]\\nUseSingleInstance=true\")\n                        : content + \"\\n\\n[KonsoleWindow]\\nUseSingleInstance=true\\n\";\n                Files.writeString(config, augmented);\n            }\n\n            AppCache.update(\"konsoleInstanceOptionSet\", true);\n        } catch (IOException e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n        }\n    }\n\n    @Override\n    public String getFlatpakId() {\n        return \"org.kde.konsole\";\n    }\n\n    @Override\n    public String getExecutable() {\n        return \"konsole\";\n    }\n\n    @Override\n    public boolean detach() {\n        return true;\n    }\n\n    @Override\n    public String getId() {\n        return \"app.konsole\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/MacOsTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\n\npublic class MacOsTerminalType implements ExternalApplicationType.MacApplication, TrackableTerminalType {\n\n    @Override\n    public TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.TABBED;\n    }\n\n    @Override\n    public boolean isRecommended() {\n        return false;\n    }\n\n    @Override\n    public boolean useColoredTitle() {\n        return true;\n    }\n\n    @Override\n    public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n        LocalShell.getShell()\n                .executeSimpleCommand(CommandBuilder.of()\n                        .add(\"open\", \"-a\")\n                        .addQuoted(\"Terminal.app\")\n                        .addFile(configuration.single().getScriptFile()));\n    }\n\n    @Override\n    public int getProcessHierarchyOffset() {\n        return 2;\n    }\n\n    @Override\n    public String getApplicationName() {\n        return \"Terminal\";\n    }\n\n    @Override\n    public String getId() {\n        return \"app.macosTerminal\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/MobaXTermTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.core.AppLocalTemp;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.util.*;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Optional;\n\npublic class MobaXTermTerminalType implements ExternalApplicationType.WindowsType, ExternalTerminalType {\n\n    @Override\n    public TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.TABBED;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://mobaxterm.mobatek.net/\";\n    }\n\n    @Override\n    public boolean isRecommended() {\n        return false;\n    }\n\n    @Override\n    public boolean useColoredTitle() {\n        return true;\n    }\n\n    @Override\n    public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n        SshLocalBridge.init();\n        var b = SshLocalBridge.get();\n        var abs = b.getIdentityKey().toAbsolutePath();\n        var drivePath = \"/drives/\" + abs.getRoot().toString().substring(0, 1).toLowerCase() + \"/\"\n                + abs.getRoot().relativize(abs).toString().replaceAll(\"\\\\\\\\\", \"/\");\n        var winPath = b.getIdentityKey().toString().replaceAll(\"\\\\\\\\\", \"\\\\\\\\\\\\\\\\\");\n        var command = CommandBuilder.of()\n                .add(\"ssh\")\n                .addQuoted(b.getUser() + \"@localhost\")\n                .add(\"-i\")\n                .add(\"\\\"$(cygpath -u \\\"\" + winPath + \"\\\" || echo \\\"\" + drivePath + \"\\\")\\\"\")\n                .add(\"-p\")\n                .add(\"\" + b.getPort());\n        // Don't use local shell to build as it uses cygwin\n        var rawCommand = command.buildSimple();\n        var script = AppLocalTemp.getLocalTempDataDirectory().resolve(\"mobaxpipe.sh\");\n        Files.writeString(Path.of(script.toString()), \"#!/usr/bin/env bash\\n\" + rawCommand);\n        var fixedFile = script.toString().replaceAll(\"\\\\\\\\\", \"/\").replaceAll(\"\\\\s\", \"\\\\$0\");\n        launch(CommandBuilder.of().add(\"-newtab\").add(fixedFile));\n    }\n\n    @Override\n    public boolean detach() {\n        return false;\n    }\n\n    @Override\n    public String getExecutable() {\n        return \"MobaXterm\";\n    }\n\n    @Override\n    public Optional<Path> determineInstallation() {\n        try {\n            var r = WindowsRegistry.local()\n                    .readStringValueIfPresent(\n                            WindowsRegistry.HKEY_LOCAL_MACHINE, \"SOFTWARE\\\\Classes\\\\mobaxterm\\\\DefaultIcon\");\n            return r.map(Path::of);\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).omit().handle();\n            return Optional.empty();\n        }\n    }\n\n    @Override\n    public String getId() {\n        return \"app.mobaXterm\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/OhMyPoshTerminalPrompt.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.process.*;\nimport io.xpipe.app.util.GithubReleaseDownloader;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.property.Property;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Getter;\nimport lombok.ToString;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@Getter\n@SuperBuilder\n@ToString\n@Jacksonized\n@JsonTypeName(\"ohmyposh\")\npublic class OhMyPoshTerminalPrompt extends ConfigFileTerminalPrompt {\n\n    @SuppressWarnings(\"unused\")\n    public static OptionsBuilder createOptions(Property<OhMyPoshTerminalPrompt> p) {\n        return createOptions(\n                p, s -> OhMyPoshTerminalPrompt.builder().configuration(s).build());\n    }\n\n    @SuppressWarnings(\"unused\")\n    public static OhMyPoshTerminalPrompt createDefault() {\n        return OhMyPoshTerminalPrompt.builder().configuration(\"\"\"\n                                                              {\n                                                                \"$schema\": \"https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json\",\n                                                                \"blocks\": [\n                                                                  {\n                                                                    \"segments\": [\n                                                                      {\n                                                                        \"foreground\": \"#007ACC\",\n                                                                        \"template\": \" {{ .CurrentDate | date .Format }} \",\n                                                                        \"properties\": {\n                                                                          \"time_format\": \"15:04:05\"\n                                                                        },\n                                                                        \"style\": \"plain\",\n                                                                        \"type\": \"time\"\n                                                                      }\n                                                                    ],\n                                                                    \"type\": \"rprompt\"\n                                                                  },\n                                                                  {\n                                                                    \"alignment\": \"left\",\n                                                                    \"newline\": true,\n                                                                    \"segments\": [\n                                                                      {\n                                                                        \"background\": \"#ffb300\",\n                                                                        \"foreground\": \"#ffffff\",\n                                                                        \"leading_diamond\": \"\",\n                                                                        \"template\": \" {{ .UserName }} \",\n                                                                        \"style\": \"diamond\",\n                                                                        \"trailing_diamond\": \"\",\n                                                                        \"type\": \"session\"\n                                                                      },\n                                                                      {\n                                                                        \"background\": \"#61AFEF\",\n                                                                        \"foreground\": \"#ffffff\",\n                                                                        \"powerline_symbol\": \"\",\n                                                                        \"template\": \" {{ .Path }} \",\n                                                                        \"properties\": {\n                                                                          \"style\": \"folder\"\n                                                                        },\n                                                                        \"exclude_folders\": [\n                                                                          \"/super/secret/project\"\n                                                                        ],\n                                                                        \"style\": \"powerline\",\n                                                                        \"type\": \"path\"\n                                                                      },\n                                                                      {\n                                                                        \"background\": \"#2e9599\",\n                                                                        \"background_templates\": [\n                                                                          \"{{ if or (.Working.Changed) (.Staging.Changed) }}#f36943{{ end }}\",\n                                                                          \"{{ if and (gt .Ahead 0) (gt .Behind 0) }}#a8216b{{ end }}\",\n                                                                          \"{{ if gt .Ahead 0 }}#35b5ff{{ end }}\",\n                                                                          \"{{ if gt .Behind 0 }}#f89cfa{{ end }}\"\n                                                                        ],\n                                                                        \"foreground\": \"#193549\",\n                                                                        \"foreground_templates\": [\n                                                                          \"{{ if and (gt .Ahead 0) (gt .Behind 0) }}#ffffff{{ end }}\"\n                                                                        ],\n                                                                        \"powerline_symbol\": \"\",\n                                                                        \"template\": \" {{ .HEAD }}{{if .BranchStatus }} {{ .BranchStatus }}{{ end }} \",\n                                                                        \"properties\": {\n                                                                          \"branch_template\": \"{{ trunc 25 .Branch }}\",\n                                                                          \"fetch_status\": true\n                                                                        },\n                                                                        \"style\": \"powerline\",\n                                                                        \"type\": \"git\"\n                                                                      },\n                                                                      {\n                                                                        \"background\": \"#00897b\",\n                                                                        \"background_templates\": [\n                                                                          \"{{ if gt .Code 0 }}#e91e63{{ end }}\"\n                                                                        ],\n                                                                        \"foreground\": \"#ffffff\",\n                                                                        \"template\": \"<parentBackground></>  \",\n                                                                        \"properties\": {\n                                                                          \"always_enabled\": true\n                                                                        },\n                                                                        \"style\": \"diamond\",\n                                                                        \"trailing_diamond\": \"\",\n                                                                        \"type\": \"status\"\n                                                                      }\n                                                                    ],\n                                                                    \"type\": \"prompt\"\n                                                                  }\n                                                                ],\n                                                                \"final_space\": true,\n                                                                \"version\": 3\n                                                              }\n                                                              \"\"\").build();\n    }\n\n    @Override\n    public String getDocsLink() {\n        return \"https://ohmyposh.dev/docs\";\n    }\n\n    @Override\n    public String getId() {\n        return \"oh-my-posh\";\n    }\n\n    @Override\n    public void checkCanInstall(ShellControl sc) throws Exception {\n        if (sc.getOsType() != OsType.WINDOWS) {\n            CommandSupport.isInPathOrThrow(sc, \"curl\");\n        }\n    }\n\n    @Override\n    public boolean checkIfInstalled(ShellControl sc) throws Exception {\n        if (sc.getShellDialect() == ShellDialects.CMD && !ClinkHelper.checkIfInstalled(sc)) {\n            return false;\n        }\n\n        if (sc.view().findProgram(\"oh-my-posh\").isPresent()) {\n            return true;\n        }\n\n        var extension = sc.getOsType() == OsType.WINDOWS ? \".exe\" : \"\";\n        return sc.view().fileExists(getBinaryDirectory(sc).join(\"oh-my-posh\" + extension));\n    }\n\n    @Override\n    public void install(ShellControl sc) throws Exception {\n        if (sc.getShellDialect() == ShellDialects.CMD) {\n            ClinkHelper.install(sc);\n        }\n\n        var dir = getBinaryDirectory(sc);\n        sc.view().mkdir(dir);\n        if (sc.getOsType() == OsType.WINDOWS) {\n            var file = GithubReleaseDownloader.getDownloadTempFile(\n                    \"JanDeDobbeleer/oh-my-posh\", \"posh-windows-amd64.exe\", s -> s.equals(\"posh-windows-amd64.exe\"));\n            sc.view().transferLocalFile(file, dir.join(\"oh-my-posh.exe\"));\n        } else {\n            var configDir = getConfigurationDirectory(sc);\n            sc.command(\"curl -s https://ohmyposh.dev/install.sh | bash -s -- -d \\\"\" + dir + \"\\\" -t \\\"\" + configDir\n                            + \"\\\"\")\n                    .execute();\n        }\n    }\n\n    @Override\n    public List<ShellDialect> getSupportedDialects() {\n        return List.of(\n                ShellDialects.BASH,\n                ShellDialects.ZSH,\n                ShellDialects.FISH,\n                ShellDialects.CMD,\n                ShellDialects.POWERSHELL,\n                ShellDialects.POWERSHELL_CORE);\n    }\n\n    @Override\n    protected String getConfigFileExtension() {\n        return \"json\";\n    }\n\n    @Override\n    protected ShellScript setupTerminalCommand(ShellControl shellControl, FilePath config) throws Exception {\n        var lines = new ArrayList<String>();\n        var dialect = shellControl.getOriginalShellDialect();\n        if (dialect == ShellDialects.CMD) {\n            var configDir = getConfigurationDirectory(shellControl);\n            shellControl.view().mkdir(configDir);\n            var configFile = configDir.join(\"oh-my-posh.lua\");\n            if (!shellControl.view().fileExists(configFile)) {\n                shellControl.view().writeTextFile(configFile, \"load(io.popen('oh-my-posh init cmd'):read(\\\"*a\\\"))()\");\n            }\n\n            lines.add(dialect.addToPathVariableCommand(\n                    List.of(ClinkHelper.getTargetDir(shellControl).toString()), false));\n            lines.add(\"clink inject --quiet --profile \\\"\" + configDir + \"\\\"\");\n        } else {\n            var configArg = config != null ? \" --config \\\"\" + config + \"\\\"\" : \"\";\n            if (ShellDialects.isPowershell(shellControl)) {\n                lines.add(\"& ([ScriptBlock]::Create((oh-my-posh init $(oh-my-posh get shell) --print\" + configArg\n                        + \") -join \\\"`n\\\"))\");\n            } else if (dialect == ShellDialects.FISH) {\n                lines.add(\"oh-my-posh init fish\" + configArg + \" | source\");\n            } else {\n                lines.add(\"eval \\\"$(oh-my-posh init \" + dialect.getId() + configArg + \")\\\"\");\n            }\n        }\n        return ShellScript.lines(lines);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/OhMyZshTerminalPrompt.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.process.CommandSupport;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.ShellDialect;\nimport io.xpipe.app.process.ShellDialects;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.core.FilePath;\n\nimport javafx.beans.property.Property;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Getter;\nimport lombok.ToString;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\n\n@Getter\n@SuperBuilder\n@ToString\n@Jacksonized\n@JsonTypeName(\"ohmyzsh\")\npublic class OhMyZshTerminalPrompt extends ConfigFileTerminalPrompt {\n\n    @SuppressWarnings(\"unused\")\n    public static OptionsBuilder createOptions(Property<OhMyZshTerminalPrompt> p) {\n        return createOptions(\n                p, s -> OhMyZshTerminalPrompt.builder().configuration(s).build());\n    }\n\n    @SuppressWarnings(\"unused\")\n    public static OhMyZshTerminalPrompt createDefault() {\n        return OhMyZshTerminalPrompt.builder().configuration(\"\"\"\n                                                             # Set name of the theme to load --- if set to \"random\", it will\n                                                             # load a random theme each time Oh My Zsh is loaded, in which case,\n                                                             # to know which specific one was loaded, run: echo $RANDOM_THEME\n                                                             # See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes\n                                                             ZSH_THEME=\"robbyrussell\"\n\n                                                             # Set list of themes to pick from when loading at random\n                                                             # Setting this variable when ZSH_THEME=random will cause zsh to load\n                                                             # a theme from this variable instead of looking in $ZSH/themes/\n                                                             # If set to an empty array, this variable will have no effect.\n                                                             # ZSH_THEME_RANDOM_CANDIDATES=( \"robbyrussell\" \"agnoster\" )\n\n                                                             # Uncomment the following line to use case-sensitive completion.\n                                                             # CASE_SENSITIVE=\"true\"\n\n                                                             # Uncomment the following line to use hyphen-insensitive completion.\n                                                             # Case-sensitive completion must be off. _ and - will be interchangeable.\n                                                             # HYPHEN_INSENSITIVE=\"true\"\n\n                                                             # Uncomment one of the following lines to change the auto-update behavior\n                                                             # zstyle ':omz:update' mode disabled  # disable automatic updates\n                                                             # zstyle ':omz:update' mode auto      # update automatically without asking\n                                                             # zstyle ':omz:update' mode reminder  # just remind me to update when it's time\n\n                                                             # Uncomment the following line to change how often to auto-update (in days).\n                                                             # zstyle ':omz:update' frequency 13\n\n                                                             # Uncomment the following line if pasting URLs and other text is messed up.\n                                                             # DISABLE_MAGIC_FUNCTIONS=\"true\"\n\n                                                             # Uncomment the following line to disable colors in ls.\n                                                             # DISABLE_LS_COLORS=\"true\"\n\n                                                             # Uncomment the following line to disable auto-setting terminal title.\n                                                             # DISABLE_AUTO_TITLE=\"true\"\n\n                                                             # Uncomment the following line to enable command auto-correction.\n                                                             # ENABLE_CORRECTION=\"true\"\n\n                                                             # Uncomment the following line to display red dots whilst waiting for completion.\n                                                             # You can also set it to another string to have that shown instead of the default red dots.\n                                                             # e.g. COMPLETION_WAITING_DOTS=\"%F{yellow}waiting...%f\"\n                                                             # Caution: this setting can cause issues with multiline prompts in zsh < 5.7.1 (see #5765)\n                                                             # COMPLETION_WAITING_DOTS=\"true\"\n\n                                                             # Uncomment the following line if you want to disable marking untracked files\n                                                             # under VCS as dirty. This makes repository status check for large repositories\n                                                             # much, much faster.\n                                                             # DISABLE_UNTRACKED_FILES_DIRTY=\"true\"\n\n                                                             # Uncomment the following line if you want to change the command execution time\n                                                             # stamp shown in the history command output.\n                                                             # You can set one of the optional three formats:\n                                                             # \"mm/dd/yyyy\"|\"dd.mm.yyyy\"|\"yyyy-mm-dd\"\n                                                             # or set a custom format using the strftime function format specifications,\n                                                             # see 'man strftime' for details.\n                                                             # HIST_STAMPS=\"mm/dd/yyyy\"\n\n                                                             # Would you like to use another custom folder than $ZSH/custom?\n                                                             # ZSH_CUSTOM=/path/to/new-custom-folder\n\n                                                             # Which plugins would you like to load?\n                                                             # Standard plugins can be found in $ZSH/plugins/\n                                                             # Custom plugins may be added to $ZSH_CUSTOM/plugins/\n                                                             # Example format: plugins=(rails git textmate ruby lighthouse)\n                                                             # Add wisely, as too many plugins slow down shell startup.\n                                                             plugins=(git)\n                                                             \"\"\").build();\n    }\n\n    @Override\n    public String getDocsLink() {\n        return \"https://github.com/ohmyzsh/ohmyzsh\";\n    }\n\n    @Override\n    public String getId() {\n        return \"oh-my-zsh\";\n    }\n\n    @Override\n    public void checkCanInstall(ShellControl sc) throws Exception {\n        CommandSupport.isInPathOrThrow(sc, \"curl\");\n    }\n\n    @Override\n    public boolean checkIfInstalled(ShellControl sc) throws Exception {\n        var configDir = getConfigurationDirectory(sc);\n        return sc.view().fileExists(configDir.join(\"oh-my-zsh.sh\"));\n    }\n\n    @Override\n    public void install(ShellControl sc) throws Exception {\n        var configDir = getConfigurationDirectory(sc);\n        sc.view().deleteDirectory(configDir);\n        sc.command(\n                        \"KEEP_ZSHRC=yes ZSH=\\\"\" + configDir\n                                + \"\\\" sh -c \\\"$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)\\\" \\\"\\\" --unattended\")\n                .execute();\n    }\n\n    @Override\n    public List<ShellDialect> getSupportedDialects() {\n        return List.of(ShellDialects.ZSH);\n    }\n\n    @Override\n    protected FilePath getTargetConfigFile(ShellControl shellControl) throws Exception {\n        FilePath configFile =\n                getConfigurationDirectory(shellControl).join(getId() + \"-custom.\" + getConfigFileExtension());\n        return configFile;\n    }\n\n    @Override\n    protected String getConfigFileExtension() {\n        return \"sh\";\n    }\n\n    @Override\n    protected ShellScript setupTerminalCommand(ShellControl shellControl, FilePath config) throws Exception {\n        var script = config != null ? shellControl.view().readTextFile(config) : \"\";\n        var fixed = script != null\n                ? script.replaceAll(\"source \\\\$ZSH/oh-my-zsh.sh\", \"\")\n                        .replaceAll(\"export ZSH=\\\"\\\\$HOME/.oh-my-zsh\\\"\", \"\")\n                : null;\n        return ShellScript.lines(\n                \"export ZSH=\\\"\" + getConfigurationDirectory(shellControl) + \"\\\"\", fixed, \"source $ZSH/oh-my-zsh.sh\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/PowerShellTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ShellDialects;\n\nimport java.nio.charset.StandardCharsets;\nimport java.util.Base64;\n\npublic class PowerShellTerminalType implements ExternalApplicationType.PathApplication, TrackableTerminalType {\n\n    @Override\n    public TerminalDockMode getDockMode() {\n        return TerminalDockMode.WITH_BORDER;\n    }\n\n    @Override\n    public TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.NEW_WINDOW;\n    }\n\n    @Override\n    public boolean isRecommended() {\n        return false;\n    }\n\n    @Override\n    public boolean useColoredTitle() {\n        return false;\n    }\n\n    @Override\n    public boolean supportsEscapes() {\n        return false;\n    }\n\n    @Override\n    public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n        launch(toCommand(configuration));\n    }\n\n    @Override\n    public int getProcessHierarchyOffset() {\n        var powershell = ShellDialects.isPowershell(LocalShell.getDialect());\n        return powershell ? -1 : 0;\n    }\n\n    protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {\n        var pane = configuration.single();\n        if (pane.getScriptDialect() == ShellDialects.POWERSHELL) {\n            return CommandBuilder.of()\n                    .add(\"-ExecutionPolicy\", \"Bypass\")\n                    .add(\"-File\")\n                    .addQuoted(pane.getScriptFile().toString());\n        }\n\n        return CommandBuilder.of()\n                .add(\"-ExecutionPolicy\", \"Bypass\")\n                .add(\"-EncodedCommand\")\n                .add(sc -> {\n                    var base64 = Base64.getEncoder()\n                            .encodeToString(\n                                    pane.getDialectLaunchCommand().buildBase(sc).getBytes(StandardCharsets.UTF_16LE));\n                    return \"\\\"\" + base64 + \"\\\"\";\n                });\n    }\n\n    @Override\n    public String getExecutable() {\n        return \"powershell.exe\";\n    }\n\n    @Override\n    public boolean detach() {\n        return true;\n    }\n\n    @Override\n    public String getId() {\n        return \"app.powershell\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/PtyxisTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.util.FlatpakCache;\n\npublic class PtyxisTerminalType implements ExternalApplicationType.LinuxApplication, TrackableTerminalType {\n\n    @Override\n    public TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.NEW_WINDOW_OR_TABBED;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://gitlab.gnome.org/chergert/ptyxis\";\n    }\n\n    @Override\n    public boolean isRecommended() {\n        return true;\n    }\n\n    @Override\n    public boolean useColoredTitle() {\n        return true;\n    }\n\n    @Override\n    public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n        var toExecute = CommandBuilder.of()\n                .addIf(configuration.isPreferTabs(), \"--tab\")\n                .addIf(!configuration.isPreferTabs(), \"--new-window\")\n                .add(\"--title\")\n                .addQuoted(configuration.getColoredTitle())\n                .add(\"--\")\n                .add(configuration.single().getDialectLaunchCommand());\n        launch(toExecute);\n    }\n\n    @Override\n    public String getExecutable() {\n        return \"ptyxis\";\n    }\n\n    @Override\n    public boolean detach() {\n        return true;\n    }\n\n    @Override\n    public String getId() {\n        return \"app.ptyxis\";\n    }\n\n    @Override\n    public String getFlatpakId() throws Exception {\n        var dev = FlatpakCache.getApp(\"org.gnome.Ptyxis.Devel\");\n        if (dev.isPresent()) {\n            return \"org.gnome.Ptyxis.Devel\";\n        }\n\n        return \"app.devsuite.Ptyxis\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/PwshTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\n\nimport java.nio.charset.StandardCharsets;\nimport java.util.Base64;\n\npublic class PwshTerminalType implements ExternalApplicationType.PathApplication, TrackableTerminalType {\n\n    @Override\n    public TerminalDockMode getDockMode() {\n        return TerminalDockMode.WITH_BORDER;\n    }\n\n    @Override\n    public TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.NEW_WINDOW;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell?view=powershell-7.4\";\n    }\n\n    @Override\n    public boolean isRecommended() {\n        return false;\n    }\n\n    @Override\n    public boolean useColoredTitle() {\n        return false;\n    }\n\n    @Override\n    public boolean supportsEscapes() {\n        return false;\n    }\n\n    @Override\n    public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n        var b = CommandBuilder.of()\n                .add(\"-ExecutionPolicy\", \"Bypass\")\n                .add(\"-EncodedCommand\")\n                .add(sc -> {\n                    // Fix for https://github.com/PowerShell/PowerShell/issues/18530#issuecomment-1325691850\n                    var c = \"$env:PSModulePath=\\\"\\\";\"\n                            + configuration.single().getDialectLaunchCommand().buildBase(sc);\n                    var base64 = Base64.getEncoder().encodeToString(c.getBytes(StandardCharsets.UTF_16LE));\n                    return \"\\\"\" + base64 + \"\\\"\";\n                });\n        launch(b);\n    }\n\n    @Override\n    public String getExecutable() {\n        return \"pwsh.exe\";\n    }\n\n    @Override\n    public boolean detach() {\n        return true;\n    }\n\n    @Override\n    public String getId() {\n        return \"app.pwsh\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/ScreenTerminalMultiplexer.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.process.CommandSupport;\nimport io.xpipe.app.process.ScriptHelper;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.ShellScript;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.ArrayList;\n\n@Builder\n@Jacksonized\n@JsonTypeName(\"screen\")\npublic class ScreenTerminalMultiplexer implements TerminalMultiplexer {\n\n    @Override\n    public boolean supportsSplitView() {\n        return false;\n    }\n\n    @Override\n    public String getDocsLink() {\n        return \"https://www.gnu.org/software/screen/manual/screen.html\";\n    }\n\n    @Override\n    public void checkSupported(ShellControl sc) throws Exception {\n        CommandSupport.isInPathOrThrow(sc, \"screen\");\n    }\n\n    @Override\n    public ShellScript launchForExistingSession(ShellControl control, TerminalLaunchConfiguration config) {\n        var l = new ArrayList<String>();\n        var firstCommand =\n                getCommand(control, config.single().getDialectLaunchCommand().buildSimple());\n        l.add(\"screen -S xpipe -X screen -t \\\"\" + escape(config.getCleanTitle(), true) + \"\\\" \"\n                + escape(firstCommand, false));\n        return ShellScript.lines(l);\n    }\n\n    @Override\n    public ShellScript launchNewSession(ShellControl control, TerminalLaunchConfiguration config) {\n        var list = new ArrayList<String>();\n        list.add(\"for scr in $(screen -ls | grep xpipe | awk '{print $1}'); do screen -S $scr -X quit; done\");\n\n        var firstCommand =\n                getCommand(control, config.single().getDialectLaunchCommand().buildSimple());\n        list.add(\"screen -S xpipe -t \\\"\" + escape(config.getCleanTitle(), true) + \"\\\" \" + escape(firstCommand, false));\n        return ShellScript.lines(list);\n    }\n\n    private String getCommand(ShellControl sc, String command) {\n        // Screen has a limit of 100 chars for commands\n        var effectiveCommand = command.length() > 90\n                ? ScriptHelper.createExecScript(sc, command).toString()\n                : command;\n        return effectiveCommand;\n    }\n\n    private String escape(String s, boolean quotes) {\n        var r = s.replaceAll(\"\\\\\\\\\", \"\\\\\\\\\\\\\\\\\");\n        if (quotes) {\n            r = r.replaceAll(\"\\\"\", \"\\\\\\\\\\\"\");\n        }\n        return r;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/SecureCrtTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.util.SshLocalBridge;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Optional;\n\npublic class SecureCrtTerminalType implements ExternalApplicationType.WindowsType, ExternalTerminalType {\n\n    @Override\n    public TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.TABBED;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://www.vandyke.com/products/securecrt/\";\n    }\n\n    @Override\n    public boolean isRecommended() {\n        return false;\n    }\n\n    @Override\n    public boolean useColoredTitle() {\n        return false;\n    }\n\n    @Override\n    public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n        SshLocalBridge.init();\n        var b = SshLocalBridge.get();\n        var command = CommandBuilder.of()\n                .add(\"/T\")\n                .add(\"/SSH2\", \"/ACCEPTHOSTKEYS\", \"/I\")\n                .addFile(b.getIdentityKey().toString())\n                .add(\"/P\", \"\" + b.getPort())\n                .add(\"/L\")\n                .addQuoted(b.getUser())\n                .add(\"localhost\");\n        launch(command);\n    }\n\n    @Override\n    public boolean detach() {\n        return false;\n    }\n\n    @Override\n    public String getExecutable() {\n        return \"SecureCRT\";\n    }\n\n    @Override\n    public Optional<Path> determineInstallation() {\n        var file = AppSystemInfo.ofWindows().getProgramFiles().resolve(\"VanDyke Software\\\\SecureCRT\\\\SecureCRT.exe\");\n        if (!Files.exists(file)) {\n            return Optional.empty();\n        }\n\n        return Optional.of(file);\n    }\n\n    @Override\n    public String getId() {\n        return \"app.secureCrt\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/StarshipTerminalPrompt.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.process.*;\nimport io.xpipe.app.util.GithubReleaseDownloader;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.property.Property;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Getter;\nimport lombok.ToString;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.nio.file.FileSystems;\nimport java.util.ArrayList;\nimport java.util.List;\n\n@Getter\n@SuperBuilder\n@ToString\n@Jacksonized\n@JsonTypeName(\"starship\")\npublic class StarshipTerminalPrompt extends ConfigFileTerminalPrompt {\n\n    @SuppressWarnings(\"unused\")\n    public static OptionsBuilder createOptions(Property<StarshipTerminalPrompt> p) {\n        return createOptions(\n                p, s -> StarshipTerminalPrompt.builder().configuration(s).build());\n    }\n\n    @SuppressWarnings(\"unused\")\n    public static StarshipTerminalPrompt createDefault() {\n        return StarshipTerminalPrompt.builder().configuration(\"\"\"\n                                                              # Get editor completions based on the config schema\n                                                              \"$schema\" = 'https://starship.rs/config-schema.json'\n\n                                                              # Inserts a blank line between shell prompts\n                                                              add_newline = true\n\n                                                              # Replace the '❯' symbol in the prompt with '➜'\n                                                              [character] # The name of the module we are configuring is 'character'\n                                                              success_symbol = '[➜](bold green)' # The 'success_symbol' segment is being set to '➜' with the color 'bold green'\n\n                                                              # Disable the package module, hiding it from the prompt completely\n                                                              [package]\n                                                              disabled = true\n                                                              \"\"\").build();\n    }\n\n    @Override\n    protected String getConfigFileExtension() {\n        return \"toml\";\n    }\n\n    @Override\n    protected ShellScript setupTerminalCommand(ShellControl shellControl, FilePath config) throws Exception {\n        var lines = new ArrayList<String>();\n        var dialect = shellControl.getOriginalShellDialect();\n        if (config != null) {\n            lines.add(dialect.getSetEnvironmentVariableCommand(\"STARSHIP_CONFIG\", config.toString()));\n        }\n        if (dialect == ShellDialects.CMD) {\n            var configDir = getConfigurationDirectory(shellControl);\n            shellControl.view().mkdir(configDir);\n            var configFile = configDir.join(\"starship.lua\");\n            if (!shellControl.view().fileExists(configFile)) {\n                shellControl.view().writeTextFile(configFile, \"load(io.popen('starship init cmd'):read(\\\"*a\\\"))()\");\n            }\n\n            lines.add(dialect.addToPathVariableCommand(\n                    List.of(ClinkHelper.getTargetDir(shellControl).toString()), false));\n            lines.add(\"clink inject --quiet --profile \\\"\" + configDir + \"\\\"\");\n        } else {\n            if (ShellDialects.isPowershell(shellControl)) {\n                lines.add(\"Invoke-Expression (&starship init powershell)\");\n            } else if (dialect == ShellDialects.FISH) {\n                lines.add(\"starship init fish | source\");\n            } else {\n                lines.add(\"eval \\\"$(starship init \" + dialect.getId() + \")\\\"\");\n            }\n        }\n        return ShellScript.lines(lines);\n    }\n\n    @Override\n    public String getDocsLink() {\n        return \"https://starship.rs/guide/\";\n    }\n\n    @Override\n    public String getId() {\n        return \"starship\";\n    }\n\n    @Override\n    public void checkCanInstall(ShellControl sc) throws Exception {\n        if (sc.getOsType() != OsType.WINDOWS) {\n            CommandSupport.isInPathOrThrow(sc, \"curl\");\n        }\n    }\n\n    @Override\n    public boolean checkIfInstalled(ShellControl sc) throws Exception {\n        if (sc.getShellDialect() == ShellDialects.CMD && !ClinkHelper.checkIfInstalled(sc)) {\n            return false;\n        }\n\n        if (sc.view().findProgram(\"starship\").isPresent()) {\n            return true;\n        }\n\n        var extension = sc.getOsType() == OsType.WINDOWS ? \".exe\" : \"\";\n        return sc.view().fileExists(getBinaryDirectory(sc).join(\"starship\" + extension));\n    }\n\n    @Override\n    public void install(ShellControl sc) throws Exception {\n        if (sc.getShellDialect() == ShellDialects.CMD) {\n            ClinkHelper.install(sc);\n        }\n\n        var dir = getBinaryDirectory(sc);\n        sc.view().mkdir(dir);\n        if (sc.getOsType() == OsType.WINDOWS) {\n            var file = GithubReleaseDownloader.getDownloadTempFile(\n                    \"starship/starship\",\n                    \"starship-x86_64-pc-windows-msvc.zip\",\n                    s -> s.equals(\"starship-x86_64-pc-windows-msvc.zip\"));\n            try (var fs = FileSystems.newFileSystem(file)) {\n                var exeFile = fs.getPath(\"starship.exe\");\n                sc.view().transferLocalFile(exeFile, dir.join(\"starship.exe\"));\n            }\n        } else {\n            sc.command(\"curl -sS https://starship.rs/install.sh | sh /dev/stdin -y --bin-dir \\\"\" + dir + \"\\\"\")\n                    .execute();\n        }\n    }\n\n    @Override\n    public List<ShellDialect> getSupportedDialects() {\n        return List.of(\n                ShellDialects.BASH,\n                ShellDialects.ZSH,\n                ShellDialects.FISH,\n                ShellDialects.CMD,\n                ShellDialects.POWERSHELL,\n                ShellDialects.POWERSHELL_CORE);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TabbyTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ShellDialects;\nimport io.xpipe.app.process.TerminalInitFunction;\nimport io.xpipe.app.util.WindowsRegistry;\n\nimport java.nio.file.Path;\nimport java.util.Optional;\n\npublic interface TabbyTerminalType extends ExternalTerminalType, TrackableTerminalType {\n\n    ExternalTerminalType TABBY_WINDOWS = new Windows();\n    ExternalTerminalType TABBY_MAC_OS = new MacOs();\n\n    @Override\n    default TerminalInitFunction additionalInitCommands() {\n        //        return TerminalInitFunction.of(sc -> {\n        //            if (sc.getShellDialect() == ShellDialects.ZSH) {\n        //                return \"export PS1=\\\"$PS1\\\\[\\\\e]1337;CurrentDir=\\\"'$(pwd)\\\\a\\\\]'\";\n        //            }\n        //            if (sc.getShellDialect() == ShellDialects.BASH) {\n        //                return \"precmd () { echo -n \\\"\\\\x1b]1337;CurrentDir=$(pwd)\\\\x07\\\" }\";\n        //            }\n        //            if (sc.getShellDialect() == ShellDialects.FISH) {\n        //                return \"\"\"\n        //                       function __tabby_working_directory_reporting --on-event fish_prompt\n        //                           echo -en \"\\\\e]1337;CurrentDir=$PWD\\\\x7\"\n        //                       end\n        //                       \"\"\";\n        //            }\n        //            return null;\n        //        });\n        return TerminalInitFunction.none();\n    }\n\n    @Override\n    default TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.TABBED;\n    }\n\n    @Override\n    default String getWebsite() {\n        return \"https://tabby.sh\";\n    }\n\n    @Override\n    default boolean useColoredTitle() {\n        return true;\n    }\n\n    class Windows implements ExternalApplicationType.WindowsType, TabbyTerminalType {\n\n        @Override\n        public int getProcessHierarchyOffset() {\n            return 1;\n        }\n\n        @Override\n        public boolean isRecommended() {\n            return false;\n        }\n\n        @Override\n        public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n            var pane = configuration.single();\n            // Tabby has a very weird handling of output, even detaching with start does not prevent it from printing\n            if (pane.getScriptDialect() == ShellDialects.CMD) {\n                // It also freezes with any other input than .bat files, why?\n                launch(CommandBuilder.of()\n                        .add(\"run\")\n                        .addFile(pane.getScriptFile())\n                        .discardAllOutput());\n            } else {\n                // This is probably not going to work as it does not launch a bat file\n                launch(CommandBuilder.of()\n                        .add(\"run\")\n                        .add(sc -> pane.getDialectLaunchCommand().buildFull(sc).replaceFirst(\"\\\\.exe\", \"\"))\n                        .discardAllOutput());\n            }\n        }\n\n        @Override\n        public boolean detach() {\n            return true;\n        }\n\n        @Override\n        public String getExecutable() {\n            return \"Tabby.exe\";\n        }\n\n        @Override\n        public Optional<Path> determineInstallation() {\n            var perUser = WindowsRegistry.local()\n                    .readStringValueIfPresent(\n                            WindowsRegistry.HKEY_CURRENT_USER,\n                            \"SOFTWARE\\\\71445fac-d6ef-5436-9da7-5a323762d7f5\",\n                            \"InstallLocation\")\n                    .map(p -> p + \"\\\\Tabby.exe\")\n                    .map(Path::of);\n            if (perUser.isPresent()) {\n                return perUser;\n            }\n\n            var systemWide = WindowsRegistry.local()\n                    .readStringValueIfPresent(\n                            WindowsRegistry.HKEY_LOCAL_MACHINE,\n                            \"SOFTWARE\\\\71445fac-d6ef-5436-9da7-5a323762d7f5\",\n                            \"InstallLocation\")\n                    .map(p -> p + \"\\\\Tabby.exe\")\n                    .map(Path::of);\n            return systemWide;\n        }\n\n        @Override\n        public String getId() {\n            return \"app.tabby\";\n        }\n    }\n\n    class MacOs implements ExternalApplicationType.MacApplication, TabbyTerminalType {\n\n        @Override\n        public boolean isRecommended() {\n            return true;\n        }\n\n        @Override\n        public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n            LocalShell.getShell()\n                    .executeSimpleCommand(CommandBuilder.of()\n                            .add(\"open\", \"-a\")\n                            .addQuoted(\"Tabby.app\")\n                            .add(\"-n\", \"--args\", \"run\")\n                            .addFile(configuration.single().getScriptFile()));\n        }\n\n        @Override\n        public String getApplicationName() {\n            return \"Tabby\";\n        }\n\n        @Override\n        public String getId() {\n            return \"app.tabby\";\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TerminalDockBrowserComp.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.comp.base.LoadingIconComp;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.window.AppMainWindow;\nimport io.xpipe.app.platform.NativeWinWindowControl;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.util.GlobalTimer;\n\nimport javafx.application.Platform;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.value.ChangeListener;\nimport javafx.beans.value.ObservableBooleanValue;\nimport javafx.beans.value.ObservableValue;\nimport javafx.css.PseudoClass;\nimport javafx.event.EventHandler;\nimport javafx.geometry.Bounds;\nimport javafx.geometry.Pos;\nimport javafx.scene.Cursor;\nimport javafx.scene.Parent;\nimport javafx.scene.control.Label;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\nimport javafx.stage.WindowEvent;\n\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.time.Duration;\nimport java.util.concurrent.atomic.AtomicReference;\n\npublic class TerminalDockBrowserComp extends SimpleRegionBuilder {\n\n    private final TerminalDockView model;\n    private final ObservableBooleanValue opened;\n\n    public TerminalDockBrowserComp(TerminalDockView model, ObservableBooleanValue opened) {\n        this.model = model;\n        this.opened = opened;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var label = new Label();\n        AppFontSizes.xl(label);\n        var stack = new StackPane(label);\n        stack.setAlignment(Pos.CENTER);\n        stack.setCursor(Cursor.HAND);\n        stack.boundsInParentProperty().addListener((observable, oldValue, newValue) -> {\n            update(stack);\n        });\n        stack.setOnMouseClicked(event -> {\n            model.attach();\n            event.consume();\n        });\n        stack.getStyleClass().add(\"terminal-dock-comp\");\n        stack.setMinWidth(100);\n        stack.setMinHeight(100);\n        setupListeners(stack);\n\n        opened.subscribe(v -> {\n            PlatformThread.runLaterIfNeeded(() -> {\n                label.textProperty().unbind();\n                if (v) {\n                    stack.pseudoClassStateChanged(PseudoClass.getPseudoClass(\"empty\"), false);\n                    label.textProperty().bind(AppI18n.observable(\"clickToDock\"));\n                    label.setGraphic(new FontIcon(\"mdi2d-dock-right\"));\n                } else {\n                    stack.pseudoClassStateChanged(PseudoClass.getPseudoClass(\"empty\"), true);\n                    label.textProperty().bind(AppI18n.observable(\"terminalStarting\"));\n                    if (!AppPrefs.get().performanceMode().get()) {\n                        var l = new LoadingIconComp(new SimpleBooleanProperty(true), AppFontSizes::sm).build();\n                        label.setGraphic(l);\n                    }\n                }\n            });\n        });\n\n        return stack;\n    }\n\n    private void setupListeners(StackPane stack) {\n        var s = AppMainWindow.get().getStage();\n\n        var bounds = new ChangeListener<Bounds>() {\n            @Override\n            public void changed(ObservableValue<? extends Bounds> observable, Bounds oldValue, Bounds newValue) {\n                update(stack);\n            }\n        };\n        var update = new ChangeListener<Number>() {\n            @Override\n            public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {\n                update(stack);\n            }\n        };\n        var iconified = new ChangeListener<Boolean>() {\n            @Override\n            public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {\n                if (newValue) {\n                    model.onWindowMinimize();\n                } else {\n                    model.onWindowShow();\n                }\n            }\n        };\n        var show = new EventHandler<WindowEvent>() {\n            @Override\n            public void handle(WindowEvent event) {\n                GlobalTimer.delay(() -> {\n                    Platform.runLater(() -> {\n                        update(stack);\n                    });\n                }, Duration.ofMillis(100));\n            }\n        };\n        var hide = new EventHandler<WindowEvent>() {\n            @Override\n            public void handle(WindowEvent event) {\n                model.onClose();\n            }\n        };\n        var scale = new ChangeListener<Number>() {\n            @Override\n            public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {\n                GlobalTimer.delay(() -> {\n                    Platform.runLater(() -> {\n                        update(stack);\n                    });\n                }, Duration.ofMillis(500));\n            }\n        };\n\n        var parent = new AtomicReference<Parent>();\n        stack.sceneProperty().subscribe(scene -> {\n            if (scene == null) {\n                s.xProperty().removeListener(update);\n                s.yProperty().removeListener(update);\n                s.widthProperty().removeListener(update);\n                s.heightProperty().removeListener(update);\n                s.iconifiedProperty().removeListener(iconified);\n                s.removeEventFilter(WindowEvent.WINDOW_SHOWN, show);\n                s.removeEventFilter(WindowEvent.WINDOW_HIDING, hide);\n                s.outputScaleXProperty().addListener(scale);\n                if (parent.get() != null) {\n                    parent.get().boundsInParentProperty().removeListener(bounds);\n                    parent.set(null);\n                }\n            } else {\n                s.xProperty().addListener(update);\n                s.yProperty().addListener(update);\n                s.widthProperty().addListener(update);\n                s.heightProperty().addListener(update);\n                s.iconifiedProperty().addListener(iconified);\n                s.addEventFilter(WindowEvent.WINDOW_SHOWN, show);\n                s.addEventFilter(WindowEvent.WINDOW_HIDING, hide);\n                s.outputScaleXProperty().removeListener(scale);\n                // As in practice this node is wrapped in another stack pane\n                // We have to listen to the parent bounds to actually receive bounds changes\n                stack.getParent().boundsInParentProperty().addListener(bounds);\n                parent.set(stack.getParent());\n                update(stack);\n            }\n        });\n    }\n\n    private void update(Region region) {\n        if (region.getScene() == null || region.getScene().getWindow() == null || NativeWinWindowControl.MAIN_WINDOW == null) {\n            return;\n        }\n\n        var bounds = region.localToScene(region.getBoundsInLocal());\n        var p = region.getPadding();\n        var sx = region.getScene().getWindow().getOutputScaleX();\n        var sy = region.getScene().getWindow().getOutputScaleY();\n\n        var scene =  region.getScene();\n        var windowRect = NativeWinWindowControl.MAIN_WINDOW.getBounds();\n        if (windowRect.getX() == 0.0 && windowRect.getY() == 0.0 && windowRect.getW() == 0 && windowRect.getH() == 0) {\n            return;\n        }\n\n        var xPadding = ((bounds.getMinX() + p.getLeft() + scene.getX()) * sx);\n        var yPadding = ((bounds.getMinY() + p.getTop() + scene.getY()) * sy);\n        var x = windowRect.getX() + xPadding;\n        var y = windowRect.getY() + yPadding;\n        var w = (bounds.getWidth() * sx) - p.getRight() - p.getLeft();\n        var h = (bounds.getHeight() * sy) - p.getBottom() - p.getTop();\n\n        if (x + w > windowRect.getX() + windowRect.getW()) {\n            x = windowRect.getX() + 10;\n            w = windowRect.getW() - 20;\n        }\n        if (y + h > windowRect.getY() + windowRect.getH()) {\n            y = windowRect.getY() + 10;\n            h = windowRect.getH() - 20;\n        }\n\n        model.resizeView(\n                (int) Math.round(x),\n                (int) Math.round(y),\n                (int) Math.round(w),\n                (int) Math.round(h));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TerminalDockHubComp.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.comp.SimpleRegionBuilder;\nimport io.xpipe.app.core.window.AppMainWindow;\n\nimport io.xpipe.app.platform.NativeWinWindowControl;\nimport io.xpipe.app.util.GlobalTimer;\nimport javafx.application.Platform;\nimport javafx.beans.value.ChangeListener;\nimport javafx.beans.value.ObservableValue;\nimport javafx.event.EventHandler;\nimport javafx.geometry.Bounds;\nimport javafx.scene.Parent;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.StackPane;\nimport javafx.stage.WindowEvent;\n\nimport java.time.Duration;\nimport java.util.concurrent.atomic.AtomicReference;\n\npublic class TerminalDockHubComp extends SimpleRegionBuilder {\n\n    private final TerminalDockView model;\n\n    public TerminalDockHubComp(TerminalDockView model) {\n        this.model = model;\n    }\n\n    @Override\n    protected Region createSimple() {\n        var stack = new StackPane();\n        stack.setPickOnBounds(false);\n        stack.boundsInParentProperty().addListener((observable, oldValue, newValue) -> {\n            update(stack);\n        });\n        stack.getStyleClass().add(\"terminal-dock-comp\");\n        stack.setMinWidth(100);\n        stack.setMinHeight(100);\n        setupListeners(stack);\n        return stack;\n    }\n\n    private void setupListeners(StackPane stack) {\n        var s = AppMainWindow.get().getStage();\n\n        var bounds = new ChangeListener<Bounds>() {\n            @Override\n            public void changed(ObservableValue<? extends Bounds> observable, Bounds oldValue, Bounds newValue) {\n                update(stack);\n            }\n        };\n        var scale = new ChangeListener<Number>() {\n            @Override\n            public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {\n                GlobalTimer.delay(() -> {\n                    Platform.runLater(() -> {\n                        update(stack);\n                    });\n                }, Duration.ofMillis(500));\n            }\n        };\n        var update = new ChangeListener<Number>() {\n            @Override\n            public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {\n                update(stack);\n            }\n        };\n        var iconified = new ChangeListener<Boolean>() {\n            @Override\n            public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {\n                if (newValue) {\n                    model.onWindowMinimize();\n                } else {\n                    Platform.runLater(() -> {\n                        model.onWindowShow();\n                    });\n                }\n            }\n        };\n        var show = new EventHandler<WindowEvent>() {\n            @Override\n            public void handle(WindowEvent event) {\n                GlobalTimer.delay(() -> {\n                    Platform.runLater(() -> {\n                        update(stack);\n                    });\n                }, Duration.ofMillis(100));\n            }\n        };\n        var hide = new EventHandler<WindowEvent>() {\n            @Override\n            public void handle(WindowEvent event) {\n                model.onClose();\n            }\n        };\n\n        var parent = new AtomicReference<Parent>();\n        stack.sceneProperty().subscribe(scene -> {\n            if (scene == null) {\n                s.xProperty().removeListener(update);\n                s.yProperty().removeListener(update);\n                s.widthProperty().removeListener(update);\n                s.heightProperty().removeListener(update);\n                s.iconifiedProperty().removeListener(iconified);\n                s.removeEventFilter(WindowEvent.WINDOW_SHOWN, show);\n                s.removeEventFilter(WindowEvent.WINDOW_HIDING, hide);\n                s.outputScaleXProperty().addListener(scale);\n                if (parent.get() != null) {\n                    parent.get().boundsInParentProperty().removeListener(bounds);\n                    parent.set(null);\n                }\n            } else {\n                s.xProperty().addListener(update);\n                s.yProperty().addListener(update);\n                s.widthProperty().addListener(update);\n                s.heightProperty().addListener(update);\n                s.iconifiedProperty().addListener(iconified);\n                s.outputScaleXProperty().removeListener(scale);\n                s.addEventFilter(WindowEvent.WINDOW_SHOWN, show);\n                s.addEventFilter(WindowEvent.WINDOW_HIDING, hide);\n                // As in practice this node is wrapped in another stack pane\n                // We have to listen to the parent bounds to actually receive bounds changes\n                stack.getParent().boundsInParentProperty().addListener(bounds);\n                parent.set(stack.getParent());\n                update(stack);\n            }\n        });\n    }\n\n    private void update(Region region) {\n        if (region.getScene() == null || region.getScene().getWindow() == null || NativeWinWindowControl.MAIN_WINDOW == null) {\n            return;\n        }\n\n        var bounds = region.localToScene(region.getBoundsInLocal());\n        var p = region.getPadding();\n        var sx = region.getScene().getWindow().getOutputScaleX();\n        var sy = region.getScene().getWindow().getOutputScaleY();\n\n        var scene =  region.getScene();\n        var windowRect = NativeWinWindowControl.MAIN_WINDOW.getBounds();\n        if (windowRect.getX() == 0.0 && windowRect.getY() == 0.0 && windowRect.getW() == 0 && windowRect.getH() == 0) {\n            return;\n        }\n\n        var xPadding = ((bounds.getMinX() + p.getLeft() + scene.getX()) * sx);\n        var yPadding = ((bounds.getMinY() + p.getTop() + scene.getY()) * sy);\n        var x = windowRect.getX() + xPadding;\n        var y = windowRect.getY() + yPadding;\n        var w = (bounds.getWidth() * sx) - p.getRight() - p.getLeft();\n        var h = (bounds.getHeight() * sy) - p.getBottom() - p.getTop();\n\n        if (x + w > windowRect.getX() + windowRect.getW()) {\n            x = windowRect.getX() + 10;\n            w = windowRect.getW() - 20;\n        }\n        if (y + h > windowRect.getY() + windowRect.getH()) {\n            y = windowRect.getY() + 10;\n            h = windowRect.getH() - 20;\n        }\n\n        model.resizeView(\n                (int) Math.round(x),\n                (int) Math.round(y),\n                (int) Math.round(w),\n                (int) Math.round(h));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TerminalDockHubManager.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.core.window.AppMainWindow;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.NativeWinWindowControl;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.util.GlobalTimer;\nimport io.xpipe.app.util.Rect;\nimport io.xpipe.core.OsType;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.BooleanProperty;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.collections.ListChangeListener;\n\nimport javafx.stage.Screen;\nimport lombok.Getter;\nimport org.kordamp.ikonli.Ikon;\nimport org.kordamp.ikonli.Ikonli;\nimport org.kordamp.ikonli.javafx.FontIcon;\nimport org.kordamp.ikonli.materialdesign2.MaterialDesignC;\n\nimport java.time.Duration;\nimport java.util.HashSet;\nimport java.util.Set;\nimport java.util.UUID;\n\n@Getter\npublic class TerminalDockHubManager {\n\n    public static boolean isAvailable() {\n        if (OsType.ofLocal() != OsType.WINDOWS) {\n            return false;\n        }\n\n        return true;\n    }\n\n    public static boolean isSupported() {\n        if (OsType.ofLocal() != OsType.WINDOWS) {\n            return false;\n        }\n\n        var term = AppPrefs.get().terminalType().getValue();\n        if (term == null) {\n            return false;\n        }\n\n        var tabsSupported = term.getOpenFormat() != TerminalOpenFormat.NEW_WINDOW\n                || TerminalMultiplexerManager.getEffectiveMultiplexer().isPresent();\n        if (!tabsSupported) {\n            return false;\n        }\n\n        var modeSupported = term instanceof TrackableTerminalType t && t.getDockMode() != TerminalDockMode.UNSUPPORTED;\n        if (!modeSupported) {\n            return false;\n        }\n\n        if (NativeWinWindowControl.MAIN_WINDOW == null) {\n            return false;\n        }\n\n        if (AppOperationMode.get() != AppOperationMode.GUI) {\n            return false;\n        }\n\n        if (AppMainWindow.get() == null || !AppMainWindow.get().getStage().isShowing() || AppMainWindow.get().getStage().isIconified()) {\n            return false;\n        }\n\n        return true;\n    }\n\n    private static TerminalDockHubManager INSTANCE;\n\n    public static void init() {\n        INSTANCE = new TerminalDockHubManager();\n        if (!isAvailable()) {\n            return;\n        }\n\n        INSTANCE.addLayoutListeners();\n        INSTANCE.addDialogListeners();\n\n        TerminalView.get().addListener(INSTANCE.createListener());\n\n        GlobalTimer.scheduleUntil(Duration.ofMillis(500), false, () -> {\n            INSTANCE.refreshDockStatus();\n            return false;\n        });\n    }\n\n    public static TerminalDockHubManager get() {\n        return INSTANCE;\n    }\n\n    private final Set<UUID> hubRequests = new HashSet<>();\n    private final BooleanProperty enabled = new SimpleBooleanProperty();\n    private final BooleanProperty showing = new SimpleBooleanProperty();\n    private final BooleanProperty detached = new SimpleBooleanProperty();\n    private final BooleanProperty minimized = new SimpleBooleanProperty();\n    private final TerminalDockView dockModel = new TerminalDockView(rect -> {\n        var term = AppPrefs.get().terminalType().getValue();\n        var adjust = term instanceof TrackableTerminalType t && t.getDockMode() != TerminalDockMode.BORDERLESS;\n        // Windows terminal has a tiny top bar in any scenario\n        var topAdjust = term instanceof WindowsTerminalType ? 1 : 0;\n        return adjust\n                ? new Rect(rect.getX() - 8, rect.getY() - 1 - topAdjust, rect.getW() + 16, rect.getH() + 9 + topAdjust)\n                : new Rect(rect.getX(), rect.getY() - topAdjust, rect.getW(), rect.getH() + topAdjust);\n    });\n    private final AppLayoutModel.QueueEntry queueEntry = new AppLayoutModel.QueueEntry(\n            AppI18n.observable(\"toggleTerminalDock\"), new LabelGraphic.NodeGraphic(() -> {\n                var inner = new FontIcon();\n                inner.iconCodeProperty().bind(PlatformThread.sync(Bindings.createObjectBinding(() -> {\n                    return detached.get() || minimized.get() || !showing.get() ? MaterialDesignC.CONSOLE_LINE : MaterialDesignC.CONSOLE;\n                }, detached, minimized, showing)));\n                inner.getStyleClass().add(\"graphic\");\n                inner.getStyleClass().add(\"terminal-dock-button\");\n                return inner;\n    }), () -> {\n                refreshDockStatus();\n\n                if (!enabled.get()) {\n                    return false;\n                }\n\n                if (!showing.get()) {\n                    // Run later to guarantee order of operations\n                    Platform.runLater(() -> {\n                        AppLayoutModel.get().selectConnections();\n                        showDock();\n                        attach();\n                    });\n                    return false;\n                }\n\n                if (minimized.get() || detached.get()) {\n                    attach();\n                    return false;\n                }\n\n                if (showing.get()) {\n                    hideDock();\n                    return false;\n                }\n\n                return false;\n            });\n\n    private void addDialogListeners() {\n        AppDialog.getModalOverlays().addListener((ListChangeListener<? super ModalOverlay>) c -> {\n            if (c.getList().size() > 0) {\n                INSTANCE.hideDock();\n            }\n        });\n    }\n\n    private void addLayoutListeners() {\n        var wasShowing = new SimpleBooleanProperty();\n        var wasAttached = new SimpleBooleanProperty();\n        AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> {\n            if (AppLayoutModel.get().getEntries().indexOf(newValue) == 0) {\n                if (wasShowing.get()) {\n                    INSTANCE.showDock();\n                }\n                if (wasAttached.get()) {\n                    INSTANCE.attach();\n                }\n            } else if (AppLayoutModel.get().getEntries().indexOf(oldValue) == 0) {\n                wasAttached.set(!INSTANCE.minimized.get() && !INSTANCE.detached.get() && INSTANCE.showing.get());\n                wasShowing.set(INSTANCE.showing.get());\n                INSTANCE.hideDock();\n            }\n        });\n    }\n\n    private TerminalView.Listener createListener() {\n        var listener = new TerminalView.Listener() {\n            @Override\n            public void onSessionOpened(TerminalView.ShellSession session) {\n                if (!hubRequests.contains(session.getRequest())) {\n                    return;\n                }\n\n                var controllable = session.getTerminal().controllable();\n                if (controllable.isEmpty()) {\n                    return;\n                }\n\n                var term = controllable.get().getTerminalType();\n                if (term instanceof TrackableTerminalType t) {\n                    if (t.getDockMode() == TerminalDockMode.UNSUPPORTED) {\n                        return;\n                    }\n                }\n\n                var dock = !detached.get();\n                dockModel.trackTerminal(controllable.get(), dock);\n                dockModel.closeOtherTerminals(session.getRequest());\n                enableDock();\n            }\n\n            @Override\n            public void onSessionClosed(TerminalView.ShellSession session) {\n                if (!hubRequests.contains(session.getRequest())) {\n                    return;\n                }\n\n                hubRequests.remove(session.getRequest());\n            }\n\n            @Override\n            public void onTerminalClosed(TerminalView.TerminalSession instance) {\n                var sessions = TerminalView.get().getSessions();\n                var remaining = sessions.stream()\n                        .filter(s -> hubRequests.contains(s.getRequest())\n                                && s.getTerminal().isRunning())\n                        .toList();\n                if (remaining.isEmpty()) {\n                    disableDock();\n                }\n            }\n        };\n        return listener;\n    }\n\n    public void refreshDockStatus() {\n        dockModel.clearDeadTerminals();\n        dockModel.updateCustomBounds();\n\n        var running = dockModel.isRunning();\n        if (!running) {\n            minimized.set(false);\n            detached.set(false);\n            disableDock();\n            return;\n        }\n\n        minimized.set(dockModel.isMinimized());\n        detached.set(!dockModel.isMinimized() && (dockModel.isCustomBounds() || AppMainWindow.get().getStage().isIconified()));\n    }\n\n    public void openTerminal(UUID request) {\n        if (!isSupported()) {\n            return;\n        }\n\n        if (!shouldOpen()) {\n            return;\n        }\n\n        hubRequests.add(request);\n        if (!enabled.get()) {\n            enableDock();\n        } else if (!showing.get()) {\n            showDock();\n        }\n    }\n\n    private boolean shouldOpen() {\n        // Check if we are in the hub interface\n        if (!AppLayoutModel.get()\n                .getEntries()\n                .getFirst()\n                .equals(AppLayoutModel.get().getSelected().getValue())) {\n            return false;\n        }\n\n        return true;\n    }\n\n    public void enableDock() {\n        PlatformThread.runLaterIfNeeded(() -> {\n            if (enabled.get()) {\n                return;\n            }\n\n            dockModel.activateView();\n            enabled.set(true);\n            showing.set(true);\n\n            NativeWinWindowControl.MAIN_WINDOW.setWindowsTransitionsEnabled(false);\n            AppLayoutModel.get().getQueueEntries().add(queueEntry);\n        });\n    }\n\n    public void disableDock() {\n        PlatformThread.runLaterIfNeeded(() -> {\n            if (!enabled.get()) {\n                return;\n            }\n\n            dockModel.deactivateView();\n            enabled.set(false);\n            showing.set(false);\n\n            NativeWinWindowControl.MAIN_WINDOW.setWindowsTransitionsEnabled(true);\n            AppLayoutModel.get().getQueueEntries().remove(queueEntry);\n        });\n    }\n\n    public void showDock() {\n        PlatformThread.runLaterIfNeeded(() -> {\n            if (showing.get()) {\n                return;\n            }\n\n            dockModel.activateView();\n            showing.set(true);\n            AppLayoutModel.get().selectConnections();\n        });\n    }\n\n    public void hideDock() {\n        PlatformThread.runLaterIfNeeded(() -> {\n            if (!showing.get()) {\n                return;\n            }\n\n            dockModel.deactivateView();\n            showing.set(false);\n        });\n    }\n\n    public void attach() {\n        dockModel.attach();\n        detached.set(false);\n        minimized.set(false);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TerminalDockMode.java",
    "content": "package io.xpipe.app.terminal;\n\npublic enum TerminalDockMode {\n    UNSUPPORTED,\n    WITH_BORDER,\n    BORDERLESS\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TerminalDockView.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.platform.NativeWinWindowControl;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.util.GlobalTimer;\nimport io.xpipe.app.util.Rect;\n\nimport io.xpipe.app.util.ThreadHelper;\nimport lombok.Getter;\n\nimport java.time.Duration;\nimport java.util.HashSet;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.function.UnaryOperator;\n\npublic class TerminalDockView {\n\n    @Getter\n    private final Set<ControllableTerminalSession> terminalInstances = new HashSet<>();\n\n    private final UnaryOperator<Rect> windowBoundsFunction;\n\n    private Rect viewBounds;\n    private boolean viewActive;\n\n    public TerminalDockView(UnaryOperator<Rect> windowBoundsFunction) {\n        this.windowBoundsFunction = windowBoundsFunction;\n    }\n\n    public synchronized void clearDeadTerminals() {\n        terminalInstances.removeIf(controllableTerminalSession ->\n                !controllableTerminalSession.getTerminalProcess().isAlive());\n    }\n\n    public synchronized boolean isRunning() {\n        return terminalInstances.stream().anyMatch(terminal -> terminal.isRunning());\n    }\n\n    public synchronized boolean isCustomBounds() {\n        return terminalInstances.stream().anyMatch(terminal -> terminal.isCustomBounds());\n    }\n\n    public synchronized boolean isMinimized() {\n        return terminalInstances.stream().noneMatch(terminal -> terminal.isActive());\n    }\n\n    public synchronized void updateCustomBounds() {\n        terminalInstances.forEach(terminal -> {\n            var wasCustom = terminal.isCustomBounds();\n            terminal.updateBoundsState();\n\n            if (wasCustom && viewBounds != null && viewActive) {\n                var currentBounds = terminal.getLastBounds();\n                var targetBounds = windowBoundsFunction.apply(viewBounds);\n                var sum = Math.abs(targetBounds.getX() - currentBounds.getX()) +\n                        Math.abs(targetBounds.getY() - currentBounds.getY()) +\n                        Math.abs(targetBounds.getW() - currentBounds.getW()) +\n                        Math.abs(targetBounds.getH() - currentBounds.getH());\n                if (sum < 30) {\n                    ThreadHelper.sleep(300);\n                    trackTerminal(terminal, true);\n                    return;\n                }\n            }\n\n            if (!wasCustom && terminal.isCustomBounds()) {\n                terminal.restoreIcon();\n                terminal.disown();\n                terminal.restoreStyle();\n            }\n        });\n    }\n\n    public synchronized void trackTerminal(ControllableTerminalSession terminal, boolean dock) {\n        if (viewActive && dock && viewBounds != null && NativeWinWindowControl.MAIN_WINDOW.isVisible() && !NativeWinWindowControl.MAIN_WINDOW.isIconified()) {\n            // Bring main window to foreground since initial launch\n            NativeWinWindowControl.MAIN_WINDOW.activate();\n\n            terminal.removeIcon();\n            terminal.own();\n            terminal.removeStyle();\n\n            // The window might be minimized\n            // We always want to show the terminal though\n            terminal.show();\n\n            // Move input focus to terminal\n            terminal.focus();\n\n            terminal.updatePosition(windowBoundsFunction.apply(viewBounds));\n            updateCustomBounds();\n        }\n\n        var wasAdded = terminalInstances.add(terminal);\n        if (wasAdded && viewActive && dock && viewBounds != null) {\n            // Ugly fix for Windows Terminal instances using size constraints on first resize\n            // This will cause the dock to interpret is as detached if we don't fix it again\n            if (AppPrefs.get().terminalType().getValue() instanceof WindowsTerminalType) {\n                GlobalTimer.delay(\n                        () -> {\n                            terminal.updatePosition(windowBoundsFunction.apply(viewBounds));\n                            updateCustomBounds();\n                        },\n                        Duration.ofMillis(100));\n            }\n        }\n    }\n\n    public synchronized boolean closeOtherTerminals(UUID request) {\n        var others = terminalInstances.stream()\n                .filter(terminal -> terminal.getTerminalProcess().isAlive())\n                .filter(terminal -> TerminalView.get().getSessions().stream()\n                        .noneMatch(shellSession -> shellSession.getRequest().equals(request) &&\n                                shellSession.getTerminal().equals(terminal)))\n                .toList();\n        for (ControllableTerminalSession other : others) {\n            closeTerminal(other);\n        }\n        return others.size() > 0;\n    }\n\n    public synchronized void closeTerminal(ControllableTerminalSession terminal) {\n        if (!terminalInstances.contains(terminal)) {\n            return;\n        }\n\n        // Reset style in case close is blocked by terminal\n        terminal.restoreIcon();\n        terminal.disown();\n        terminal.restoreStyle();\n\n        terminal.close();\n        terminalInstances.remove(terminal);\n    }\n\n    public synchronized void activateView() {\n        TrackEvent.withTrace(\"Terminal view activated\").handle();\n        if (viewActive) {\n            return;\n        }\n\n        this.viewActive = true;\n        terminalInstances.forEach(terminalInstance -> {\n            if (!terminalInstance.isActive()) {\n                return;\n            }\n\n            terminalInstance.updateBoundsState();\n            if (terminalInstance.isCustomBounds()) {\n                return;\n            }\n\n            terminalInstance.removeIcon();\n            terminalInstance.own();\n            terminalInstance.removeStyle();\n            terminalInstance.focus();\n        });\n        updatePositions();\n    }\n\n    public synchronized void deactivateView() {\n        TrackEvent.withTrace(\"Terminal view deactivated\").handle();\n        if (!viewActive) {\n            return;\n        }\n\n        this.viewActive = false;\n        terminalInstances.forEach(terminalInstance -> {\n            terminalInstance.disown();\n            terminalInstance.backOfMainWindow();\n        });\n        updatePositions();\n    }\n\n    public synchronized void onWindowShow() {\n        TrackEvent.withTrace(\"Terminal view window shown\").handle();\n        terminalInstances.forEach(terminalInstance -> {\n            if (terminalInstance.isActive()) {\n                return;\n            }\n\n            terminalInstance.updateBoundsState();\n            if (terminalInstance.isCustomBounds()) {\n                return;\n            }\n\n            terminalInstance.show();\n            if (viewActive) {\n                terminalInstance.removeIcon();\n                terminalInstance.own();\n                terminalInstance.removeStyle();\n                terminalInstance.focus();\n            } else {\n                terminalInstance.restoreIcon();\n                terminalInstance.disown();\n                terminalInstance.backOfMainWindow();\n            }\n        });\n    }\n\n    public synchronized void onWindowMinimize() {\n        TrackEvent.withTrace(\"Terminal view window minimized\").handle();\n\n        terminalInstances.forEach(terminalInstance -> {\n            terminalInstance.updateBoundsState();\n            if (terminalInstance.isCustomBounds()) {\n                return;\n            }\n\n            terminalInstance.minimize();\n        });\n    }\n\n    public synchronized void onClose() {\n        TrackEvent.withTrace(\"Terminal view closed\").handle();\n\n        terminalInstances.forEach(terminalInstance -> {\n            terminalInstance.updateBoundsState();\n            if (terminalInstance.isCustomBounds()) {\n                return;\n            }\n\n            closeTerminal(terminalInstance);\n        });\n        terminalInstances.clear();\n    }\n\n    private void updatePositions() {\n        if (viewBounds == null) {\n            return;\n        }\n\n        terminalInstances.forEach(terminalInstance -> {\n            if (!terminalInstance.isActive()) {\n                return;\n            }\n\n            terminalInstance.updateBoundsState();\n            if (terminalInstance.isCustomBounds()) {\n                return;\n            }\n\n            terminalInstance.updatePosition(windowBoundsFunction.apply(viewBounds));\n        });\n    }\n\n    public synchronized void resizeView(int x, int y, int w, int h) {\n        if (w < 100 || h < 100) {\n            return;\n        }\n\n        this.viewBounds = new Rect(x, y, w, h);\n        updatePositions();\n    }\n\n    public void attach() {\n        if (viewBounds == null) {\n            return;\n        }\n\n        TrackEvent.withTrace(\"Terminal view attached\").handle();\n\n        terminalInstances.forEach(terminalInstance -> {\n            terminalInstance.show();\n            terminalInstance.updatePosition(windowBoundsFunction.apply(viewBounds));\n            terminalInstance.removeIcon();\n            terminalInstance.own();\n            terminalInstance.removeStyle();\n            terminalInstance.focus();\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TerminalLaunch.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.CommandControl;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ProcessControl;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.core.FailableFunction;\nimport io.xpipe.core.FilePath;\n\nimport lombok.Builder;\nimport lombok.Value;\n\nimport java.util.List;\nimport java.util.UUID;\n\n@Value\n@Builder\npublic class TerminalLaunch {\n\n    DataStoreEntry entry;\n    String title;\n    FilePath directory;\n    ProcessControl command;\n    UUID request;\n\n    @Builder.Default\n    boolean preferTabs = true;\n\n    @Builder.Default\n    boolean logIfEnabled = true;\n\n    @Builder.Default\n    boolean pauseOnExit = AppPrefs.get().terminalAlwaysPauseOnExit().getValue();\n\n    ExternalTerminalType terminal;\n\n    public String getFullTitle() {\n        return entry != null\n                ? (title != null ? title + \" - \" : \"\") + DataStorage.get().getStoreEntryDisplayName(entry)\n                : title != null ? title : \"\";\n    }\n\n    public void launch() throws Exception {\n        var type = AppPrefs.get().terminalType().getValue();\n        if (type == null) {\n            throw ErrorEventFactory.expected(new IllegalStateException(AppI18n.get(\"noTerminalSet\")));\n        }\n\n        if (AppOperationMode.get() == null) {\n            if (command instanceof CommandControl cc) {\n                TerminalLauncher.openDirect(\n                        getFullTitle(),\n                        sc -> new ShellScript(cc.getTerminalCommand().buildFull(sc)),\n                        ExternalTerminalType.determineFallbackTerminalToOpen(type));\n            }\n            return;\n        }\n\n        var pane = new TerminalLauncher.Config(\n                entry,\n                getFullTitle(),\n                directory,\n                request != null ? request : UUID.randomUUID(),\n                logIfEnabled, pauseOnExit,\n                command);\n        TerminalLauncher.open(List.of(pane), preferTabs, type);\n    }\n\n    public static class TerminalLaunchBuilder {\n\n        public void launch() throws Exception {\n            var l = build();\n            l.launch();\n        }\n\n        public TerminalLaunchBuilder localScript(ShellScript script) {\n            var c = LocalShell.getShell().command(script);\n            return command(c);\n        }\n\n        public TerminalLaunchBuilder localScript(FailableFunction<ShellControl, ShellScript, Exception> script)\n                throws Exception {\n            var c = LocalShell.getShell().command(script.apply(LocalShell.getShell()));\n            return command(c);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TerminalLaunchConfiguration.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.storage.DataStoreColor;\n\nimport lombok.*;\n\nimport java.util.List;\n\n@Value\n@AllArgsConstructor\npublic class TerminalLaunchConfiguration {\n\n    DataStoreColor color;\n    String coloredTitle;\n    String cleanTitle;\n    boolean preferTabs;\n    List<TerminalPaneConfiguration> panes;\n\n    public TerminalPaneConfiguration single() {\n        if (panes.size() != 1) {\n            throw new IllegalStateException(\"Not a single pane config\");\n        }\n\n        return panes.getFirst();\n    }\n\n    public TerminalLaunchConfiguration withPanes(List<TerminalPaneConfiguration> panes) {\n        return new TerminalLaunchConfiguration(color, coloredTitle, cleanTitle, preferTabs, panes);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TerminalLaunchRequest.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.ProcessControl;\nimport io.xpipe.app.process.ScriptHelper;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.TerminalInitScriptConfig;\nimport io.xpipe.app.process.WorkingDirectoryFunction;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.beacon.BeaconServerException;\nimport io.xpipe.core.FilePath;\n\nimport lombok.Setter;\nimport lombok.Value;\nimport lombok.experimental.NonFinal;\n\nimport java.nio.file.Path;\nimport java.util.UUID;\nimport java.util.concurrent.CountDownLatch;\n\n@Value\npublic class TerminalLaunchRequest {\n\n    UUID request;\n    ProcessControl processControl;\n    TerminalInitScriptConfig config;\n    FilePath workingDirectory;\n\n    @Setter\n    @NonFinal\n    long shellPid;\n\n    @Setter\n    @NonFinal\n    TerminalLaunchResult result;\n\n    @Setter\n    @NonFinal\n    boolean setupCompleted;\n\n    @NonFinal\n    CountDownLatch latch;\n\n    public Path waitForCompletion() throws BeaconServerException {\n        while (true) {\n            if (latch.getCount() > 0) {\n                ThreadHelper.sleep(10);\n                continue;\n            }\n\n            if (getResult() == null) {\n                throw ErrorEventFactory.expected(new BeaconServerException(\"Launch request aborted\"));\n            }\n\n            var r = getResult();\n            if (r instanceof TerminalLaunchResult.ResultFailure failure) {\n                var t = failure.getThrowable();\n                throw new BeaconServerException(t);\n            }\n\n            return ((TerminalLaunchResult.ResultSuccess) r).getTargetScript();\n        }\n    }\n\n    public void setupRequestAsync() {\n        if (latch == null || latch.getCount() == 0) {\n            latch = new CountDownLatch(1);\n        }\n        ThreadHelper.runAsync(() -> {\n            setupRequest();\n            latch.countDown();\n        });\n    }\n\n    public void abort() {\n        latch.countDown();\n    }\n\n    private void setupRequest() {\n        var wd = new WorkingDirectoryFunction() {\n\n            @Override\n            public boolean isFixed() {\n                return true;\n            }\n\n            @Override\n            public boolean isSpecified() {\n                return workingDirectory != null;\n            }\n\n            @Override\n            public FilePath apply(ShellControl shellControl) {\n                if (workingDirectory == null) {\n                    return null;\n                }\n\n                return workingDirectory;\n            }\n        };\n\n        try {\n            var openCommand = processControl.prepareTerminalOpen(config, wd);\n            var file = ScriptHelper.createLocalExecScript(openCommand);\n            setResult(new TerminalLaunchResult.ResultSuccess(file.asLocalPath()));\n        } catch (Exception e) {\n            setResult(new TerminalLaunchResult.ResultFailure(e));\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TerminalLaunchResult.java",
    "content": "package io.xpipe.app.terminal;\n\nimport lombok.Value;\n\nimport java.nio.file.Path;\n\npublic interface TerminalLaunchResult {\n\n    @Value\n    class ResultSuccess implements TerminalLaunchResult {\n        Path targetScript;\n    }\n\n    @Value\n    class ResultFailure implements TerminalLaunchResult {\n        Throwable throwable;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TerminalLauncher.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.core.AppInstallation;\nimport io.xpipe.app.core.AppNames;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.*;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.core.FailableFunction;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport lombok.Value;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.UUID;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.stream.Collectors;\n\npublic class TerminalLauncher {\n\n    public static FilePath constructTerminalInitFile(\n            ShellDialect t,\n            ShellControl processControl,\n            WorkingDirectoryFunction workingDirectory,\n            List<String> preInit,\n            List<String> postInit,\n            TerminalInitScriptConfig config,\n            boolean exit)\n            throws Exception {\n        var content = constructTerminalInitScript(t, processControl, workingDirectory, preInit, postInit, config, exit);\n        var hash = ScriptHelper.getScriptHash(processControl, content);\n        var file = t.getInitFileName(processControl, hash);\n        return ScriptHelper.createExecScriptRaw(processControl, file, content);\n    }\n\n    private static String constructTerminalInitScript(\n            ShellDialect t,\n            ShellControl processControl,\n            WorkingDirectoryFunction workingDirectory,\n            List<String> preInit,\n            List<String> postInit,\n            TerminalInitScriptConfig config,\n            boolean exit)\n            throws Exception {\n        String nl = t.getNewLine().getNewLineString();\n        var content = \"\";\n\n        var clear = t.clearDisplayCommand();\n        if (clear != null && config.isClearScreen()) {\n            content += clear + nl;\n        }\n\n        // Normalize line endings\n        content += nl + preInit.stream().flatMap(s -> s.lines()).collect(Collectors.joining(nl)) + nl;\n\n        // We just apply the profile files always, as we can't be sure that they definitely have been applied.\n        // Especially if we launch something that is not the system default shell\n        var applyCommand = t.applyInitFileCommand(processControl);\n        if (applyCommand != null) {\n            content += nl + applyCommand + nl;\n        }\n\n        if (config.getDisplayName() != null) {\n            content += nl + t.changeTitleCommand(config.getDisplayName()) + nl;\n        }\n\n        if (workingDirectory != null && workingDirectory.isSpecified()) {\n            var wd = workingDirectory.apply(processControl);\n            if (wd != null) {\n                content += t.getCdCommand(wd.toString()) + nl;\n            }\n        }\n\n        // Normalize line endings\n        content += nl + postInit.stream().flatMap(s -> s.lines()).collect(Collectors.joining(nl)) + nl;\n\n        if (exit) {\n            content += nl + t.getPassthroughExitCommand();\n        }\n\n        content = t.prepareScriptContent(processControl, content);\n        return content;\n    }\n\n    static void openDirect(\n            String title, FailableFunction<ShellControl, ShellScript, Exception> command, ExternalTerminalType type)\n            throws Exception {\n        try (var sc = LocalShell.getShell().start()) {\n            var script = constructTerminalInitScript(\n                    sc.getShellDialect(),\n                    sc,\n                    WorkingDirectoryFunction.none(),\n                    List.of(),\n                    List.of(command.apply(sc).toString()),\n                    new TerminalInitScriptConfig(\n                            title,\n                            type.shouldClear()\n                                    && AppPrefs.get().clearTerminalOnInit().get()\n                                    && !AppPrefs.get().developerPrintInitFiles().get(),\n                            TerminalInitFunction.none()),\n                    true);\n            var singlePane = new TerminalPaneConfiguration(UUID.randomUUID(), title, 0, script, sc.getShellDialect());\n            var config = new TerminalLaunchConfiguration(null, title, title, true, List.of(singlePane));\n            launch(type, config, new CountDownLatch(0));\n        }\n    }\n\n    @Value\n    public static class Config {\n        DataStoreEntry entry;\n        String title;\n        FilePath directory;\n        UUID request;\n        boolean enableLogging;\n        boolean alwaysKeepOpen;\n        ProcessControl processControl;\n    }\n\n    public static void open(List<Config> configs, boolean preferTabs, ExternalTerminalType type) throws Exception {\n        var latch = new CountDownLatch(configs.size());\n        var paneList = new ArrayList<TerminalPaneConfiguration>();\n        for (Config config : configs) {\n            var entry = config.getEntry();\n            var color = entry != null ? DataStorage.get().getEffectiveColor(entry) : null;\n            var prefix = entry != null && color != null && type.useColoredTitle() ? color.getEmoji() + \" \" : \"\";\n            var cleanTitle =\n                    (config.getTitle() != null ? config.getTitle() : entry != null ? entry.getName() : \"Unknown\");\n            var adjustedTitle = prefix + cleanTitle;\n\n            var log = config.isEnableLogging()\n                    && AppPrefs.get().enableTerminalLogging().get();\n            var terminalConfig = new TerminalInitScriptConfig(\n                    adjustedTitle,\n                    !log\n                            && type.shouldClear()\n                            && AppPrefs.get().clearTerminalOnInit().get()\n                            && !AppPrefs.get().developerPrintInitFiles().get(),\n                    config.getProcessControl() instanceof ShellControl\n                            ? type.additionalInitCommands()\n                            : TerminalInitFunction.none());\n            TerminalLauncherManager.submitAsync(\n                    config.getRequest(), config.getProcessControl(), terminalConfig, config.getDirectory(), latch);\n            var effectivePreferTabs =\n                    preferTabs && AppPrefs.get().preferTerminalTabs().get();\n\n            var paneIndex = configs.indexOf(config);\n            var paneConfig = TerminalPaneConfiguration.create(\n                    config.getRequest(), entry, config.getTitle(), paneIndex, effectivePreferTabs, config.isAlwaysKeepOpen());\n            paneList.add(paneConfig);\n        }\n\n        var title = configs.size() == 1\n                ? configs.getFirst().getTitle()\n                : AppNames.ofCurrent().getName() + \" (\" + configs.size() + \" tabs)\";\n        var entry = configs.size() == 1 ? configs.getFirst().getEntry() : null;\n        var color = entry != null ? DataStorage.get().getEffectiveColor(entry) : null;\n        var prefix = entry != null && color != null && type.useColoredTitle() ? color.getEmoji() + \" \" : \"\";\n        var cleanTitle = (title != null ? title : entry != null ? entry.getName() : \"Unknown\");\n        var adjustedTitle = prefix + cleanTitle;\n\n        var effectivePreferTabs =\n                preferTabs && AppPrefs.get().preferTerminalTabs().get();\n        var launchConfig = new TerminalLaunchConfiguration(color, adjustedTitle, cleanTitle, effectivePreferTabs, paneList);\n\n        if (effectivePreferTabs\n                && AppPrefs.get().enableConnectionHubTerminalDocking().get()\n                && TerminalDockHubManager.isAvailable()) {\n            // Dock terminal if needed\n            for (TerminalPaneConfiguration pane : launchConfig.getPanes()) {\n                TerminalDockHubManager.get().openTerminal(pane.getRequest());\n            }\n        }\n\n        if (effectivePreferTabs) {\n            synchronized (TerminalLauncher.class) {\n                // There will be timing issues when launching multiple tabs in a short time span\n                TerminalMultiplexerManager.synchronizeMultiplexerLaunchTiming();\n\n                // Track sessions that are used for the multiplexer\n                // Used to figure out when it dies\n                TerminalMultiplexerManager.registerSessionLaunch(launchConfig);\n\n                if (launchMultiplexerTabInExistingTerminal(launchConfig)) {\n                    latch.await();\n                    return;\n                }\n\n                var multiplexerConfig = launchMultiplexerTabInNewTerminal(launchConfig);\n                if (multiplexerConfig.isPresent()) {\n                    launch(type, multiplexerConfig.get(), latch);\n                    return;\n                }\n            }\n        }\n\n        var proxyConfig = launchProxy(launchConfig);\n        if (proxyConfig.isPresent()) {\n            launch(type, proxyConfig.get(), latch);\n            return;\n        }\n\n        launch(type, launchConfig, latch);\n    }\n\n    private static void launch(ExternalTerminalType type, TerminalLaunchConfiguration config, CountDownLatch latch)\n            throws Exception {\n        if (type == null) {\n            return;\n        }\n\n        try {\n            type.launch(config);\n            latch.await();\n        } catch (Exception ex) {\n            var modMsg = ex.getMessage() != null && ex.getMessage().contains(\"Unable to find application named\")\n                    ? ex.getMessage() + \" in installed /Applications on this system\"\n                    : ex.getMessage();\n            throw ErrorEventFactory.expected(new IOException(\n                    \"Unable to launch terminal \" + type.toTranslatedString().getValue() + \": \" + modMsg, ex));\n        }\n    }\n\n    private static boolean launchMultiplexerTabInExistingTerminal(TerminalLaunchConfiguration launchConfiguration)\n            throws Exception {\n        var multiplexer = TerminalMultiplexerManager.getEffectiveMultiplexer();\n        if (multiplexer.isEmpty()) {\n            return false;\n        }\n\n        var control = TerminalProxyManager.getProxy().orElse(LocalShell.getShell());\n\n        // Throw if not supported\n        multiplexer.get().checkSupported(control);\n\n        var session = TerminalMultiplexerManager.getActiveMultiplexerSession();\n        if (session.isEmpty()) {\n            return false;\n        }\n\n        // Map panes to original multiplexer request session\n        var mapped = TerminalMultiplexerManager.getActiveMultiplexerContainerRequest();\n        if (mapped.isPresent()) {\n            for (TerminalPaneConfiguration pane : launchConfiguration.getPanes()) {\n                TerminalView.get().addSubstitution(pane.getRequest(), mapped.get());\n            }\n        }\n\n        var multiplexerCommandString = multiplexer\n                .get()\n                .launchForExistingSession(control, launchConfiguration)\n                .toString();\n        CommandControl multiplexerCommand = control.command(multiplexerCommandString);\n        // Multiplexer might freeze\n        multiplexerCommand.killOnTimeout(CountDown.of().start(10000));\n        multiplexerCommand.execute();\n        TerminalView.focus(session.get());\n        return true;\n    }\n\n    private static Optional<TerminalLaunchConfiguration> launchMultiplexerTabInNewTerminal(\n            TerminalLaunchConfiguration launchConfiguration) throws Exception {\n        var multiplexer = TerminalMultiplexerManager.getEffectiveMultiplexer();\n        if (multiplexer.isEmpty()) {\n            return Optional.empty();\n        }\n\n        // Throw if not supported\n        multiplexer.get().checkSupported(TerminalProxyManager.getProxy().orElse(LocalShell.getShell()));\n\n        if (TerminalMultiplexerManager.getActiveMultiplexerContainerRequest().isPresent()) {\n            return Optional.empty();\n        }\n\n        var multiplexerContainerLaunchRequest = UUID.randomUUID();\n        // Map panes to original multiplexer request session\n        for (TerminalPaneConfiguration pane : launchConfiguration.getPanes()) {\n            TerminalView.get().addSubstitution(pane.getRequest(), multiplexerContainerLaunchRequest);\n        }\n        // Use initial shell session to track when multiplexer has started up\n        TerminalMultiplexerManager.registerMultiplexerContainerLaunch(multiplexerContainerLaunchRequest);\n\n        var multiplexerTabLaunchRequest = UUID.randomUUID();\n\n        var proxyControl = TerminalProxyManager.getProxy();\n        if (proxyControl.isPresent()) {\n            var proxyMultiplexerCommand = multiplexer\n                    .get()\n                    .launchNewSession(proxyControl.get(), launchConfiguration)\n                    .toString();\n            var proxyLaunchCommand = proxyControl\n                    .get()\n                    .prepareIntermediateTerminalOpen(\n                            TerminalInitFunction.fixed(proxyMultiplexerCommand),\n                            TerminalInitScriptConfig.ofName(AppNames.ofCurrent().getName()),\n                            WorkingDirectoryFunction.none());\n            // Restart for the next time\n            proxyControl.get().start();\n            var fullLocalCommand = getTerminalRegisterCommand(multiplexerContainerLaunchRequest, LocalShell.getShell())\n                    + \"\\n\" + proxyLaunchCommand;\n            var pane = new TerminalPaneConfiguration(\n                    multiplexerTabLaunchRequest,\n                    AppNames.ofCurrent().getName(),\n                    0,\n                    fullLocalCommand,\n                    LocalShell.getDialect());\n            return Optional.of(new TerminalLaunchConfiguration(\n                    null, AppNames.ofCurrent().getName(), AppNames.ofCurrent().getName(), false, List.of(pane)));\n        } else {\n            var multiplexerCommand = multiplexer\n                    .get()\n                    .launchNewSession(LocalShell.getShell(), launchConfiguration)\n                    .toString();\n            var launchCommand = LocalShell.getShell()\n                    .prepareIntermediateTerminalOpen(\n                            TerminalInitFunction.fixed(multiplexerCommand),\n                            TerminalInitScriptConfig.ofName(AppNames.ofCurrent().getName()),\n                            WorkingDirectoryFunction.none());\n            var fullLocalCommand = getTerminalRegisterCommand(multiplexerContainerLaunchRequest, LocalShell.getShell())\n                    + \"\\n\" + launchCommand;\n            var pane = new TerminalPaneConfiguration(\n                    multiplexerTabLaunchRequest,\n                    AppNames.ofCurrent().getName(),\n                    0,\n                    fullLocalCommand,\n                    LocalShell.getDialect());\n            return Optional.of(new TerminalLaunchConfiguration(\n                    null, AppNames.ofCurrent().getName(), AppNames.ofCurrent().getName(), false, List.of(pane)));\n        }\n    }\n\n    private static Optional<TerminalLaunchConfiguration> launchProxy(TerminalLaunchConfiguration launchConfiguration)\n            throws Exception {\n        var proxyControl = TerminalProxyManager.getProxy();\n        if (proxyControl.isEmpty()) {\n            return Optional.empty();\n        }\n\n        // We can't track sessions inside another environment, so map the ids to the proxy container process\n        var proxyContainerLaunchRequest = UUID.randomUUID();\n        for (TerminalPaneConfiguration pane : launchConfiguration.getPanes()) {\n            TerminalView.get().addSubstitution(pane.getRequest(), proxyContainerLaunchRequest);\n        }\n\n        var panes = new ArrayList<TerminalPaneConfiguration>();\n        for (TerminalPaneConfiguration pane : launchConfiguration.getPanes()) {\n            var openCommand = pane.getDialectLaunchCommand().buildSimple();\n            var launchCommand = proxyControl\n                    .get()\n                    .prepareIntermediateTerminalOpen(\n                            TerminalInitFunction.fixed(openCommand),\n                            TerminalInitScriptConfig.ofName(AppNames.ofCurrent().getName()),\n                            WorkingDirectoryFunction.none());\n            var fullLocalCommand = getTerminalRegisterCommand(proxyContainerLaunchRequest, LocalShell.getShell()) + \"\\n\"\n                    + launchCommand;\n            // Restart for the next time\n            proxyControl.get().start();\n            panes.add(pane.withScript(LocalShell.getDialect(), fullLocalCommand));\n        }\n\n        return Optional.ofNullable(launchConfiguration.withPanes(panes));\n    }\n\n    public static String getTerminalRegisterCommand(UUID request, ShellControl sc) throws Exception {\n        var exec = AppInstallation.ofCurrent().getCliExecutablePath();\n        var registerLine = CommandBuilder.of()\n                .addFile(sc.getLocalSystemAccess().translateFromLocalSystemPath(FilePath.of(exec)))\n                .add(\"terminal-register\", \"--request\", request.toString())\n                .buildSimple();\n        var powershell = ShellDialects.isPowershell(sc);\n        var bellLine = \"printf \\\"\\\\a\\\"\";\n        var printBell = OsType.ofLocal() != OsType.WINDOWS\n                && AppPrefs.get().enableTerminalStartupBell().get();\n        var lines = ShellScript.lines((powershell ? \"& \" + registerLine : registerLine), printBell ? bellLine : null);\n        return lines.toString();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TerminalLauncherManager.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.core.AppNames;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.process.*;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.beacon.BeaconClientException;\nimport io.xpipe.beacon.BeaconServerException;\nimport io.xpipe.core.FilePath;\n\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.concurrent.CountDownLatch;\n\npublic class TerminalLauncherManager {\n\n    private static final SequencedMap<UUID, TerminalLaunchRequest> entries = new LinkedHashMap<>();\n\n    public static void init() {\n        TerminalView.get().addListener(new TerminalView.Listener() {\n            @Override\n            public void onSessionClosed(TerminalView.ShellSession session) {\n                var affectedEntry = entries.values().stream()\n                        .filter(terminalLaunchRequest -> {\n                            return terminalLaunchRequest.getRequest().equals(session.getRequest());\n                        })\n                        .findFirst();\n                if (affectedEntry.isEmpty()) {\n                    return;\n                }\n\n                affectedEntry.get().abort();\n            }\n        });\n    }\n\n    public static CountDownLatch submitAsync(\n            UUID request,\n            ProcessControl processControl,\n            TerminalInitScriptConfig config,\n            FilePath directory,\n            CountDownLatch latch) {\n        synchronized (entries) {\n            var req = entries.get(request);\n            if (req == null) {\n                req = new TerminalLaunchRequest(request, processControl, config, directory, -1, null, false, latch);\n                entries.put(request, req);\n            } else {\n                req.setResult(null);\n            }\n\n            req.setupRequestAsync();\n            return req.getLatch();\n        }\n    }\n\n    public static Path sshLaunchExchange() throws BeaconClientException, BeaconServerException {\n        TerminalLaunchRequest last;\n        synchronized (entries) {\n            var all = entries.values().stream().toList();\n            last = !all.isEmpty() ? all.getLast() : null;\n            if (last == null) {\n                throw new BeaconClientException(\"Unknown launch request\");\n            }\n        }\n        return last.waitForCompletion();\n    }\n\n    @SuppressWarnings(\"unused\")\n    public static boolean isCompletedSuccessfully(UUID request) {\n        synchronized (entries) {\n            var req = entries.get(request);\n            return req.getResult() instanceof TerminalLaunchResult.ResultSuccess;\n        }\n    }\n\n    public static void registerPid(UUID request, long pid) throws BeaconClientException {\n        TerminalLaunchRequest req;\n        synchronized (entries) {\n            req = entries.get(request);\n        }\n        if (req == null) {\n            return;\n        }\n        var byPid = ProcessHandle.of(pid);\n        if (byPid.isEmpty()) {\n            throw new BeaconClientException(\"Unable to find terminal child process \" + pid);\n        }\n        var shell = byPid.get().parent().orElseThrow();\n        if (req.getShellPid() != -1 && shell.pid() != req.getShellPid()) {\n            throw new BeaconClientException(\"Wrong launch context\");\n        }\n        req.setShellPid(shell.pid());\n    }\n\n    public static void waitExchange(UUID request) throws BeaconServerException {\n        TerminalLaunchRequest req;\n        synchronized (entries) {\n            req = entries.get(request);\n        }\n        if (req == null) {\n            return;\n        }\n\n        if (req.isSetupCompleted()) {\n            submitAsync(req.getRequest(), req.getProcessControl(), req.getConfig(), req.getWorkingDirectory(), null);\n        }\n        try {\n            req.waitForCompletion();\n        } finally {\n            req.setSetupCompleted(true);\n        }\n    }\n\n    public static Path launchExchange(UUID request) throws BeaconClientException, BeaconServerException {\n        synchronized (entries) {\n            var e = entries.values().stream()\n                    .filter(entry -> entry.getRequest().equals(request))\n                    .findFirst()\n                    .orElse(null);\n            if (e == null) {\n                // It seems like that some terminals might enter a restart loop to try to start an older process again\n                // This would spam XPipe continuously with launch requests if we returned an error here\n                // Therefore, we just return a new local shell session\n                TrackEvent.withTrace(\"Unknown launch request\")\n                        .tag(\"request\", request.toString())\n                        .handle();\n                try (var sc = LocalShell.getShell().start()) {\n                    var defaultShell = sc.getShellDialect();\n                    var shellExec = defaultShell.getExecutableName();\n                    var script = ScriptHelper.createExecScript(\n                            sc,\n                            sc.getShellDialect()\n                                            .getEchoCommand(\n                                                    \"Unknown \"\n                                                            + AppNames.ofCurrent()\n                                                                    .getName() + \" launch request\",\n                                                    false)\n                                    + \"\\n\" + shellExec);\n                    return Path.of(script.toString());\n                } catch (Exception ex) {\n                    throw new BeaconServerException(ex);\n                }\n            }\n\n            if (!(e.getResult() instanceof TerminalLaunchResult.ResultSuccess)) {\n                throw new BeaconClientException(\"Invalid launch request state \" + request);\n            }\n\n            return ((TerminalLaunchResult.ResultSuccess) e.getResult()).getTargetScript();\n        }\n    }\n\n    public static List<String> externalExchange(DataStoreEntryRef<ShellStore> ref, List<String> arguments)\n            throws BeaconClientException, BeaconServerException {\n        var request = UUID.randomUUID();\n        ShellControl session;\n        try {\n            session = ref.getStore().getOrStartSession();\n        } catch (Exception e) {\n            throw new BeaconServerException(e);\n        }\n\n        ProcessControl control;\n        if (arguments.size() > 0) {\n            control = session.command(CommandBuilder.of().addAll(arguments));\n        } else {\n            control = session;\n        }\n\n        var config = new TerminalInitScriptConfig(ref.get().getName(), false, TerminalInitFunction.none());\n        submitAsync(request, control, config, null, null);\n        waitExchange(request);\n        var script = launchExchange(request);\n        try (var sc = LocalShell.getShell().start()) {\n            var runCommand = ProcessControlProvider.get()\n                    .getEffectiveLocalDialect()\n                    .getOpenScriptCommand(script.toString())\n                    .buildBaseParts(sc);\n            var cleaned = runCommand.stream()\n                    .map(s -> {\n                        if (s.startsWith(\"\\\"\") && s.endsWith(\"\\\"\")) {\n                            s = s.substring(1, s.length() - 1);\n                        } else if (s.startsWith(\"'\") && s.endsWith(\"'\")) {\n                            s = s.substring(1, s.length() - 1);\n                        }\n                        return s;\n                    })\n                    .toList();\n            return cleaned;\n        } catch (Exception e) {\n            throw new BeaconServerException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TerminalMultiplexer.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.ShellScript;\n\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\npublic interface TerminalMultiplexer {\n\n    static List<Class<?>> getClasses() {\n        var l = new ArrayList<Class<?>>();\n        l.add(TmuxTerminalMultiplexer.class);\n        l.add(ZellijTerminalMultiplexer.class);\n        l.add(ScreenTerminalMultiplexer.class);\n        return l;\n    }\n\n    boolean supportsSplitView();\n\n    String getDocsLink();\n\n    void checkSupported(ShellControl sc) throws Exception;\n\n    ShellScript launchForExistingSession(ShellControl control, TerminalLaunchConfiguration config);\n\n    ShellScript launchNewSession(ShellControl control, TerminalLaunchConfiguration config) throws Exception;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TerminalMultiplexerManager.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.OsType;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.*;\n\npublic class TerminalMultiplexerManager {\n\n    private static final Map<UUID, TerminalMultiplexer> connectionHubRequests = new HashMap<>();\n    private static UUID pendingMultiplexerLaunch;\n    private static Instant lastCheck = Instant.now();\n    private static UUID runningMultiplexerContainer;\n\n    public static void registerMultiplexerContainerLaunch(UUID uuid) {\n        pendingMultiplexerLaunch = uuid;\n        var listener = new TerminalView.Listener() {\n            @Override\n            public void onSessionOpened(TerminalView.ShellSession session) {\n                if (session.getRequest().equals(pendingMultiplexerLaunch)) {\n                    pendingMultiplexerLaunch = null;\n                    runningMultiplexerContainer = uuid;\n                }\n            }\n\n            @Override\n            public void onSessionClosed(TerminalView.ShellSession session) {\n                // Technically, due to how multiplexers handle, this can only be 0 or 1\n                // as it only tracks the base shell session the multiplexer runs in\n                var left = TerminalView.get().getSessions().stream()\n                        .filter(shellSession -> {\n                            return connectionHubRequests.containsKey(shellSession.getRequest())\n                                    && shellSession.getTerminal().isRunning();\n                        })\n                        .count();\n                if (left == 0) {\n                    runningMultiplexerContainer = null;\n                    TerminalView.get().removeListener(this);\n                }\n            }\n        };\n        TerminalView.get().addListener(listener);\n    }\n\n    public static Optional<TerminalMultiplexer> getEffectiveMultiplexer() {\n        var multiplexer = AppPrefs.get().terminalMultiplexer().getValue();\n        if (multiplexer == null) {\n            return Optional.empty();\n        }\n\n        if (OsType.ofLocal() == OsType.WINDOWS) {\n            var hasProxy = AppPrefs.get().terminalProxy().getValue() != null;\n            if (!hasProxy) {\n                return Optional.empty();\n            }\n        }\n\n        return Optional.of(multiplexer);\n    }\n\n    public static void synchronizeMultiplexerLaunchTiming() {\n        var mult = getEffectiveMultiplexer();\n        if (mult.isEmpty()) {\n            return;\n        }\n\n        // Wait if we are currently opening a new multiplexer\n        if (pendingMultiplexerLaunch != null) {\n            // Wait for max 10s\n            for (int i = 0; i < 100; i++) {\n                if (pendingMultiplexerLaunch == null) {\n                    break;\n                }\n\n                ThreadHelper.sleep(100);\n            }\n            // Give multiplexer a second to start in terminal\n            ThreadHelper.sleep(1000);\n        }\n\n        // Synchronize between multiple existing tab launches as well as some multiplexers might break there\n        var elapsed = Duration.between(lastCheck, Instant.now()).toMillis();\n        if (elapsed < 1000) {\n            ThreadHelper.sleep(1000 - elapsed);\n        }\n\n        lastCheck = Instant.now();\n    }\n\n    public static void registerSessionLaunch(TerminalLaunchConfiguration configuration) {\n        var mult = getEffectiveMultiplexer();\n\n        for (TerminalPaneConfiguration pane : configuration.getPanes()) {\n            if (mult.isEmpty()) {\n                connectionHubRequests.put(pane.getRequest(), null);\n                return;\n            }\n\n            connectionHubRequests.put(pane.getRequest(), mult.orElse(null));\n        }\n    }\n\n    public static Optional<UUID> getActiveMultiplexerContainerRequest() {\n        return Optional.ofNullable(runningMultiplexerContainer);\n    }\n\n    public static Optional<TerminalView.TerminalSession> getActiveMultiplexerSession() {\n        var mult = getEffectiveMultiplexer();\n        if (mult.isEmpty()) {\n            return Optional.empty();\n        }\n\n        var session = TerminalView.get().getSessions().stream()\n                .filter(shellSession -> shellSession.getTerminal().isRunning()\n                        && mult.get() == connectionHubRequests.get(shellSession.getRequest()))\n                .findFirst();\n        return session.map(shellSession -> shellSession.getTerminal());\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TerminalOpenFormat.java",
    "content": "package io.xpipe.app.terminal;\n\npublic enum TerminalOpenFormat {\n    NEW_WINDOW,\n    TABBED,\n    NEW_WINDOW_OR_TABBED\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TerminalPaneConfiguration.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.core.AppInstallation;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.*;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.util.LicenseProvider;\nimport io.xpipe.app.util.LicenseRequiredException;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport lombok.AllArgsConstructor;\nimport lombok.RequiredArgsConstructor;\nimport lombok.SneakyThrows;\nimport lombok.Value;\nimport lombok.experimental.NonFinal;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\nimport java.util.UUID;\n\n@Value\n@RequiredArgsConstructor\n@AllArgsConstructor\npublic class TerminalPaneConfiguration {\n\n    private static final DateTimeFormatter DATE_FORMATTER =\n            DateTimeFormatter.ofPattern(\"yyyy-MM-dd_HH-mm-ss\").withZone(ZoneId.systemDefault());\n\n    UUID request;\n    String title;\n    int paneIndex;\n    String scriptContent;\n    ShellDialect scriptDialect;\n\n    @NonFinal\n    FilePath scriptFile = null;\n\n    private static Path getLogFile(int index, DataStoreEntry entry) throws Exception {\n        var logDir = AppProperties.get().getDataDir().resolve(\"sessions\");\n        Files.createDirectories(logDir);\n        var suffix = index > 0 ? \"-\" + (index + 1) : \"\";\n        var name = DataStorage.get().getStoreEntryDisplayName(entry) + \"_\" + DATE_FORMATTER.format(Instant.now())\n                + suffix + \".log\";\n        var logName = OsFileSystem.ofLocal()\n                .makeFileSystemCompatible(FilePath.of(name))\n                .toString()\n                .replaceAll(\" \", \"_\");\n        return logDir.resolve(logName);\n    }\n\n    public static TerminalPaneConfiguration create(\n            UUID request,\n            DataStoreEntry entry,\n            String title,\n            int paneIndex,\n            boolean enableLogging,\n            boolean alwaysPromptRestart)\n            throws Exception {\n        if (!enableLogging || !AppPrefs.get().enableTerminalLogging().get()) {\n            var sc = LocalShell.getShell();\n            var register = TerminalLauncher.getTerminalRegisterCommand(request, sc);\n            var launcherScript =\n                    register + \"\\n\" + sc.getShellDialect().terminalLauncherScript(request, title, alwaysPromptRestart);\n            var config = new TerminalPaneConfiguration(request, title, paneIndex, launcherScript, sc.getShellDialect());\n            return config;\n        }\n\n        var feature = LicenseProvider.get().getFeature(\"logging\");\n        var supported = feature.isSupported();\n        if (!supported) {\n            throw new LicenseRequiredException(feature);\n        }\n\n        var log = getLogFile(paneIndex, entry);\n        var sc = TerminalProxyManager.getProxy().orElse(LocalShell.getShell()).start();\n        var logFile = sc.getLocalSystemAccess().translateFromLocalSystemPath(FilePath.of(log));\n\n        if (sc.getOsType() == OsType.WINDOWS) {\n            var launcherScript = ScriptHelper.createExecScript(\n                    ShellDialects.POWERSHELL,\n                    sc,\n                    ShellDialects.POWERSHELL.terminalLauncherScript(request, title, alwaysPromptRestart));\n            var content = \"\"\"\n                          %s\n                          echo 'Transcript started, output file is \"sessions\\\\%s\"'\n                          Start-Transcript -Force -LiteralPath \"%s\" > $Out-Null\n                          & \"%s\"\n                          Stop-Transcript > $Out-Null\n                          echo 'Transcript stopped, output file is \"sessions\\\\%s\"'\n                          \"\"\".formatted(\n                            TerminalLauncher.getTerminalRegisterCommand(\n                                    request, LocalShell.getLocalPowershell().orElseThrow()),\n                            logFile.getFileName(),\n                            logFile,\n                            launcherScript,\n                            logFile.getFileName());\n            var config = new TerminalPaneConfiguration(request, title, paneIndex, content, ShellDialects.POWERSHELL);\n            return config;\n        } else {\n            var found = sc.view().findProgram(\"script\").isPresent();\n            if (!found) {\n                var suffix = sc.getOsType() == OsType.MACOS\n                        ? \"This command is available in the util-linux package which can be installed via homebrew.\"\n                        : \"This command is available in the util-linux package.\";\n                throw ErrorEventFactory.expected(\n                        new IllegalStateException(\"Logging requires the script command to be installed. \" + suffix));\n            }\n\n            var launcherScript = ScriptHelper.createExecScript(\n                    LocalShell.getShell(),\n                    LocalShell.getShell()\n                            .getShellDialect()\n                            .terminalLauncherScript(request, title, alwaysPromptRestart));\n            var command = sc == LocalShell.getShell()\n                    ? launcherScript\n                    : LocalShell.getShell()\n                            .getShellDialect()\n                            .getOpenScriptCommand(launcherScript.toString())\n                            .buildFull(LocalShell.getShell());\n            var cliExecutable = TerminalProxyManager.getProxy()\n                    .orElse(LocalShell.getShell())\n                    .getLocalSystemAccess()\n                    .translateFromLocalSystemPath(\n                            FilePath.of(AppInstallation.ofCurrent().getCliExecutablePath()));\n            var scriptCommand = sc.getOsType() == OsType.MACOS || sc.getOsType() == OsType.BSD\n                    ? \"script -e -q '%s' \\\"%s\\\"\".formatted(logFile, command)\n                    : \"script --quiet --command '%s' \\\"%s\\\"\".formatted(command, logFile);\n            var content = \"\"\"\n                          %s\n                          echo \"Transcript started, output file is sessions/%s\"\n                          %s\n                          echo \"Transcript stopped, output file is sessions/%s\"\n                          cat \"%s\" | \"%s\" terminal-clean > \"%s.txt\"\n                          \"\"\".formatted(\n                            TerminalLauncher.getTerminalRegisterCommand(request, sc),\n                            logFile.getFileName(),\n                            scriptCommand,\n                            logFile.getFileName(),\n                            logFile,\n                            cliExecutable,\n                            logFile.getBaseName());\n            var config = new TerminalPaneConfiguration(request, title, paneIndex, content, sc.getShellDialect());\n            config.scriptFile = ScriptHelper.createExecScript(sc.getShellDialect(), sc, content);\n            return config;\n        }\n    }\n\n    public TerminalPaneConfiguration withScript(ShellDialect d, String content) {\n        return new TerminalPaneConfiguration(request, title, paneIndex, content, d);\n    }\n\n    @SneakyThrows\n    public synchronized FilePath getScriptFile() {\n        if (scriptFile == null) {\n            scriptFile = ScriptHelper.createExecScript(scriptDialect, LocalShell.getShell(), scriptContent);\n        }\n        return scriptFile;\n    }\n\n    public synchronized CommandBuilder getDialectLaunchCommand() {\n        var open = scriptDialect.getOpenScriptCommand(getScriptFile().toString());\n        return open;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TerminalPrompt.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.ShellDialect;\nimport io.xpipe.app.process.ShellTemp;\nimport io.xpipe.app.process.ShellTerminalInitCommand;\nimport io.xpipe.core.FilePath;\n\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\npublic interface TerminalPrompt {\n\n    static List<Class<?>> getClasses() {\n        var l = new ArrayList<Class<?>>();\n        l.add(StarshipTerminalPrompt.class);\n        l.add(OhMyPoshTerminalPrompt.class);\n        l.add(OhMyZshTerminalPrompt.class);\n        return l;\n    }\n\n    String getDocsLink();\n\n    default FilePath getConfigurationDirectory(ShellControl sc) throws Exception {\n        var d = ShellTemp.createUserSpecificTempDataDirectory(sc, \"prompt\").join(getId());\n        sc.view().mkdir(d);\n        return d;\n    }\n\n    default FilePath getBinaryDirectory(ShellControl sc) throws Exception {\n        var d = ShellTemp.createUserSpecificTempDataDirectory(sc, \"bin\").join(getId());\n        sc.view().mkdir(d);\n        return d;\n    }\n\n    String getId();\n\n    default boolean installIfNeeded(ShellControl sc) throws Exception {\n        if (!checkIfInstalled(sc)) {\n            try {\n                checkCanInstall(sc);\n                install(sc);\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e)\n                        .omit()\n                        .description(\"Prompt installation for \" + getId() + \" failed on remote system\")\n                        .expected()\n                        .handle();\n                return false;\n            }\n            return true;\n        }\n        return true;\n    }\n\n    void checkCanInstall(ShellControl sc) throws Exception;\n\n    boolean checkIfInstalled(ShellControl sc) throws Exception;\n\n    void install(ShellControl sc) throws Exception;\n\n    ShellTerminalInitCommand terminalCommand() throws Exception;\n\n    List<ShellDialect> getSupportedDialects();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TerminalPromptManager.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.ShellControl;\n\npublic class TerminalPromptManager {\n\n    public static void configurePromptScript(ShellControl sc) {\n        var p = AppPrefs.get().terminalPrompt().getValue();\n        if (p == null) {\n            return;\n        }\n\n        try {\n            sc.withInitSnippet(p.terminalCommand(), false);\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TerminalProxyManager.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport lombok.Value;\n\nimport java.util.Optional;\nimport java.util.UUID;\n\npublic class TerminalProxyManager {\n\n    private static ActiveSession activeSession;\n\n    public static boolean canUseAsProxy(DataStoreEntryRef<ShellStore> ref) {\n        if (!ref.get().getValidity().isUsable()) {\n            return false;\n        }\n\n        var parent = DataStorage.get().getDefaultDisplayParent(ref.get());\n        if (parent.isEmpty()) {\n            return false;\n        }\n\n        if (!parent.get().equals(DataStorage.get().local())\n                && !DataStorage.get()\n                        .local()\n                        .equals(DataStorage.get()\n                                .getDefaultDisplayParent(parent.get())\n                                .orElse(null))) {\n            return false;\n        }\n\n        var id = ref.get().getProvider().getId();\n        return id.equals(\"wsl\");\n    }\n\n    public static Optional<ShellControl> getProxy() {\n        var uuid = AppPrefs.get().terminalProxy().getValue();\n        var hasCustomTerminalShell =\n                uuid != null && !DataStorage.get().local().getUuid().equals(uuid);\n        if (!hasCustomTerminalShell) {\n            return Optional.empty();\n        }\n\n        var foundEntry = DataStorage.get().getStoreEntryIfPresent(uuid);\n        if (foundEntry.isEmpty()) {\n            return Optional.empty();\n        }\n\n        var matchingSession = activeSession != null && activeSession.uuid.equals(uuid) ? activeSession : null;\n        if (matchingSession != null) {\n            // Probably incompatible\n            if (matchingSession.control == null) {\n                return Optional.empty();\n            }\n\n            try {\n                matchingSession.getControl().start();\n                return Optional.of(matchingSession.getControl());\n            } catch (Exception ex) {\n                ErrorEventFactory.fromThrowable(ex).handle();\n                activeSession = new ActiveSession(uuid, null);\n                return Optional.empty();\n            }\n        }\n\n        try {\n            var control = createControl(foundEntry.get().ref());\n            if (control.isPresent()) {\n                control.get().start();\n                activeSession = new ActiveSession(uuid, control.get());\n                return control;\n            }\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).handle();\n        }\n        activeSession = new ActiveSession(uuid, null);\n        return Optional.empty();\n    }\n\n    private static Optional<ShellControl> createControl(DataStoreEntryRef<DataStore> ref) throws Exception {\n        if (ref == null || !ref.get().getValidity().isUsable() || !(ref.getStore() instanceof ShellStore ss)) {\n            return Optional.empty();\n        }\n\n        var store = ss;\n        var control = store.standaloneControl();\n        return Optional.of(control);\n    }\n\n    @Value\n    private static class ActiveSession {\n        UUID uuid;\n        ShellControl control;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TerminalSplitStrategy.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.ext.PrefsChoiceValue;\nimport io.xpipe.app.prefs.AppPrefs;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.value.ObservableObjectValue;\n\nimport lombok.Getter;\n\n@Getter\npublic enum TerminalSplitStrategy implements PrefsChoiceValue {\n    HORIZONTAL(\"horizontal\") {\n        @Override\n        public SplitIterator iterator() {\n            return new OrderedSplitIterator() {\n\n                @Override\n                public SplitDirection getSplitDirection() {\n                    return SplitDirection.HORIZONTAL;\n                }\n            };\n        }\n    },\n\n    VERTICAL(\"vertical\") {\n        @Override\n        public SplitIterator iterator() {\n            return new OrderedSplitIterator() {\n\n                @Override\n                public SplitDirection getSplitDirection() {\n                    return SplitDirection.VERTICAL;\n                }\n            };\n        }\n    },\n\n    BALANCED(\"balanced\") {\n        @Override\n        public SplitIterator iterator() {\n            return new OrderedSplitIterator() {\n\n                @Override\n                public SplitDirection getSplitDirection() {\n                    return level % 2 == 0 ? SplitDirection.HORIZONTAL : SplitDirection.VERTICAL;\n                }\n            };\n        }\n    };\n\n    private static ObservableObjectValue<TerminalSplitStrategy> splitStrategy = null;\n\n    public static synchronized ObservableObjectValue<TerminalSplitStrategy> getEffectiveSplitStrategyObservable() {\n        if (splitStrategy != null) {\n            return splitStrategy;\n        }\n\n        splitStrategy = Bindings.createObjectBinding(\n                () -> {\n                    var prefsValue = AppPrefs.get().terminalSplitStrategy().getValue();\n                    if (prefsValue == null) {\n                        return null;\n                    }\n\n                    var multiplexer = AppPrefs.get().terminalMultiplexer().getValue();\n                    if (multiplexer != null && multiplexer.supportsSplitView()) {\n                        return prefsValue;\n                    }\n\n                    var term = AppPrefs.get().terminalType().getValue();\n                    if (term == null || !term.supportsSplitView()) {\n                        return null;\n                    }\n\n                    return prefsValue;\n                },\n                AppPrefs.get().terminalSplitStrategy(),\n                AppPrefs.get().terminalMultiplexer(),\n                AppPrefs.get().terminalType());\n        return splitStrategy;\n    }\n\n    private final String id;\n\n    TerminalSplitStrategy(String id) {\n        this.id = id;\n    }\n\n    public abstract SplitIterator iterator();\n\n    public enum SplitDirection {\n        HORIZONTAL,\n        VERTICAL\n    }\n\n    public abstract static class SplitIterator {\n\n        public void next() {}\n\n        public abstract SplitDirection getSplitDirection();\n\n        public abstract int getTargetPaneIndex();\n    }\n\n    public abstract static class OrderedSplitIterator extends SplitIterator {\n\n        private int index;\n        protected int level;\n\n        @Override\n        public void next() {\n            index++;\n            if (index >= Math.powExact(2, level)) {\n                index = 0;\n                level++;\n            }\n        }\n\n        @Override\n        public int getTargetPaneIndex() {\n            return index;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TerminalView.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.platform.NativeWinWindowControl;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.OsType;\n\nimport lombok.Getter;\nimport lombok.Value;\n\nimport java.util.*;\nimport java.util.function.Consumer;\n\npublic class TerminalView {\n\n    private static TerminalView INSTANCE;\n    private final List<ShellSession> sessions = new ArrayList<>();\n    private final List<TerminalSession> terminalInstances = new ArrayList<>();\n    private final List<Listener> listeners = new ArrayList<>();\n    private final Map<UUID, UUID> substitutions = new HashMap<>();\n\n    public void addSubstitution(UUID request, UUID target) {\n        substitutions.put(request, target);\n    }\n\n    public static void focus(TerminalSession term) {\n        var control = term.controllable();\n        if (control.isPresent()) {\n            control.get().show();\n            control.get().focus();\n        } else {\n            if (OsType.ofLocal() == OsType.MACOS) {\n                // Just focus the app, this is correct most of the time\n                var terminalType = AppPrefs.get().terminalType().getValue();\n                if (terminalType instanceof ExternalApplicationType.MacApplication m) {\n                    m.focus();\n                }\n            }\n        }\n    }\n\n    public static void init() {\n        var instance = new TerminalView();\n        ThreadHelper.createPlatformThread(\"terminal-view\", true, () -> {\n                    while (true) {\n                        instance.tick();\n                        ThreadHelper.sleep(500);\n                    }\n                })\n                .start();\n        INSTANCE = instance;\n    }\n\n    public static TerminalView get() {\n        return INSTANCE;\n    }\n\n    public synchronized List<ShellSession> getSessions() {\n        return new ArrayList<>(sessions);\n    }\n\n    public synchronized List<TerminalSession> getTerminalInstances() {\n        return new ArrayList<>(terminalInstances);\n    }\n\n    public synchronized void addListener(Listener listener) {\n        this.listeners.add(listener);\n    }\n\n    public synchronized void removeListener(Listener listener) {\n        this.listeners.remove(listener);\n    }\n\n    public synchronized void open(UUID request, long pid) {\n        var substitution = substitutions.get(request);\n        if (substitution != null) {\n            var substitutedSession = sessions.stream()\n                    .filter(shellSession -> {\n                        return shellSession.getRequest().equals(substitution);\n                    })\n                    .findFirst();\n            if (substitutedSession.isEmpty()) {\n                return;\n            }\n\n            TrackEvent.withTrace(\"Substituted shell session opened\")\n                    .tag(\"request\", request.toString())\n                    .handle();\n            var session = new ShellSession(\n                    request,\n                    substitutedSession.get().getShell(),\n                    substitutedSession.get().getTerminal());\n            sessions.add(session);\n            forListeners(listener -> listener.onSessionOpened(session));\n            return;\n        }\n\n        var processHandle = ProcessHandle.of(pid);\n        if (processHandle.isEmpty() || !processHandle.get().isAlive()) {\n            return;\n        }\n\n        var shell = processHandle.get().parent();\n        TrackEvent.withTrace(\"Shell session opened\")\n                .tag(\"pid\", shell.map(p -> p.pid()).orElse(-1L))\n                .handle();\n        if (shell.isEmpty()) {\n            return;\n        }\n\n        var terminal = getTerminalProcess(shell.get());\n        TrackEvent.withTrace(\"Terminal session opened\")\n                .tag(\"pid\", terminal.map(p -> p.pid()).orElse(-1L))\n                .tag(\"exec\", terminal.flatMap(p -> p.info().command()).orElse(\"?\"))\n                .handle();\n        if (terminal.isEmpty()) {\n            return;\n        }\n\n        var tv = createTerminalSession(terminal.get());\n        if (tv.isEmpty()) {\n            return;\n        }\n\n        if (!terminalInstances.contains(tv.get())) {\n            terminalInstances.add(tv.get());\n            forListeners(listener -> listener.onTerminalOpened(tv.get()));\n        }\n\n        var session = new ShellSession(request, shell.get(), tv.get());\n        sessions.add(session);\n        forListeners(listener -> listener.onSessionOpened(session));\n\n        TrackEvent.withTrace(\"Shell session opened in terminal instance\")\n                .tag(\"terminalPid\", terminal.get().pid())\n                .handle();\n    }\n\n    private void forListeners(Consumer<Listener> consumer) {\n        var copy = new ArrayList<>(listeners);\n        copy.forEach(consumer);\n    }\n\n    private Optional<TerminalSession> createTerminalSession(ProcessHandle terminalProcess) {\n        var type = AppPrefs.get().terminalType().getValue();\n        return switch (OsType.ofLocal()) {\n            case OsType.Linux ignored -> Optional.of(new TerminalSession(terminalProcess, type));\n            case OsType.MacOs ignored -> Optional.of(new TerminalSession(terminalProcess, type));\n            case OsType.Windows ignored -> {\n                var controls = NativeWinWindowControl.byPid(terminalProcess.pid());\n                if (controls.isEmpty()) {\n                    // Some terminals might delay the window show\n                    ThreadHelper.sleep(1000);\n                    controls = NativeWinWindowControl.byPid(terminalProcess.pid());\n                    if (controls.isEmpty()) {\n                        yield Optional.empty();\n                    }\n                }\n\n                yield Optional.of(new WindowsTerminalSession(terminalProcess, type, controls.getFirst()));\n            }\n        };\n    }\n\n    private Optional<ProcessHandle> getTerminalProcess(ProcessHandle shell) {\n        var t = AppPrefs.get().terminalType().getValue();\n        if (!(t instanceof TrackableTerminalType trackableTerminalType)) {\n            return Optional.empty();\n        }\n\n        var off = trackableTerminalType.getProcessHierarchyOffset();\n        var current = Optional.of(shell);\n        for (int i = 0; i < 1 + off; i++) {\n            current = current.flatMap(processHandle -> processHandle.parent());\n        }\n        return current;\n    }\n\n    public synchronized Optional<ShellSession> findSession(long pid) {\n        var proc = ProcessHandle.of(pid);\n        while (true) {\n            if (proc.isEmpty()) {\n                return Optional.empty();\n            }\n\n            var finalProc = proc;\n            var found = TerminalView.get().getSessions().stream()\n                    .filter(session -> session.getShell().equals(finalProc.get()))\n                    .findFirst();\n            if (found.isPresent()) {\n                return found;\n            }\n\n            proc = proc.get().parent();\n        }\n    }\n\n    public synchronized void tick() {\n        for (ShellSession session : new ArrayList<>(sessions)) {\n            var alive = session.shell.isAlive() && session.getTerminal().isRunning();\n            if (!alive) {\n                sessions.remove(session);\n                forListeners(listener -> listener.onSessionClosed(session));\n            }\n        }\n\n        for (TerminalSession terminalInstance : new ArrayList<>(terminalInstances)) {\n            var alive = terminalInstance.isRunning();\n            if (!alive) {\n                terminalInstances.remove(terminalInstance);\n                TrackEvent.withTrace(\"Terminal session is dead\")\n                        .tag(\"pid\", terminalInstance.getTerminalProcess().pid())\n                        .handle();\n                forListeners(listener -> listener.onTerminalClosed(terminalInstance));\n            }\n        }\n    }\n\n    public interface Listener {\n\n        default void onSessionOpened(ShellSession session) {}\n\n        default void onSessionClosed(ShellSession session) {}\n\n        default void onTerminalOpened(TerminalSession instance) {}\n\n        default void onTerminalClosed(TerminalSession instance) {}\n    }\n\n    @Value\n    public static class ShellSession {\n        UUID request;\n        ProcessHandle shell;\n        TerminalSession terminal;\n    }\n\n    @Getter\n    public static class TerminalSession {\n\n        protected final ProcessHandle terminalProcess;\n        protected final ExternalTerminalType terminalType;\n\n        protected TerminalSession(ProcessHandle terminalProcess, ExternalTerminalType terminalType) {\n            this.terminalProcess = terminalProcess;\n            this.terminalType = terminalType;\n        }\n\n        public boolean isRunning() {\n            return terminalProcess.isAlive();\n        }\n\n        public Optional<ControllableTerminalSession> controllable() {\n            return Optional.ofNullable(this instanceof ControllableTerminalSession c ? c : null);\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (!(o instanceof TerminalSession that)) {\n                return false;\n            }\n            return Objects.equals(terminalProcess.pid(), that.terminalProcess.pid());\n        }\n\n        @Override\n        public int hashCode() {\n            return Objects.hashCode(terminalProcess.pid());\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TermiusTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.comp.base.MarkdownComp;\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.util.*;\nimport io.xpipe.core.OsType;\n\nimport java.io.IOException;\nimport java.net.URLEncoder;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic class TermiusTerminalType implements ExternalTerminalType {\n\n    @Override\n    public String getId() {\n        return \"app.termius\";\n    }\n\n    @Override\n    public TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.TABBED;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://termius.com/\";\n    }\n\n    @Override\n    public boolean isRecommended() {\n        return false;\n    }\n\n    @Override\n    public boolean useColoredTitle() {\n        return true;\n    }\n\n    @Override\n    public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n        SshLocalBridge.init();\n        if (!showInfo()) {\n            return;\n        }\n\n        var b = SshLocalBridge.get();\n        var host = b.getHost();\n        var port = b.getPort();\n        var user = URLEncoder.encode(b.getUser(), StandardCharsets.UTF_8);\n        var name = b.getName();\n        DesktopHelper.openAssociatedApplication(\"termius://app/host-sharing#label=\" + name + \"&ip=\" + host + \"&port=\" + port + \"&username=\"\n                + user + \"&os=undefined\");\n    }\n\n    @Override\n    public boolean isAvailable() {\n        try {\n            return switch (OsType.ofLocal()) {\n                case OsType.Linux ignored -> {\n                    yield Files.exists(Path.of(\"/opt/Termius\"));\n                }\n                case OsType.MacOs ignored -> {\n                    yield Files.exists(Path.of(\"/Applications/Termius.app\"));\n                }\n                case OsType.Windows ignored -> {\n                    var r = WindowsRegistry.local()\n                            .readStringValueIfPresent(WindowsRegistry.HKEY_CURRENT_USER, \"SOFTWARE\\\\Classes\\\\termius\");\n                    yield r.isPresent();\n                }\n            };\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).omit().handle();\n            return false;\n        }\n    }\n\n    private boolean showInfo() throws IOException {\n        boolean set = AppCache.getBoolean(\"termiusSetup\", false);\n        if (set) {\n            return true;\n        }\n\n        var b = SshLocalBridge.get();\n        var keyContent = Files.readString(b.getIdentityKey());\n        var activated =\n                AppI18n.get().getMarkdownTranslation(\"app:termiusSetup\").formatted(b.getIdentityKey(), keyContent);\n        var modal = ModalOverlay.of(\"termiusSetup\", new MarkdownComp(activated, s -> s, false).prefWidth(550));\n        modal.addButton(ModalButton.ok(() -> {\n            AppCache.update(\"termiusSetup\", true);\n        }));\n        modal.showAndWait();\n        return AppCache.getBoolean(\"termiusSetup\", false);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TmuxTerminalMultiplexer.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.CommandSupport;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.ShellScript;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@Builder\n@Jacksonized\n@JsonTypeName(\"tmux\")\npublic class TmuxTerminalMultiplexer implements TerminalMultiplexer {\n\n    @Override\n    public boolean supportsSplitView() {\n        return true;\n    }\n\n    @Override\n    public String getDocsLink() {\n        return \"https://github.com/tmux/tmux/wiki/Getting-Started\";\n    }\n\n    @Override\n    public void checkSupported(ShellControl sc) throws Exception {\n        CommandSupport.isInPathOrThrow(sc, \"tmux\");\n    }\n\n    @Override\n    public ShellScript launchForExistingSession(ShellControl control, TerminalLaunchConfiguration config) {\n        var l = new ArrayList<String>();\n        var firstCommand =\n                config.getPanes().getFirst().getDialectLaunchCommand().buildSimple();\n        l.add(\"tmux new-window -t xpipe -n \\\"\" + escape(config.getColoredTitle(), true) + \"\\\" \"\n                + escape(firstCommand, false));\n\n        if (config.getPanes().size() > 1) {\n            for (int i = 1; i < config.getPanes().size(); i++) {\n                var iCommand =\n                        config.getPanes().get(i).getDialectLaunchCommand().buildSimple();\n                l.add(\"tmux split-window -t xpipe \" + escape(iCommand, false));\n            }\n\n            var splitStrategy = AppPrefs.get().terminalSplitStrategy().getValue();\n            var layoutName = splitStrategy == TerminalSplitStrategy.HORIZONTAL\n                    ? \"even-horizontal\"\n                    : splitStrategy == TerminalSplitStrategy.VERTICAL ? \"even-vertical\" : \"tiled\";\n            l.add(\"tmux select-layout -t xpipe \" + layoutName);\n        }\n\n        return ShellScript.lines(l);\n    }\n\n    @Override\n    public ShellScript launchNewSession(ShellControl control, TerminalLaunchConfiguration config) {\n        var l = new ArrayList<String>();\n        var firstCommand =\n                config.getPanes().getFirst().getDialectLaunchCommand().buildSimple();\n        l.addAll(List.of(\n                \"tmux kill-session -t xpipe >/dev/null 2>&1\",\n                \"tmux new-session -d -s xpipe\",\n                \"tmux rename-window \\\"\" + escape(config.getColoredTitle(), true) + \"\\\"\",\n                \"tmux send-keys -t xpipe ' clear; \" + escape(firstCommand, false) + \"; exit' Enter\"));\n\n        if (config.getPanes().size() > 1) {\n            for (int i = 1; i < config.getPanes().size(); i++) {\n                var iCommand =\n                        config.getPanes().get(i).getDialectLaunchCommand().buildSimple();\n                l.add(\"tmux split-window -t xpipe \" + escape(iCommand, false));\n            }\n\n            var splitStrategy = AppPrefs.get().terminalSplitStrategy().getValue();\n            var layoutName = splitStrategy == TerminalSplitStrategy.HORIZONTAL\n                    ? \"even-horizontal\"\n                    : splitStrategy == TerminalSplitStrategy.VERTICAL ? \"even-vertical\" : \"tiled\";\n            l.add(\"tmux select-layout -t xpipe \" + layoutName);\n        }\n\n        l.add(\"tmux attach -d -t xpipe\");\n\n        return ShellScript.lines(l);\n    }\n\n    private String escape(String s, boolean quotes) {\n        var r = s.replaceAll(\"\\\\\\\\\", \"\\\\\\\\\\\\\\\\\");\n        if (quotes) {\n            r = r.replaceAll(\"\\\"\", \"\\\\\\\\\\\"\");\n        }\n        return r;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/TrackableTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\npublic interface TrackableTerminalType extends ExternalTerminalType {\n\n    default int getProcessHierarchyOffset() {\n        return 0;\n    }\n\n    default TerminalDockMode getDockMode() {\n        return TerminalDockMode.UNSUPPORTED;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/WarpTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.*;\nimport io.xpipe.app.util.*;\n\nimport java.net.URLEncoder;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic interface WarpTerminalType extends ExternalTerminalType, TrackableTerminalType {\n\n    WarpTerminalType WINDOWS = new Windows();\n    WarpTerminalType LINUX = new Linux();\n    WarpTerminalType MACOS = new MacOs();\n\n    @Override\n    default TerminalInitFunction additionalInitCommands() {\n        return TerminalInitFunction.of(sc -> {\n            if (sc.getShellDialect() == ShellDialects.ZSH) {\n                return \"printf '\\\\eP$f{\\\"hook\\\": \\\"SourcedRcFileForWarp\\\", \\\"value\\\": { \\\"shell\\\": \\\"zsh\\\"}}\\\\x9c'\";\n            }\n            if (sc.getShellDialect() == ShellDialects.BASH) {\n                return \"printf '\\\\eP$f{\\\"hook\\\": \\\"SourcedRcFileForWarp\\\", \\\"value\\\": { \\\"shell\\\": \\\"bash\\\"}}\\\\x9c'\";\n            }\n            if (sc.getShellDialect() == ShellDialects.FISH) {\n                return \"printf '\\\\eP$f{\\\"hook\\\": \\\"SourcedRcFileForWarp\\\", \\\"value\\\": { \\\"shell\\\": \\\"fish\\\"}}\\\\x9c'\";\n            }\n            return null;\n        });\n    }\n\n    @Override\n    default String getWebsite() {\n        return \"https://www.warp.dev/\";\n    }\n\n    @Override\n    default boolean isRecommended() {\n        return true;\n    }\n\n    @Override\n    default boolean useColoredTitle() {\n        return true;\n    }\n\n    @Override\n    default boolean shouldClear() {\n        return false;\n    }\n\n    class Windows implements WarpTerminalType {\n\n        @Override\n        public boolean isAvailable() {\n            return WindowsRegistry.local().keyExists(WindowsRegistry.HKEY_CURRENT_USER, \"Software\\\\Classes\\\\warp\");\n        }\n\n        @Override\n        public String getId() {\n            return \"app.warp\";\n        }\n\n        @Override\n        public TerminalOpenFormat getOpenFormat() {\n            // Warp always opens the new separate window, so we don't want to use it in the file browser for docking\n            // Just say that we don't support new windows, that way it doesn't dock\n            return TerminalOpenFormat.TABBED;\n        }\n\n        @Override\n        public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n            var pane = configuration.single();\n            try (var sc = LocalShell.getShell().start()) {\n                var command = pane.getScriptDialect().getSetEnvironmentVariableCommand(\"PSModulePath\", \"\")\n                        + \"\\n\"\n                        + pane.getScriptDialect()\n                                .runScriptCommand(sc, pane.getScriptFile().toString());\n\n                // Move to subdir as Warp tries to index the parent dir, which would be temp in this case\n                var scriptFile = ScriptHelper.createExecScript(pane.getScriptDialect(), sc, command);\n                var movedScriptFile =\n                        AppSystemInfo.ofCurrent().getTemp().resolve(\"warp\").resolve(scriptFile.getFileName());\n                Files.createDirectories(movedScriptFile.getParent());\n                Files.move(scriptFile.asLocalPath(), movedScriptFile);\n\n                var scriptArg = URLEncoder.encode(movedScriptFile.toString(), StandardCharsets.UTF_8);\n                if (!configuration.isPreferTabs()) {\n                    DesktopHelper.openAssociatedApplication(\"warp://action/new_window?path=\" + scriptArg);\n                } else {\n                    DesktopHelper.openAssociatedApplication(\"warp://action/new_tab?path=\" + scriptArg);\n                }\n            }\n        }\n    }\n\n    class Linux implements WarpTerminalType {\n\n        @Override\n        public int getProcessHierarchyOffset() {\n            return 2;\n        }\n\n        @Override\n        public boolean isAvailable() {\n            return Files.exists(Path.of(\"/opt/warpdotdev\"));\n        }\n\n        @Override\n        public String getId() {\n            return \"app.warp\";\n        }\n\n        @Override\n        public TerminalOpenFormat getOpenFormat() {\n            // Warp always opens the new separate window, so we don't want to use it in the file browser for docking\n            // Just say that we don't support new windows, that way it doesn't dock\n            return TerminalOpenFormat.TABBED;\n        }\n\n        @Override\n        public void launch(TerminalLaunchConfiguration configuration) {\n            var pane = configuration.single();\n            if (!configuration.isPreferTabs()) {\n                DesktopHelper.openAssociatedApplication(\"warp://action/new_window?path=\" + pane.getScriptFile());\n            } else {\n                DesktopHelper.openAssociatedApplication(\"warp://action/new_tab?path=\" + pane.getScriptFile());\n            }\n        }\n    }\n\n    class MacOs implements ExternalApplicationType.MacApplication, WarpTerminalType {\n\n        @Override\n        public int getProcessHierarchyOffset() {\n            return 2;\n        }\n\n        @Override\n        public TerminalOpenFormat getOpenFormat() {\n            return TerminalOpenFormat.TABBED;\n        }\n\n        @Override\n        public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n            LocalShell.getShell()\n                    .executeSimpleCommand(CommandBuilder.of()\n                            .add(\"open\", \"-a\")\n                            .addQuoted(\"Warp.app\")\n                            .addFile(configuration.single().getScriptFile()));\n        }\n\n        @Override\n        public String getApplicationName() {\n            return \"Warp\";\n        }\n\n        @Override\n        public String getId() {\n            return \"app.warp\";\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/WaveTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.core.AppInstallation;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\n\npublic interface WaveTerminalType extends ExternalTerminalType, TrackableTerminalType {\n\n    ExternalTerminalType WAVE_WINDOWS = new Windows();\n    ExternalTerminalType WAVE_LINUX = new Linux();\n    ExternalTerminalType WAVE_MAC_OS = new MacOs();\n\n    @Override\n    default boolean isAvailable() {\n        try (var sc = LocalShell.getShell().start()) {\n            var wsh = sc.view().findProgram(\"wsh\");\n            return wsh.isPresent();\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).handle();\n            return false;\n        }\n    }\n\n    @Override\n    default TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.NEW_WINDOW;\n    }\n\n    @Override\n    default String getWebsite() {\n        return \"https://www.waveterm.dev/\";\n    }\n\n    @Override\n    default boolean isRecommended() {\n        return false;\n    }\n\n    @Override\n    default boolean useColoredTitle() {\n        return true;\n    }\n\n    @Override\n    default void launch(TerminalLaunchConfiguration configuration) throws Exception {\n        try (var sc = LocalShell.getShell().start()) {\n            var wsh = sc.view().findProgram(\"wsh\");\n            var env = sc.view().getEnvironmentVariable(\"WAVETERM_JWT\");\n            if (wsh.isEmpty() || env.isEmpty()) {\n                var inPath = sc.view().findProgram(\"xpipe\").isPresent();\n                var msg = \"\"\"\n                          The Wave integration requires XPipe to be launched from Wave itself to have access to its environment variables. Otherwise, XPipe does not have access to the token to control Wave.\n\n                          You can do this by first making sure that XPipe is shut down and then running the command \"%s\" in a local terminal block inside Wave.\n                          \"\"\".formatted(\n                                inPath\n                                        ? \"xpipe open\"\n                                        : \"\\\"\" + AppInstallation.ofCurrent().getCliExecutablePath() + \"\\\" open\");\n                throw ErrorEventFactory.expected(new IllegalStateException(msg));\n            }\n\n            sc.command(CommandBuilder.of()\n                            .addFile(\"wsh\")\n                            .add(\"run\", \"--forceexit\", \"--delay\", \"0\", \"--\")\n                            .add(configuration.single().getDialectLaunchCommand()))\n                    .execute();\n        }\n    }\n\n    @Override\n    default String getId() {\n        return \"app.wave\";\n    }\n\n    class Windows implements WaveTerminalType {}\n\n    class Linux implements WaveTerminalType {}\n\n    class MacOs implements WaveTerminalType {}\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.prefs.ExternalApplicationHelper;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.CommandSupport;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.app.util.WindowsRegistry;\nimport io.xpipe.core.FilePath;\n\nimport io.xpipe.core.OsType;\nimport lombok.SneakyThrows;\n\nimport java.io.IOException;\nimport java.net.StandardProtocolFamily;\nimport java.net.UnixDomainSocketAddress;\nimport java.nio.channels.SocketChannel;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Instant;\nimport java.util.Comparator;\nimport java.util.Optional;\n\npublic interface WezTerminalType extends ExternalTerminalType, TrackableTerminalType {\n\n    ExternalTerminalType WEZTERM_WINDOWS = new Windows();\n    ExternalTerminalType WEZTERM_LINUX = new Linux();\n    ExternalTerminalType WEZTERM_MAC_OS = new MacOs();\n\n    @Override\n    default TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.NEW_WINDOW_OR_TABBED;\n    }\n\n    @Override\n    default String getWebsite() {\n        return \"https://wezfurlong.org/wezterm/index.html\";\n    }\n\n    @Override\n    default boolean isRecommended() {\n        return true;\n    }\n\n    @Override\n    default boolean supportsSplitView() {\n        return true;\n    }\n\n    @Override\n    default boolean useColoredTitle() {\n        return true;\n    }\n\n    default Path getSocketDir() {\n        if (OsType.ofLocal() == OsType.LINUX) {\n            return Path.of(System.getenv(\"XDG_RUNTIME_DIR\"), \"wezterm\");\n        } else {\n            return AppSystemInfo.ofCurrent().getUserHome().resolve(\".local\", \"share\", \"wezterm\");\n        }\n    }\n\n    default Optional<Path> waitForInstanceStart(int count) {\n        Path dir = getSocketDir();\n        if (!Files.exists(dir)) {\n            return Optional.empty();\n        }\n\n        for (int i = 0; i < count; i++) {\n            ThreadHelper.sleep(100);\n            var active = getActiveSocket();\n            if (active.isPresent()) {\n                return active;\n            }\n        }\n\n        return Optional.empty();\n    }\n\n    default Optional<Path> getActiveSocket() {\n        Path dir = getSocketDir();\n        if (!Files.exists(dir)) {\n            return Optional.empty();\n        }\n\n        try (var stream = Files.list(dir)) {\n            var files = stream.sorted(Comparator.<Path, Instant>comparing(path -> {\n                                try {\n                                    return Files.getLastModifiedTime(path).toInstant();\n                                } catch (IOException e) {\n                                    return Instant.MIN;\n                                }\n                            })\n                            .reversed())\n                    .toList();\n            for (Path file : files) {\n                if (file.getFileName().toString().contains(\"gui-sock\")) {\n                    try (SocketChannel channel = SocketChannel.open(StandardProtocolFamily.UNIX)) {\n                        if (channel.connect(UnixDomainSocketAddress.of(file))) {\n                            if (channel.isConnected()) {\n                                return Optional.of(file);\n                            }\n                        }\n                    } catch (IOException ignored) {\n                    }\n                }\n            }\n        } catch (IOException ignored) {\n        }\n\n        return Optional.empty();\n    }\n\n    @Override\n    default void launch(TerminalLaunchConfiguration configuration) throws Exception {\n        var base = getWeztermCommandBase();\n        var activeSocket = waitForInstanceStart(1);\n        var tabid = \"0\";\n        // Always start a new window for split panes as we can't find the pane index to start with\n        if (activeSocket.isEmpty() || configuration.getPanes().size() > 1 || !configuration.isPreferTabs()) {\n            var gui = CommandBuilder.of().add(base.buildSimple().replace(\"wezterm.exe\", \"wezterm-gui.exe\"));\n            var command = CommandBuilder.of()\n                    .add(gui)\n                    .add(\"start\", \"--always-new-process\")\n                    .add(configuration.getPanes().getFirst().getDialectLaunchCommand());\n            ExternalApplicationHelper.startAsync(command);\n            activeSocket = waitForInstanceStart(50);\n            if (activeSocket.isEmpty()) {\n                return;\n            }\n        } else {\n            var command = CommandBuilder.of()\n                    .add(base)\n                    .add(\"cli\", \"spawn\")\n                    .add(configuration.getPanes().getFirst().getDialectLaunchCommand());\n            command.fixedEnvironment(\"WEZTERM_UNIX_SOCKET\", activeSocket.get().toString());\n            tabid = LocalShell.getShell()\n                    .command(command)\n                    .withWorkingDirectory(FilePath.of(getSocketDir()))\n                    .readStdoutOrThrow();\n        }\n\n        var titleCommand = CommandBuilder.of()\n                .add(base)\n                .add(\"cli\", \"set-tab-title\")\n                .add(\"--tab-id\", tabid)\n                .addQuoted(configuration.getColoredTitle());\n        titleCommand.fixedEnvironment(\"WEZTERM_UNIX_SOCKET\", activeSocket.get().toString());\n        // Sometimes the tab ids don't exist even though it just returned them to us\n        // So just ignore any errors\n        LocalShell.getShell()\n                .command(titleCommand)\n                .withWorkingDirectory(FilePath.of(getSocketDir()))\n                .executeAndCheck();\n\n        if (configuration.getPanes().size() > 1) {\n            var direction = AppPrefs.get().terminalSplitStrategy().getValue();\n            var directionIterator = direction.iterator();\n            for (int i = 1; i < configuration.getPanes().size(); i++) {\n                LocalShell.getShell()\n                        .command(CommandBuilder.of()\n                                .add(base)\n                                .add(\"cli\", \"split-pane\")\n                                .addIf(\n                                        directionIterator.getSplitDirection()\n                                                == TerminalSplitStrategy.SplitDirection.HORIZONTAL,\n                                        \"--horizontal\")\n                                .addIf(\n                                        directionIterator.getSplitDirection()\n                                                == TerminalSplitStrategy.SplitDirection.VERTICAL,\n                                        \"--bottom\")\n                                .add(\"--pane-id\", \"\" + directionIterator.getTargetPaneIndex())\n                                .add(\"--percent\", \"50\")\n                                .add(configuration.getPanes().get(i).getDialectLaunchCommand())\n                                .fixedEnvironment(\n                                        \"WEZTERM_UNIX_SOCKET\",\n                                        activeSocket.get().toString()))\n                        .withWorkingDirectory(FilePath.of(getSocketDir()))\n                        .execute();\n                directionIterator.next();            }\n        }\n    }\n\n    CommandBuilder getWeztermCommandBase() throws Exception;\n\n    class Windows implements ExternalApplicationType.WindowsType, ExternalTerminalType, WezTerminalType {\n\n        @Override\n        public TerminalDockMode getDockMode() {\n            return TerminalDockMode.BORDERLESS;\n        }\n\n        @Override\n        public CommandBuilder getWeztermCommandBase() {\n            return CommandBuilder.of().addFile(findExecutable());\n        }\n\n        @Override\n        public boolean detach() {\n            return true;\n        }\n\n        @Override\n        public String getExecutable() {\n            return \"wezterm\";\n        }\n\n        @Override\n        public Optional<Path> determineInstallation() {\n            try {\n                var foundKey = WindowsRegistry.local()\n                        .findKeyForEqualValueMatchRecursive(\n                                WindowsRegistry.HKEY_LOCAL_MACHINE,\n                                \"SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\",\n                                \"http://wezfurlong.org/wezterm\");\n                if (foundKey.isPresent()) {\n                    var installKey = WindowsRegistry.local()\n                            .readStringValueIfPresent(\n                                    foundKey.get().getHkey(), foundKey.get().getKey(), \"InstallLocation\");\n                    if (installKey.isPresent()) {\n                        return installKey.map(p -> p + \"\\\\wezterm.exe\").map(Path::of);\n                    }\n                }\n            } catch (Exception ex) {\n                ErrorEventFactory.fromThrowable(ex).omit().handle();\n            }\n\n            try {\n                if (CommandSupport.isInLocalPath(\"wezterm\")) {\n                    return Optional.of(Path.of(\"wezterm\"));\n                }\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).omit().handle();\n            }\n\n            return Optional.empty();\n        }\n\n        @Override\n        public String getId() {\n            return \"app.wezterm\";\n        }\n\n        @Override\n        public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n            // WezTerm does not focus the window\n            var listener = new TerminalView.Listener() {\n                @Override\n                @SneakyThrows\n                public void onSessionOpened(TerminalView.ShellSession session) {\n                    TerminalView.get().removeListener(this);\n                    var term = (WindowsTerminalSession) session.getTerminal();\n                    term.frontOfMainWindow();\n                    term.focus();\n                }\n            };\n            TerminalView.get().addListener(listener);\n\n            WezTerminalType.super.launch(configuration);\n        }\n    }\n\n    class Linux implements ExternalApplicationType.LinuxApplication, WezTerminalType {\n\n        @Override\n        public CommandBuilder getWeztermCommandBase() throws Exception {\n            return getCommandBase();\n        }\n\n        @Override\n        public String getExecutable() {\n            return \"wezterm\";\n        }\n\n        @Override\n        public boolean detach() {\n            return true;\n        }\n\n        public boolean isAvailable() {\n            try {\n                return CommandSupport.isInLocalPath(\"wezterm\");\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).omit().handle();\n                return false;\n            }\n        }\n\n        @Override\n        public String getId() {\n            return \"app.wezterm\";\n        }\n\n        @Override\n        public String getFlatpakId() {\n            return \"org.wezfurlong.wezterm\";\n        }\n    }\n\n    class MacOs implements ExternalApplicationType.MacApplication, WezTerminalType {\n\n        @Override\n        public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n            try (var sc = LocalShell.getShell()) {\n                var pathOut = sc.command(String.format(\n                                \"mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications 2>/dev/null\",\n                                getApplicationName()))\n                        .readStdoutOrThrow();\n                var path = Path.of(pathOut);\n\n                boolean runGui = true;\n                if (configuration.isPreferTabs()) {\n                    runGui = !sc.command(CommandBuilder.of()\n                                    .addFile(path.resolve(\"Contents\")\n                                            .resolve(\"MacOS\")\n                                            .resolve(\"wezterm\")\n                                            .toString())\n                                    .add(\"cli\", \"spawn\", \"--pane-id\", \"0\")\n                                    .addFile(configuration.single().getScriptFile()))\n                            .executeAndCheck();\n                }\n                if (runGui) {\n                    ExternalApplicationHelper.startAsync(CommandBuilder.of()\n                            .addFile(path.resolve(\"Contents\")\n                                    .resolve(\"MacOS\")\n                                    .resolve(\"wezterm-gui\")\n                                    .toString())\n                            .add(\"start\")\n                            .addFile(configuration.single().getScriptFile()));\n                }\n            }\n        }\n\n        @Override\n        public CommandBuilder getWeztermCommandBase() throws Exception {\n            try (var sc = LocalShell.getShell()) {\n                var pathOut = sc.command(String.format(\n                                \"mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications 2>/dev/null\",\n                                getApplicationName()))\n                        .readStdoutOrThrow();\n                var path = Path.of(pathOut);\n                return CommandBuilder.of()\n                        .addFile(path.resolve(\"Contents\").resolve(\"MacOS\").resolve(\"wezterm\"));\n            }\n        }\n\n        @Override\n        public String getApplicationName() {\n            return \"WezTerm\";\n        }\n\n        @Override\n        public String getId() {\n            return \"app.wezterm\";\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/WindowsTerminalSession.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.platform.NativeWinWindowControl;\nimport io.xpipe.app.util.Rect;\n\nimport lombok.AccessLevel;\nimport lombok.Getter;\nimport lombok.experimental.FieldDefaults;\n\nimport java.util.Objects;\n\n@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)\n@Getter\npublic final class WindowsTerminalSession extends ControllableTerminalSession {\n\n    NativeWinWindowControl control;\n\n    public WindowsTerminalSession(ProcessHandle terminal, ExternalTerminalType terminalType, NativeWinWindowControl control) {\n        super(terminal, terminalType);\n        this.control = control;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (!(o instanceof WindowsTerminalSession that)) {\n            return false;\n        }\n        if (!super.equals(o)) {\n            return false;\n        }\n        return Objects.equals(control.getWindowHandle(), that.control.getWindowHandle());\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(super.hashCode(), control.getWindowHandle());\n    }\n\n    @Override\n    public boolean isRunning() {\n        return super.isRunning() && control.isVisible();\n    }\n\n    @Override\n    public void own() {\n        control.takeOwnership(NativeWinWindowControl.MAIN_WINDOW.getWindowHandle());\n    }\n\n    @Override\n    public void disown() {\n        control.releaseOwnership();\n    }\n\n    @Override\n    public void removeIcon() {\n        control.removeIcon();\n    }\n\n    @Override\n    public void restoreIcon() {\n        control.restoreIcon();\n    }\n\n    @Override\n    public void removeStyle() {\n        control.setWindowsTransitionsEnabled(false);\n        if (terminalType != null && terminalType instanceof TrackableTerminalType t && t.getDockMode() == TerminalDockMode.BORDERLESS) {\n            control.removeBorders();\n        }\n    }\n\n    @Override\n    public void restoreStyle() {\n        control.setWindowsTransitionsEnabled(true);\n        if (terminalType != null && terminalType instanceof TrackableTerminalType t && t.getDockMode() == TerminalDockMode.BORDERLESS) {\n            control.restoreBorders();\n        }\n    }\n\n    @Override\n    public void show() {\n        this.control.show();\n    }\n\n    @Override\n    public void minimize() {\n        this.control.minimize();\n    }\n\n    @Override\n    public void backOfMainWindow() {\n        getControl().orderRelative(NativeWinWindowControl.MAIN_WINDOW.getWindowHandle());\n    }\n\n    @Override\n    public void frontOfMainWindow() {\n        this.control.moveToFront();\n    }\n\n    @Override\n    public void focus() {\n        this.control.activate();\n    }\n\n    @Override\n    public void updatePosition(Rect bounds) {\n        control.move(bounds);\n        this.lastBounds = queryBounds();\n        this.customBounds = false;\n    }\n\n    @Override\n    public void close() {\n        this.control.close();\n    }\n\n    @Override\n    public boolean isActive() {\n        if (control.isIconified()) {\n            return false;\n        }\n\n        if (!control.isVisible()) {\n            return false;\n        }\n\n        var bounds = queryBounds();\n        if (bounds.getX() == 0 && bounds.getY() == 0 && bounds.getW() == 0 && bounds.getH() == 0) {\n            return false;\n        }\n\n        return true;\n    }\n\n    @Override\n    public Rect queryBounds() {\n        return control.getBounds();\n    }\n\n    public void updateBoundsState() {\n        if (!isActive()) {\n            return;\n        }\n\n        var bounds = queryBounds();\n        if (bounds.getX() == -32000 || bounds.getY() == -32000) {\n            return;\n        }\n\n        if (lastBounds != null && (lastBounds.getX() == -32000 || lastBounds.getY() == -32000)) {\n            return;\n        }\n\n        if (lastBounds != null && !lastBounds.equals(bounds)) {\n            customBounds = true;\n        }\n        lastBounds = bounds;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/WindowsTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.AppInstallation;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ShellDialects;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.JacksonMapper;\n\nimport com.fasterxml.jackson.databind.node.JsonNodeFactory;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.LocalDate;\nimport java.time.ZoneId;\nimport java.util.concurrent.atomic.AtomicInteger;\n\npublic interface WindowsTerminalType extends ExternalTerminalType, TrackableTerminalType {\n\n    ExternalTerminalType WINDOWS_TERMINAL = new Standard();\n    ExternalTerminalType WINDOWS_TERMINAL_PREVIEW = new Preview();\n    ExternalTerminalType WINDOWS_TERMINAL_CANARY = new Canary();\n\n    AtomicInteger windowCounter = new AtomicInteger(101);\n\n    private static String getFixedTitle(String s) {\n        // A weird behavior in Windows Terminal causes the trailing\n        // backslash of a filepath to escape the closing quote in the title argument\n        // So just remove that slash\n        var fixedName = FilePath.of(s).removeTrailingSlash().toString();\n        // To fix https://github.com/microsoft/terminal/issues/13264\n        fixedName = fixedName.replaceAll(\";\", \"_\");\n        return fixedName;\n    }\n\n    private static CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {\n        // Start from high window index to guarantee that xpipe uses its own window\n        var cmd = CommandBuilder.of()\n                .addIf(configuration.isPreferTabs(), \"-w\", \"100\", \"nt\")\n                .addIf(!configuration.isPreferTabs(), \"-w\", \"\" + windowCounter.getAndIncrement());\n\n        if (configuration.getColor() != null) {\n            cmd.add(\"--tabColor\").addQuoted(configuration.getColor().toHexString());\n        }\n\n        // We assume that all scripts are located in the same dir\n        var spaces =\n                configuration.getPanes().getFirst().getScriptFile().toString().contains(\" \");\n        var splitIterator = AppPrefs.get().terminalSplitStrategy().getValue().iterator();\n        for (int i = 0; i < configuration.getPanes().size(); i++) {\n            splitIterator.next();\n\n            var pane = configuration.getPanes().get(i);\n            var scriptFile = spaces\n                    ? pane.getScriptFile().getFileName()\n                    : pane.getScriptFile().toString();\n            var scriptOpenCommand = pane.getScriptDialect().getOpenScriptCommand(scriptFile);\n            if (i > 0) {\n                cmd.add(sc -> ShellDialects.isPowershell(sc) ? \"`;\" : \";\");\n                cmd.add(\"sp\");\n                // The names are not intuitive\n                cmd.addIf(\n                        splitIterator.getSplitDirection() == TerminalSplitStrategy.SplitDirection.VERTICAL,\n                        \"--horizontal\");\n                cmd.addIf(\n                        splitIterator.getSplitDirection() == TerminalSplitStrategy.SplitDirection.HORIZONTAL,\n                        \"--vertical\");\n            }\n            cmd.add(\"--title\").addQuoted(getFixedTitle(configuration.getColoredTitle()));\n            cmd.add(\"--profile\").addQuoted(\"{021eff0f-b38a-45f9-895d-41467e9d510f}\");\n            cmd.add(scriptOpenCommand);\n\n            var targetPaneIndex = splitIterator.getTargetPaneIndex();\n            cmd.add(sc -> ShellDialects.isPowershell(sc) ? \"`;\" : \";\");\n            cmd.add(\"mf\", targetPaneIndex == 0 ? \"first\" : \"previousInOrder\");\n            if (targetPaneIndex > 0) {\n                cmd.add(sc -> ShellDialects.isPowershell(sc) ? \"`;\" : \";\");\n                cmd.add(\"mf\");\n                cmd.add(\"previousInOrder\");\n            }\n        }\n        return cmd;\n    }\n\n    @Override\n    default boolean supportsSplitView() {\n        return true;\n    }\n\n    @Override\n    default TerminalDockMode getDockMode() {\n        return TerminalDockMode.BORDERLESS;\n    }\n\n    default void checkProfile() throws IOException {\n        // Update old configs\n        var before =\n                LocalDate.of(2026, 2, 8).atStartOfDay(ZoneId.systemDefault()).toInstant();\n        var outdated = AppCache.getModifiedTime(\"wtProfileSet\")\n                .map(instant -> instant.isBefore(before))\n                .orElse(false);\n\n        var profileSet = AppCache.getBoolean(\"wtProfileSet\", false);\n        if (profileSet && !outdated) {\n            return;\n        }\n\n        if (outdated) {\n            AppCache.clear(\"wtProfileSet\");\n        }\n\n        if (!Files.exists(getConfigFile())) {\n            return;\n        }\n\n        var uuid = \"{021eff0f-b38a-45f9-895d-41467e9d510f}\";\n        var config = JacksonMapper.getDefault().readTree(getConfigFile().toFile());\n        var profiles = config.withObjectProperty(\"profiles\").withArrayProperty(\"list\");\n        for (int i = 0; i < profiles.size(); i++) {\n            var profile = profiles.get(i);\n            var profileId = profile.get(\"guid\");\n            if (profileId != null && profileId.asText().equals(uuid)) {\n                profiles.remove(i);\n                break;\n            }\n        }\n\n        var newProfile = JsonNodeFactory.instance.objectNode();\n        newProfile.put(\"guid\", uuid);\n        newProfile.put(\"hidden\", true);\n        newProfile.put(\"name\", \"XPipe\");\n        newProfile.put(\"closeOnExit\", \"always\");\n        newProfile.put(\"suppressApplicationTitle\", true);\n        // To make docking a better experience\n        newProfile.put(\"showTabsInTitlebar\", true);\n        newProfile.putNull(\"startingDirectory\");\n        newProfile.put(\"elevate\", false);\n        if (!AppProperties.get().isDevelopmentEnvironment()) {\n            var logoFile = AppInstallation.ofCurrent().getLogoPath();\n            newProfile.put(\"icon\", logoFile.toString());\n        }\n        profiles.add(newProfile);\n        JacksonMapper.getDefault().writeValue(getConfigFile().toFile(), config);\n        AppCache.update(\"wtProfileSet\", true);\n    }\n\n    @Override\n    default TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.NEW_WINDOW_OR_TABBED;\n    }\n\n    @Override\n    default boolean isRecommended() {\n        return true;\n    }\n\n    @Override\n    default boolean useColoredTitle() {\n        return false;\n    }\n\n    Path getConfigFile();\n\n    class Standard extends SimplePathType implements WindowsTerminalType {\n\n        public Standard() {\n            super(\"app.windowsTerminal\", \"wt.exe\", false);\n        }\n\n        @Override\n        public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n            checkProfile();\n            try (var sc = LocalShell.getShell().start()) {\n                var inPath = sc.view().findProgram(\"wt\");\n                var exec = inPath.orElse(FilePath.of(getPath()));\n                var firstScriptFile = configuration.getPanes().getFirst().getScriptFile();\n                var spaces = firstScriptFile.toString().contains(\" \");\n\n                if (spaces) {\n                    var wd = sc.view().pwd();\n                    sc.command(CommandBuilder.of().addFile(exec).add(toCommand(configuration)))\n                            .withWorkingDirectory(firstScriptFile.getParent())\n                            .execute();\n                    sc.view().cd(wd);\n                } else {\n                    sc.command(CommandBuilder.of().addFile(exec).add(toCommand(configuration)))\n                            .execute();\n                }\n            }\n        }\n\n        @Override\n        protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {\n            return WindowsTerminalType.toCommand(configuration);\n        }\n\n        @Override\n        public String getWebsite() {\n            return \"https://aka.ms/terminal\";\n        }\n\n        private Path getPath() {\n            return AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Microsoft\\\\WindowsApps\\\\Microsoft.WindowsTerminal_8wekyb3d8bbwe\\\\wt.exe\");\n        }\n\n        @Override\n        public Path getConfigFile() {\n            return AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Packages\\\\Microsoft.WindowsTerminal_8wekyb3d8bbwe\\\\LocalState\\\\settings.json\");\n        }\n    }\n\n    class Preview implements WindowsTerminalType {\n\n        @Override\n        public String getWebsite() {\n            return \"https://aka.ms/terminal-preview\";\n        }\n\n        @Override\n        public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n            if (!isAvailable()) {\n                throw ErrorEventFactory.expected(\n                        new IllegalArgumentException(\"Windows Terminal Preview is not installed at \" + getPath()));\n            }\n\n            checkProfile();\n            try (var sc = LocalShell.getShell().start()) {\n                var exec = getPath();\n                var firstScriptFile = configuration.getPanes().getFirst().getScriptFile();\n                var spaces = firstScriptFile.toString().contains(\" \");\n\n                if (spaces) {\n                    var wd = sc.view().pwd();\n                    sc.command(CommandBuilder.of().addFile(exec).add(toCommand(configuration)))\n                            .withWorkingDirectory(firstScriptFile.getParent())\n                            .execute();\n                    sc.view().cd(wd);\n                } else {\n                    sc.command(CommandBuilder.of().addFile(exec).add(toCommand(configuration)))\n                            .execute();\n                }\n            }\n        }\n\n        private Path getPath() {\n            return AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Microsoft\\\\WindowsApps\\\\Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe\\\\wt.exe\");\n        }\n\n        @Override\n        public boolean isAvailable() {\n            // The executable is a weird link\n            return Files.exists(getPath().getParent());\n        }\n\n        @Override\n        public String getId() {\n            return \"app.windowsTerminalPreview\";\n        }\n\n        @Override\n        public Path getConfigFile() {\n            return AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Packages\\\\Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe\\\\LocalState\\\\settings.json\");\n        }\n    }\n\n    class Canary implements WindowsTerminalType {\n\n        @Override\n        public String getWebsite() {\n            return \"https://devblogs.microsoft.com/commandline/introducing-windows-terminal-canary/\";\n        }\n\n        @Override\n        public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n            if (!isAvailable()) {\n                throw ErrorEventFactory.expected(\n                        new IllegalArgumentException(\"Windows Terminal Canary is not installed at \" + getPath()));\n            }\n\n            checkProfile();\n            try (var sc = LocalShell.getShell().start()) {\n                var exec = getPath();\n                var firstScriptFile = configuration.getPanes().getFirst().getScriptFile();\n                var spaces = firstScriptFile.toString().contains(\" \");\n\n                if (spaces) {\n                    var wd = sc.view().pwd();\n                    sc.command(CommandBuilder.of().addFile(exec).add(toCommand(configuration)))\n                            .withWorkingDirectory(firstScriptFile.getParent())\n                            .execute();\n                    sc.view().cd(wd);\n                } else {\n                    sc.command(CommandBuilder.of().addFile(exec).add(toCommand(configuration)))\n                            .execute();\n                }\n            }\n        }\n\n        private Path getPath() {\n            return AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Microsoft\\\\WindowsApps\\\\Microsoft.WindowsTerminalCanary_8wekyb3d8bbwe\\\\wt.exe\");\n        }\n\n        @Override\n        public boolean isAvailable() {\n            // The executable is a weird link\n            return Files.exists(getPath().getParent());\n        }\n\n        @Override\n        public String getId() {\n            return \"app.windowsTerminalCanary\";\n        }\n\n        @Override\n        public Path getConfigFile() {\n            return AppSystemInfo.ofWindows()\n                    .getLocalAppData()\n                    .resolve(\"Packages\\\\Microsoft.WindowsTerminalCanary_8wekyb3d8bbwe\\\\LocalState\\\\settings.json\");\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/XShellTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.comp.base.MarkdownComp;\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.util.SshLocalBridge;\nimport io.xpipe.app.util.WindowsRegistry;\n\nimport java.nio.file.Path;\nimport java.util.Optional;\n\npublic class XShellTerminalType implements ExternalApplicationType.WindowsType, ExternalTerminalType {\n\n    @Override\n    public TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.TABBED;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://www.netsarang.com/en/xshell/\";\n    }\n\n    @Override\n    public boolean isRecommended() {\n        return false;\n    }\n\n    @Override\n    public boolean useColoredTitle() {\n        return false;\n    }\n\n    @Override\n    public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n        SshLocalBridge.init();\n        if (!showInfo()) {\n            return;\n        }\n\n        var b = SshLocalBridge.get();\n        var keyName = b.getIdentityKey().getFileName().toString();\n        var command = CommandBuilder.of()\n                .add(\"-url\")\n                .addQuoted(\"ssh://\" + b.getUser() + \"@localhost:\" + b.getPort())\n                .add(\"-i\", keyName);\n        launch(command);\n    }\n\n    @Override\n    public boolean detach() {\n        return false;\n    }\n\n    @Override\n    public String getExecutable() {\n        return \"Xshell\";\n    }\n\n    @Override\n    public Optional<Path> determineInstallation() {\n        try {\n            var r = WindowsRegistry.local()\n                    .readStringValueIfPresent(\n                            WindowsRegistry.HKEY_LOCAL_MACHINE,\n                            \"SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\App Paths\\\\Xshell.exe\");\n            return r.map(Path::of);\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).omit().handle();\n            return Optional.empty();\n        }\n    }\n\n    private boolean showInfo() {\n        boolean set = AppCache.getBoolean(\"xshellSetup\", false);\n        if (set) {\n            return true;\n        }\n\n        var b = SshLocalBridge.get();\n        var keyName = b.getIdentityKey().getFileName().toString();\n        var activated = AppI18n.get().getMarkdownTranslation(\"app:xshellSetup\").formatted(b.getIdentityKey(), keyName);\n        var modal = ModalOverlay.of(\"xshellSetup\", new MarkdownComp(activated, s -> s, false).prefWidth(450));\n        modal.addButton(ModalButton.ok(() -> {\n            AppCache.update(\"xshellSetup\", true);\n        }));\n        modal.showAndWait();\n        return AppCache.getBoolean(\"xshellSetup\", false);\n    }\n\n    @Override\n    public String getId() {\n        return \"app.xShell\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/YakuakeTerminalType.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.CommandSupport;\nimport io.xpipe.app.process.LocalShell;\n\npublic class YakuakeTerminalType implements ExternalApplicationType.PathApplication, TrackableTerminalType {\n\n    @Override\n    public TerminalOpenFormat getOpenFormat() {\n        return TerminalOpenFormat.TABBED;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://apps.kde.org/en-gb/yakuake/\";\n    }\n\n    @Override\n    public boolean isRecommended() {\n        return false;\n    }\n\n    @Override\n    public boolean useColoredTitle() {\n        return false;\n    }\n\n    @Override\n    public void launch(TerminalLaunchConfiguration configuration) throws Exception {\n        CommandSupport.isInLocalPathOrThrow(\"Yakuake\", \"yakuake\");\n\n        var toggle = CommandBuilder.of()\n                .add(\"qdbus\", \"org.kde.yakuake\", \"/yakuake/window\", \"org.kde.yakuake.toggleWindowState\");\n        LocalShell.getShell().command(toggle).execute();\n\n        var newTab =\n                CommandBuilder.of().add(\"qdbus\", \"org.kde.yakuake\", \"/yakuake/sessions\", \"org.kde.yakuake.addSession\");\n        var index = LocalShell.getShell().command(newTab).readStdoutOrThrow();\n\n        var renameTab = CommandBuilder.of()\n                .add(\"qdbus\", \"org.kde.yakuake\", \"/yakuake/tabs\", \"setTabTitle\", index)\n                .addLiteral(configuration.getColoredTitle());\n        LocalShell.getShell().command(renameTab).execute();\n\n        var run = CommandBuilder.of()\n                .add(\"qdbus\", \"org.kde.yakuake\", \"/yakuake/sessions\", \"runCommandInTerminal\", index)\n                .addFile(configuration.single().getScriptFile());\n        LocalShell.getShell().command(run).execute();\n    }\n\n    @Override\n    public String getExecutable() {\n        return \"yakuake\";\n    }\n\n    @Override\n    public boolean detach() {\n        return false;\n    }\n\n    @Override\n    public String getId() {\n        return \"app.yakuake\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/terminal/ZellijTerminalMultiplexer.java",
    "content": "package io.xpipe.app.terminal;\n\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.*;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.SneakyThrows;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@Builder\n@Jacksonized\n@JsonTypeName(\"zellij\")\npublic class ZellijTerminalMultiplexer implements TerminalMultiplexer {\n\n    @Override\n    public boolean supportsSplitView() {\n        return true;\n    }\n\n    @Override\n    public String getDocsLink() {\n        return \"https://zellij.dev/\";\n    }\n\n    @Override\n    public void checkSupported(ShellControl sc) throws Exception {\n        CommandSupport.isInPathOrThrow(sc, \"zellij\");\n    }\n\n    @Override\n    public ShellScript launchForExistingSession(ShellControl control, TerminalLaunchConfiguration config) {\n        var l = new ArrayList<String>();\n        var firstCommand =\n                config.getPanes().getFirst().getDialectLaunchCommand().buildSimple();\n        l.addAll(List.of(\n                \"zellij attach --create-background xpipe\",\n                \"zellij -s xpipe action new-tab --name \\\"\" + escape(config.getColoredTitle(), false, true) + \"\\\"\",\n                \"zellij -s xpipe action write-chars -- \" + escape(\" \" + firstCommand, true, true) + \"\\\\;exit\",\n                \"zellij -s xpipe action write 10\",\n                \"zellij -s xpipe action clear\"));\n\n        if (config.getPanes().size() > 1) {\n            var splitIterator =\n                    AppPrefs.get().terminalSplitStrategy().getValue().iterator();\n            splitIterator.next();\n\n            for (int i = 1; i < config.getPanes().size(); i++) {\n                var iCommand =\n                        config.getPanes().get(i).getDialectLaunchCommand().buildSimple();\n                var direction = splitIterator.getSplitDirection();\n                var directionString = direction == TerminalSplitStrategy.SplitDirection.HORIZONTAL\n                        ? \"--direction right\"\n                        : \"--direction down\";\n                l.addAll(List.of(\n                        \"zellij -s xpipe action new-pane \" + directionString\n                                + \" --name \\\"\"\n                                + escape(config.getPanes().get(i).getTitle(), false, true)\n                                + \"\\\"\",\n                        \"zellij -s xpipe action write-chars -- \" + escape(\" \" + iCommand, true, true) + \"\\\\;exit\",\n                        \"zellij -s xpipe action write 10\",\n                        \"zellij -s xpipe action clear\",\n                        \"zellij -s xpipe action focus-next-pane\"));\n                splitIterator.next();\n            }\n        }\n\n        return ShellScript.lines(l);\n    }\n\n    @Override\n    public ShellScript launchNewSession(ShellControl control, TerminalLaunchConfiguration config) throws Exception {\n        var l = new ArrayList<String>();\n        var firstConfig = config.getPanes().getFirst();\n        var firstCommand = firstConfig.getDialectLaunchCommand().buildSimple();\n        l.add(\"zellij attach xpipe\");\n\n        var sc = TerminalProxyManager.getProxy().orElse(LocalShell.getShell());\n        sc.command(\"zellij delete-session -f xpipe > /dev/null 2>&1\").executeAndCheck();\n        sc.command(\"zellij attach --create-background xpipe\").executeAndCheck();\n\n        var asyncLines = new ArrayList<String>();\n        asyncLines.addAll(List.of(\n                \"sleep 0.5\",\n                \"zellij -s xpipe action new-tab --name \\\"\" + escape(config.getColoredTitle(), false, true) + \"\\\"\",\n                \"zellij -s xpipe action write-chars -- \" + escape(\" \" + firstCommand, true, true) + \"\\\\;exit\",\n                \"zellij -s xpipe action write 10\",\n                \"zellij -s xpipe action clear\",\n                \"zellij -s xpipe action rename-tab \\\"\" + escape(config.getColoredTitle(), false, true) + \"\\\"\",\n                \"zellij -s xpipe action go-to-previous-tab\",\n                \"zellij -s xpipe action close-tab\"));\n\n        if (config.getPanes().size() > 1) {\n            var splitIterator =\n                    AppPrefs.get().terminalSplitStrategy().getValue().iterator();\n            splitIterator.next();\n            for (int i = 1; i < config.getPanes().size(); i++) {\n                var iCommand =\n                        config.getPanes().get(i).getDialectLaunchCommand().buildSimple();\n                var direction = splitIterator.getSplitDirection();\n                var directionString = direction == TerminalSplitStrategy.SplitDirection.HORIZONTAL\n                        ? \"--direction right\"\n                        : \"--direction down\";\n                asyncLines.addAll(List.of(\n                        \"zellij -s xpipe action new-pane \" + directionString + \" --name \\\"\"\n                                + escape(config.getPanes().get(i).getTitle(), false, true) + \"\\\"\",\n                        \"zellij -s xpipe action write-chars -- \" + escape(\" \" + iCommand, true, true) + \"\\\\;exit\",\n                        \"zellij -s xpipe action write 10\",\n                        \"zellij -s xpipe action clear\",\n                        \"zellij -s xpipe action focus-next-pane\"));\n                splitIterator.next();\n            }\n        }\n\n        var listener = new TerminalView.Listener() {\n            @Override\n            @SneakyThrows\n            public void onSessionOpened(TerminalView.ShellSession session) {\n                TerminalView.get().removeListener(this);\n                ThreadHelper.runFailableAsync(() -> {\n                    var sc = TerminalProxyManager.getProxy().orElse(LocalShell.getShell());\n                    var command = sc.command(String.join(\"\\n\", asyncLines));\n                    // Zellij sometimes freezes\n                    command.killOnTimeout(CountDown.of().start(10_000));\n                    command.executeAndCheck();\n                });\n            }\n        };\n        TerminalView.get().addListener(listener);\n\n        return ShellScript.lines(l);\n    }\n\n    private String escape(String s, boolean spaces, boolean quotes) {\n        var r = s.replaceAll(\"\\\\\\\\\", \"\\\\\\\\\\\\\\\\\");\n        if (quotes) {\n            r = r.replaceAll(\"\\\"\", \"\\\\\\\\\\\"\");\n        }\n        if (spaces) {\n            r = r.replaceAll(\" \", \"\\\\\\\\ \");\n        }\n        return r;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/test/ExtensionTest.java",
    "content": "package io.xpipe.app.test;\n\nimport io.xpipe.core.FilePath;\n\nimport lombok.SneakyThrows;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic class ExtensionTest {\n\n    @SneakyThrows\n    @SuppressWarnings(\"unused\")\n    public static Path getResourcePath(Class<?> c, String name) {\n        var loc = Path.of(c.getProtectionDomain().getCodeSource().getLocation().toURI());\n        var testName = FilePath.of(loc.getFileName()).getBaseName().toString().split(\"-\")[1];\n        var f = loc.getParent()\n                .getParent()\n                .resolve(\"resources\")\n                .resolve(testName)\n                .resolve(name);\n        if (!Files.exists(f)) {\n            throw new IllegalArgumentException(String.format(\"File %s does not exist\", name));\n        }\n        return f;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/test/LocalExtensionTest.java",
    "content": "package io.xpipe.app.test;\n\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.core.OsType;\n\nimport lombok.SneakyThrows;\nimport org.junit.jupiter.api.BeforeAll;\n\npublic class LocalExtensionTest extends ExtensionTest {\n\n    @BeforeAll\n    @SneakyThrows\n    public static void setup() {\n        if (AppOperationMode.get() != null) {\n            return;\n        }\n\n        var mode = OsType.ofLocal() == OsType.WINDOWS ? \"tray\" : \"background\";\n        AppOperationMode.init(new String[] {\"-Dio.xpipe.app.mode=\" + mode});\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/test/TestModule.java",
    "content": "package io.xpipe.app.test;\n\nimport io.xpipe.core.FailableSupplier;\n\nimport lombok.SneakyThrows;\nimport org.junit.jupiter.api.Named;\n\nimport java.util.*;\nimport java.util.stream.Stream;\n\npublic abstract class TestModule<V> {\n\n    private static final Map<Class<?>, Map<String, ?>> values = new LinkedHashMap<>();\n\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    @SneakyThrows\n    public static <T> Map<String, T> get(Class<T> c, Module module, String... classes) {\n        if (!values.containsKey(c)) {\n            List<Class<?>> loadedClasses = Arrays.stream(classes)\n                    .map(s -> {\n                        return Optional.<Class<?>>of(Class.forName(module, s));\n                    })\n                    .flatMap(Optional::stream)\n                    .toList();\n            loadedClasses.forEach(o -> {\n                try {\n                    var instance = (TestModule<?>) o.getConstructor().newInstance();\n                    Map list = values.computeIfAbsent(instance.getValueClass(), aClass -> new LinkedHashMap());\n                    instance.init(list);\n                } catch (Exception e) {\n                    throw new RuntimeException(e);\n                }\n            });\n        }\n\n        Map<String, Object> map = new HashMap<>();\n        for (Map.Entry<String, ?> o : values.get(c).entrySet()) {\n            if (map.put(o.getKey(), ((FailableSupplier<?>) o.getValue()).get()) != null) {\n                throw new IllegalStateException(\"Duplicate key\");\n            }\n        }\n        return (Map<String, T>) map;\n    }\n\n    public static <T> Stream<Named<T>> getArguments(Class<T> c, Module module, String... classes) {\n        Stream.Builder<Named<T>> argumentBuilder = Stream.builder();\n        for (var s : TestModule.get(c, module, classes).entrySet()) {\n            argumentBuilder.add(Named.of(s.getKey(), s.getValue()));\n        }\n        return argumentBuilder.build();\n    }\n\n    protected abstract void init(Map<String, FailableSupplier<V>> list);\n\n    protected abstract Class<V> getValueClass();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/update/AppDistributionType.java",
    "content": "package io.xpipe.app.update;\n\nimport io.xpipe.app.core.*;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.app.util.LocalExec;\nimport io.xpipe.app.util.Translatable;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.Getter;\n\nimport java.nio.file.Files;\nimport java.nio.file.InvalidPathException;\nimport java.nio.file.Path;\nimport java.util.Arrays;\nimport java.util.function.Supplier;\n\npublic enum AppDistributionType implements Translatable {\n    UNKNOWN(\"unknown\", false, () -> new GitHubUpdater(false)),\n    DEVELOPMENT(\"development\", true, () -> new GitHubUpdater(false)),\n    PORTABLE(\"portable\", false, () -> new PortableUpdater(true)),\n    NATIVE_INSTALLATION(\"install\", true, () -> new GitHubUpdater(true)),\n    APP_IMAGE(\"appImage\", false, () -> new PortableUpdater(true)),\n    NIX(\"nix\", false, () -> new PortableUpdater(true)),\n    HOMEBREW(\"homebrew\", true, () -> {\n        var pkg = AppNames.ofCurrent().getKebapName();\n        return new CommandUpdater(\n                ShellScript.lines(\n                        \"brew upgrade --cask xpipe-io/tap/\" + pkg,\n                        \"if [ \\\"$?\\\" != 0 ]; then echo \\\"Update failed ...\\\"; read key; fi\",\n                        AppRestart.getTerminalRestartCommand()));\n    }),\n    APT_REPO(\"apt\", true, () -> {\n        var pkg = AppNames.ofCurrent().getKebapName();\n        return new CommandUpdater(ShellScript.lines(\n                \"echo \\\"+ sudo apt update && sudo apt install -y \" + pkg + \"\\\"\",\n                \"sudo apt update\",\n                \"sudo apt install -y \" + pkg,\n                \"if [ \\\"$?\\\" != 0 ]; then echo \\\"Update failed ...\\\"; read key; fi\",\n                AppRestart.getTerminalRestartCommand()));\n    }),\n    RPM_REPO(\"rpm\", true, () -> {\n        var pkg = AppNames.ofCurrent().getKebapName();\n        return new CommandUpdater(ShellScript.lines(\n                \"echo \\\"+ sudo yum upgrade \" + pkg + \" --refresh -y\\\"\",\n                \"sudo yum upgrade \" + pkg + \" --refresh -y\",\n                \"if [ \\\"$?\\\" != 0 ]; then echo \\\"Update failed ...\\\"; read key; fi\",\n                AppRestart.getTerminalRestartCommand()));\n    }),\n    AUR(\"aur\", true, () -> {\n        var pkg = AppNames.ofCurrent().getKebapName();\n        return new CommandUpdater(ShellScript.lines(\n                \"echo \\\"+ git -c core.autocrlf=false clone https://aur.archlinux.org/\" + pkg + \" . && makepkg -si\\\"\",\n                \"cd $(mktemp -d) && git -c core.autocrlf=false clone https://aur.archlinux.org/\" + pkg + \" . && makepkg -si --noconfirm\",\n                \"if [ \\\"$?\\\" != 0 ]; then echo \\\"Update failed ...\\\"; read key; fi\",\n                AppRestart.getTerminalRestartCommand()));\n    }),\n    WEBTOP(\"webtop\", true, () -> new WebtopUpdater()),\n    CHOCO(\"choco\", true, () -> new ChocoUpdater()),\n    WINGET(\"winget\", true, () -> new WingetUpdater()),\n    SCOOP(\"scoop\", false, () -> new PortableUpdater(true));\n\n    private static AppDistributionType type;\n\n    @Getter\n    private final String id;\n\n    @Getter\n    private final boolean supportsUrls;\n\n    private final Supplier<UpdateHandler> updateHandlerSupplier;\n    private UpdateHandler updateHandler;\n\n    AppDistributionType(String id, boolean supportsUrls, Supplier<UpdateHandler> updateHandlerSupplier) {\n        this.id = id;\n        this.supportsUrls = supportsUrls;\n        this.updateHandlerSupplier = updateHandlerSupplier;\n    }\n\n    public static void init() {\n        if (type != null) {\n            return;\n        }\n\n        if (!AppProperties.get().isImage()) {\n            type = DEVELOPMENT;\n            return;\n        }\n\n        if (!AppProperties.get().isNewBuildSession() && !isDifferentDaemonExecutable()) {\n            var cached = AppCache.getNonNull(\"dist\", String.class, () -> null);\n            var cachedType = Arrays.stream(values())\n                    .filter(xPipeDistributionType ->\n                            xPipeDistributionType.getId().equals(cached))\n                    .findAny()\n                    .orElse(null);\n            if (cachedType != null) {\n                type = cachedType;\n                return;\n            }\n        }\n\n        var det = determine();\n\n        // Don't cache unknown type\n        if (det == UNKNOWN) {\n            return;\n        }\n\n        type = det;\n        AppCache.update(\"dist\", type.getId());\n        TrackEvent.withInfo(\"Determined distribution type\")\n                .tag(\"type\", type.getId())\n                .handle();\n    }\n\n    private static boolean isDifferentDaemonExecutable() {\n        var cached = AppCache.getNonNull(\"daemonExecutable\", String.class, () -> null);\n        var current = AppInstallation.ofCurrent().getDaemonExecutablePath().toString();\n        if (current.equals(cached)) {\n            return false;\n        }\n\n        AppCache.update(\"daemonExecutable\", current);\n        return true;\n    }\n\n    public static AppDistributionType get() {\n        if (type == null) {\n            return UNKNOWN;\n        }\n\n        return type;\n    }\n\n    public static AppDistributionType determine() {\n        var base = AppInstallation.ofCurrent().getBaseInstallationPath();\n        if (OsType.ofLocal() == OsType.MACOS) {\n            if (!base.equals(AppInstallation.ofDefault().getBaseInstallationPath())) {\n                return PORTABLE;\n            }\n\n            try {\n                var r = LocalExec.readStdoutIfPossible(\n                        \"pkgutil\",\n                        \"--pkg-info\",\n                        AppNames.ofCurrent().getGroupName() + \".\"\n                                + AppNames.ofCurrent().getKebapName());\n                if (r.isEmpty()) {\n                    return PORTABLE;\n                }\n            } catch (Exception ex) {\n                ErrorEventFactory.fromThrowable(ex).omit().handle();\n                return PORTABLE;\n            }\n        } else {\n            var file = base.resolve(\"installation\");\n            if (!Files.exists(file)) {\n                if (OsType.ofLocal() == OsType.LINUX && Files.exists(base.resolve(\"aur\"))) {\n                    return AUR;\n                }\n\n                if (OsType.ofLocal() == OsType.WINDOWS\n                        && AppInstallation.ofCurrent()\n                                .getBaseInstallationPath()\n                                .startsWith(\n                                        AppSystemInfo.ofWindows().getUserHome().resolve(\"scoop\"))) {\n                    return SCOOP;\n                }\n\n                if (AppInstallation.ofCurrent()\n                        .getBaseInstallationPath()\n                        .toString()\n                        .startsWith(\"/nix/store\")) {\n                    return NIX;\n                }\n\n                if (OsType.ofLocal() == OsType.LINUX\n                        && System.getenv(\"APPDIR\") != null\n                        && System.getenv(\"APPIMAGE\") != null) {\n                    try {\n                        var dir = Path.of(System.getenv(\"APPDIR\"));\n                        if (AppInstallation.ofCurrent()\n                                .getBaseInstallationPath()\n                                .startsWith(dir)) {\n                            return APP_IMAGE;\n                        }\n\n                    } catch (InvalidPathException ignored) {\n                    }\n                }\n\n                return PORTABLE;\n            }\n        }\n\n        if (OsType.ofLocal() == OsType.LINUX && Files.isDirectory(Path.of(\"/kclient\"))) {\n            return WEBTOP;\n        }\n\n        if (OsType.ofLocal() == OsType.WINDOWS && !AppProperties.get().isStaging()) {\n            var chocoOut = LocalExec.readStdoutIfPossible(\"choco\", \"list\", \"xpipe\");\n            if (chocoOut.isPresent()) {\n                if (chocoOut.get().contains(\"xpipe\")\n                        && chocoOut.get().contains(AppProperties.get().getVersion())) {\n                    return CHOCO;\n                }\n            }\n        }\n\n        if (OsType.ofLocal() == OsType.MACOS) {\n            var out = LocalExec.readStdoutIfPossible(\"/opt/homebrew/bin/brew\", \"list\", \"--casks\", \"--versions\");\n            if (out.isPresent()) {\n                if (out.get().lines().anyMatch(s -> {\n                    var split = s.split(\" \");\n                    return split.length == 2\n                            && split[0].equals(AppNames.ofCurrent().getKebapName())\n                            && split[1].equals(AppProperties.get().getVersion());\n                })) {\n                    return HOMEBREW;\n                }\n            }\n        }\n\n        if (OsType.ofLocal() == OsType.LINUX) {\n            if (base.startsWith(\"/opt\")) {\n                var aptOut = LocalExec.readStdoutIfPossible(\n                        \"apt\", \"show\", AppNames.ofCurrent().getKebapName());\n                if (aptOut.isPresent()) {\n                    var fromRepo = aptOut.get().lines().anyMatch(s -> {\n                        return s.contains(\"APT-Sources\") && s.contains(\"apt.xpipe.io\");\n                    });\n                    if (fromRepo) {\n                        return APT_REPO;\n                    }\n                }\n\n                var yumRepo = LocalExec.readStdoutIfPossible(\"test\", \"-f\", \"/etc/yum.repos.d/xpipe.repo\");\n                if (yumRepo.isPresent()) {\n                    return RPM_REPO;\n                }\n            }\n        }\n\n        // Fix for community AUR builds that use the RPM dist\n        if (OsType.ofLocal() == OsType.LINUX && Files.exists(Path.of(\"/etc/arch-release\"))) {\n            return PORTABLE;\n        }\n\n        return AppDistributionType.NATIVE_INSTALLATION;\n    }\n\n    public UpdateHandler getUpdateHandler() {\n        if (updateHandler == null) {\n            updateHandler = updateHandlerSupplier.get();\n        }\n        return updateHandler;\n    }\n\n    @Override\n    public ObservableValue<String> toTranslatedString() {\n        return AppI18n.observable(getId() + \"Dist\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/update/AppDownloads.java",
    "content": "package io.xpipe.app.update;\n\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.util.*;\nimport io.xpipe.core.JacksonMapper;\nimport io.xpipe.core.OsType;\n\nimport com.fasterxml.jackson.databind.node.JsonNodeFactory;\nimport org.apache.commons.io.FileUtils;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Duration;\n\npublic class AppDownloads {\n\n    public static Path downloadInstaller(String version) throws Exception {\n        try {\n            var release = AppRelease.of(version);\n            var builder = HttpRequest.newBuilder();\n            var httpRequest = builder.uri(URI.create(release.getUrl())).GET().build();\n            var client = HttpHelper.client();\n            var response = client.send(httpRequest, HttpResponse.BodyHandlers.ofByteArray());\n            if (response.statusCode() >= 400) {\n                throw new IOException(new String(response.body(), StandardCharsets.UTF_8));\n            }\n\n            // Fix file name to not have dashes to not be included in temp dir clean\n            var downloadFile = AppSystemInfo.ofCurrent()\n                    .getTemp()\n                    .resolve(release.getFile().replaceAll(\"-\", \"_\"));\n            Files.write(downloadFile, response.body());\n            TrackEvent.withInfo(\"Downloaded asset\")\n                    .tag(\"version\", version)\n                    .tag(\"url\", release.getUrl())\n                    .tag(\"size\", FileUtils.byteCountToDisplaySize(response.body().length))\n                    .tag(\"target\", downloadFile)\n                    .handle();\n\n            return downloadFile;\n        } catch (IOException ex) {\n            // All sorts of things can go wrong when downloading, this is expected\n            ErrorEventFactory.expected(ex);\n            throw ex;\n        }\n    }\n\n    public static String downloadChangelog(String version) throws Exception {\n        try {\n            var uri = URI.create(\n                    \"https://api.xpipe.io/changelog?from=\" + AppProperties.get().getVersion()\n                            + \"&to=\"\n                            + version\n                            + \"&stage=\"\n                            + AppProperties.get().isStaging());\n            var builder = HttpRequest.newBuilder();\n            var httpRequest = builder.uri(uri).GET().build();\n            var client = HttpHelper.client();\n            var response = client.send(httpRequest, HttpResponse.BodyHandlers.ofString());\n            if (response.statusCode() >= 400) {\n                var s = response.body();\n                throw new IOException(\"Changelog not found\" + (s != null && !s.isEmpty() ? \": \" + s : \"\"));\n            }\n            var json = JacksonMapper.getDefault().readTree(response.body());\n            var changelog = json.required(\"changelog\").asText();\n            return changelog;\n        } catch (IOException ex) {\n            // All sorts of things can go wrong when downloading, this is expected\n            ErrorEventFactory.expected(ex);\n            throw ex;\n        }\n    }\n\n    public static AppRelease queryLatestVersion(boolean first, boolean securityOnly) throws Exception {\n        try {\n            var req = JsonNodeFactory.instance.objectNode();\n            req.put(\"securityOnly\", securityOnly);\n            req.put(\"initial\", AppProperties.get().isInitialLaunch() && first);\n            req.put(\"ptb\", AppProperties.get().isStaging());\n            req.put(\"os\", OsType.ofLocal().getId());\n            req.put(\"arch\", AppProperties.get().getArch());\n            req.put(\"uuid\", AppProperties.get().getUuid().toString());\n            req.put(\"version\", AppProperties.get().getVersion());\n            req.put(\"first\", first);\n            req.put(\"license\", LicenseProvider.get().getLicenseId());\n            req.put(\"dist\", AppDistributionType.get().getId());\n            req.put(\n                    \"lang\",\n                    AppPrefs.get() != null\n                            ? AppPrefs.get().language().getValue().getId()\n                            : null);\n            var url = URI.create(\"https://api.xpipe.io/version\");\n\n            var builder = HttpRequest.newBuilder();\n            var httpRequest = builder.uri(url)\n                    .POST(HttpRequest.BodyPublishers.ofString(req.toPrettyString()))\n                    .build();\n            var client = HttpHelper.client();\n            var response = client.send(httpRequest, HttpResponse.BodyHandlers.ofString());\n            if (response.statusCode() >= 400) {\n                throw new IOException(response.body());\n            }\n\n            var dateEntry = response.headers().firstValue(\"Date\");\n            if (dateEntry.isPresent()) {\n                LicenseProvider.get().updateDate(dateEntry.get());\n            }\n\n            var json = JacksonMapper.getDefault().readTree(response.body());\n            var ver = json.required(\"version\").asText();\n            var ptbAvailable = json.get(\"ptbAvailable\");\n            if (ptbAvailable != null) {\n                var b = ptbAvailable.asBoolean();\n                if (b && AppLayoutModel.get() != null && !AppProperties.get().isStaging()) {\n                    GlobalTimer.delay(\n                            () -> {\n                                AppLayoutModel.get().getPtbAvailable().set(true);\n                            },\n                            Duration.ofSeconds(20));\n                }\n            }\n            return AppRelease.of(ver);\n        } catch (Exception e) {\n            throw ErrorEventFactory.expected(e);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/update/AppInstaller.java",
    "content": "package io.xpipe.app.update;\n\nimport io.xpipe.app.core.*;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ScriptHelper;\nimport io.xpipe.app.process.ShellDialects;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.app.terminal.TerminalLaunch;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport com.fasterxml.jackson.annotation.JsonSubTypes;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Getter;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic class AppInstaller {\n\n    public static InstallerAssetType getSuitablePlatformAsset() {\n        if (OsType.ofLocal() == OsType.WINDOWS) {\n            return new InstallerAssetType.Msi();\n        }\n\n        if (OsType.ofLocal() == OsType.LINUX) {\n            return AppSystemInfo.ofLinux().isDebianBased()\n                    ? new InstallerAssetType.Debian()\n                    : new InstallerAssetType.Rpm();\n        }\n\n        if (OsType.ofLocal() == OsType.MACOS) {\n            return new InstallerAssetType.Pkg();\n        }\n\n        throw new AssertionError();\n    }\n\n    @Getter\n    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\n    @JsonSubTypes({\n        @JsonSubTypes.Type(value = InstallerAssetType.Msi.class),\n                @JsonSubTypes.Type(value = InstallerAssetType.Debian.class),\n        @JsonSubTypes.Type(value = InstallerAssetType.Rpm.class),\n                @JsonSubTypes.Type(value = InstallerAssetType.Pkg.class)\n    })\n    public abstract static class InstallerAssetType {\n\n        public abstract void installLocal(Path file);\n\n        public abstract String getExtension();\n\n        @JsonTypeName(\"msi\")\n        public static final class Msi extends InstallerAssetType {\n\n            @Override\n            public void installLocal(Path file) {\n                var logsDir =\n                        AppLogs.get().getSessionLogsDirectory().getParent().toString();\n                var logFile = FilePath.of(logsDir, \"installer.log\");\n                var systemWide = isSystemWide();\n                var cmdScript = LocalShell.getDialect() == ShellDialects.CMD && !systemWide;\n                var command = cmdScript\n                        ? getCmdCommand(file.toString(), logFile.toString())\n                        : getPowershellCommand(file.toString(), logFile.toString(), systemWide);\n\n                AppOperationMode.executeAfterShutdown(() -> {\n                    try (var sc = LocalShell.getShell().start()) {\n                        String toRun;\n                        if (cmdScript) {\n                            toRun = \"start \\\"\" + AppNames.ofCurrent().getName() + \" Updater\\\" /min cmd /c \\\"\"\n                                    + ScriptHelper.createExecScript(ShellDialects.CMD, sc, command) + \"\\\"\";\n                        } else {\n                            toRun = sc.getShellDialect() == ShellDialects.POWERSHELL\n                                    ? \"Start-Process -WindowStyle Minimized -FilePath powershell -ArgumentList  \\\"-ExecutionPolicy\\\", \\\"Bypass\\\", \"\n                                            + \"\\\"-File\\\", \\\"`\\\"\"\n                                            + ScriptHelper.createExecScript(ShellDialects.POWERSHELL, sc, command)\n                                            + \"`\\\"\\\"\"\n                                    : \"start \\\"\" + AppNames.ofCurrent().getName()\n                                            + \" Updater\\\" /min powershell -ExecutionPolicy Bypass -File \\\"\"\n                                            + ScriptHelper.createExecScript(ShellDialects.POWERSHELL, sc, command)\n                                            + \"\\\"\";\n                        }\n                        sc.command(toRun).execute();\n                    }\n                });\n            }\n\n            @Override\n            public String getExtension() {\n                return \"msi\";\n            }\n\n            private boolean isSystemWide() {\n                return Files.exists(\n                        AppInstallation.ofCurrent().getBaseInstallationPath().resolve(\"system\"));\n            }\n\n            private String getCmdCommand(String file, String logFile) {\n                var args = \"MSIFASTINSTALL=7 DISABLEROLLBACK=1\";\n                return String.format(\n                        \"\"\"\n                                     echo Installing %s ...\n                                     cd /D \"%%HOMEDRIVE%%%%HOMEPATH%%\"\n                                     echo + msiexec /i \"%s\" /lv \"%s\" /qb %s\n                                     start \"\" /wait msiexec /i \"%s\" /lv \"%s\" /qb %s\n                                     %s\n                                     \"\"\",\n                        file,\n                        file,\n                        logFile,\n                        args,\n                        file,\n                        logFile,\n                        args,\n                        AppRestart.getBackgroundRestartCommand(\n                                AppProperties.get().getDataDir(), null, ShellDialects.CMD));\n            }\n\n            private String getPowershellCommand(String file, String logFile, boolean systemWide) {\n                var property = \"MSIFASTINSTALL=7 DISABLEROLLBACK=1\" + (systemWide ? \" ALLUSERS=1\" : \"\");\n                var startProcessProperty = \", MSIFASTINSTALL=7, DISABLEROLLBACK=1\" + (systemWide ? \", ALLUSERS=1\" : \"\");\n                var runas = systemWide ? \"-Verb runAs\" : \"\";\n                return String.format(\n                        \"\"\"\n                                     echo Installing %s ...\n                                     cd \"$env:HOMEDRIVE\\\\$env:HOMEPATH\"\n                                     echo '+ msiexec /i \"%s\" /lv \"%s\" /qb%s'\n                                     Start-Process %s -FilePath msiexec -Wait -ArgumentList \"/i\", \"`\"%s`\"\", \"/lv\", \"`\"%s`\"\", \"/qb\"%s\n                                     %s\n                                     \"\"\",\n                        file,\n                        file,\n                        logFile,\n                        property,\n                        runas,\n                        file,\n                        logFile,\n                        startProcessProperty,\n                        AppRestart.getBackgroundRestartCommand(\n                                AppProperties.get().getDataDir(), null, ShellDialects.POWERSHELL));\n            }\n        }\n\n        @JsonTypeName(\"debian\")\n        public static final class Debian extends InstallerAssetType {\n\n            @Override\n            public void installLocal(Path file) {\n                var command = new ShellScript(String.format(\"\"\"\n                                                            runinstaller() {\n                                                                echo \"Installing downloaded .deb installer ...\"\n                                                                echo \"+ sudo apt install \\\\\"%s\\\\\"\"\n                                                                DEBIAN_FRONTEND=noninteractive sudo apt install -y \"%s\" || return 1\n                                                                %s || return 1\n                                                            }\n\n                                                            cd ~\n                                                            runinstaller\n                                                            if [ \"$?\" != 0 ]; then\n                                                              echo \"Update failed ...\"\n                                                              read key\n                                                            fi\n                                                            \"\"\", file, file, AppRestart.getTerminalRestartCommand()));\n                AppOperationMode.executeAfterShutdown(() -> {\n                    TerminalLaunch.builder()\n                            .title(AppNames.ofCurrent().getName() + \" Updater\")\n                            .localScript(command)\n                            .launch();\n                });\n            }\n\n            @Override\n            public String getExtension() {\n                return \"deb\";\n            }\n        }\n\n        @JsonTypeName(\"rpm\")\n        public static final class Rpm extends InstallerAssetType {\n\n            @Override\n            public void installLocal(Path file) {\n                var command = new ShellScript(String.format(\"\"\"\n                                                            runinstaller() {\n                                                                echo \"Installing downloaded .rpm installer ...\"\n                                                                echo \"+ sudo rpm -U -v --force \\\\\"%s\\\\\"\"\n                                                                sudo rpm -U -v --force \"%s\" || return 1\n                                                                %s || return 1\n                                                            }\n\n                                                            cd ~\n                                                            runinstaller\n                                                            if [ \"$?\" != 0 ]; then\n                                                              echo \"Update failed ...\"\n                                                              read key\n                                                            fi\n                                                            \"\"\", file, file, AppRestart.getTerminalRestartCommand()));\n                AppOperationMode.executeAfterShutdown(() -> {\n                    TerminalLaunch.builder()\n                            .title(AppNames.ofCurrent().getName() + \" Updater\")\n                            .localScript(command)\n                            .launch();\n                });\n            }\n\n            @Override\n            public String getExtension() {\n                return \"rpm\";\n            }\n        }\n\n        @JsonTypeName(\"pkg\")\n        public static final class Pkg extends InstallerAssetType {\n\n            @Override\n            public void installLocal(Path file) {\n                var command = new ShellScript(String.format(\"\"\"\n                                                            runinstaller() {\n                                                                echo \"Installing downloaded .pkg installer ...\"\n                                                                echo \"+ sudo installer -verboseR -pkg \\\\\"%s\\\\\" -target /\"\n                                                                sudo installer -verboseR -pkg \"%s\" -target / || return 1\n                                                                %s || return 1\n                                                            }\n\n                                                            cd ~\n                                                            runinstaller\n                                                            if [ \"$?\" != 0 ]; then\n                                                              echo \"Update failed ...\"\n                                                              read key\n                                                            fi\n                                                            \"\"\", file, file, AppRestart.getTerminalRestartCommand()));\n                AppOperationMode.executeAfterShutdown(() -> {\n                    TerminalLaunch.builder()\n                            .title(AppNames.ofCurrent().getName() + \" Updater\")\n                            .localScript(command)\n                            .launch();\n                });\n            }\n\n            @Override\n            public String getExtension() {\n                return \"pkg\";\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/update/AppRelease.java",
    "content": "package io.xpipe.app.update;\n\nimport io.xpipe.app.core.AppNames;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.core.OsType;\n\nimport lombok.Value;\n\n@Value\npublic class AppRelease {\n\n    String tag;\n    String url;\n    String browserUrl;\n    String file;\n\n    public static AppRelease of(String tag) {\n        var type = AppInstaller.getSuitablePlatformAsset();\n        var os =\n                switch (OsType.ofLocal()) {\n                    case OsType.Linux ignored -> \"linux\";\n                    case OsType.MacOs ignored -> \"macos\";\n                    case OsType.Windows ignored -> \"windows\";\n                };\n        var arch = AppProperties.get().getArch();\n        var name = \"xpipe-installer-%s-%s.%s\".formatted(os, arch, type.getExtension());\n        var url = \"https://github.com/xpipe-io/%s/releases/download/%s/%s\"\n                .formatted(AppNames.ofCurrent().getKebapName(), tag, name);\n        var browser = \"https://github.com/xpipe-io/%s/releases/%s\"\n                .formatted(AppNames.ofCurrent().getKebapName(), tag);\n        return new AppRelease(tag, url, browser, name);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/update/ChocoUpdater.java",
    "content": "package io.xpipe.app.update;\n\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.AppInstallation;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.core.AppRestart;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ShellDialects;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.app.terminal.TerminalLaunch;\nimport io.xpipe.app.util.Hyperlinks;\n\nimport java.nio.file.Files;\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\n\npublic class ChocoUpdater extends UpdateHandler {\n\n    public ChocoUpdater() {\n        super(true);\n    }\n\n    @Override\n    public boolean supportsDirectInstallation() {\n        return true;\n    }\n\n    @Override\n    public List<ModalButton> createActions() {\n        var l = new ArrayList<ModalButton>();\n        l.add(new ModalButton(\"ignore\", null, true, false));\n        l.add(new ModalButton(\n                \"checkOutUpdate\",\n                () -> {\n                    if (getPreparedUpdate().getValue() == null) {\n                        return;\n                    }\n\n                    Hyperlinks.open(getPreparedUpdate().getValue().getReleaseUrl());\n                },\n                false,\n                false));\n        l.add(new ModalButton(\n                \"install\",\n                () -> {\n                    executeUpdateAndClose();\n                },\n                true,\n                true));\n        return l;\n    }\n\n    @Override\n    public void executeUpdate() {\n        try {\n            var p = preparedUpdate.getValue();\n            var performedUpdate = new PerformedUpdate(p.getVersion(), p.getBody(), p.getVersion());\n            AppCache.update(\"performedUpdate\", performedUpdate);\n            AppOperationMode.executeAfterShutdown(() -> {\n                var systemWide = Files.exists(\n                        AppInstallation.ofCurrent().getBaseInstallationPath().resolve(\"system\"));\n                var propertiesArguments = systemWide ? \", --install-arguments=\\\"'ALLUSERS=1'\\\"\" : \"\";\n                TerminalLaunch.builder().title(\"XPipe Updater\").localScript(sc -> {\n                    var pkg = \"xpipe\";\n                    var commandToRun = \"Start-Process -Wait -Verb runAs -FilePath choco -ArgumentList upgrade, \" + pkg\n                            + \", -y\" + propertiesArguments;\n                    var powershell = ShellDialects.isPowershell(sc);\n                    var powershellCommand = powershell\n                            ? \"powershell -Command \" + sc.getShellDialect().quoteArgument(commandToRun)\n                            : \"powershell -Command \" + commandToRun;\n                    return ShellScript.lines(powershellCommand, AppRestart.getTerminalRestartCommand());\n                });\n            });\n        } catch (Throwable t) {\n            ErrorEventFactory.fromThrowable(t).handle();\n            preparedUpdate.setValue(null);\n        }\n    }\n\n    public synchronized AvailableRelease refreshUpdateCheckImpl(boolean first, boolean securityOnly) throws Exception {\n        var rel = AppDownloads.queryLatestVersion(first, securityOnly);\n        event(\"Determined latest suitable release \" + rel.getTag());\n\n        var chocoRelease = getOutdatedPackageUpdateVersion();\n        // Use current release if the update is not available for choco yet\n        if (chocoRelease.isEmpty() || !chocoRelease.get().equals(rel.getTag())) {\n            rel = AppRelease.of(AppProperties.get().getVersion());\n        }\n\n        var isUpdate = isUpdate(rel.getTag());\n        lastUpdateCheckResult.setValue(new AvailableRelease(\n                AppProperties.get().getVersion(),\n                AppDistributionType.get().getId(),\n                rel.getTag(),\n                rel.getBrowserUrl(),\n                null,\n                null,\n                Instant.now(),\n                isUpdate,\n                securityOnly));\n        return lastUpdateCheckResult.getValue();\n    }\n\n    private Optional<String> getOutdatedPackageUpdateVersion() throws Exception {\n        if (AppProperties.get().isStaging()) {\n            return Optional.empty();\n        }\n\n        var pkg = \"xpipe\";\n        var out = LocalShell.getShell()\n                .command(CommandBuilder.of().add(\"choco\", \"outdated\"))\n                .readStdoutIfPossible();\n        if (out.isEmpty()) {\n            return Optional.empty();\n        }\n\n        var line = out.get()\n                .lines()\n                .filter(s -> {\n                    var split = s.split(\"\\\\|\");\n                    if (split.length != 4) {\n                        return false;\n                    } else {\n                        return split[0].equals(pkg);\n                    }\n                })\n                .findFirst();\n        if (line.isEmpty()) {\n            return Optional.empty();\n        }\n\n        var v = line.get().split(\"\\\\|\")[2];\n        return Optional.of(v);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/update/CommandUpdater.java",
    "content": "package io.xpipe.app.update;\n\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.app.terminal.TerminalLaunch;\nimport io.xpipe.app.util.Hyperlinks;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class CommandUpdater extends PortableUpdater {\n\n    private final ShellScript script;\n\n    public CommandUpdater(ShellScript script) {\n        super(true);\n        this.script = script;\n    }\n\n    @Override\n    public boolean supportsDirectInstallation() {\n        return true;\n    }\n\n    @Override\n    public List<ModalButton> createActions() {\n        var l = new ArrayList<ModalButton>();\n        l.add(new ModalButton(\"ignore\", null, true, false));\n        l.add(new ModalButton(\n                \"checkOutUpdate\",\n                () -> {\n                    if (getPreparedUpdate().getValue() == null) {\n                        return;\n                    }\n\n                    Hyperlinks.open(getPreparedUpdate().getValue().getReleaseUrl());\n                },\n                false,\n                false));\n        l.add(new ModalButton(\n                \"install\",\n                () -> {\n                    executeUpdateAndClose();\n                },\n                true,\n                true));\n        return l;\n    }\n\n    @Override\n    public void executeUpdate() {\n        try {\n            var p = preparedUpdate.getValue();\n            var performedUpdate = new PerformedUpdate(p.getVersion(), p.getBody(), p.getVersion());\n            AppCache.update(\"performedUpdate\", performedUpdate);\n            AppOperationMode.executeAfterShutdown(() -> {\n                TerminalLaunch.builder()\n                        .title(\"XPipe Updater\")\n                        .localScript(script)\n                        .launch();\n            });\n        } catch (Throwable t) {\n            ErrorEventFactory.fromThrowable(t).handle();\n            preparedUpdate.setValue(null);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/update/GitHubUpdater.java",
    "content": "package io.xpipe.app.update;\n\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.util.Hyperlinks;\n\nimport java.nio.file.Files;\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class GitHubUpdater extends UpdateHandler {\n\n    public GitHubUpdater(boolean startBackgroundThread) {\n        super(startBackgroundThread);\n    }\n\n    @Override\n    public boolean supportsDirectInstallation() {\n        return true;\n    }\n\n    @Override\n    public List<ModalButton> createActions() {\n        var list = new ArrayList<ModalButton>();\n        list.add(new ModalButton(\"ignore\", null, true, false));\n        list.add(new ModalButton(\n                \"checkOutUpdate\",\n                () -> {\n                    if (getPreparedUpdate().getValue() == null) {\n                        return;\n                    }\n\n                    Hyperlinks.open(getPreparedUpdate().getValue().getReleaseUrl());\n                },\n                false,\n                false));\n        list.add(new ModalButton(\n                \"install\",\n                () -> {\n                    executeUpdateAndClose();\n                },\n                true,\n                true));\n        return list;\n    }\n\n    public void prepareUpdateImpl() throws Exception {\n        var downloadFile =\n                AppDownloads.downloadInstaller(lastUpdateCheckResult.getValue().getVersion());\n        var changelogString =\n                AppDownloads.downloadChangelog(lastUpdateCheckResult.getValue().getVersion());\n        var rel = new PreparedUpdate(\n                AppProperties.get().getVersion(),\n                AppDistributionType.get().getId(),\n                lastUpdateCheckResult.getValue().getVersion(),\n                lastUpdateCheckResult.getValue().getReleaseUrl(),\n                downloadFile,\n                changelogString,\n                lastUpdateCheckResult.getValue().getAssetType(),\n                lastUpdateCheckResult.getValue().isSecurityOnly());\n        preparedUpdate.setValue(rel);\n    }\n\n    public void executeUpdate() {\n        var p = preparedUpdate.getValue();\n        var downloadFile = p.getFile();\n        if (!Files.exists(downloadFile)) {\n            event(\"Prepared update file does not exist\");\n            return;\n        }\n\n        try {\n            var performedUpdate = new PerformedUpdate(p.getVersion(), p.getBody(), p.getVersion());\n            AppCache.update(\"performedUpdate\", performedUpdate);\n\n            var a = p.getAssetType();\n            a.installLocal(downloadFile);\n        } catch (Throwable t) {\n            ErrorEventFactory.fromThrowable(t).handle();\n            preparedUpdate.setValue(null);\n        }\n    }\n\n    public synchronized AvailableRelease refreshUpdateCheckImpl(boolean first, boolean securityOnly) throws Exception {\n        var rel = AppDownloads.queryLatestVersion(first, securityOnly);\n        event(\"Determined latest suitable release \" + rel.getTag());\n        var isUpdate = isUpdate(rel.getTag());\n        var assetType = AppInstaller.getSuitablePlatformAsset();\n        event(\"Selected asset \" + rel.getFile());\n        lastUpdateCheckResult.setValue(new AvailableRelease(\n                AppProperties.get().getVersion(),\n                AppDistributionType.get().getId(),\n                rel.getTag(),\n                rel.getBrowserUrl(),\n                rel.getUrl(),\n                assetType,\n                Instant.now(),\n                isUpdate,\n                securityOnly));\n        return lastUpdateCheckResult.getValue();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/update/PortableUpdater.java",
    "content": "package io.xpipe.app.update;\n\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.util.Hyperlinks;\n\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class PortableUpdater extends UpdateHandler {\n\n    public PortableUpdater(boolean thread) {\n        super(thread);\n    }\n\n    @Override\n    public boolean supportsDirectInstallation() {\n        return false;\n    }\n\n    @Override\n    public List<ModalButton> createActions() {\n        var list = new ArrayList<ModalButton>();\n        list.add(new ModalButton(\"ignore\", null, true, false));\n        list.add(new ModalButton(\n                \"checkOutUpdate\",\n                () -> {\n                    if (getPreparedUpdate().getValue() == null) {\n                        return;\n                    }\n\n                    Hyperlinks.open(getPreparedUpdate().getValue().getReleaseUrl());\n                },\n                false,\n                true));\n        return list;\n    }\n\n    public synchronized AvailableRelease refreshUpdateCheckImpl(boolean first, boolean securityOnly) throws Exception {\n        var rel = AppDownloads.queryLatestVersion(first, securityOnly);\n        event(\"Determined latest suitable release \" + rel.getTag());\n        var isUpdate = isUpdate(rel.getTag());\n        lastUpdateCheckResult.setValue(new AvailableRelease(\n                AppProperties.get().getVersion(),\n                AppDistributionType.get().getId(),\n                rel.getTag(),\n                rel.getBrowserUrl(),\n                null,\n                null,\n                Instant.now(),\n                isUpdate,\n                securityOnly));\n        return lastUpdateCheckResult.getValue();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/update/UpdateAvailableDialog.java",
    "content": "package io.xpipe.app.update;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.MarkdownComp;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.issue.TrackEvent;\n\npublic class UpdateAvailableDialog {\n\n    public static void showIfNeeded(boolean wait) {\n        UpdateHandler uh = AppDistributionType.get().getUpdateHandler();\n        if (uh.getPreparedUpdate().getValue() == null) {\n            return;\n        }\n\n        // Check whether we still have the latest version prepared\n        uh.refreshUpdateCheckSilent(false, uh.getPreparedUpdate().getValue().isSecurityOnly());\n        if (uh.getPreparedUpdate().getValue() == null) {\n            return;\n        }\n\n        TrackEvent.withInfo(\"Showing update alert ...\")\n                .tag(\"version\", uh.getPreparedUpdate().getValue().getVersion())\n                .handle();\n        var u = uh.getPreparedUpdate().getValue();\n\n        var comp = RegionBuilder.of(() -> {\n            var markdown = new MarkdownComp(u.getBody() != null ? u.getBody() : \"\", s -> s, false).build();\n            return markdown;\n        });\n        var modal = ModalOverlay.of(\"updateReadyAlertTitle\", comp.prefWidth(600), null);\n        for (var action : uh.createActions()) {\n            modal.addButton(action);\n        }\n\n        if (wait) {\n            modal.showAndWait();\n        } else {\n            modal.show();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/update/UpdateChangelogDialog.java",
    "content": "package io.xpipe.app.update;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.MarkdownComp;\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppLogs;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.issue.ErrorAction;\nimport io.xpipe.app.issue.ErrorEvent;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.app.util.Hyperlinks;\nimport io.xpipe.core.OsType;\n\nimport java.nio.file.Files;\n\npublic class UpdateChangelogDialog {\n\n    private static boolean shown = false;\n\n    public static void showIfNeeded() {\n        var update = AppDistributionType.get().getUpdateHandler().getPerformedUpdate();\n        if (update != null && !AppDistributionType.get().getUpdateHandler().isUpdateSucceeded()) {\n            ErrorEvent.ErrorEventBuilder eventBuilder = ErrorEventFactory.fromMessage(AppI18n.get(\"updateFail\"))\n                    .documentationLink(DocumentationLink.UPDATE_FAIL)\n                    .customAction(ErrorAction.translated(\"updateFailAction\", () -> {\n                        Hyperlinks.open(Hyperlinks.GITHUB_LATEST);\n                        return true;\n                    }));\n            if (OsType.ofLocal() == OsType.WINDOWS) {\n                var installerLog =\n                        AppLogs.get().getSessionLogsDirectory().getParent().resolve(\"installer.log\");\n                if (Files.exists(installerLog)) {\n                    eventBuilder.attachment(installerLog);\n                }\n            }\n            eventBuilder.handle();\n            return;\n        }\n\n        if (update == null || update.getRawDescription() == null) {\n            return;\n        }\n\n        if (shown) {\n            return;\n        }\n        shown = true;\n\n        var comp = RegionBuilder.of(() -> {\n            var markdown = new MarkdownComp(update.getRawDescription(), s -> s, false).build();\n            return markdown;\n        });\n        var modal = ModalOverlay.of(\"updateChangelogAlertTitle\", comp.prefWidth(600), null);\n        modal.addButton(ModalButton.ok());\n        AppDialog.show(modal);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/update/UpdateHandler.java",
    "content": "package io.xpipe.app.update;\n\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.util.BooleanScope;\nimport io.xpipe.app.util.ThreadHelper;\n\nimport javafx.beans.property.BooleanProperty;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleObjectProperty;\n\nimport lombok.Builder;\nimport lombok.Getter;\nimport lombok.Value;\nimport lombok.With;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.List;\n\n@SuppressWarnings(\"InfiniteLoopStatement\")\n@Getter\npublic abstract class UpdateHandler {\n\n    protected final Property<AvailableRelease> lastUpdateCheckResult = new SimpleObjectProperty<>();\n    protected final Property<PreparedUpdate> preparedUpdate = new SimpleObjectProperty<>();\n    protected final BooleanProperty busy = new SimpleBooleanProperty();\n    protected final PerformedUpdate performedUpdate;\n    protected final boolean updateSucceeded;\n\n    protected UpdateHandler(boolean startBackgroundThread) {\n        performedUpdate = AppCache.getNonNull(\"performedUpdate\", PerformedUpdate.class, () -> null);\n        var hasUpdated = performedUpdate != null;\n        event(\"Was updated is \" + hasUpdated);\n        if (hasUpdated) {\n            AppCache.clear(\"performedUpdate\");\n            updateSucceeded = AppProperties.get().getVersion().equals(performedUpdate.getNewVersion());\n            AppCache.clear(\"preparedUpdate\");\n            event(\"Found information about recent update\");\n        } else {\n            updateSucceeded = false;\n        }\n\n        preparedUpdate.setValue(AppCache.getNonNull(\"preparedUpdate\", PreparedUpdate.class, () -> null));\n\n        // Check if the original version this was downloaded from is still the same\n        if (preparedUpdate.getValue() != null\n                && (!preparedUpdate\n                                .getValue()\n                                .getSourceVersion()\n                                .equals(AppProperties.get().getVersion())\n                        || !AppDistributionType.get()\n                                .getId()\n                                .equals(preparedUpdate.getValue().getSourceDist()))) {\n            preparedUpdate.setValue(null);\n        }\n\n        // Check if somehow the downloaded version is equal to the current one\n        if (preparedUpdate.getValue() != null\n                && preparedUpdate\n                        .getValue()\n                        .getVersion()\n                        .equals(AppProperties.get().getVersion())) {\n            preparedUpdate.setValue(null);\n        }\n\n        // Check if file has been deleted\n        if (preparedUpdate.getValue() != null\n                && preparedUpdate.getValue().getFile() != null\n                && !Files.exists(preparedUpdate.getValue().getFile())) {\n            preparedUpdate.setValue(null);\n        }\n\n        preparedUpdate.addListener((c, o, n) -> {\n            AppCache.update(\"preparedUpdate\", n);\n        });\n        lastUpdateCheckResult.addListener((c, o, n) -> {\n            if (n != null\n                    && preparedUpdate.getValue() != null\n                    && n.isUpdate()\n                    && n.getVersion().equals(preparedUpdate.getValue().getVersion())) {\n                return;\n            }\n\n            preparedUpdate.setValue(null);\n        });\n\n        if (startBackgroundThread) {\n            startBackgroundUpdater();\n        }\n    }\n\n    private void startBackgroundUpdater() {\n        ThreadHelper.createPlatformThread(\"updater\", true, () -> {\n                    var checked = false;\n                    ThreadHelper.sleep(Duration.ofMinutes(1).toMillis());\n                    event(\"Starting background updater thread\");\n                    var run = !AppProperties.get().isRestarted();\n                    while (true) {\n                        if (run\n                                && (AppPrefs.get() != null\n                                        && (AppPrefs.get().automaticallyUpdate().get()\n                                                || AppPrefs.get()\n                                                        .checkForSecurityUpdates()\n                                                        .get()))) {\n                            event(\"Performing background update\");\n                            refreshUpdateCheckSilent(\n                                    !checked,\n                                    !AppPrefs.get().automaticallyUpdate().get());\n                            checked = true;\n                            prepareUpdate();\n                        }\n\n                        ThreadHelper.sleep(Duration.ofHours(1).toMillis());\n                        run = true;\n                    }\n                })\n                .start();\n    }\n\n    protected void event(String msg) {\n        TrackEvent.builder().type(\"info\").message(msg).handle();\n    }\n\n    protected final boolean isUpdate(String releaseVersion) {\n        if (AppPrefs.get() != null\n                && AppPrefs.get().developerMode().getValue()\n                && AppPrefs.get().developerDisableUpdateVersionCheck().get()) {\n            event(\"Bypassing version check\");\n            return true;\n        }\n\n        if (!AppProperties.get().getVersion().equals(releaseVersion)) {\n            event(\"Release has a different version\");\n            return true;\n        }\n\n        return false;\n    }\n\n    public abstract boolean supportsDirectInstallation();\n\n    public final AvailableRelease refreshUpdateCheckSilent(boolean first, boolean securityOnly) {\n        try {\n            return refreshUpdateCheck(first, securityOnly);\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).discard().handle();\n            return null;\n        }\n    }\n\n    public final void prepareUpdate() {\n        if (busy.getValue()) {\n            return;\n        }\n\n        if (lastUpdateCheckResult.getValue() == null) {\n            return;\n        }\n\n        if (!lastUpdateCheckResult.getValue().isUpdate()) {\n            return;\n        }\n\n        if (preparedUpdate.getValue() != null) {\n            if (lastUpdateCheckResult\n                    .getValue()\n                    .getVersion()\n                    .equals(preparedUpdate.getValue().getVersion())) {\n                event(\"Update is already prepared ...\");\n                return;\n            }\n        }\n\n        try (var ignored = new BooleanScope(busy).start()) {\n            event(\"Performing update download ...\");\n            prepareUpdateImpl();\n\n            // Show available update in PTB more aggressively\n            if (AppProperties.get().isStaging()\n                    && preparedUpdate.getValue() != null\n                    && !AppOperationMode.isInStartup()) {\n                UpdateAvailableDialog.showIfNeeded(false);\n            }\n        } catch (Throwable t) {\n            ErrorEventFactory.fromThrowable(t).handle();\n        }\n    }\n\n    public abstract List<ModalButton> createActions();\n\n    public void prepareUpdateImpl() throws Exception {\n        var changelogString =\n                AppDownloads.downloadChangelog(lastUpdateCheckResult.getValue().getVersion());\n        var rel = new PreparedUpdate(\n                AppProperties.get().getVersion(),\n                AppDistributionType.get().getId(),\n                lastUpdateCheckResult.getValue().getVersion(),\n                lastUpdateCheckResult.getValue().getReleaseUrl(),\n                null,\n                changelogString,\n                lastUpdateCheckResult.getValue().getAssetType(),\n                lastUpdateCheckResult.getValue().isSecurityOnly());\n        preparedUpdate.setValue(rel);\n    }\n\n    public final void executeUpdateAndClose() {\n        if (busy.getValue()) {\n            return;\n        }\n\n        if (preparedUpdate.getValue() == null) {\n            return;\n        }\n\n        var downloadFile = preparedUpdate.getValue().getFile();\n        if (downloadFile != null && !Files.exists(downloadFile)) {\n            return;\n        }\n\n        // Check if prepared update is still the latest.\n        // We only do that here to minimize the sent requests by only executing when it's really necessary\n        var available = AppDistributionType.get()\n                .getUpdateHandler()\n                .refreshUpdateCheckSilent(false, preparedUpdate.getValue().isSecurityOnly());\n        if (preparedUpdate.getValue() == null) {\n            return;\n        }\n\n        if (available != null\n                && !available.getVersion().equals(preparedUpdate.getValue().getVersion())) {\n            preparedUpdate.setValue(null);\n            ThreadHelper.runAsync(() -> {\n                prepareUpdate();\n            });\n            ErrorEventFactory.fromMessage(\"A newer update is available than the one which was prepared.\")\n                    .expected()\n                    .handle();\n            return;\n        }\n\n        event(\"Executing update ...\");\n        executeUpdate();\n    }\n\n    public void executeUpdate() {\n        throw new UnsupportedOperationException();\n    }\n\n    public final AvailableRelease refreshUpdateCheck(boolean first, boolean securityOnly) throws Exception {\n        if (busy.getValue()) {\n            return lastUpdateCheckResult.getValue();\n        }\n\n        try (var ignored = new BooleanScope(busy).start()) {\n            return refreshUpdateCheckImpl(first, securityOnly);\n        }\n    }\n\n    public abstract AvailableRelease refreshUpdateCheckImpl(boolean first, boolean securityOnly) throws Exception;\n\n    @Value\n    @Builder\n    @Jacksonized\n    public static class PerformedUpdate {\n        String name;\n        String rawDescription;\n        String newVersion;\n    }\n\n    @Value\n    @Builder\n    @Jacksonized\n    @With\n    public static class AvailableRelease {\n        String sourceVersion;\n        String sourceDist;\n        String version;\n        String releaseUrl;\n        String downloadUrl;\n        AppInstaller.InstallerAssetType assetType;\n        Instant checkTime;\n        boolean isUpdate;\n        boolean securityOnly;\n    }\n\n    @Value\n    @Builder\n    @Jacksonized\n    public static class PreparedUpdate {\n        String sourceVersion;\n        String sourceDist;\n        String version;\n        String releaseUrl;\n        Path file;\n        String body;\n        AppInstaller.InstallerAssetType assetType;\n        boolean securityOnly;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/update/UpdateNagDialog.java",
    "content": "package io.xpipe.app.update;\n\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.util.Hyperlinks;\n\nimport java.time.Duration;\nimport java.time.Instant;\n\npublic class UpdateNagDialog {\n\n    public static void showAndWaitIfNeeded() {\n        UpdateHandler uh = AppDistributionType.get().getUpdateHandler();\n        if (uh.getPerformedUpdate() != null || uh.getPreparedUpdate().getValue() != null) {\n            AppCache.clear(\"lastUpdateNag\");\n            return;\n        }\n\n        if (AppPrefs.get().checkForSecurityUpdates().get()\n                || AppPrefs.get().automaticallyUpdate().getValue()) {\n            return;\n        }\n\n        Instant lastCheck = AppCache.getNonNull(\"lastUpdateNag\", Instant.class, () -> null);\n        if (lastCheck == null) {\n            AppCache.update(\"lastUpdateNag\", Instant.now());\n            return;\n        }\n\n        if (Duration.between(lastCheck, Instant.now()).compareTo(Duration.ofDays(90)) < 0) {\n            return;\n        }\n\n        var text = AppDialog.dialogTextKey(\"updateNag\");\n        var modal = ModalOverlay.of(\"updateNagTitle\", text, null);\n        modal.addButton(ModalButton.cancel());\n        modal.addButton(new ModalButton(\n                \"updateNagButton\",\n                () -> {\n                    Hyperlinks.open(Hyperlinks.GITHUB_LATEST);\n                },\n                true,\n                true));\n        AppDialog.showAndWait(modal);\n        AppCache.update(\"lastUpdateNag\", Instant.now());\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/update/WebtopUpdater.java",
    "content": "package io.xpipe.app.update;\n\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.util.DocumentationLink;\n\nimport java.util.List;\n\npublic class WebtopUpdater extends PortableUpdater {\n\n    public WebtopUpdater() {\n        super(false);\n    }\n\n    @Override\n    public List<ModalButton> createActions() {\n        var l = super.createActions();\n        l.add(new ModalButton(\n                \"upgradeInstructions\",\n                () -> {\n                    if (getPreparedUpdate().getValue() == null) {\n                        return;\n                    }\n\n                    DocumentationLink.WEBTOP_UPDATE.open();\n                },\n                false,\n                false));\n        return l;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/update/WingetUpdater.java",
    "content": "package io.xpipe.app.update;\n\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.AppInstallation;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.core.AppRestart;\nimport io.xpipe.app.core.mode.AppOperationMode;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.app.terminal.TerminalLaunch;\nimport io.xpipe.app.util.Hyperlinks;\n\nimport java.nio.file.Files;\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\n\npublic class WingetUpdater extends UpdateHandler {\n\n    public WingetUpdater() {\n        super(true);\n    }\n\n    @Override\n    public boolean supportsDirectInstallation() {\n        return true;\n    }\n\n    @Override\n    public List<ModalButton> createActions() {\n        var l = new ArrayList<ModalButton>();\n        l.add(new ModalButton(\"ignore\", null, true, false));\n        l.add(new ModalButton(\n                \"checkOutUpdate\",\n                () -> {\n                    if (getPreparedUpdate().getValue() == null) {\n                        return;\n                    }\n\n                    Hyperlinks.open(getPreparedUpdate().getValue().getReleaseUrl());\n                },\n                false,\n                false));\n        l.add(new ModalButton(\n                \"install\",\n                () -> {\n                    executeUpdateAndClose();\n                },\n                true,\n                true));\n        return l;\n    }\n\n    @Override\n    public void executeUpdate() {\n        try {\n            var p = preparedUpdate.getValue();\n            var performedUpdate = new PerformedUpdate(p.getVersion(), p.getBody(), p.getVersion());\n            AppCache.update(\"performedUpdate\", performedUpdate);\n            AppOperationMode.executeAfterShutdown(() -> {\n                TerminalLaunch.builder().title(\"XPipe Updater\").localScript(sc -> {\n                    var systemWide = Files.exists(AppInstallation.ofCurrent()\n                            .getBaseInstallationPath()\n                            .resolve(\"system\"));\n                    var pkgId = \"xpipe-io.xpipe\";\n                    if (systemWide) {\n                        return ShellScript.lines(\n                                \"powershell -Command \\\"Start-Process -Verb runAs -FilePath winget -ArgumentList upgrade, --id, \"\n                                        + pkgId + \"\\\"\",\n                                AppRestart.getTerminalRestartCommand());\n                    } else {\n                        return ShellScript.lines(\n                                \"winget upgrade --id \" + pkgId, AppRestart.getTerminalRestartCommand());\n                    }\n                });\n            });\n        } catch (Throwable t) {\n            ErrorEventFactory.fromThrowable(t).handle();\n            preparedUpdate.setValue(null);\n        }\n    }\n\n    public synchronized AvailableRelease refreshUpdateCheckImpl(boolean first, boolean securityOnly) throws Exception {\n        var rel = AppDownloads.queryLatestVersion(first, securityOnly);\n        event(\"Determined latest suitable release \" + rel.getTag());\n\n        var wingetRelease = getOutdatedPackageUpdateVersion();\n        // Use current release if the update is not available for winget yet\n        if (wingetRelease.isPresent() && !wingetRelease.get().equals(rel.getTag())) {\n            rel = AppRelease.of(AppProperties.get().getVersion());\n        }\n\n        var isUpdate = isUpdate(rel.getTag());\n        lastUpdateCheckResult.setValue(new AvailableRelease(\n                AppProperties.get().getVersion(),\n                AppDistributionType.get().getId(),\n                rel.getTag(),\n                rel.getBrowserUrl(),\n                null,\n                null,\n                Instant.now(),\n                isUpdate,\n                securityOnly));\n        return lastUpdateCheckResult.getValue();\n    }\n\n    private Optional<String> getOutdatedPackageUpdateVersion() throws Exception {\n        if (AppProperties.get().isStaging()) {\n            return Optional.empty();\n        }\n\n        var pkgId = \"xpipe-io.xpipe\";\n        var out = LocalShell.getShell()\n                .command(CommandBuilder.of()\n                        .add(\"winget\", \"list\", \"--upgrade-available\", \"--source=winget\", \"--id\", pkgId))\n                .readStdoutIfPossible();\n        if (out.isEmpty()) {\n            return Optional.empty();\n        }\n\n        var line = out.get()\n                .lines()\n                .filter(s -> {\n                    var split = s.split(\"\\\\s+\");\n                    if (split.length != 4) {\n                        return false;\n                    } else {\n                        return split[1].equals(pkgId);\n                    }\n                })\n                .findFirst();\n        if (line.isEmpty()) {\n            return Optional.empty();\n        }\n\n        var v = line.get().split(\"\\\\s+\")[3];\n        return Optional.of(v);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/AppJacksonModule.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.ext.HostAddress;\nimport io.xpipe.app.process.ShellDialect;\nimport io.xpipe.app.process.ShellDialects;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.app.pwman.KeePassXcAssociationKey;\nimport io.xpipe.app.pwman.KeePassXcPasswordManager;\nimport io.xpipe.app.pwman.KeeperPasswordManager;\nimport io.xpipe.app.pwman.PasswordManager;\nimport io.xpipe.app.rdp.ExternalRdpClient;\nimport io.xpipe.app.secret.*;\nimport io.xpipe.app.spice.ExternalSpiceClient;\nimport io.xpipe.app.storage.*;\nimport io.xpipe.app.terminal.ExternalTerminalType;\nimport io.xpipe.app.terminal.TerminalMultiplexer;\nimport io.xpipe.app.terminal.TerminalPrompt;\nimport io.xpipe.app.vnc.ExternalVncClient;\nimport io.xpipe.core.InPlaceSecretValue;\nimport io.xpipe.core.JacksonMapper;\nimport io.xpipe.core.OsType;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.databind.*;\nimport com.fasterxml.jackson.databind.deser.ContextualDeserializer;\nimport com.fasterxml.jackson.databind.jsontype.NamedType;\nimport com.fasterxml.jackson.databind.jsontype.TypeDeserializer;\nimport com.fasterxml.jackson.databind.jsontype.TypeSerializer;\nimport com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer;\nimport com.fasterxml.jackson.databind.module.SimpleModule;\nimport com.fasterxml.jackson.databind.node.JsonNodeFactory;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.fasterxml.jackson.databind.type.SimpleType;\n\nimport java.io.CharArrayReader;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.stream.Stream;\n\npublic class AppJacksonModule extends SimpleModule {\n\n    @Override\n    public void setupModule(SetupContext context) {\n        context.registerSubtypes(VaultKeySecretValue.class);\n        context.registerSubtypes(PasswordLockSecretValue.class);\n\n        addSerializer(DataStoreEntryRef.class, new DataStoreEntryRefSerializer());\n        addDeserializer(DataStoreEntryRef.class, new DataStoreEntryRefDeserializer());\n        addSerializer(ContextualFileReference.class, new LocalFileReferenceSerializer());\n        addDeserializer(ContextualFileReference.class, new LocalFileReferenceDeserializer());\n        addSerializer(ExternalTerminalType.class, new ExternalTerminalTypeSerializer());\n        addDeserializer(ExternalTerminalType.class, new ExternalTerminalTypeDeserializer());\n        addSerializer(EncryptedValue.class, new EncryptedValueSerializer());\n        addDeserializer(EncryptedValue.class, new EncryptedValueDeserializer<>());\n        addSerializer(EncryptedValue.CurrentKey.class, new EncryptedValueSerializer());\n        addDeserializer(EncryptedValue.CurrentKey.class, new EncryptedValueDeserializer<>());\n        addSerializer(EncryptedValue.VaultKey.class, new EncryptedValueSerializer());\n        addDeserializer(EncryptedValue.VaultKey.class, new EncryptedValueDeserializer<>());\n\n        addSerializer(ShellDialect.class, new ShellDialectSerializer());\n        addDeserializer(ShellDialect.class, new ShellDialectDeserializer());\n\n        addSerializer(OsType.class, new OsTypeSerializer());\n        addDeserializer(OsType.Local.class, new OsTypeLocalDeserializer());\n        addDeserializer(OsType.Any.class, new OsTypeAnyDeserializer());\n\n        addSerializer(ShellScript.class, new ShellScriptSerializer());\n        addDeserializer(ShellScript.class, new ShellScriptDeserializer());\n\n        addSerializer(HostAddress.class, new HostAddressSerializer());\n        addDeserializer(HostAddress.class, new HostAddressDeserializer());\n\n        addSerializer(KeePassXcPasswordManager.class, new KeePassXcPasswordManagerSerializer());\n        addDeserializer(KeePassXcPasswordManager.class, new KeePassXcPasswordManagerDeserializer());\n\n        for (ShellDialect t : ShellDialects.ALL) {\n            context.registerSubtypes(new NamedType(t.getClass()));\n        }\n\n        context.registerSubtypes(PasswordManager.getClasses());\n        context.registerSubtypes(TerminalMultiplexer.getClasses());\n        context.registerSubtypes(TerminalPrompt.getClasses());\n        context.registerSubtypes(ExternalVncClient.getClasses());\n        context.registerSubtypes(ExternalRdpClient.getClasses());\n        context.registerSubtypes(ExternalSpiceClient.getClasses());\n        context.registerSubtypes(SecretRetrievalStrategy.getClasses());\n        context.registerSubtypes(DataStorageGroupStrategy.getClasses());\n        context.registerSubtypes(KeeperPasswordManager.KeeperAuth.getClasses());\n\n        super.setupModule(context);\n    }\n\n    public static class KeePassXcPasswordManagerSerializer extends JsonSerializer<KeePassXcPasswordManager> {\n\n        @Override\n        public void serialize(KeePassXcPasswordManager value, JsonGenerator jgen, SerializerProvider provider)\n                throws IOException {\n            if (value == null) {\n                jgen.writeNull();\n                return;\n            }\n\n            var tree = JacksonMapper.getDefault().valueToTree(value.getAssociationKeys());\n            var object = JsonNodeFactory.instance.objectNode();\n            object.put(\"type\", \"keePassXc\");\n            object.set(\"associationKeys\", tree);\n            jgen.writeTree(object);\n        }\n\n        @Override\n        public void serializeWithType(\n                KeePassXcPasswordManager value,\n                JsonGenerator gen,\n                SerializerProvider serializers,\n                TypeSerializer typeSer)\n                throws IOException {\n            serialize(value, gen, serializers);\n        }\n    }\n\n    public static class KeePassXcPasswordManagerDeserializer extends JsonDeserializer<KeePassXcPasswordManager> {\n\n        @Override\n        @SuppressWarnings(\"unchecked\")\n        public KeePassXcPasswordManager deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {\n            JsonNode tree = JacksonMapper.getDefault().readTree(p);\n            if (tree == null || !tree.isObject()) {\n                return null;\n            }\n\n            if (tree.has(\"associationKey\")) {\n                var parsed = JacksonMapper.getDefault()\n                        .treeToValue(tree.required(\"associationKey\"), KeePassXcAssociationKey.class);\n                return KeePassXcPasswordManager.builder()\n                        .associationKeys(parsed != null ? List.of(parsed) : List.of())\n                        .build();\n            } else {\n                var javaType = JacksonMapper.getDefault()\n                        .getTypeFactory()\n                        .constructCollectionLikeType(List.class, KeePassXcAssociationKey.class);\n                var parsed = (List<KeePassXcAssociationKey>)\n                        JacksonMapper.getDefault().treeToValue(tree.required(\"associationKeys\"), javaType);\n                return KeePassXcPasswordManager.builder()\n                        .associationKeys(parsed)\n                        .build();\n            }\n        }\n    }\n\n    public static class OsTypeSerializer extends JsonSerializer<OsType> {\n\n        @Override\n        public void serialize(OsType value, JsonGenerator jgen, SerializerProvider provider) throws IOException {\n            jgen.writeString(value.getId());\n        }\n    }\n\n    public static class OsTypeLocalDeserializer extends JsonDeserializer<OsType.Local> {\n\n        @Override\n        public OsType.Local deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {\n            var stream = Stream.of(OsType.WINDOWS, OsType.LINUX, OsType.MACOS);\n            var n = p.getValueAsString();\n            return stream.filter(osType ->\n                            osType.getName().equals(n) || osType.getId().equals(n))\n                    .findFirst()\n                    .orElse(null);\n        }\n    }\n\n    public static class OsTypeAnyDeserializer extends JsonDeserializer<OsType.Any> {\n\n        @Override\n        public OsType.Any deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {\n            var stream = Stream.of(\n                    OsType.WINDOWS, OsType.LINUX, OsType.BSD, OsType.SOLARIS, OsType.MACOS, OsType.AIX, OsType.UNIX);\n            var n = p.getValueAsString();\n            return stream.filter(osType ->\n                            osType.getName().equals(n) || osType.getId().equals(n))\n                    .findFirst()\n                    .orElse(null);\n        }\n    }\n\n    public static class LocalFileReferenceSerializer extends JsonSerializer<ContextualFileReference> {\n\n        @Override\n        public void serialize(ContextualFileReference value, JsonGenerator jgen, SerializerProvider provider)\n                throws IOException {\n            jgen.writeString(value.serialize());\n        }\n    }\n\n    public static class ShellDialectSerializer extends JsonSerializer<ShellDialect> {\n\n        @Override\n        public void serialize(ShellDialect value, JsonGenerator jgen, SerializerProvider provider) throws IOException {\n            jgen.writeString(value.getId());\n        }\n    }\n\n    public static class ShellDialectDeserializer extends JsonDeserializer<ShellDialect> {\n\n        @Override\n        public ShellDialect deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {\n            JsonNode tree = JacksonMapper.getDefault().readTree(p);\n            if (tree.isObject()) {\n                var t = tree.get(\"type\");\n                if (t == null) {\n                    return null;\n                }\n                return ShellDialects.byIdIfPresent(t.asText()).orElse(null);\n            }\n\n            return ShellDialects.byIdIfPresent(tree.asText()).orElse(null);\n        }\n    }\n\n    public static class ShellScriptSerializer extends JsonSerializer<ShellScript> {\n\n        @Override\n        public void serialize(ShellScript value, JsonGenerator jgen, SerializerProvider provider) throws IOException {\n            jgen.writeString(value.getValue());\n        }\n    }\n\n    public static class ShellScriptDeserializer extends JsonDeserializer<ShellScript> {\n\n        @Override\n        public ShellScript deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {\n            return new ShellScript(p.getValueAsString());\n        }\n    }\n\n    public static class LocalFileReferenceDeserializer extends JsonDeserializer<ContextualFileReference> {\n\n        @Override\n        public ContextualFileReference deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {\n            return ContextualFileReference.of(p.getValueAsString());\n        }\n    }\n\n    public static class ExternalTerminalTypeSerializer extends JsonSerializer<ExternalTerminalType> {\n\n        @Override\n        public void serialize(ExternalTerminalType value, JsonGenerator jgen, SerializerProvider provider)\n                throws IOException {\n            jgen.writeString(value.getId());\n        }\n    }\n\n    public static class ExternalTerminalTypeDeserializer extends JsonDeserializer<ExternalTerminalType> {\n\n        @Override\n        public ExternalTerminalType deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {\n            var id = p.getValueAsString();\n            return ExternalTerminalType.ALL_ON_ALL_PLATFORMS.stream()\n                    .filter(terminalType -> terminalType.getId().equals(id))\n                    .findFirst()\n                    .orElse(null);\n        }\n    }\n\n    @SuppressWarnings(\"all\")\n    public static class EncryptedValueSerializer extends JsonSerializer<EncryptedValue> {\n\n        @Override\n        public void serialize(EncryptedValue value, JsonGenerator jgen, SerializerProvider provider)\n                throws IOException {\n            if (value.getValue() == null) {\n                jgen.writeNull();\n                return;\n            }\n\n            jgen.writeTree(value.getSecret().serialize(value.allowUserSecretKey()));\n        }\n\n        @Override\n        public void serializeWithType(\n                EncryptedValue value, JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer)\n                throws IOException {\n            if (value.getValue() == null) {\n                gen.writeNull();\n                return;\n            }\n\n            gen.writeTree(value.getSecret().serialize(value.allowUserSecretKey()));\n        }\n    }\n\n    @SuppressWarnings(\"all\")\n    public static class EncryptedValueDeserializer<T extends EncryptedValue<?>> extends JsonDeserializer<T>\n            implements ContextualDeserializer {\n\n        private boolean useCurrentSecretKeyIfPossible;\n        private boolean forceCurrentSecretKey;\n        private Class<?> type;\n\n        @Override\n        @SuppressWarnings(\"unchecked\")\n        public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)\n                throws JsonMappingException {\n            var deserializer = new EncryptedValueDeserializer();\n            if (property == null) {\n                return deserializer;\n            }\n\n            JavaType wrapperType = property.getType();\n            JavaType valueType = wrapperType.containedType(0);\n            deserializer.useCurrentSecretKeyIfPossible =\n                    !wrapperType.getRawClass().equals(EncryptedValue.VaultKey.class);\n            deserializer.forceCurrentSecretKey = wrapperType.getRawClass().equals(EncryptedValue.CurrentKey.class);\n            deserializer.type = valueType.getRawClass();\n            return deserializer;\n        }\n\n        @Override\n        @SuppressWarnings(\"unchecked\")\n        public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {\n            if (type == null) {\n                return null;\n            }\n\n            return (T) get(p, type, useCurrentSecretKeyIfPossible, forceCurrentSecretKey);\n        }\n\n        public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer typeDeserializer)\n                throws IOException {\n            var type = ((AsPropertyTypeDeserializer) typeDeserializer).baseType();\n            JavaType wrapperType = type;\n            JavaType valueType = wrapperType.containedType(0);\n            var useCurrentSecretKey = !wrapperType.equals(SimpleType.constructUnsafe(EncryptedValue.VaultKey.class));\n            var forceCurrentSecretKey = wrapperType.equals(SimpleType.constructUnsafe(EncryptedValue.CurrentKey.class));\n            return get(jp, valueType.getRawClass(), useCurrentSecretKey, forceCurrentSecretKey);\n        }\n\n        private EncryptedValue get(\n                JsonParser p, Class<?> type, boolean useCurrentSecretKey, boolean forceCurrentSecretKey)\n                throws IOException {\n            if (forceCurrentSecretKey && DataStorageUserHandler.getInstance().getActiveUser() == null) {\n                return null;\n            }\n\n            Object value;\n            JsonNode tree = JacksonMapper.getDefault().readTree(p);\n            var secret = DataStorageSecret.deserialize(tree);\n            if (secret == null) {\n                var raw = JacksonMapper.getDefault().treeToValue(tree, type);\n                if (raw != null) {\n                    value = raw;\n                    var s = JacksonMapper.getDefault().writeValueAsString(value);\n                    var internalSecret = InPlaceSecretValue.of(s.toCharArray());\n                    secret = DataStorageSecret.ofSecret(\n                            internalSecret,\n                            useCurrentSecretKey\n                                            && DataStorageUserHandler.getInstance()\n                                                            .getActiveUser()\n                                                    != null\n                                    ? EncryptionToken.ofUser()\n                                    : EncryptionToken.ofVaultKey());\n                } else {\n                    return null;\n                }\n            } else {\n                if (!secret.getEncryptedToken().canDecrypt()) {\n                    return null;\n                }\n\n                var s = secret.getSecret();\n                if (s.length == 0) {\n                    return null;\n                }\n                value = JacksonMapper.getDefault().readValue(new CharArrayReader(s), type);\n                if (value == null) {\n                    return null;\n                }\n            }\n            var perUser = useCurrentSecretKey;\n            return perUser\n                    ? new EncryptedValue.CurrentKey<>(value, secret)\n                    : new EncryptedValue.VaultKey<>(value, secret);\n        }\n    }\n\n    @SuppressWarnings(\"all\")\n    public static class DataStoreEntryRefSerializer extends JsonSerializer<DataStoreEntryRef> {\n\n        @Override\n        public void serialize(DataStoreEntryRef value, JsonGenerator jgen, SerializerProvider provider)\n                throws IOException {\n            if (value == null) {\n                jgen.writeNull();\n                return;\n            }\n\n            jgen.writeString(value.get().getUuid().toString());\n        }\n    }\n\n    public static class DataStoreEntryRefDeserializer extends JsonDeserializer<DataStoreEntryRef<?>> {\n\n        @Override\n        public DataStoreEntryRef<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {\n            JsonNode tree = p.getCodec().readTree(p);\n            if (tree == null) {\n                return null;\n            }\n\n            String text;\n            if (tree.isObject()) {\n                var obj = (ObjectNode) tree;\n                if (!obj.has(\"storeId\") || !obj.required(\"storeId\").isTextual()) {\n                    return null;\n                }\n\n                text = obj.required(\"storeId\").asText();\n                if (text.isBlank()) {\n                    return null;\n                }\n            } else {\n                if (!tree.isTextual()) {\n                    return null;\n                }\n                text = tree.asText();\n            }\n\n            var id = UUID.fromString(text);\n            // Keep an invalid entry if it is per-user, meaning that it will get removed later on\n            var e = DataStorage.get()\n                    .getStoreEntryIfPresent(id)\n                    .filter(dataStoreEntry -> dataStoreEntry.getValidity() != DataStoreEntry.Validity.LOAD_FAILED\n                            || !dataStoreEntry.getStoreNode().isReadableForUser())\n                    .orElse(null);\n            if (e == null) {\n                return null;\n            }\n\n            return new DataStoreEntryRef<>(e);\n        }\n    }\n\n    public static class HostAddressSerializer extends JsonSerializer<HostAddress> {\n\n        @Override\n        public void serialize(HostAddress value, JsonGenerator jgen, SerializerProvider provider) throws IOException {\n            if (value.isSingle()) {\n                jgen.writeString(value.get());\n            } else {\n                var tree = JsonNodeFactory.instance.objectNode();\n                tree.put(\"value\", value.get());\n                tree.set(\"available\", JacksonMapper.getDefault().valueToTree(value.getAvailable()));\n                jgen.writeTree(tree);\n            }\n        }\n    }\n\n    public static class HostAddressDeserializer extends JsonDeserializer<HostAddress> {\n\n        @Override\n        public HostAddress deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {\n            var tree = (JsonNode) p.getCodec().readTree(p);\n            if (tree.isTextual()) {\n                return !tree.textValue().isBlank() ? HostAddress.of(tree.textValue()) : null;\n            } else {\n                var value = tree.get(\"value\");\n                var available = tree.get(\"available\");\n                if (value == null || !value.isTextual() || available == null || !available.isArray()) {\n                    return null;\n                }\n\n                var l = new ArrayList<String>();\n                for (JsonNode jsonNode : available) {\n                    l.add(jsonNode.textValue());\n                }\n                return HostAddress.of(value.textValue(), l);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/AskpassAlert.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.comp.base.SecretFieldComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.window.AppSideWindow;\nimport io.xpipe.app.secret.SecretManager;\nimport io.xpipe.app.secret.SecretQueryResult;\nimport io.xpipe.app.secret.SecretQueryState;\nimport io.xpipe.core.InPlaceSecretValue;\n\nimport javafx.animation.AnimationTimer;\nimport javafx.application.Platform;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.scene.control.Alert;\nimport javafx.scene.control.ButtonBar;\nimport javafx.scene.control.ButtonType;\nimport javafx.scene.input.MouseEvent;\nimport javafx.scene.layout.StackPane;\nimport javafx.stage.Modality;\nimport javafx.stage.Stage;\nimport javafx.stage.Window;\n\npublic class AskpassAlert {\n\n    public static SecretQueryResult queryRaw(String prompt, InPlaceSecretValue secretValue, boolean stealFocus) {\n        var prop = new SimpleObjectProperty<>(secretValue);\n        var r = AppSideWindow.showBlockingAlert(alert -> {\n                    alert.initModality(Modality.NONE);\n                    alert.setTitle(AppI18n.get(\"askpassAlertTitle\"));\n                    alert.setHeaderText(prompt);\n                    alert.setAlertType(Alert.AlertType.CONFIRMATION);\n\n                    // Link to help page for double prompt\n                    if (SecretManager.disableCachingForPrompt(prompt)) {\n                        var type = new ButtonType(\"Help\", ButtonBar.ButtonData.HELP);\n                        alert.getButtonTypes().add(type);\n                        var button = alert.getDialogPane().lookupButton(type);\n                        button.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {\n                            DocumentationLink.DOUBLE_PROMPT.open();\n                            event.consume();\n                        });\n                    }\n\n                    var text = new SecretFieldComp(prop, false).buildStructure();\n                    alert.getDialogPane().setContent(new StackPane(text.get()));\n                    var stage = (Stage) alert.getDialogPane().getScene().getWindow();\n                    stage.setAlwaysOnTop(true);\n\n                    var anim = new AnimationTimer() {\n\n                        private long lastRun = 0;\n                        private int regainedFocusCount;\n\n                        @Override\n                        public void handle(long now) {\n                            if (!stage.isShowing()) {\n                                return;\n                            }\n\n                            if (regainedFocusCount >= 3) {\n                                return;\n                            }\n\n                            var hasInternalFocus = Window.getWindows().stream()\n                                    .filter(window -> window != stage)\n                                    .anyMatch(window -> window instanceof Stage s\n                                            && s.focusedProperty().get());\n                            if (hasInternalFocus) {\n                                return;\n                            }\n\n                            if (lastRun == 0) {\n                                lastRun = now;\n                                return;\n                            }\n\n                            long elapsed = (now - lastRun) / 1_000_000;\n                            if (elapsed < 500) {\n                                return;\n                            }\n\n                            var hasFocus = stage.isFocused();\n                            if (!hasFocus) {\n                                regainedFocusCount++;\n                            }\n\n                            stage.requestFocus();\n                            lastRun = now;\n                        }\n                    };\n\n                    alert.setOnShown(event -> {\n                        stage.requestFocus();\n                        if (stealFocus) {\n                            anim.start();\n                        }\n                        // Wait 1 pulse before focus so that the scene can be assigned to text\n                        Platform.runLater(() -> {\n                            text.getField().requestFocus();\n                            text.getField().end();\n                        });\n                        event.consume();\n                    });\n\n                    alert.setOnHiding(event -> {\n                        anim.stop();\n                    });\n                })\n                .filter(b -> b.getButtonData().isDefaultButton())\n                .map(t -> {\n                    return prop.getValue() != null ? prop.getValue() : InPlaceSecretValue.of(\"\");\n                })\n                .orElse(null);\n        return new SecretQueryResult(r, r == null ? SecretQueryState.CANCELLED : SecretQueryState.NORMAL);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/AsktextAlert.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.comp.base.TextFieldComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.window.AppSideWindow;\n\nimport javafx.animation.AnimationTimer;\nimport javafx.application.Platform;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.scene.control.Alert;\nimport javafx.scene.layout.StackPane;\nimport javafx.stage.Stage;\n\nimport java.util.Optional;\n\npublic class AsktextAlert {\n\n    public static Optional<String> query(String prompt, String value) {\n        var prop = new SimpleObjectProperty<>(value);\n        var r = AppSideWindow.showBlockingAlert(alert -> {\n                    alert.setTitle(AppI18n.get(\"asktextAlertTitle\"));\n                    alert.setHeaderText(prompt);\n                    alert.setAlertType(Alert.AlertType.CONFIRMATION);\n\n                    var text = new TextFieldComp(prop, false).build();\n                    alert.getDialogPane().setContent(new StackPane(text));\n                    var stage = (Stage) alert.getDialogPane().getScene().getWindow();\n                    stage.setAlwaysOnTop(true);\n\n                    var anim = new AnimationTimer() {\n\n                        private long lastRun = 0;\n                        private int regainedFocusCount;\n\n                        @Override\n                        public void handle(long now) {\n                            if (!stage.isShowing()) {\n                                return;\n                            }\n\n                            if (regainedFocusCount >= 2) {\n                                return;\n                            }\n\n                            if (lastRun == 0) {\n                                lastRun = now;\n                                return;\n                            }\n\n                            long elapsed = (now - lastRun) / 1_000_000;\n                            if (elapsed < 500) {\n                                return;\n                            }\n\n                            var hasFocus = stage.isFocused();\n                            if (!hasFocus) {\n                                regainedFocusCount++;\n                            }\n                            stage.requestFocus();\n                            lastRun = now;\n                        }\n                    };\n\n                    alert.setOnShown(event -> {\n                        stage.requestFocus();\n                        anim.start();\n                        // Wait 1 pulse before focus so that the scene can be assigned to text\n                        Platform.runLater(() -> {\n                            text.requestFocus();\n                            text.end();\n                        });\n                        event.consume();\n                    });\n\n                    alert.setOnHiding(event -> {\n                        anim.stop();\n                    });\n                })\n                .filter(b -> b.getButtonData().isDefaultButton())\n                .map(t -> {\n                    return prop.getValue() != null ? prop.getValue() : null;\n                });\n        return r;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/BooleanScope.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.core.FailableRunnable;\n\nimport javafx.beans.property.BooleanProperty;\nimport javafx.beans.property.SimpleBooleanProperty;\n\npublic class BooleanScope implements AutoCloseable {\n\n    private final BooleanProperty prop;\n    private boolean wait;\n\n    public BooleanScope(BooleanProperty prop) {\n        this.prop = prop;\n    }\n\n    public static BooleanScope noop() {\n        return new BooleanScope(new SimpleBooleanProperty());\n    }\n\n    public static <E extends Throwable> void executeExclusive(BooleanProperty prop, FailableRunnable<E> r) throws E {\n        try (var ignored = new BooleanScope(prop).exclusive().start()) {\n            r.run();\n        }\n    }\n\n    public boolean get() {\n        return prop.get();\n    }\n\n    public BooleanScope exclusive() {\n        this.wait = true;\n        return this;\n    }\n\n    public synchronized BooleanScope start() {\n        if (wait) {\n            while (prop.get()) {\n                ThreadHelper.sleep(50);\n            }\n        }\n        prop.setValue(true);\n\n        return this;\n    }\n\n    @Override\n    public synchronized void close() {\n        prop.setValue(false);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/CommandDialog.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.process.CommandControl;\nimport io.xpipe.app.process.ProcessOutputException;\n\nimport javafx.scene.control.TextArea;\nimport javafx.scene.layout.StackPane;\n\nimport lombok.Value;\nimport org.apache.commons.lang3.exception.ExceptionUtils;\n\nimport java.util.List;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.stream.Collectors;\n\npublic class CommandDialog {\n\n    @Value\n    public static class CommandEntry {\n\n        String name;\n        CommandControl command;\n    }\n\n    public static void runMultipleAndShow(List<CommandEntry> cmds) {\n        var parts = new String[cmds.size()];\n        var latch = new CountDownLatch(parts.length);\n        for (int i = 0; i < cmds.size(); i++) {\n            var e = cmds.get(i);\n            var ii = i;\n            ThreadHelper.runAsync(() -> {\n                String out;\n                try {\n                    out = e.getCommand().readStdoutOrThrow();\n                    out = formatOutput(out);\n                } catch (ProcessOutputException ex) {\n                    out = ex.getMessage();\n                } catch (Throwable t) {\n                    out = ExceptionUtils.getStackTrace(t);\n                }\n\n                var s = e.getName() + \" (exit code \" + e.getCommand().getExitCode() + \"):\\n\" + out;\n                parts[ii] = s;\n                latch.countDown();\n            });\n        }\n\n        try {\n            latch.await();\n        } catch (InterruptedException ignored) {\n        }\n\n        var joined = String.join(\"\\n\\n\", parts);\n        show(joined);\n    }\n\n    public static void runAndShow(CommandControl cmd) {\n        String out;\n        try {\n            out = cmd.readStdoutOrThrow();\n            out = formatOutput(out);\n        } catch (ProcessOutputException e) {\n            out = e.getMessage();\n        } catch (Throwable t) {\n            out = ExceptionUtils.getStackTrace(t);\n        }\n        show(out);\n    }\n\n    private static void show(String out) {\n        var modal = ModalOverlay.of(\n                \"commandOutput\",\n                RegionBuilder.of(() -> {\n                            var text = new TextArea(out);\n                            text.setWrapText(true);\n                            text.setEditable(false);\n                            text.setPrefRowCount(Math.max(8, (int) out.lines().count()));\n                            var sp = new StackPane(text);\n                            return sp;\n                        })\n                        .prefWidth(650));\n        modal.show();\n    }\n\n    public static String formatOutput(String out) {\n        if (out.isEmpty()) {\n            out = \"<empty>\";\n        }\n\n        if (out.length() > 10000) {\n            var counter = new AtomicInteger();\n            var start = out.lines()\n                    .filter(s -> {\n                        counter.incrementAndGet();\n                        return true;\n                    })\n                    .limit(100)\n                    .collect(Collectors.joining(\"\\n\"));\n            var notShownLines = counter.get() - 100;\n            if (notShownLines > 0) {\n                out = start + \"\\n\\n... \" + notShownLines + \" more lines\";\n            } else {\n                out = start;\n            }\n        }\n\n        return out;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/DataStoreFormatter.java",
    "content": "package io.xpipe.app.util;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.regex.Pattern;\n\npublic class DataStoreFormatter {\n\n    public static String camelCaseToName(String camelCase) {\n        var id = camelCase;\n        var matcher = Pattern.compile(\"[A-Z][a-z]+\").matcher(id);\n        var name = new ArrayList<String>();\n        while (matcher.find()) {\n            name.add(matcher.group());\n        }\n\n        var firstMatcher = Pattern.compile(\"^[a-z]+\").matcher(id);\n        if (firstMatcher.find()) {\n            var s = firstMatcher.group();\n            name.addFirst(s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase());\n        }\n\n        return String.join(\" \", name);\n    }\n\n    public static String join(String... elements) {\n        return String.join(\" \", Arrays.stream(elements).filter(s -> s != null).toList());\n    }\n\n    public static String capitalize(String name) {\n        if (name == null) {\n            return null;\n        }\n\n        if (name.isEmpty()) {\n            return name;\n        }\n\n        return name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase();\n    }\n\n    public static String cut(String input, int length) {\n        if (input == null) {\n            return \"\";\n        }\n\n        var end = Math.min(input.length(), length);\n        if (end < input.length()) {\n            return input.substring(0, end) + \"...\";\n        }\n        return input;\n    }\n\n    public static String formatHostName(String input, int length) {\n        if (input == null) {\n            return null;\n        }\n\n        // Check for amazon web services\n        if (input.endsWith(\".rds.amazonaws.com\")) {\n            var split = input.split(\"\\\\.\");\n            var name = split[0];\n            var region = split[2];\n            var lengthShare = (length - 3) / 2;\n            return String.format(\n                    \"%s.%s\",\n                    DataStoreFormatter.cut(name, lengthShare), DataStoreFormatter.cut(region, length - lengthShare));\n        }\n\n        if (input.endsWith(\".compute.amazonaws.com\") || input.endsWith(\".compute.internal\")) {\n            var split = input.split(\"\\\\.\");\n            var name = split[0];\n            var region = split[1];\n            var lengthShare = (length - 3) / 2;\n            return String.format(\n                    \"%s.%s\",\n                    DataStoreFormatter.cut(name, lengthShare), DataStoreFormatter.cut(region, length - lengthShare));\n        }\n\n        return cut(input, length);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/Deobfuscator.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.core.AppNames;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.process.ShellDialects;\nimport io.xpipe.core.OsType;\n\nimport org.apache.commons.lang3.exception.ExceptionUtils;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic class Deobfuscator {\n\n    public static String deobfuscateToString(Throwable t) {\n        String stackTrace = ExceptionUtils.getStackTrace(t);\n        stackTrace = stackTrace.replaceAll(\"\\tat .+/(.+)\", \"\\tat $1\");\n\n        try {\n            if (!canDeobfuscate()) {\n                return stackTrace;\n            }\n\n            var file = Files.createTempFile(AppNames.ofCurrent().getKebapName() + \"-stacktrace\", null);\n            Files.writeString(file, stackTrace);\n            var proc = new ProcessBuilder(\n                            \"retrace.\" + (OsType.ofLocal() == OsType.WINDOWS ? \"bat\" : \"sh\"),\n                            System.getenv(AppNames.ofMain().getUppercaseName() + \"_MAPPING\"),\n                            file.toString())\n                    .redirectErrorStream(true);\n            var active = proc.start();\n            var out = new String(active.getInputStream().readAllBytes()).replaceAll(\"\\r\\n\", \"\\n\");\n            var code = active.waitFor();\n            if (code == 0) {\n                return out;\n            } else {\n                System.err.println(\"Deobfuscation failed: \" + out);\n            }\n        } catch (Exception ex) {\n            System.err.println(\"Deobfuscation failed\");\n            return stackTrace;\n        }\n\n        return stackTrace;\n    }\n\n    private static boolean canDeobfuscate() {\n        if (AppProperties.get().isDevelopmentEnvironment()) {\n            return false;\n        }\n\n        if (!System.getenv().containsKey(\"XPIPE_MAPPING\")) {\n            return false;\n        }\n\n        var file = Path.of(System.getenv(\"XPIPE_MAPPING\"));\n        if (!Files.exists(file)) {\n            return false;\n        }\n\n        // We probably can't run .bat scripts in this case\n        if (OsType.ofLocal() == OsType.WINDOWS\n                && ProcessControlProvider.get().getEffectiveLocalDialect() != ShellDialects.CMD) {\n            return false;\n        }\n\n        if (OsType.ofLocal() == OsType.WINDOWS) {\n            var reg = WindowsRegistry.local()\n                    .readStringValueIfPresent(\n                            WindowsRegistry.HKEY_CURRENT_USER,\n                            \"Software\\\\Policies\\\\Microsoft\\\\Windows\\\\System\",\n                            \"DisableCMD\");\n            if (reg.isPresent() && reg.get().equals(\"1\")) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/DesktopHelper.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.ext.FileKind;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.ExternalApplicationHelper;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport java.awt.*;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\n\npublic class DesktopHelper {\n\n    public static void openBrowser(String uri) {\n        if (uri == null) {\n            return;\n        }\n\n        URI parsed;\n        try {\n            parsed = URI.create(uri);\n        } catch (IllegalArgumentException e) {\n            ErrorEventFactory.fromThrowable(\"Invalid URI: \" + uri, e.getCause() != null ? e.getCause() : e)\n                    .handle();\n            return;\n        }\n\n        if (!Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {\n            if (OsType.ofLocal() == OsType.LINUX) {\n                LocalExec.executeAsync(\"xdg-open\", parsed.toString());\n                return;\n            }\n        }\n\n        // This can be a blocking operation\n        ThreadHelper.runAsync(() -> {\n            try {\n                Desktop.getDesktop().browse(parsed);\n                return;\n            } catch (Exception e) {\n                // Some basic linux systems have trouble with the API call\n                ErrorEventFactory.fromThrowable(e)\n                        .expected()\n                        .omitted(OsType.ofLocal() == OsType.LINUX)\n                        .handle();\n            }\n\n            if (OsType.ofLocal() == OsType.LINUX) {\n                LocalExec.executeAsync(\"xdg-open\", parsed.toString());\n            }\n        });\n    }\n\n    public static void openAssociatedApplication(String uri) {\n        if (uri == null) {\n            return;\n        }\n\n        URI parsed;\n        try {\n            parsed = URI.create(uri);\n        } catch (IllegalArgumentException e) {\n            ErrorEventFactory.fromThrowable(\"Invalid URI: \" + uri, e.getCause() != null ? e.getCause() : e)\n                    .handle();\n            return;\n        }\n\n        // Windows URL open always uses browser\n        if (OsType.ofLocal() == OsType.WINDOWS) {\n            LocalExec.executeAsync(\"rundll32\", \"url.dll,FileProtocolHandler\", parsed.toString());\n            return;\n        }\n\n        // Other OS use associated app\n        openBrowser(uri);\n    }\n\n    public static void browseFile(Path file) {\n        if (file == null || !Files.exists(file)) {\n            return;\n        }\n\n        if (!Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {\n            if (OsType.ofLocal() == OsType.LINUX) {\n                LocalExec.executeAsync(\"xdg-open\", file.toString());\n                return;\n            }\n        }\n\n        // This can be a blocking operation\n        ThreadHelper.runAsync(() -> {\n            if (Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {\n                try {\n                    Desktop.getDesktop().open(file.toFile());\n                    return;\n                } catch (Exception e) {\n                    // Some basic linux systems have trouble with the API call\n                    ErrorEventFactory.fromThrowable(e)\n                            .expected()\n                            .omitted(OsType.ofLocal() == OsType.LINUX)\n                            .handle();\n                }\n            }\n\n            if (OsType.ofLocal() == OsType.LINUX) {\n                LocalExec.executeAsync(\"xdg-open\", file.toString());\n            }\n        });\n    }\n\n    public static void browseFileInDirectory(Path file) {\n        if (file == null || !Files.exists(file)) {\n            return;\n        }\n\n        // This can be a blocking operation\n        ThreadHelper.runAsync(() -> {\n            // Windows does not support Action.BROWSE_FILE_DIR\n            if (OsType.ofLocal() == OsType.WINDOWS) {\n                // Explorer does not support single quotes, so use normal quotes\n                LocalExec.executeAsync(\"explorer\", \"/select,\", \"\\\"\" + file + \"\\\"\");\n                return;\n            }\n\n            // Linux does not support Action.BROWSE_FILE_DIR\n            if (OsType.ofLocal() == OsType.LINUX) {\n                var action = Files.isDirectory(file)\n                        ? \"org.freedesktop.FileManager1.ShowFolders\"\n                        : \"org.freedesktop.FileManager1.ShowItems\";\n                var args = List.of(\n                        \"dbus-send\",\n                        \"--session\",\n                        \"--print-reply\",\n                        \"--dest=org.freedesktop.FileManager1\",\n                        \"--type=method_call\",\n                        \"/org/freedesktop/FileManager1\",\n                        action,\n                        \"array:string:file://\" + file,\n                        \"string:\");\n                try {\n                    var success = LocalExec.readStdoutIfPossible(args.toArray(String[]::new))\n                            .isPresent();\n                    if (success) {\n                        return;\n                    }\n                } catch (Exception e) {\n                    ErrorEventFactory.fromThrowable(e).omit().handle();\n                }\n            }\n\n            if (!Desktop.getDesktop().isSupported(Desktop.Action.BROWSE_FILE_DIR)) {\n                browseFile(file.getParent());\n                return;\n            }\n\n            try {\n                Desktop.getDesktop().browseFileDirectory(file.toFile());\n            } catch (Exception e) {\n                // Some basic linux systems have trouble with the API call\n                ErrorEventFactory.fromThrowable(e)\n                        .expected()\n                        .omitted(OsType.ofLocal() == OsType.LINUX)\n                        .handle();\n                if (OsType.ofLocal() == OsType.LINUX) {\n                    browseFile(file.getParent());\n                }\n            }\n        });\n    }\n\n    public static void browsePathRemote(ShellControl sc, FilePath path, FileKind kind) throws Exception {\n        switch (sc.getOsType()) {\n            case OsType.Windows ignored -> {\n                // Explorer does not support single quotes, so use normal quotes\n                if (kind == FileKind.DIRECTORY) {\n                    sc.command(CommandBuilder.of().add(\"explorer\").addQuoted(path.toString()))\n                            .execute();\n                } else {\n                    sc.command(CommandBuilder.of().add(\"explorer\", \"/select,\", \"\\\"\" + path.toString() + \"\\\"\"))\n                            .execute();\n                }\n            }\n            case OsType.Linux ignored -> {\n                var action = kind == FileKind.DIRECTORY\n                        ? \"org.freedesktop.FileManager1.ShowFolders\"\n                        : \"org.freedesktop.FileManager1.ShowItems\";\n                var dbus = String.format(\"\"\"\n                                         dbus-send --session --print-reply --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 %s array:string:\"file://%s\" string:\"\"\n                                         \"\"\", action, path);\n                var success = sc.executeSimpleBooleanCommand(dbus);\n                if (success) {\n                    return;\n                }\n\n                var b = CommandBuilder.of()\n                        .add(\"xdg-open\")\n                        .addFile(kind == FileKind.DIRECTORY ? path : path.getParent());\n                ExternalApplicationHelper.startAsync(b);\n                sc.command(b).execute();\n            }\n            case OsType.MacOs ignored -> {\n                sc.command(CommandBuilder.of()\n                                .add(\"open\")\n                                .addIf(kind == FileKind.DIRECTORY, \"-R\")\n                                .addFile(path))\n                        .execute();\n            }\n            default -> {}\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/DesktopShortcuts.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.core.AppInstallation;\nimport io.xpipe.app.core.AppNames;\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.OsFileSystem;\nimport io.xpipe.app.update.AppDistributionType;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic class DesktopShortcuts {\n\n    private static Path createWindowsShortcut(String executable, String args, String name) throws Exception {\n        var shortcutPath = AppSystemInfo.ofCurrent().getDesktop().resolve(name + \".lnk\");\n\n        var shell = LocalShell.getLocalPowershell();\n        if (shell.isEmpty()) {\n            Files.createFile(shortcutPath);\n            return shortcutPath;\n        }\n\n        var icon = AppInstallation.ofCurrent().getLogoPath();\n        var content = String.format(\"\"\"\n                                    $TARGET=\"%s\"\n                                    $SHORTCUT=\"%s\"\n                                    $ws = New-Object -ComObject WScript.Shell\n                                    $s = $ws.CreateShortcut(\"$SHORTCUT\")\n                                    $S.IconLocation='%s'\n                                    $S.WindowStyle=7\n                                    $S.TargetPath = \"$TARGET\"\n                                    $S.Arguments = '%s'\n                                    $S.Save()\n                                    \"\"\", executable, shortcutPath, icon, args).replaceAll(\"\\n\", \";\");\n        shell.get().command(content).execute();\n        return shortcutPath;\n    }\n\n    private static Path getOrCreateIcon() throws IOException {\n        if (AppDistributionType.get() != AppDistributionType.APP_IMAGE\n                && AppDistributionType.get() != AppDistributionType.NIX) {\n            return AppInstallation.ofCurrent().getLogoPath();\n        }\n\n        var target = AppSystemInfo.ofCurrent().getUserHome().resolve(\".local\", \"share\", \"icons\", \"128x128\", \"apps\");\n        var file = target.resolve(AppNames.ofCurrent().getKebapName() + \".png\");\n        if (Files.exists(file)) {\n            return file;\n        }\n\n        Files.createDirectories(target);\n        Files.copy(AppInstallation.ofCurrent().getLogoPath(), file);\n        return file;\n    }\n\n    private static Path createLinuxShortcut(String executable, String args, String name) throws Exception {\n        // Linux .desktop names are very restrictive\n        var fixedName = name.replaceAll(\"[^\\\\w _]\", \"\");\n        var icon = getOrCreateIcon();\n        var content = String.format(\"\"\"\n                                    [Desktop Entry]\n                                    Type=Application\n                                    Name=%s\n                                    Comment=Open with XPipe\n                                    Exec=\"%s\" %s\n                                    Icon=%s\n                                    Terminal=false\n                                    Categories=Utility;Development;\n                                    \"\"\", fixedName, executable, args, icon);\n\n        var osFile = Path.of(\"/etc/os-release\");\n        var ubuntu =\n                Files.exists(osFile) && Files.readString(osFile).toLowerCase().contains(\"ubuntu\");\n        var file = ubuntu\n                ? AppSystemInfo.ofCurrent().getDesktop().resolve(name + \".desktop\")\n                : AppSystemInfo.ofCurrent().getUserHome().resolve(\".local\", \"share\", \"applications\", name + \".desktop\");\n        Files.createDirectories(file.getParent());\n        Files.writeString(file, content);\n        file.toFile().setExecutable(true);\n\n        // Mark shortcuts as trusted on gnome\n        LocalShell.getShell()\n                .command(CommandBuilder.of()\n                        .add(\"gio\", \"set\")\n                        .addFile(file)\n                        .addQuoted(\"metadata::trusted\")\n                        .add(\"true\"))\n                .executeAndCheck();\n\n        return file;\n    }\n\n    private static Path createMacOSShortcut(String executable, String args, String name) throws Exception {\n        var icon = AppInstallation.ofCurrent().getLogoPath();\n        var assets = icon.getParent().resolve(\"Assets.car\");\n        var base = AppSystemInfo.ofCurrent().getDesktop().resolve(name + \".app\");\n        var content = String.format(\"\"\"\n                                    #!/usr/bin/env sh\n                                    \"%s\" %s\n                                    \"\"\", executable, args);\n\n        try (var pc = LocalShell.getShell()) {\n            pc.getShellDialect().deleteFileOrDirectory(pc, base.toString()).executeAndCheck();\n            pc.executeSimpleCommand(pc.getShellDialect().getMkdirsCommand(base + \"/Contents/MacOS\"));\n            pc.executeSimpleCommand(pc.getShellDialect().getMkdirsCommand(base + \"/Contents/Resources\"));\n\n            var macExec = base + \"/Contents/MacOS/\" + name;\n            pc.view().writeScriptFile(FilePath.of(macExec), content);\n            pc.executeSimpleCommand(\"chmod ugo+x \\\"\" + macExec + \"\\\"\");\n\n            pc.view().writeTextFile(FilePath.of(base + \"/Contents/PkgInfo\"), \"APPL????\");\n            pc.view().writeTextFile(FilePath.of(base + \"/Contents/Info.plist\"), \"\"\"\n                                                                                <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n                                                                                <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n                                                                                <plist version=\"1.0\">\n                                                                                <dict>\n                                                                                    <key>CFBundleIconName</key>\n                                                                                    <string>xpipe</string>\n                                                                                \t<key>CFBundleIconFile</key>\n                                                                                \t<string>xpipe</string>\n                                                                                </dict>\n                                                                                </plist>\n                                                                                \"\"\");\n            pc.command(\"cp \\\"\" + icon + \"\\\" \\\"\" + base + \"/Contents/Resources/xpipe.icns\\\"\")\n                    .execute();\n            pc.command(\"cp \\\"\" + assets + \"\\\" \\\"\" + base + \"/Contents/Resources/Assets.car\\\"\")\n                    .execute();\n        }\n        return base;\n    }\n\n    public static Path createOpen(String name, String cliArgs, String desktopArgs) throws Exception {\n        var compat = OsFileSystem.ofLocal().makeFileSystemCompatible(name);\n        if (OsType.ofLocal() == OsType.WINDOWS) {\n            var exec = AppInstallation.ofCurrent().getCliExecutablePath().toString();\n            return createWindowsShortcut(exec, cliArgs, compat);\n        } else if (OsType.ofLocal() == OsType.LINUX) {\n            // AppImages are mounted and can't be called normally\n            if (AppDistributionType.get() == AppDistributionType.APP_IMAGE) {\n                if (desktopArgs == null) {\n                    throw ErrorEventFactory.expected(\n                            new UnsupportedOperationException(\"This desktop shortcut operation is not supported in the \"\n                                    + AppDistributionType.get()\n                                            .toTranslatedString()\n                                            .getValue() + \" distribution\"));\n                }\n\n                var exec = System.getenv(\"APPIMAGE\");\n                return createLinuxShortcut(exec, desktopArgs, compat);\n            }\n\n            // Nix store locations change on updates\n            // These installations always add the executable to the path\n            if (AppDistributionType.get() == AppDistributionType.NIX) {\n                return createLinuxShortcut(AppNames.ofCurrent().getKebapName(), cliArgs, compat);\n            }\n\n            var exec = AppInstallation.ofCurrent().getCliExecutablePath().toString();\n            return createLinuxShortcut(exec, cliArgs, compat);\n        } else {\n            var exec = AppInstallation.ofCurrent().getCliExecutablePath().toString();\n            return createMacOSShortcut(exec, cliArgs, compat);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/DocumentationLink.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.core.AppProperties;\n\npublic enum DocumentationLink {\n    API(\"api\"),\n    TTY(\"troubleshoot/tty\"),\n    NETWORK_SWITCH(\"guide/network-switch\"),\n    SSH_BROKEN_PIPE(\"troubleshoot/ssh#client-loop-send-disconnect--connection-reset--broken-pipe\"),\n    WINDOWS_SETUP(\"guide/installation#windows\"),\n    MACOS_SETUP(\"guide/installation#macos\"),\n    DOUBLE_PROMPT(\"troubleshoot/two-step-connections\"),\n    LICENSE_ACTIVATION(\"troubleshoot/license-activation\"),\n    TLS_DECRYPTION(\"troubleshoot/license-activation#tls-decryption\"),\n    UPDATE_FAIL(\"troubleshoot/update-fail\"),\n    PRIVACY(\"legal/privacy-policy\"),\n    EULA(\"legal/end-user-license-agreement\"),\n    WEBTOP_UPDATE(\"guide/webtop#updating\"),\n    WEBTOP_TUN(\"guide/webtop#networking-tailscale-and-netbird\"),\n    SYNC(\"guide/sync\"),\n    SYNC_LOCAL(\"guide/sync#local-repositories\"),\n    SYNC_MODE(\"guide/sync#sync-frequency\"),\n    SYNC_PLAIN(\"guide/sync#plain-directories\"),\n    DESKTOP_APPLICATIONS(\"guide/desktop-applications\"),\n    SERVICES(\"guide/services\"),\n    SCRIPTING(\"guide/scripting\"),\n    SCRIPTING_COMPATIBILITY(\"guide/scripting#shell-compatibility\"),\n    SCRIPTING_EDITING(\"guide/scripting#editing\"),\n    SCRIPTING_TYPES(\"guide/scripting#init-scripts\"),\n    SCRIPTING_DEPENDENCIES(\"guide/scripting#dependencies\"),\n    SCRIPTING_GROUPS(\"guide/scripting#groups\"),\n    KUBERNETES(\"guide/kubernetes\"),\n    DOCKER(\"guide/docker\"),\n    PROXMOX(\"guide/proxmox\"),\n    PROXMOX_GUEST_AGENT(\"guide/proxmox#guest-agent\"),\n    PROXMOX_NETWORKING(\"guide/proxmox#networking\"),\n    TAILSCALE(\"guide/tailscale\"),\n    TAILSCALE_AUTH(\"guide/tailscale#tailscale-authentication\"),\n    IDENTITY_APPLY(\"guide/ssh#applying-identities\"),\n    NETBIRD(\"guide/netbird\"),\n    NETBIRD_DAEMON(\"guide/netbird#daemon\"),\n    TELEPORT(\"guide/teleport\"),\n    LXC(\"guide/lxc\"),\n    APPLE_CONTAINERS(\"guide/apple-containers\"),\n    PODMAN(\"guide/podman\"),\n    KVM(\"guide/kvm\"),\n    KVM_VNC(\"guide/kvm#vnc-access\"),\n    KVM_GUEST_AGENT(\"guide/kvm#guest-agent\"),\n    KVM_NETWORKING(\"guide/kvm#networking\"),\n    HCLOUD(\"guide/hcloud\"),\n    VMWARE(\"guide/vmware\"),\n    VMWARE_NETWORKING(\"guide/vmware#networking\"),\n    AWS(\"guide/aws\"),\n    AWS_PROFILES(\"guide/aws#profiles\"),\n    AWS_EC2(\"guide/aws#ec2-instances\"),\n    AWS_EC2_SSM(\"guide/aws#ssm\"),\n    AWS_S3(\"guide/aws#s3-buckets\"),\n    VNC(\"guide/vnc\"),\n    ABSTRACT_HOSTS(\"guide/abstract-hosts\"),\n    REAL_VNC(\"guide/vnc#realvnc-server\"),\n    SSH(\"guide/ssh\"),\n    SSH_KEYGEN(\"guide/ssh#generating-keys\"),\n    SSH_PUBLIC_KEY(\"guide/ssh#public-key-handling\"),\n    SSH_GATEWAYS(\"guide/ssh#gateways-and-jump-servers\"),\n    SSH_HOST_KEYS(\"troubleshoot/ssh#no-matching-host-key-type-found\"),\n    SSH_BAD_FILE_DESCRIPTOR(\"troubleshoot/ssh#bad-file-descriptor\"),\n    SSH_KEX(\"troubleshoot/ssh#no-matching-key-exchange-method\"),\n    SSH_IPV6(\"troubleshoot/ssh#ipv6-issues\"),\n    SSH_CONNECTION_RESET(\"troubleshoot/ssh#connection-reset\"),\n    SSH_CONNECTION_CLOSED(\"troubleshoot/ssh#connection-closed-by-remote-host\"),\n    SSH_KEY_PERMISSIONS(\"troubleshoot/ssh#key-permissions-too-open\"),\n    SSH_NO_ROUTE(\"troubleshoot/ssh#no-route-to-host\"),\n    SSH_CONNECTION_TIMEOUT(\"troubleshoot/ssh#connection-timeout\"),\n    SSH_SHELL_TIMEOUT(\"troubleshoot/ssh#shell-timeout\"),\n    SSH_CONFIG(\"guide/ssh#config-files\"),\n    SSH_KEYS(\"guide/ssh#key-based-authentication\"),\n    SSH_OPTIONS(\"guide/ssh#adding-ssh-options\"),\n    SSH_X11(\"guide/ssh#x11-forwarding\"),\n    SSH_LIMITED(\"guide/ssh#limited--embedded-systems\"),\n    PSSESSION(\"guide/pssession\"),\n    RDP_ADDITIONAL_OPTIONS(\"guide/rdp#additional-rdp-options\"),\n    RDP_ALLOW_LIST(\"guide/desktop-applications#allow-lists\"),\n    RDP_TUNNEL_HOST(\"guide/rdp#rdp-tunnels\"),\n    RDP(\"guide/rdp\"),\n    TUNNELS(\"guide/ssh#tunnels\"),\n    TUNNELS_LOCAL(\"guide/ssh#local-tunnels\"),\n    TUNNELS_REMOTE(\"guide/ssh#remote-tunnels\"),\n    TUNNELS_DYNAMIC(\"guide/ssh#dynamic-tunnels\"),\n    HYPERV(\"guide/hyperv\"),\n    HYPERV_NETWORKING(\"guide/hyperv#custom-networking\"),\n    SSH_MACS(\"troubleshoot/ssh#no-matching-mac-found\"),\n    SSH_FEATURE_NOT_SUPPORTED(\"troubleshoot/ssh#requested-feature-not-supported\"),\n    SSH_JUMP_SERVERS(\"guide/ssh#gateways-and-jump-servers\"),\n    SSH_CUSTOM(\"guide/ssh-config#custom-ssh-connections\"),\n    SSH_CUSTOM_ORDER(\"guide/ssh-config#jump-hosts\"),\n    KEEPASSXC(\"guide/password-manager#keepassxc\"),\n    PASSWORD_MANAGER(\"guide/password-manager\"),\n    VNC_CLIENTS(\"guide/vnc#external-clients\"),\n    SHELL_ENVIRONMENTS(\"guide/environments\"),\n    SHELL_ENVIRONMENTS_USER(\"guide/environments#users\"),\n    SHELL_ENVIRONMENTS_SCRIPTS(\"guide/environments#scripts\"),\n    SERIAL(\"guide/serial\"),\n    ICONS(\"guide/hub#icons\"),\n    ONE_PASSWORD_KEYS(\"guide/password-manager#key-format\"),\n    GNOME_WAYLAND_SCALING(\"troubleshoot/wayland-blur\"),\n    BEACON_PORT_BIND(\"troubleshoot/beacon-port\"),\n    SERIAL_IMPLEMENTATION(\"guide/serial#serial-implementations\"),\n    SERIAL_PORTS(\"guide/serial#serial-ports\"),\n    TERMINAL(\"guide/terminals#noteworthy-integrations\"),\n    TERMINAL_LOGGING(\"guide/terminals#logging\"),\n    TERMINAL_LOGGING_FILES(\"guide/terminals#output-format\"),\n    TERMINAL_MULTIPLEXER(\"guide/terminals#multiplexers\"),\n    TERMINAL_PROMPT(\"guide/terminals#prompts\"),\n    TERMINAL_SPLIT(\"guide/terminals#split-views\"),\n    TERMINAL_ENVIRONMENT(\"guide/terminals#windows-environments\"),\n    TEAM_VAULTS(\"guide/sync#team-vaults\"),\n    GROUP_VAULTS(\"guide/sync#team-vaults\"),\n    SSH_TROUBLESHOOT(\"troubleshoot/ssh\"),\n    NO_EXEC(\"troubleshoot/noexec\"),\n    LOCAL_SHELL_ERROR(\"troubleshoot/local-shell\"),\n    LOCAL_SHELL_WARNING(\"troubleshoot/local-shell#startup-warnings\"),\n    LOCAL_SHELL_OCCASIONAL(\"troubleshoot/local-shell#occasional-failures\"),\n    MCP(\"guide/mcp\"),\n    INTRO(\"guide/first-steps#adding-remote-connections\");\n\n    private final String page;\n\n    DocumentationLink(String page) {\n        this.page = page;\n    }\n\n    public static String getRoot() {\n        var ptbDocs = AppProperties.get().isDevelopmentEnvironment()\n                || AppProperties.get().isStaging();\n        return ptbDocs ? \"https://docs-ptb.xpipe.io\" : \"https://docs.xpipe.io\";\n    }\n\n    public void open() {\n        Hyperlinks.open(getLink());\n    }\n\n    public String getLink() {\n        return getRoot() + \"/\" + page;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/FileBridge.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.browser.action.impl.ApplyFileEditActionProvider;\nimport io.xpipe.app.browser.file.BrowserFileInput;\nimport io.xpipe.app.browser.file.BrowserFileOutput;\nimport io.xpipe.app.core.AppFileWatcher;\nimport io.xpipe.app.core.AppLocalTemp;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.process.OsFileSystem;\nimport io.xpipe.core.FailableFunction;\nimport io.xpipe.core.FailableSupplier;\n\nimport lombok.Getter;\nimport org.apache.commons.io.FileUtils;\n\nimport java.io.BufferedInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.file.*;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.*;\nimport java.util.function.BiConsumer;\nimport java.util.function.Consumer;\n\npublic class FileBridge {\n\n    private static final Path TEMP = AppLocalTemp.getLocalTempDataDirectory(\"bridge\");\n    private static FileBridge INSTANCE;\n    private final Set<Entry> openEntries = new HashSet<>();\n\n    public static FileBridge get() {\n        return INSTANCE;\n    }\n\n    private static void event(String msg) {\n        TrackEvent.builder().type(\"debug\").message(msg).handle();\n    }\n\n    public static void init() {\n        INSTANCE = new FileBridge();\n        try {\n            FileUtils.forceMkdir(TEMP.toFile());\n\n            try {\n                // Remove old editor files in dir\n                FileUtils.cleanDirectory(TEMP.toFile());\n            } catch (IOException ignored) {\n            }\n\n            AppFileWatcher.getInstance().startWatchersInDirectories(List.of(TEMP), (changed, kind) -> {\n                if (INSTANCE != null) {\n                    INSTANCE.handleWatchEvent(changed, kind);\n                }\n            });\n        } catch (IOException e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n        }\n    }\n\n    public static void reset() {\n        try {\n            FileUtils.cleanDirectory(TEMP.toFile());\n        } catch (IOException ignored) {\n        }\n        INSTANCE = null;\n    }\n\n    private synchronized void handleWatchEvent(Path changed, WatchEvent.Kind<Path> kind) {\n        if (kind == StandardWatchEventKinds.ENTRY_DELETE) {\n            event(\"Editor entry file \" + changed.toString() + \" has been removed\");\n            removeForFile(changed);\n            return;\n        }\n\n        var entry = getForFile(changed);\n        if (entry.isEmpty()) {\n            return;\n        }\n\n        var e = entry.get();\n        // Wait for edit to finish in case external editor has write lock\n        if (!Files.exists(changed)) {\n            event(\"File \" + TEMP.relativize(e.file) + \" is probably still writing ...\");\n            ThreadHelper.sleep(1000);\n\n            // If still no read lock after some time, just don't parse it\n            if (!Files.exists(changed)) {\n                event(\"Could not obtain read lock even after timeout. Ignoring change ...\");\n                return;\n            }\n        }\n\n        try {\n            // Guard against fragmented write operations\n            // It's not perfect but should wait long enough for any multipart write to finish after waiting\n            if (Files.size(changed) == 0) {\n                event(\"File \" + TEMP.relativize(e.file) + \" is empty and probably still writing ...\");\n                ThreadHelper.sleep(1000);\n            }\n\n            event(\"Registering modification for file \" + TEMP.relativize(e.file));\n            event(\"Last modification for file: \" + e.lastModified.toString() + \" vs current one: \"\n                    + e.getLastModified());\n            if (e.registerChange()) {\n                event(\"Registering change for file \" + TEMP.relativize(e.file) + \" for editor entry \" + e.getName());\n                try (var in = Files.newInputStream(e.file)) {\n                    var actualSize = (long) in.available();\n                    var started = Instant.now();\n                    var fixedIn = new FixedSizeInputStream(new BufferedInputStream(in), actualSize);\n                    e.writer.accept(fixedIn, actualSize);\n                    in.transferTo(OutputStream.nullOutputStream());\n                    var taken = Duration.between(started, Instant.now());\n                    event(\"Wrote \" + HumanReadableFormat.byteCount(actualSize) + \" in \" + taken.toMillis() + \"ms\");\n                } catch (NoSuchFileException ex) {\n                    // The file might be removed meanwhile\n                    ErrorEventFactory.fromThrowable(ex).expected().omit().handle();\n                }\n            } else {\n                event(\"File doesn't seem to be changed\");\n            }\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).omit().handle();\n        }\n    }\n\n    private synchronized void removeForFile(Path file) {\n        openEntries.removeIf(es -> es.file.equals(file));\n    }\n\n    private synchronized Optional<Entry> getForKey(Object node) {\n        for (var es : openEntries) {\n            if (es.key.equals(node)) {\n                return Optional.of(es);\n            }\n        }\n        return Optional.empty();\n    }\n\n    private synchronized Optional<Entry> getForFile(Path file) {\n        for (var es : openEntries) {\n            if (es.file.equals(file)) {\n                return Optional.of(es);\n            }\n        }\n        event(\"No editor entry found for change file \" + file.toString());\n        return Optional.empty();\n    }\n\n    public synchronized void openIO(\n            String keyName,\n            Object key,\n            BooleanScope scope,\n            FailableSupplier<BrowserFileInput> inputSupplier,\n            FailableFunction<Long, BrowserFileOutput, Exception> outputSupplier,\n            Consumer<String> consumer) {\n        var ext = getForKey(key);\n        if (ext.isPresent()) {\n            var existingFile = ext.get().file;\n            try {\n                var input = inputSupplier.get();\n                try (var out = Files.newOutputStream(existingFile);\n                        var in = input.open()) {\n                    in.transferTo(out);\n                } finally {\n                    input.onFinish();\n                }\n            } catch (Exception ex) {\n                ErrorEventFactory.fromThrowable(ex).handle();\n                return;\n            }\n            ext.get().registerChange();\n            consumer.accept(existingFile.toString());\n            return;\n        }\n\n        Path file = TEMP.resolve(UUID.randomUUID().toString().substring(0, 6))\n                .resolve(OsFileSystem.ofLocal().makeFileSystemCompatible(keyName));\n        try {\n            FileUtils.forceMkdirParent(file.toFile());\n            var input = inputSupplier.get();\n            try (var out = Files.newOutputStream(file);\n                    var in = input.open()) {\n                in.transferTo(out);\n            } finally {\n                input.onFinish();\n            }\n        } catch (Exception ex) {\n            ErrorEventFactory.fromThrowable(ex).handle();\n            return;\n        }\n\n        var entry = new Entry(file, key, keyName, scope, (in, size) -> {\n            if (outputSupplier != null) {\n                var effectiveScope = scope != null ? scope : BooleanScope.noop();\n                try (var ignored = effectiveScope.start()) {\n                    var outSupplier = outputSupplier.apply(size);\n                    if (!outSupplier.hasOutput()) {\n                        return;\n                    }\n\n                    var action = ApplyFileEditActionProvider.Action.builder()\n                            .input(BrowserFileInput.of(in))\n                            .output(outSupplier)\n                            .target(file.getFileName().toString())\n                            .build();\n                    action.executeSync();\n                } catch (Exception ex) {\n                    ErrorEventFactory.fromThrowable(ex).handle();\n                }\n            }\n        });\n        entry.registerChange();\n        openEntries.add(entry);\n\n        ext = getForKey(key);\n        consumer.accept(ext.orElseThrow().file.toString());\n    }\n\n    @Getter\n    public static class Entry {\n        private final Path file;\n        private final Object key;\n        private final String name;\n        private final BooleanScope scope;\n        private final BiConsumer<InputStream, Long> writer;\n        private Instant lastModified;\n        private long lastSize;\n\n        public Entry(Path file, Object key, String name, BooleanScope scope, BiConsumer<InputStream, Long> writer) {\n            this.file = file;\n            this.key = key;\n            this.name = name;\n            this.scope = scope;\n            this.writer = writer;\n        }\n\n        public Instant getLastModified() {\n            try {\n                return Files.getLastModifiedTime(file).toInstant();\n            } catch (IOException e) {\n                return Instant.EPOCH;\n            }\n        }\n\n        public long getSize() {\n            try {\n                return Files.size(file);\n            } catch (IOException e) {\n                return 0;\n            }\n        }\n\n        public boolean registerChange() {\n            var newSize = getSize();\n            var newDate = getLastModified();\n            // The size check is intended for cases in which editors first clear a file prior to writing it\n            // In that case, multiple watch events are sent. If these happened very fast, it might be possible that\n            // the modified time is the same for both write operations due to the file system modified time resolution\n            // being limited\n            // We then can't identify changes purely based on the modified time, so the file size is the next best\n            // option\n            // This might result in double change detection in rare cases, but that is irrelevant as it prevents files\n            // from being blanked\n            var changed = !newDate.equals(lastModified) || newSize > lastSize;\n            lastSize = newSize;\n            lastModified = newDate;\n            return changed;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/FileOpener.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.browser.file.BrowserFileInput;\nimport io.xpipe.app.browser.file.BrowserFileOutput;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ShellDialects;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.core.OsType;\n\nimport com.sun.jna.platform.win32.Shell32;\nimport com.sun.jna.platform.win32.ShellAPI;\nimport com.sun.jna.platform.win32.User32;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Path;\nimport java.util.Optional;\nimport java.util.UUID;\nimport java.util.function.Consumer;\n\npublic class FileOpener {\n\n    public static void openInTextEditor(String localFile) {\n        var editor = AppPrefs.get().externalEditor().getValue();\n        if (editor == null) {\n            return;\n        }\n\n        try {\n            editor.launch(Path.of(localFile).toRealPath());\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(\n                            \"Unable to launch editor \"\n                                    + editor.toTranslatedString().getValue()\n                                    + \".\\nMaybe try to use a different editor in the settings.\",\n                            e)\n                    .expected()\n                    .handle();\n        }\n    }\n\n    public static void openWithAnyApplication(String localFile) {\n        try {\n            switch (OsType.ofLocal()) {\n                case OsType.Windows ignored -> {\n                    // See https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-shellexecuteinfoa\n                    var struct = new ShellAPI.SHELLEXECUTEINFO();\n                    struct.fMask = 0x100 | 0xC;\n                    struct.lpVerb = \"openas\";\n                    struct.lpFile = localFile;\n                    struct.nShow = User32.SW_SHOWDEFAULT;\n                    Shell32.INSTANCE.ShellExecuteEx(struct);\n\n                    // This solution does not support spaces in file names\n                    // var cmd = CommandBuilder.of().add(\"rundll32.exe\", \"shell32.dll,OpenAs_RunDLL\", localFile);\n                    // LocalShell.getShell().executeSimpleCommand(cmd);\n                }\n                case OsType.Linux ignored -> {\n                    throw new UnsupportedOperationException();\n                }\n                case OsType.MacOs ignored -> {\n                    throw new UnsupportedOperationException();\n                }\n            }\n        } catch (Throwable e) {\n            ErrorEventFactory.fromThrowable(\"Unable to open file \" + localFile, e)\n                    .handle();\n        }\n    }\n\n    public static void openInDefaultApplication(String localFile) {\n        try (var pc = LocalShell.getShell().start()) {\n            if (pc.getOsType() == OsType.WINDOWS) {\n                if (pc.getShellDialect() == ShellDialects.POWERSHELL) {\n                    pc.command(CommandBuilder.of().add(\"Invoke-Item\").addFile(localFile))\n                            .execute();\n                } else {\n                    pc.executeSimpleCommand(\"start \\\"\\\" \\\"\" + localFile + \"\\\"\");\n                }\n            } else if (pc.getOsType() == OsType.LINUX) {\n                pc.executeSimpleCommand(\"xdg-open \\\"\" + localFile + \"\\\"\");\n            } else {\n                pc.executeSimpleCommand(\"open \\\"\" + localFile + \"\\\"\");\n            }\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(\"Unable to open file \" + localFile, e)\n                    .handle();\n        }\n    }\n\n    public static void openReadOnlyString(String input) {\n        if (input == null) {\n            input = \"\";\n        }\n\n        var id = UUID.randomUUID();\n        String s = input;\n        FileBridge.get()\n                .openIO(\n                        id.toString(),\n                        id,\n                        null,\n                        () -> BrowserFileInput.of(new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8))),\n                        null,\n                        v -> openInTextEditor(v));\n    }\n\n    public static void openString(String keyName, Object key, String input, Consumer<String> output) {\n        if (input == null) {\n            input = \"\";\n        }\n\n        String s = input;\n        FileBridge.get()\n                .openIO(\n                        keyName,\n                        key,\n                        null,\n                        () -> BrowserFileInput.of(new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8))),\n                        (size) -> {\n                            return new BrowserFileOutput() {\n                                @Override\n                                public Optional<DataStoreEntry> target() {\n                                    return Optional.empty();\n                                }\n\n                                @Override\n                                public boolean hasOutput() {\n                                    return true;\n                                }\n\n                                @Override\n                                public OutputStream open() {\n                                    return new ByteArrayOutputStream(s.length()) {\n                                        @Override\n                                        public void close() throws IOException {\n                                            super.close();\n                                            output.accept(new String(toByteArray(), StandardCharsets.UTF_8));\n                                        }\n                                    };\n                                }\n\n                                @Override\n                                public void beforeTransfer() {}\n\n                                @Override\n                                public void onFinish() {}\n                            };\n                        },\n                        file -> openInTextEditor(file));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/FileReference.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.ext.FileSystemStore;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.core.FilePath;\n\nimport lombok.Value;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\n/**\n * Represents a file located on a file system.\n */\n@SuperBuilder\n@Jacksonized\n@Value\npublic class FileReference {\n\n    DataStoreEntryRef<? extends FileSystemStore> fileSystem;\n    FilePath path;\n\n    public FileReference(DataStoreEntryRef<? extends FileSystemStore> fileSystem, FilePath path) {\n        this.fileSystem = fileSystem;\n        this.path = path;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/FixedSizeInputStream.java",
    "content": "package io.xpipe.app.util;\n\nimport java.io.IOException;\nimport java.io.InputStream;\n\npublic class FixedSizeInputStream extends SimpleFilterInputStream {\n\n    private final long size;\n    private long count;\n\n    public FixedSizeInputStream(InputStream in, long size) {\n        super(in);\n        this.size = size;\n    }\n\n    @Override\n    public int read() throws IOException {\n        if (count >= size) {\n            return -1;\n        }\n\n        var read = in.read();\n        count++;\n        if (read == -1) {\n            return 0;\n        } else {\n            return read;\n        }\n    }\n\n    @Override\n    public int available() {\n        return (int) (size - count);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/FlatpakCache.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.PropertiesFormatsParser;\n\nimport lombok.Builder;\nimport lombok.Value;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.Optional;\n\npublic class FlatpakCache {\n\n    @Value\n    @Builder\n    public static class App {\n\n        String id;\n        String name;\n    }\n\n    private static final Map<String, App> apps = new LinkedHashMap<>();\n\n    public static synchronized Optional<App> getApp(String id) throws Exception {\n        if (apps.containsKey(id)) {\n            return Optional.ofNullable(apps.get(id));\n        }\n\n        var info = LocalShell.getShell()\n                .command(CommandBuilder.of().add(\"flatpak\", \"info\").addQuoted(id))\n                .readStdoutIfPossible();\n        if (info.isEmpty()) {\n            apps.put(id, null);\n            return Optional.empty();\n        }\n\n        var props = PropertiesFormatsParser.parse(info.get(), \":\");\n        var name = props.get(\"Name\");\n        var app = App.builder().id(id).name(name).build();\n        apps.put(id, app);\n        return Optional.ofNullable(app);\n    }\n\n    public static CommandBuilder getRunCommand(String id) {\n        return CommandBuilder.of()\n                .add(\"flatpak\", \"run\")\n                .add(\"--filesystem=\" + AppSystemInfo.ofLinux().getTemp())\n                .add(\"--filesystem=host\")\n                .addQuoted(id);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/GithubReleaseDownloader.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.core.AppLocalTemp;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.core.JacksonMapper;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.function.Predicate;\n\npublic class GithubReleaseDownloader {\n\n    public static Path getDownloadTempFile(String repository, String id, Predicate<String> filter) throws Exception {\n        var tempDir = AppLocalTemp.getLocalTempDataDirectory(\"github\");\n        var temp = tempDir.resolve(id);\n        if (Files.exists(temp)) {\n            return temp;\n        }\n\n        var request = HttpRequest.newBuilder()\n                .GET()\n                .uri(URI.create(getDownloadUrl(repository, filter)))\n                .build();\n        var r = HttpHelper.client().send(request, HttpResponse.BodyHandlers.ofByteArray());\n        if (r.statusCode() >= 400) {\n            throw new IOException(new String(r.body(), StandardCharsets.UTF_8));\n        }\n\n        Files.createDirectories(tempDir);\n        Files.write(temp, r.body());\n        return temp;\n    }\n\n    public static void extractTarEntry(Path tarFile, String path, Path target) throws Exception {\n        var c = CommandBuilder.of().add(\"tar\");\n        c.add(\"-C\").addFile(target.getParent());\n        var gz = tarFile.getFileName().toString().endsWith(\".gz\");\n        c.add(\"-x\").addIf(gz, \"-z\").add(\"-f\");\n        c.addFile(tarFile);\n        c.addFile(path);\n\n        Files.createDirectories(target.getParent());\n        LocalShell.getShell().command(c).execute();\n    }\n\n    private static String getDownloadUrl(String repository, Predicate<String> filter) throws Exception {\n        var request = HttpRequest.newBuilder()\n                .GET()\n                .uri(URI.create(\"https://api.github.com/repos/\" + repository + \"/releases\"))\n                .build();\n        var r = HttpHelper.client().send(request, HttpResponse.BodyHandlers.ofString());\n        if (r.statusCode() >= 400) {\n            throw new IOException(r.body());\n        }\n\n        var json = JacksonMapper.getDefault().readTree(r.body());\n        var latest = json.get(0);\n        var assets = latest.required(\"assets\");\n        for (var asset : assets) {\n            var name = asset.required(\"name\").asText();\n            if (filter.test(name)) {\n                var url = asset.required(\"browser_download_url\").asText();\n                return url;\n            }\n        }\n\n        throw new IllegalStateException(\"Unable to find download url for \" + repository);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/GlobalTimer.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\n\nimport java.time.Duration;\nimport java.util.Timer;\nimport java.util.TimerTask;\nimport java.util.function.Supplier;\n\npublic class GlobalTimer {\n\n    private static Timer TIMER;\n\n    public static void init() {\n        TIMER = new Timer(\"global-timer\", true);\n    }\n\n    public static void reset() {\n        if (TIMER == null) {\n            return;\n        }\n\n        TIMER.cancel();\n        TIMER = null;\n    }\n\n    private static TimerTask createDelayedTask(Duration interval, Supplier<Boolean> s) {\n        return new TimerTask() {\n            @Override\n            public void run() {\n                try {\n                    if (!s.get()) {\n                        // Use this approach instead of scheduleAtFixedRate\n                        // to prevent it from being run rapidly in case the timer is trying\n                        // to catch up. For example with system hibernation\n                        TIMER.schedule(createDelayedTask(interval, s), interval.toMillis());\n                    }\n                } catch (IllegalStateException e) {\n                    // The timer might be shutdown already\n                    ErrorEventFactory.fromThrowable(e).omit().expected().handle();\n                } catch (Throwable t) {\n                    ErrorEventFactory.fromThrowable(t).handle();\n                }\n            }\n        };\n    }\n\n    private static void schedule(TimerTask task, long delay) {\n        if (TIMER == null) {\n            return;\n        }\n\n        try {\n            TIMER.schedule(\n                    new TimerTask() {\n                        @Override\n                        public void run() {\n                            try {\n                                task.run();\n                            } catch (Throwable t) {\n                                ErrorEventFactory.fromThrowable(t).handle();\n                            }\n                        }\n                    },\n                    delay);\n        } catch (IllegalStateException e) {\n            // The timer might be shutdown already\n            ErrorEventFactory.fromThrowable(e).omit().expected().handle();\n        }\n    }\n\n    public static void scheduleUntil(Duration interval, boolean runInstantly, Supplier<Boolean> s) {\n        var task = createDelayedTask(interval, s);\n        schedule(task, runInstantly ? 0 : interval.toMillis());\n    }\n\n    public static void delay(Runnable r, Duration delay) {\n        schedule(\n                new TimerTask() {\n                    @Override\n                    public void run() {\n                        r.run();\n                    }\n                },\n                delay.toMillis());\n    }\n\n    public static void delayAsync(Runnable r, Duration delay) {\n        schedule(\n                new TimerTask() {\n                    @Override\n                    public void run() {\n                        ThreadHelper.runAsync(r);\n                    }\n                },\n                delay.toMillis());\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/GroupFile.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.core.OsType;\n\nimport lombok.Getter;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.OptionalInt;\n\n@Getter\npublic class GroupFile {\n\n    private final Map<Integer, String> groups = new LinkedHashMap<>();\n\n    public static GroupFile parse(ShellControl sc) throws Exception {\n        var f = new GroupFile();\n        f.loadGroups(sc);\n        return f;\n    }\n\n    public OptionalInt getGidForGroupIfPresent(String name) {\n        var found = groups.entrySet().stream()\n                .filter(e -> e.getValue().equals(name))\n                .findFirst()\n                .map(e -> e.getKey())\n                .orElse(null);\n        return found != null ? OptionalInt.of(found) : OptionalInt.empty();\n    }\n\n    public int getGidForGroup(String name) {\n        return getGidForGroupIfPresent(name).orElse(0);\n    }\n\n    private void loadGroups(ShellControl sc) throws Exception {\n        if (sc.getOsType() == OsType.WINDOWS || sc.getOsType() == OsType.MACOS) {\n            return;\n        }\n\n        var lines = sc.command(CommandBuilder.of().add(\"cat\").addFile(\"/etc/group\"))\n                .sensitive()\n                .readStdoutIfPossible()\n                .orElse(\"\");\n        lines.lines().forEach(s -> {\n            var split = s.split(\":\");\n            try {\n                groups.putIfAbsent(Integer.parseInt(split[2]), split[0]);\n            } catch (Exception ignored) {\n            }\n        });\n\n        if (groups.isEmpty()) {\n            groups.put(0, \"root\");\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/HostHelper.java",
    "content": "package io.xpipe.app.util;\n\nimport java.io.IOException;\nimport java.net.Inet4Address;\nimport java.net.ServerSocket;\n\npublic class HostHelper {\n\n    private static int portCounter = 0;\n\n    public static int randomPort() {\n        var p = 40000 + portCounter;\n        portCounter = portCounter + 1 % 1000;\n        return p;\n    }\n\n    public static int findRandomOpenPortOnAllLocalInterfaces() {\n        try (ServerSocket socket = new ServerSocket(0)) {\n            return socket.getLocalPort();\n        } catch (IOException e) {\n            return randomPort();\n        }\n    }\n\n    public static boolean isLocalNetworkAddress(String host) {\n        Inet4Address inet4Address;\n        try {\n            inet4Address = Inet4Address.ofLiteral(host);\n        } catch (IllegalArgumentException ignored) {\n            return false;\n        }\n\n        return inet4Address.isSiteLocalAddress();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/HttpHelper.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.prefs.AppPrefs;\n\nimport lombok.SneakyThrows;\n\nimport java.net.http.HttpClient;\nimport java.security.SecureRandom;\nimport java.security.cert.X509Certificate;\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.TrustManager;\nimport javax.net.ssl.X509TrustManager;\n\npublic class HttpHelper {\n\n    @SneakyThrows\n    public static HttpClient client() {\n        var builder = HttpClient.newBuilder();\n        builder.version(HttpClient.Version.HTTP_1_1);\n        builder.followRedirects(HttpClient.Redirect.NORMAL);\n        if (AppPrefs.get() != null && AppPrefs.get().disableHttpsTlsCheck().getValue()) {\n            var sslContext = SSLContext.getInstance(\"TLS\");\n            var trustManager = new X509TrustManager() {\n                @Override\n                public void checkClientTrusted(X509Certificate[] certs, String authType) {}\n\n                @Override\n                public void checkServerTrusted(X509Certificate[] certs, String authType) {}\n\n                @Override\n                public X509Certificate[] getAcceptedIssuers() {\n                    return new X509Certificate[] {};\n                }\n            };\n            sslContext.init(null, new TrustManager[] {trustManager}, new SecureRandom());\n            builder.sslContext(sslContext);\n        }\n        return builder.build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/HumanReadableFormat.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.core.AppI18n;\n\nimport java.text.CharacterIterator;\nimport java.text.StringCharacterIterator;\nimport java.time.Duration;\nimport java.time.LocalDateTime;\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Objects;\nimport java.util.regex.Pattern;\n\npublic final class HumanReadableFormat {\n\n    public static final DateTimeFormatter DAY_MONTH_YEAR = DateTimeFormatter.ofPattern(\"d LLL yyyy\");\n    public static final DateTimeFormatter DAY_MONTH = DateTimeFormatter.ofPattern(\"d LLL\");\n    public static final DateTimeFormatter HOUR_MINUTE = DateTimeFormatter.ofPattern(\"HH:mm\");\n\n    public static String byteCount(long bytes) {\n        var b = 1024;\n        if (-b < bytes && bytes < b) {\n            return bytes + \" B\";\n        }\n        CharacterIterator ci = new StringCharacterIterator(\"kMGTPE\");\n        var mb = b * b;\n        while (bytes <= -mb || bytes >= mb) {\n            bytes /= b;\n            ci.next();\n        }\n        var f = \"%.1f\";\n        var r = String.format(f + \" %cB\", bytes / (double) b, ci.current());\n        if (r.endsWith(\".0\")) {\n            r = r.substring(0, r.length() - 2);\n        }\n        return r;\n    }\n\n    public static String progressByteCount(long bytes) {\n        var b = 1024;\n        if (-b < bytes && bytes < b) {\n            return bytes + \" B\";\n        }\n        CharacterIterator ci = new StringCharacterIterator(\"kMGTPE\");\n        var mb = b * b;\n        while (bytes <= -mb || bytes >= mb) {\n            bytes /= b;\n            ci.next();\n        }\n\n        var f = ci.getIndex() >= 2 ? \"%.3f\" : \"%.1f\";\n        var r = String.format(f + \" %cB\", bytes / (double) b, ci.current());\n        if (r.endsWith(\".0\")) {\n            r = r.substring(0, r.length() - 2);\n        }\n        return r;\n    }\n\n    public static String date(LocalDateTime x) {\n        Objects.requireNonNull(x);\n        var now = LocalDateTime.now(ZoneId.systemDefault());\n\n        // not this year\n        if (x.getYear() != now.getYear()) {\n            return DAY_MONTH_YEAR\n                    .withLocale(AppI18n.activeLanguage().getValue().getLocale())\n                    .format(x);\n        }\n\n        var time = HOUR_MINUTE\n                .withLocale(AppI18n.activeLanguage().getValue().getLocale())\n                .format(x);\n        var date = DAY_MONTH\n                .withLocale(AppI18n.activeLanguage().getValue().getLocale())\n                .format(x);\n        return date + \" \" + time;\n    }\n\n    public static String transferSpeed(long bps) {\n        var s = progressByteCount(bps);\n        return s + \"/s\";\n    }\n\n    public static String duration(Duration duration) {\n        var s = duration.toString()\n                .substring(2)\n                .replaceAll(\"(\\\\d[HMS])(?!$)\", \"$1 \")\n                .replaceAll(\"\\\\.\\\\d+\", \"\")\n                .toLowerCase();\n        var padded = Pattern.compile(\"\\\\d+\").matcher(s).replaceAll(matchResult -> {\n            var r = matchResult.group();\n            if (r.length() == 1) {\n                return \"0\" + r;\n            } else {\n                return r;\n            }\n        });\n        return padded;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/Hyperlinks.java",
    "content": "package io.xpipe.app.util;\n\npublic class Hyperlinks {\n\n    public static final String GITHUB = \"https://github.com/xpipe-io/xpipe\";\n    public static final String GITHUB_PTB = \"https://github.com/xpipe-io/xpipe-ptb\";\n    public static final String GITHUB_LATEST = \"https://github.com/xpipe-io/xpipe/releases/latest\";\n    public static final String TRANSLATE = \"https://github.com/xpipe-io/xpipe/tree/master/lang\";\n    public static final String DISCORD = \"https://discord.gg/8y89vS8cRb\";\n    public static final String REDDIT = \"https://reddit.com/r/xpipe\";\n    public static final String GITHUB_WEBTOP = \"https://github.com/xpipe-io/xpipe-webtop\";\n\n    public static void open(String uri) {\n        DesktopHelper.openBrowser(uri);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/IniFile.java",
    "content": "package io.xpipe.app.util;\n\nimport lombok.Value;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n@Value\npublic class IniFile {\n\n    private static final Pattern SECTION_FORMAT = Pattern.compile(\"\\\\s*\\\\[([^]]*)\\\\]\\\\s*\");\n    private static final Pattern VALUE_FORMAT = Pattern.compile(\"\\\\s*([^=]*)=(.*)\");\n\n    Map<String, Map<String, String>> entries;\n\n    public static IniFile load(Path path) throws IOException {\n        Map<String, Map<String, String>> entries = new HashMap<>();\n        try (BufferedReader br = Files.newBufferedReader(path)) {\n            String line;\n            String section = null;\n            while ((line = br.readLine()) != null) {\n                Matcher m = SECTION_FORMAT.matcher(line);\n                if (m.matches()) {\n                    section = m.group(1).strip();\n                } else if (section != null) {\n                    m = VALUE_FORMAT.matcher(line);\n                    if (m.matches()) {\n                        String key = m.group(1).strip();\n                        String value = m.group(2).strip();\n                        Map<String, String> kv = entries.computeIfAbsent(section, k -> new HashMap<>());\n                        kv.put(key, value);\n                    }\n                }\n            }\n        }\n        return new IniFile(entries);\n    }\n\n    public String get(String section, String key) {\n        Map<String, String> kv = entries.get(section);\n        if (kv == null) {\n            return null;\n        }\n        return kv.get(key);\n    }\n\n    public String getOrDefault(String section, String key, String defaultvalue) {\n        Map<String, String> kv = entries.get(section);\n        if (kv == null) {\n            return defaultvalue;\n        }\n        return kv.get(key);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/LicenseProvider.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.ext.ExtensionException;\nimport io.xpipe.core.ModuleLayerLoader;\n\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.ServiceLoader;\n\npublic abstract class LicenseProvider {\n\n    private static LicenseProvider INSTANCE = null;\n\n    public static LicenseProvider get() {\n        return INSTANCE;\n    }\n\n    public abstract void updateDate(String date);\n\n    public abstract String formatExceptionMessage(String name, boolean plural, LicensedFeature licensedFeature);\n\n    public abstract String getLicenseId();\n\n    public abstract ObservableValue<String> licenseTitle();\n\n    public abstract LicensedFeature getFeature(String id);\n\n    public abstract LicensedFeature checkOsName(String name);\n\n    public abstract void checkOsNameOrThrow(String s);\n\n    public abstract void showLicenseAlert(LicenseRequiredException ex);\n\n    public abstract void init();\n\n    public abstract BaseRegionBuilder<?, ?> overviewPage();\n\n    public abstract boolean hasPaidLicense();\n\n    public abstract boolean shouldReportError();\n\n    public static class Loader implements ModuleLayerLoader {\n\n        @Override\n        public void init(ModuleLayer layer) {\n            INSTANCE = ServiceLoader.load(layer, LicenseProvider.class).stream()\n                    .map(ServiceLoader.Provider::get)\n                    .findFirst()\n                    .orElseThrow(() -> ExtensionException.corrupt(\"Missing license provider\"));\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/LicenseRequiredException.java",
    "content": "package io.xpipe.app.util;\n\nimport lombok.Getter;\n\n@Getter\npublic class LicenseRequiredException extends RuntimeException {\n\n    private final LicensedFeature feature;\n\n    public LicenseRequiredException(LicensedFeature feature) {\n        super(LicenseProvider.get().formatExceptionMessage(feature.getDisplayName(), feature.isPlural(), feature));\n        this.feature = feature;\n    }\n\n    public LicenseRequiredException(String featureName, boolean plural, LicensedFeature feature) {\n        super(LicenseProvider.get().formatExceptionMessage(featureName, plural, feature));\n        this.feature = feature;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/LicensedFeature.java",
    "content": "package io.xpipe.app.util;\n\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.Optional;\n\npublic interface LicensedFeature {\n\n    Optional<String> getDescriptionSuffix();\n\n    ObservableValue<String> suffixObservable(ObservableValue<String> s);\n\n    default String suffix(String s) {\n        return getDescriptionSuffix().map(suffix -> s + \" (\" + suffix + \")\").orElse(s);\n    }\n\n    String getId();\n\n    String getDisplayName();\n\n    boolean isPlural();\n\n    boolean isSupported();\n\n    boolean supportsFeatureInPreview();\n\n    boolean recentlySupportedFeatureInPreview();\n\n    void throwIfUnsupported() throws LicenseRequiredException;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/LocalExec.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.issue.TrackEvent;\n\nimport java.nio.charset.StandardCharsets;\nimport java.util.Optional;\n\npublic class LocalExec {\n\n    public static void executeAsync(String... command) {\n        try {\n            TrackEvent.withTrace(\"Running local command\")\n                    .tag(\"command\", String.join(\" \", command))\n                    .handle();\n\n            var pb = new ProcessBuilder(command)\n                    .redirectOutput(ProcessBuilder.Redirect.DISCARD)\n                    .redirectError(ProcessBuilder.Redirect.DISCARD);\n            pb.directory(AppSystemInfo.ofCurrent().getUserHome().toFile());\n\n            var env = pb.environment();\n            // https://bugs.openjdk.org/browse/JDK-8360500\n            env.remove(\"_JPACKAGE_LAUNCHER\");\n\n            pb.start();\n        } catch (Exception ex) {\n            TrackEvent.withTrace(\"Local command finished\")\n                    .tag(\"command\", String.join(\" \", command))\n                    .tag(\"error\", ex.toString())\n                    .handle();\n        }\n    }\n\n    public static Optional<String> readStdoutIfPossible(String... command) {\n        try {\n            TrackEvent.withTrace(\"Running local command\")\n                    .tag(\"command\", String.join(\" \", command))\n                    .handle();\n\n            var pb = new ProcessBuilder(command).redirectError(ProcessBuilder.Redirect.DISCARD);\n            pb.directory(AppSystemInfo.ofCurrent().getUserHome().toFile());\n\n            var env = pb.environment();\n            // https://bugs.openjdk.org/browse/JDK-8360500\n            env.remove(\"_JPACKAGE_LAUNCHER\");\n\n            var process = pb.start();\n            var out = process.getInputStream().readAllBytes();\n            process.waitFor();\n            if (process.exitValue() != 0) {\n                return Optional.empty();\n            } else {\n                var s = new String(out, StandardCharsets.UTF_8).strip();\n                TrackEvent.withTrace(\"Local command finished\")\n                        .tag(\"command\", String.join(\" \", command))\n                        .tag(\"stdout\", s)\n                        .handle();\n                return Optional.of(s);\n            }\n        } catch (Exception ex) {\n            TrackEvent.withTrace(\"Local command finished\")\n                    .tag(\"command\", String.join(\" \", command))\n                    .tag(\"error\", ex.toString())\n                    .handle();\n            return Optional.empty();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/LocalFileTracker.java",
    "content": "package io.xpipe.app.util;\n\nimport org.apache.commons.io.FileUtils;\n\nimport java.nio.file.Path;\nimport java.time.Duration;\nimport java.util.HashSet;\nimport java.util.Set;\n\npublic class LocalFileTracker {\n\n    private static final Set<Path> localFiles = new HashSet<>();\n\n    public static void deleteOnExit(Path file) {\n        synchronized (localFiles) {\n            localFiles.add(file);\n        }\n\n        GlobalTimer.scheduleUntil(Duration.ofHours(1), false, () -> {\n            synchronized (localFiles) {\n                var copy = new HashSet<>(localFiles);\n                GlobalTimer.delay(() -> {\n                    for (Path localFile : copy) {\n                        FileUtils.deleteQuietly(localFile.toFile());\n                    }\n                }, Duration.ofMinutes(1));\n            }\n            return false;\n        });\n    }\n\n    public static void reset() {\n        synchronized (localFiles) {\n            for (Path localFile : localFiles) {\n                FileUtils.deleteQuietly(localFile.toFile());\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/ModuleAccess.java",
    "content": "package io.xpipe.app.util;\n\nimport java.lang.reflect.Method;\n\npublic class ModuleAccess {\n\n    public static void exportAndOpen(Module source, String pkg, Module target) throws Exception {\n        if (source.isExported(pkg, target) && source.isOpen(pkg, target)) {\n            return;\n        }\n\n        Method getDeclaredFields0 = Class.class.getDeclaredMethod(\"getDeclaredMethods0\", boolean.class);\n        getDeclaredFields0.setAccessible(true);\n        Method[] fields = (Method[]) getDeclaredFields0.invoke(Module.class, false);\n        Method modifiers = null;\n        for (Method each : fields) {\n            if (\"implAddExportsOrOpens\".equals(each.getName())) {\n                modifiers = each;\n                break;\n            }\n        }\n\n        // Maybe an unknown JDK version?\n        if (modifiers == null) {\n            return;\n        }\n\n        modifiers.setAccessible(true);\n        modifiers.invoke(source, pkg, target, false, true);\n        modifiers.invoke(source, pkg, target, true, true);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/ObservableSubscriber.java",
    "content": "package io.xpipe.app.util;\n\nimport javafx.beans.InvalidationListener;\nimport javafx.beans.Observable;\nimport javafx.beans.property.IntegerProperty;\nimport javafx.beans.property.SimpleIntegerProperty;\n\npublic class ObservableSubscriber implements Observable {\n\n    private final IntegerProperty property = new SimpleIntegerProperty();\n\n    public void trigger() {\n        property.set(property.get() + 1);\n        property.getValue();\n    }\n\n    @Override\n    public void addListener(InvalidationListener listener) {\n        property.addListener(listener);\n    }\n\n    @Override\n    public void removeListener(InvalidationListener listener) {\n        property.removeListener(listener);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/PasswdFile.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.core.OsType;\n\nimport lombok.Getter;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.OptionalInt;\n\n@Getter\npublic class PasswdFile {\n\n    private final Map<Integer, String> users = new LinkedHashMap<>();\n\n    public static PasswdFile parse(ShellControl sc) throws Exception {\n        var passwdFile = new PasswdFile();\n        passwdFile.loadUsers(sc);\n        return passwdFile;\n    }\n\n    public OptionalInt getUidForUserIfPresent(String name) {\n        var found = users.entrySet().stream()\n                .filter(e -> e.getValue().equals(name))\n                .findFirst()\n                .map(e -> e.getKey())\n                .orElse(null);\n        return found != null ? OptionalInt.of(found) : OptionalInt.empty();\n    }\n\n    public int getUidForUser(String name) {\n        return getUidForUserIfPresent(name).orElse(0);\n    }\n\n    private void loadUsers(ShellControl sc) throws Exception {\n        if (sc.getOsType() == OsType.WINDOWS || sc.getOsType() == OsType.MACOS) {\n            return;\n        }\n\n        var lines = sc.command(CommandBuilder.of().add(\"cat\").addFile(\"/etc/passwd\"))\n                .sensitive()\n                .readStdoutIfPossible()\n                .orElse(\"\");\n        lines.lines().forEach(s -> {\n            var split = s.split(\":\");\n            try {\n                users.putIfAbsent(Integer.parseInt(split[2]), split[0]);\n            } catch (Exception ignored) {\n            }\n        });\n\n        if (users.isEmpty()) {\n            users.put(0, \"root\");\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/RdpConfig.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.StreamCharset;\n\nimport lombok.Value;\n\nimport java.io.BufferedInputStream;\nimport java.io.BufferedReader;\nimport java.nio.file.Files;\nimport java.nio.file.NoSuchFileException;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\n\n@Value\npublic class RdpConfig {\n\n    Map<String, TypedValue> content;\n\n    public static RdpConfig parseFile(FilePath file) throws Exception {\n        try (var in = new BufferedReader(StreamCharset.detectedReader(\n                new BufferedInputStream(Files.newInputStream(Path.of(file.toString())))))) {\n            var content = in.lines().collect(Collectors.joining(\"\\n\"));\n            return parseContent(content);\n        } catch (NoSuchFileException e) {\n            // Users deleting files is expected\n            ErrorEventFactory.expected(e);\n            throw e;\n        }\n    }\n\n    public static RdpConfig parseContent(String content) {\n        var map = new LinkedHashMap<String, TypedValue>();\n        if (content == null) {\n            return new RdpConfig(map);\n        }\n\n        content.lines().forEach(s -> {\n            var split = s.split(\":\", 3);\n            if (split.length < 2) {\n                return;\n            }\n\n            if (split.length == 2) {\n                map.put(split[0].strip(), new RdpConfig.TypedValue(\"s\", split[1].strip()));\n            }\n\n            if (split.length == 3) {\n                map.put(split[0].strip(), new RdpConfig.TypedValue(split[1].strip(), split[2].strip()));\n            }\n        });\n        return new RdpConfig(map);\n    }\n\n    public RdpConfig overlay(Map<String, TypedValue> override) {\n        var newMap = new LinkedHashMap<>(content);\n        newMap.putAll(override);\n        return new RdpConfig(newMap);\n    }\n\n    public String toString() {\n        return content.entrySet().stream()\n                .map(e -> {\n                    return e.getKey() + \":\" + e.getValue().getType() + \":\"\n                            + e.getValue().getValue();\n                })\n                .collect(Collectors.joining(\"\\n\"));\n    }\n\n    public Optional<TypedValue> get(String key) {\n        return Optional.ofNullable(content.get(key));\n    }\n\n    @Value\n    public static class TypedValue {\n        String type;\n        String value;\n\n        public static TypedValue string(String value) {\n            return new TypedValue(\"s\", value);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/Rect.java",
    "content": "package io.xpipe.app.util;\n\nimport lombok.Value;\n\n@Value\npublic class Rect {\n    int x, y;\n    int w, h;\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/RemminaHelper.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.core.AppLocalTemp;\nimport io.xpipe.app.core.window.AppMainWindow;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.OsFileSystem;\nimport io.xpipe.app.rdp.RdpLaunchConfig;\nimport io.xpipe.app.vnc.VncLaunchConfig;\nimport io.xpipe.core.SecretValue;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Arrays;\nimport java.util.Base64;\nimport java.util.Optional;\nimport javax.crypto.Cipher;\nimport javax.crypto.spec.IvParameterSpec;\nimport javax.crypto.spec.SecretKeySpec;\n\npublic class RemminaHelper {\n\n    public static Optional<String> encryptPassword(SecretValue password) throws Exception {\n        if (password == null) {\n            return Optional.empty();\n        }\n\n        try (var sc = LocalShell.getShell().start()) {\n            var prefSecretBase64 = sc.command(\"sed -n 's/^secret=//p' ~/.config/remmina/remmina.pref\")\n                    .sensitive()\n                    .readStdoutIfPossible();\n            if (prefSecretBase64.isEmpty()) {\n                return Optional.empty();\n            }\n\n            var rawPassword = password.getSecretRaw();\n            var toPad = 8 - (rawPassword.length % 8);\n            var paddedPassword = new byte[rawPassword.length + toPad];\n            System.arraycopy(rawPassword, 0, paddedPassword, 0, rawPassword.length);\n\n            var prefSecret = Base64.getDecoder().decode(prefSecretBase64.get());\n            var key = Arrays.copyOfRange(prefSecret, 0, 24);\n            var iv = Arrays.copyOfRange(prefSecret, 24, prefSecret.length);\n\n            var cipher = Cipher.getInstance(\"DESede/CBC/Nopadding\");\n            var keySpec = new SecretKeySpec(key, \"DESede\");\n            var ivspec = new IvParameterSpec(iv);\n            cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivspec);\n            byte[] encryptedText = cipher.doFinal(paddedPassword);\n            var base64Encrypted = Base64.getEncoder().encodeToString(encryptedText);\n            return Optional.ofNullable(base64Encrypted);\n        }\n    }\n\n    public static Path writeRemminaRdpConfigFile(RdpLaunchConfig configuration, String password) throws Exception {\n        var user = configuration.getConfig().get(\"username\").orElseThrow().getValue();\n        var domain = user.contains(\"\\\\\") ? user.split(\"\\\\\\\\\")[0] : null;\n        if (domain != null) {\n            user = user.split(\"\\\\\\\\\")[1];\n        }\n\n        var w = Math.round(AppMainWindow.get().getStage().getWidth());\n        // Remmina's height calculation does not take the titlebar into account\n        var h = Math.round(AppMainWindow.get().getStage().getHeight()) - 38;\n        // Use window size as remmina's autosize is broken\n        var maximize = \"0\"; // AppMainWindow.get().getStage().isMaximized() ? \"1\" : \"0\";\n\n        var name = OsFileSystem.ofLocal().makeFileSystemCompatible(configuration.getTitle());\n        var file = AppLocalTemp.getLocalTempDataDirectory(\"remmina\").resolve(\"xpipe-\" + name + \".remmina\");\n        var string = \"\"\"\n                     [remmina]\n                     protocol=RDP\n                     name=%s\n                     username=%s\n                     domain=%s\n                     server=%s\n                     password=%s\n                     cert_ignore=1\n                     scale=2\n                     window_width=%s\n                     window_height=%s\n                     window_maximize=%s\n                     \"\"\".formatted(\n                configuration.getTitle(),\n                user,\n                domain != null ? domain : \"\",\n                configuration.getConfig().get(\"full address\").orElseThrow().getValue(),\n                password != null ? password : \"\",\n                w,\n                h,\n                maximize);\n        Files.createDirectories(file.getParent());\n        Files.writeString(file, string);\n        return file;\n    }\n\n    public static Path writeRemminaVncConfigFile(VncLaunchConfig configuration, String password) throws Exception {\n        var name = OsFileSystem.ofLocal().makeFileSystemCompatible(configuration.getTitle());\n        var file = AppLocalTemp.getLocalTempDataDirectory(\"remmina\").resolve(\"xpipe-\" + name + \".remmina\");\n\n        var w = Math.round(AppMainWindow.get().getStage().getWidth());\n        // Remmina's height calculation does not take the titlebar into account\n        var h = Math.round(AppMainWindow.get().getStage().getHeight()) - 38;\n        // Use window size as remmina's autosize is broken\n        var maximize = \"0\"; // AppMainWindow.get().getStage().isMaximized() ? \"1\" : \"0\";\n\n        var string = \"\"\"\n                     [remmina]\n                     protocol=VNC\n                     name=%s\n                     username=%s\n                     server=%s\n                     password=%s\n                     colordepth=32\n                     window_width=%s\n                     window_height=%s\n                     window_maximize=%s\n                     \"\"\".formatted(\n                        configuration.getTitle(),\n                        configuration.retrieveUsername().orElse(\"\"),\n                        configuration.getHost() + \":\" + configuration.getPort(),\n                        password != null ? password : \"\",\n                        w,\n                        h,\n                        maximize);\n        Files.createDirectories(file.getParent());\n        Files.writeString(file, string);\n        return file;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/ScanDialog.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.comp.base.ModalButton;\nimport io.xpipe.app.comp.base.ModalOverlay;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport java.util.List;\n\npublic class ScanDialog {\n\n    public static void showSingleAsync(DataStoreEntry entry) {\n        var showForCon = entry == null || entry.getStore() instanceof ShellStore;\n        if (showForCon) {\n            showSingle(entry, ScanDialogAction.shellScanAction());\n        }\n    }\n\n    private static void showSingle(DataStoreEntry initialStore, ScanDialogAction action) {\n        var comp = new ScanSingleDialogComp(initialStore != null ? initialStore.ref() : null, action);\n        var modal = ModalOverlay.of(\"scanAlertTitle\", comp);\n        var queueEntry = new AppLayoutModel.QueueEntry(\n                AppI18n.observable(\"scanConnections\"), new LabelGraphic.IconGraphic(\"mdi2l-layers-plus\"), () -> false);\n        var button = new ModalButton(\n                \"ok\",\n                () -> {\n                    AppLayoutModel.get().getQueueEntries().add(queueEntry);\n                    ThreadHelper.runAsync(() -> {\n                        comp.finish();\n                        AppLayoutModel.get().getQueueEntries().remove(queueEntry);\n                    });\n                },\n                true,\n                true);\n        button.augment(r -> r.disableProperty().bind(PlatformThread.sync(comp.getBusy())));\n        modal.addButton(button);\n        modal.show();\n    }\n\n    public static void showMulti(List<DataStoreEntryRef<ShellStore>> entries, ScanDialogAction action) {\n        var comp = new ScanMultiDialogComp(entries, action);\n        var modal = ModalOverlay.of(\"scanAlertTitle\", comp);\n        var queueEntry = new AppLayoutModel.QueueEntry(\n                AppI18n.observable(\"scanConnections\"), new LabelGraphic.IconGraphic(\"mdi2l-layers-plus\"), () -> false);\n        var button = new ModalButton(\n                \"ok\",\n                () -> {\n                    AppLayoutModel.get().getQueueEntries().add(queueEntry);\n                    ThreadHelper.runAsync(() -> {\n                        comp.finish();\n                        AppLayoutModel.get().getQueueEntries().remove(queueEntry);\n                    });\n                },\n                true,\n                true);\n        button.augment(r -> r.disableProperty().bind(PlatformThread.sync(comp.getBusy())));\n        modal.addButton(button);\n        modal.show();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/ScanDialogAction.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.ext.ScanProvider;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.ShellTtyState;\nimport io.xpipe.app.storage.DataStoreEntry;\n\nimport javafx.collections.ObservableList;\n\npublic interface ScanDialogAction {\n\n    static ScanDialogAction shellScanAction() {\n        var action = new ScanDialogAction() {\n\n            @Override\n            public boolean scan(\n                    ObservableList<ScanProvider.ScanOpportunity> all,\n                    ObservableList<ScanProvider.ScanOpportunity> selected,\n                    DataStoreEntry entry,\n                    ShellControl sc) {\n                var fullShell = sc.canHaveSubshells()\n                        && sc.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()\n                        && sc.getTtyState() == ShellTtyState.NONE;\n                var providers = ScanProvider.getAll();\n                for (ScanProvider scanProvider : providers) {\n                    if (!fullShell && scanProvider.requiresFullShell()) {\n                        continue;\n                    }\n\n                    try {\n                        // Previous scan operation could have exited the shell\n                        sc.start();\n                        ScanProvider.ScanOpportunity operation = scanProvider.create(entry, sc);\n                        if (operation != null) {\n                            if (!operation.isDisabled()) {\n                                selected.removeIf(\n                                        o -> o.getProvider().equals(operation.getProvider()) && o.isDisabled());\n                                all.removeIf(o -> o.getProvider().equals(operation.getProvider()) && o.isDisabled());\n                            }\n                            if (!operation.isDisabled()\n                                    && selected.stream()\n                                            .noneMatch(o -> o.getProvider().equals(operation.getProvider()))) {\n                                selected.add(operation);\n                            }\n                            if (!all.contains(operation)\n                                    && all.stream()\n                                            .noneMatch(o -> o.getProvider().equals(operation.getProvider()))) {\n                                all.add(operation);\n                            }\n                        }\n                    } catch (Exception ex) {\n                        ErrorEventFactory.fromThrowable(ex).handle();\n                    }\n                }\n                return true;\n            }\n        };\n        return action;\n    }\n\n    boolean scan(\n            ObservableList<ScanProvider.ScanOpportunity> all,\n            ObservableList<ScanProvider.ScanOpportunity> selected,\n            DataStoreEntry entry,\n            ShellControl shellControl);\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/ScanDialogBase.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.LabelComp;\nimport io.xpipe.app.comp.base.ListSelectorComp;\nimport io.xpipe.app.comp.base.LoadingOverlayComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.ScanProvider;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.*;\nimport javafx.collections.FXCollections;\nimport javafx.collections.ListChangeListener;\nimport javafx.collections.ObservableList;\nimport javafx.scene.control.Button;\nimport javafx.scene.layout.StackPane;\nimport javafx.scene.layout.VBox;\n\nimport lombok.Getter;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.ArrayList;\nimport java.util.function.Function;\n\nimport static javafx.scene.layout.Priority.ALWAYS;\n\npublic class ScanDialogBase {\n\n    private final boolean expand;\n    private final Runnable closeAction;\n    private final ScanDialogAction action;\n    private final ObservableList<DataStoreEntryRef<ShellStore>> entries;\n    private final boolean showButton;\n    private final ObservableList<ScanProvider.ScanOpportunity> available =\n            FXCollections.synchronizedObservableList(FXCollections.observableArrayList());\n    private final ListProperty<ScanProvider.ScanOpportunity> selected =\n            new SimpleListProperty<>(FXCollections.synchronizedObservableList(FXCollections.observableArrayList()));\n\n    @Getter\n    private final BooleanProperty busy = new SimpleBooleanProperty();\n\n    public ScanDialogBase(\n            boolean expand,\n            Runnable closeAction,\n            ScanDialogAction action,\n            ObservableList<DataStoreEntryRef<ShellStore>> entries,\n            boolean showButton) {\n        this.expand = expand;\n        this.closeAction = closeAction;\n        this.action = action;\n        this.entries = entries;\n        this.showButton = showButton;\n    }\n\n    public void finish() throws Exception {\n        if (entries.isEmpty()) {\n            closeAction.run();\n            return;\n        }\n\n        BooleanScope.executeExclusive(busy, () -> {\n            for (var entry : entries) {\n                if (expand) {\n                    entry.get().setExpanded(true);\n                }\n                var copy = new ArrayList<>(selected);\n                for (var a : copy) {\n                    // If the user decided to remove the selected entry\n                    // while the scan is running, just return instantly\n                    if (!DataStorage.get().getStoreEntriesSet().contains(entry.get())) {\n                        return;\n                    }\n\n                    // Previous scan operation could have exited the shell\n                    var sc = entry.getStore().getOrStartSession();\n\n                    // Multi-selection compat check\n                    if (entries.size() > 1) {\n                        var supported = a.getProvider().create(entry.get(), sc);\n                        if (supported == null || supported.isDisabled()) {\n                            continue;\n                        }\n                    }\n\n                    try {\n                        a.getProvider().scan(entry.get(), sc);\n                    } catch (Throwable ex) {\n                        ErrorEventFactory.fromThrowable(ex).handle();\n                    }\n                }\n            }\n        });\n        closeAction.run();\n    }\n\n    public void reset() {\n        available.clear();\n        selected.clear();\n    }\n\n    public void onUpdate() {\n        available.clear();\n        selected.clear();\n\n        if (entries.isEmpty()) {\n            return;\n        }\n\n        ThreadHelper.runFailableAsync(() -> {\n            BooleanScope.executeExclusive(busy, () -> {\n                for (var entry : entries) {\n                    boolean r;\n                    try {\n                        var sc = entry.getStore().getOrStartSession();\n                        r = action.scan(available, selected, entry.get(), sc);\n                    } catch (Throwable t) {\n                        closeAction.run();\n                        throw t;\n                    }\n                    if (!r) {\n                        closeAction.run();\n                        entry.getStore().stopSessionIfNeeded();\n                    }\n                }\n            });\n        });\n    }\n\n    public BaseRegionBuilder<?, ?> createComp() {\n        StackPane stackPane = new StackPane();\n        stackPane.getStyleClass().add(\"scan-list\");\n        VBox.setVgrow(stackPane, ALWAYS);\n\n        if (!showButton) {\n            var emptyLabel = new LabelComp(AppI18n.observable(\"noScanPossible\"))\n                    .visible(busy.not().and(Bindings.isEmpty(available)))\n                    .build();\n            stackPane.getChildren().add(emptyLabel);\n        }\n\n        Function<ScanProvider.ScanOpportunity, String> nameFunc = (ScanProvider.ScanOpportunity s) -> {\n            var n = s.getName().getValue();\n            if (s.getLicensedFeatureId() == null || s.isDisabled()) {\n                return n;\n            }\n\n            var suffix = LicenseProvider.get().getFeature(s.getLicensedFeatureId());\n            return n + suffix.getDescriptionSuffix().map(d -> \" (\" + d + \")\").orElse(\"\");\n        };\n        var r = new ListSelectorComp<>(\n                        available,\n                        nameFunc,\n                        so -> null,\n                        selected,\n                        scanOperation -> scanOperation.isDisabled(),\n                        () -> available.size() > 3)\n                .build();\n        stackPane.getChildren().add(r);\n\n        if (showButton) {\n            var button = new Button();\n            button.textProperty().bind(AppI18n.observable(\"start\"));\n            button.setGraphic(new FontIcon(\"mdi2p-play\"));\n            button.setOnAction(e -> {\n                onUpdate();\n            });\n\n            var show = PlatformThread.sync(busy.not().and(Bindings.isEmpty(available)));\n            button.visibleProperty().bind(show);\n            button.managedProperty().bind(show);\n\n            button.disableProperty().bind(Bindings.isEmpty(entries));\n\n            stackPane.getChildren().add(button);\n        } else {\n            onUpdate();\n            entries.addListener((ListChangeListener<? super DataStoreEntryRef<ShellStore>>) c -> onUpdate());\n        }\n\n        var comp = new LoadingOverlayComp(RegionBuilder.of(() -> stackPane), busy, true).vgrow();\n        return comp;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/ScanMultiDialogComp.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.comp.base.ModalOverlayContentComp;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.property.BooleanProperty;\nimport javafx.collections.FXCollections;\nimport javafx.collections.ObservableList;\nimport javafx.scene.layout.Region;\nimport javafx.scene.layout.VBox;\n\nimport java.util.List;\n\nimport static javafx.scene.layout.Priority.ALWAYS;\n\nclass ScanMultiDialogComp extends ModalOverlayContentComp {\n\n    private final ScanDialogBase base;\n\n    ScanMultiDialogComp(List<DataStoreEntryRef<ShellStore>> entries, ScanDialogAction action) {\n        ObservableList<DataStoreEntryRef<ShellStore>> list = FXCollections.observableArrayList(entries);\n        this.base = new ScanDialogBase(\n                true,\n                () -> {\n                    var modal = getModalOverlay();\n                    if (modal != null) {\n                        modal.close();\n                    }\n                },\n                action,\n                list,\n                false);\n    }\n\n    void finish() {\n        try {\n            base.finish();\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n        }\n    }\n\n    BooleanProperty getBusy() {\n        return base.getBusy();\n    }\n\n    @Override\n    protected Region createSimple() {\n        var list = base.createComp();\n        var b = new OptionsBuilder()\n                .name(\"scanAlertHeader\")\n                .description(\"scanAlertHeaderDescription\")\n                .addComp(list.vgrow())\n                .buildComp()\n                .prefWidth(500)\n                .prefHeight(680)\n                .apply(struc -> {\n                    VBox.setVgrow(struc.getChildren().getFirst(), ALWAYS);\n                });\n        return b.build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/ScanSingleDialogComp.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.comp.base.ModalOverlayContentComp;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.hub.comp.StoreChoiceComp;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.property.*;\nimport javafx.collections.FXCollections;\nimport javafx.collections.ObservableList;\nimport javafx.scene.layout.Region;\n\nclass ScanSingleDialogComp extends ModalOverlayContentComp {\n\n    private final DataStoreEntryRef<ShellStore> initialStore;\n    private final ObjectProperty<DataStoreEntryRef<ShellStore>> entry;\n    private final ScanDialogBase base;\n\n    @SuppressWarnings(\"unchecked\")\n    ScanSingleDialogComp(DataStoreEntryRef<ShellStore> entry, ScanDialogAction action) {\n        this.initialStore = entry;\n        this.entry = new SimpleObjectProperty<>(entry);\n\n        ObservableList<DataStoreEntryRef<ShellStore>> list = FXCollections.observableArrayList();\n        this.entry.subscribe(v -> {\n            if (v != null) {\n                list.setAll(v);\n            } else {\n                list.clear();\n            }\n        });\n        this.base = new ScanDialogBase(\n                true,\n                () -> {\n                    var modal = getModalOverlay();\n                    if (initialStore != null && modal != null) {\n                        modal.close();\n                    }\n                },\n                action,\n                list,\n                false);\n    }\n\n    void finish() {\n        try {\n            base.finish();\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n        }\n    }\n\n    BooleanProperty getBusy() {\n        return base.getBusy();\n    }\n\n    @Override\n    protected Region createSimple() {\n        var list = base.createComp();\n        var b = new OptionsBuilder()\n                .name(\"scanAlertChoiceHeader\")\n                .description(\"scanAlertChoiceHeaderDescription\")\n                .addComp(new StoreChoiceComp<>(\n                                null,\n                                entry,\n                                ShellStore.class,\n                                null,\n                                StoreViewState.get().getAllConnectionsCategory())\n                        .disable(base.getBusy().or(new SimpleBooleanProperty(initialStore != null))))\n                .name(\"scanAlertHeader\")\n                .description(\"scanAlertHeaderDescription\")\n                .addComp(list.vgrow())\n                .buildComp()\n                .prefWidth(500)\n                .prefHeight(680);\n        return b.build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/SimpleFilterInputStream.java",
    "content": "package io.xpipe.app.util;\n\nimport lombok.NonNull;\n\nimport java.io.FilterInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\n\npublic abstract class SimpleFilterInputStream extends FilterInputStream {\n\n    protected SimpleFilterInputStream(InputStream in) {\n        super(in);\n    }\n\n    @Override\n    public abstract int read() throws IOException;\n\n    @Override\n    public int read(byte @NonNull [] b, int off, int len) throws IOException {\n        for (int i = off; i < off + len; i++) {\n            var r = read();\n            if (r == -1) {\n                return i - off == 0 ? -1 : i - off;\n            }\n\n            b[i] = (byte) r;\n        }\n        return len;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/SshLocalBridge.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.beacon.AppBeaconServer;\nimport io.xpipe.app.core.AppInstallation;\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.*;\nimport io.xpipe.core.FilePath;\n\nimport lombok.Getter;\nimport lombok.Setter;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\n\n@Getter\npublic class SshLocalBridge {\n\n    private static SshLocalBridge INSTANCE;\n    private final Path directory;\n    private final int port;\n    private final String user;\n\n    @Setter\n    private ShellControl runningShell;\n\n    public SshLocalBridge(Path directory, int port, String user) {\n        this.directory = directory;\n        this.port = port;\n        this.user = user;\n    }\n\n    public static SshLocalBridge get() {\n        return INSTANCE;\n    }\n\n    public static void init() throws Exception {\n        if (INSTANCE != null) {\n            return;\n        }\n\n        var server = AppBeaconServer.get();\n        if (server == null) {\n            return;\n        }\n        // Add a gap to not interfere with PTB or dev ports\n        var port = server.getPort() + 10;\n\n        try (var sc = LocalShell.getShell().start()) {\n            var bridgeDir = AppProperties.get().getDataDir().resolve(\"ssh_bridge\");\n            Files.createDirectories(bridgeDir);\n            var user = sc.getShellDialect().printUsernameCommand(sc).readStdoutOrThrow();\n            INSTANCE = new SshLocalBridge(bridgeDir, port, user);\n\n            var hostKey = INSTANCE.getHostKey();\n            if (!sc.getShellDialect()\n                    .createFileExistsCommand(sc, hostKey.toString())\n                    .executeAndCheck()) {\n                sc.command(CommandBuilder.of()\n                                .add(\"ssh-keygen\", \"-q\")\n                                .add(\"-C\")\n                                .addQuoted(\"XPipe SSH bridge host key\")\n                                .add(\"-t\", \"ed25519\")\n                                .add(\"-f\")\n                                .addQuoted(hostKey.toString())\n                                .add(ssc -> {\n                                    // Powershell breaks when just using quotes\n                                    if (ShellDialects.isPowershell(ssc)) {\n                                        return \"-N '\\\"\\\"'\";\n                                    } else {\n                                        return \"-N \\\"\\\"\";\n                                    }\n                                }))\n                        .execute();\n            }\n\n            var idKey = INSTANCE.getIdentityKey();\n            if (!sc.getShellDialect()\n                    .createFileExistsCommand(sc, idKey.toString())\n                    .executeAndCheck()) {\n                sc.command(CommandBuilder.of()\n                                .add(\"ssh-keygen\", \"-q\")\n                                .add(\"-C\")\n                                .addQuoted(\"XPipe SSH bridge identity\")\n                                .add(\"-t\", \"ed25519\")\n                                .add(\"-f\")\n                                .addQuoted(idKey.toString())\n                                .add(ssc -> {\n                                    // Powershell breaks when just using quotes\n                                    if (ShellDialects.isPowershell(ssc)) {\n                                        return \"-N '\\\"\\\"'\";\n                                    } else {\n                                        return \"-N \\\"\\\"\";\n                                    }\n                                }))\n                        .execute();\n            }\n\n            var config = INSTANCE.getConfig();\n            var command = get().getRemoteCommand(sc);\n            var pidFile = bridgeDir.resolve(\"sshd.pid\");\n            var content = \"\"\"\n                          ForceCommand %s\n                          PidFile \"%s\"\n                          StrictModes no\n                          SyslogFacility USER\n                          LogLevel Debug3\n                          Port %s\n                          PasswordAuthentication no\n                          HostKey \"%s\"\n                          PubkeyAuthentication yes\n                          AuthorizedKeysFile \"%s\"\n                          \"\"\".formatted(\n                            command,\n                            pidFile.toString(),\n                            \"\" + port,\n                            INSTANCE.getHostKey().toString(),\n                            INSTANCE.getPubIdentityKey());\n            Files.writeString(config, content);\n\n            // Write to local SSH client config\n            INSTANCE.updateConfig();\n\n            var exec = getSshd(sc);\n            var launchCommand = CommandBuilder.of()\n                    .addFile(exec)\n                    .add(\"-f\")\n                    .addFile(INSTANCE.getConfig().toString())\n                    .add(\"-p\", \"\" + port);\n            var control =\n                    ProcessControlProvider.get().createLocalProcessControl(true).start();\n            control.writeLine(launchCommand.buildFull(control));\n            INSTANCE.setRunningShell(control);\n        }\n    }\n\n    private static FilePath getSshd(ShellControl sc) throws Exception {\n        var exec = sc.view().findProgram(\"sshd\");\n        if (exec.isEmpty()) {\n            throw ErrorEventFactory.expected(\n                    new IllegalStateException(\n                            \"No sshd executable found in PATH. The SSH terminal bridge for SSH clients requires a local ssh server to be installed\"));\n        }\n        return exec.get();\n    }\n\n    public static void reset() {\n        if (INSTANCE == null || INSTANCE.getRunningShell() == null) {\n            return;\n        }\n\n        try {\n            INSTANCE.getRunningShell().closeStdin();\n        } catch (IOException e) {\n            ErrorEventFactory.fromThrowable(e).omit().handle();\n        }\n        INSTANCE.getRunningShell().kill();\n        INSTANCE = null;\n    }\n\n    public String getName() {\n        return AppProperties.get().isStaging() ? \"xpipe_ptb_bridge\" : \"xpipe_bridge\";\n    }\n\n    public String getHost() {\n        return \"127.0.0.1\";\n    }\n\n    public Path getHostKey() {\n        return directory.resolve(getName() + \"_host_key\");\n    }\n\n    public Path getPubIdentityKey() {\n        return directory.resolve(getName() + \".pub\");\n    }\n\n    public Path getIdentityKey() {\n        return directory.resolve(getName());\n    }\n\n    public Path getConfig() {\n        return directory.resolve(\"sshd_config\");\n    }\n\n    private String getRemoteCommand(ShellControl sc) {\n        var command = \"\\\"\" + AppInstallation.ofCurrent().getCliExecutablePath() + \"\\\" ssh-launch \"\n                + sc.getShellDialect().environmentVariable(\"SSH_ORIGINAL_COMMAND\");\n        var p = Pattern.compile(\"\\\".+?\\\\\\\\Users\\\\\\\\([^\\\\\\\\]+)\\\\\\\\(.+)\\\"\");\n        var matcher = p.matcher(command);\n        if (matcher.find() && matcher.group(1).contains(\" \")) {\n            return matcher.replaceFirst(\"\\\"$2\\\"\");\n        } else {\n            return command;\n        }\n    }\n\n    private void updateConfig() throws IOException {\n        var hostEntry = \"\"\"\n                        Host %s\n                            HostName localhost\n                            User \"%s\"\n                            Port %s\n                            IdentityFile \"%s\"\n                        \"\"\".formatted(getName(), user, port, getIdentityKey());\n\n        var file = AppSystemInfo.ofCurrent().getUserHome().resolve(\".ssh\", \"config\");\n        if (!Files.exists(file)) {\n            Files.writeString(file, hostEntry);\n            return;\n        }\n\n        var content = Files.readString(file).lines().collect(Collectors.joining(\"\\n\")) + \"\\n\";\n        var pattern = Pattern.compile(\"\"\"\n                                      Host %s\n                                       {4}HostName localhost\n                                       {4}User \"(.+)\"\n                                       {4}Port (\\\\d+)\n                                       {4}IdentityFile \"(.+)\"\n                                      \"\"\".formatted(getName()));\n        var matcher = pattern.matcher(content);\n        if (matcher.find()) {\n            var replaced = matcher.replaceFirst(Matcher.quoteReplacement(hostEntry));\n            Files.writeString(file, replaced);\n            return;\n        }\n\n        // Probably an invalid entry that did not match\n        if (content.contains(\"Host \" + getName())) {\n            return;\n        }\n\n        var updated = content + \"\\n\" + hostEntry + \"\\n\";\n        Files.writeString(file, updated);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/StoreStateFormat.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.hub.comp.StoreSection;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.process.*;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.Value;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n@Value\npublic class StoreStateFormat {\n\n    List<LicensedFeature> licensedFeatures;\n    String name;\n    String[] states;\n\n    public StoreStateFormat(List<LicensedFeature> licensedFeatures, String name, String... states) {\n        this.licensedFeatures = licensedFeatures;\n        this.name = name;\n        this.states = states;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <T extends SystemState> ObservableValue<String> shellStore(\n            StoreSection section, Function<T, String[]> f, LicensedFeature licensedFeature) {\n        return BindingsHelper.map(section.getWrapper().getPersistentState(), o -> {\n            var s = (T) o;\n            var info = f.apply(s);\n\n            var osFeature = LicenseProvider.get().checkOsName(s.getOsName());\n            var features = new ArrayList<LicensedFeature>();\n            if (osFeature != null) {\n                features.add(osFeature);\n            }\n            if (licensedFeature != null) {\n                features.add(licensedFeature);\n            }\n\n            if (s.getShellDialect() != null\n                    && !s.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) {\n                if (s.getOsName() != null) {\n                    return new StoreStateFormat(features, formattedOsName(s.getOsName()), info).format();\n                }\n\n                if (s.getShellDialect() == ShellDialects.NO_INTERACTION) {\n                    return new StoreStateFormat(List.of(), null, info).format();\n                }\n\n                return new StoreStateFormat(\n                                features,\n                                s.getOsName() != null\n                                        ? s.getOsName()\n                                        : s.getShellDialect().getDisplayName(),\n                                info)\n                        .format();\n            }\n\n            var joined = Stream.concat(\n                            Stream.of(s.getTtyState() != null && s.getTtyState() != ShellTtyState.NONE ? \"TTY\" : null),\n                            info != null ? Arrays.stream(info) : Stream.of())\n                    .toArray(String[]::new);\n            return new StoreStateFormat(features, formattedOsName(s.getOsName()), joined).format();\n        });\n    }\n\n    public static String formattedOsName(String osName) {\n        if (osName == null) {\n            return null;\n        }\n\n        osName = osName.replaceAll(\"^Microsoft \", \"\");\n        osName = osName.replaceAll(\"Enterprise Evaluation\", \"Enterprise\");\n        return osName;\n    }\n\n    public String format() {\n        var licenseReq = licensedFeatures.stream()\n                .map(licensedFeature -> licensedFeature.getDescriptionSuffix())\n                .flatMap(Optional::stream)\n                .findFirst()\n                .orElse(null);\n        var lic = licenseReq != null ? \"[\" + licenseReq + \"]\" : null;\n        var name = this.name;\n        var state = getStates() != null\n                ? Arrays.stream(getStates())\n                        .filter(s -> s != null)\n                        .map(s -> \"[\" + s + \"]\")\n                        .collect(Collectors.joining(\" \"))\n                : null;\n        if (state != null && state.isEmpty()) {\n            state = null;\n        }\n        return DataStoreFormatter.join(lic, name, state);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/ThreadHelper.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.core.FailableRunnable;\n\nimport lombok.SneakyThrows;\n\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.atomic.AtomicInteger;\n\npublic class ThreadHelper {\n\n    private static final AtomicInteger counter = new AtomicInteger();\n\n    private static Runnable wrap(Runnable r) {\n        return () -> {\n            if (AppProperties.get().isDebugThreads()) {\n                TrackEvent.trace(\"Started. Active threads: \" + counter.incrementAndGet());\n            }\n            r.run();\n            if (AppProperties.get().isDebugThreads()) {\n                TrackEvent.trace(\"Finished. Active threads: \" + counter.decrementAndGet());\n            }\n        };\n    }\n\n    public static Thread unstarted(Runnable r) {\n        return AppProperties.get().isUseVirtualThreads()\n                ? Thread.ofVirtual().unstarted(wrap(r))\n                : Thread.ofPlatform().unstarted(wrap(r));\n    }\n\n    public static Thread runAsync(Runnable r) {\n        var t = unstarted(r);\n        t.setDaemon(true);\n        t.start();\n        return t;\n    }\n\n    public static Thread runFailableAsync(FailableRunnable<Throwable> r) {\n        var t = unstarted(() -> {\n            try {\n                r.run();\n            } catch (Throwable e) {\n                ErrorEventFactory.fromThrowable(e).handle();\n            }\n        });\n        t.setDaemon(true);\n        t.start();\n        return t;\n    }\n\n    public static Thread createPlatformThread(String name, boolean daemon, Runnable r) {\n        var t = new Thread(r);\n        t.setDaemon(daemon);\n        t.setName(name);\n        return t;\n    }\n\n    public static void sleep(long ms) {\n        try {\n            Thread.sleep(ms);\n        } catch (InterruptedException e) {\n            e.printStackTrace();\n        }\n    }\n\n    @SafeVarargs\n    @SneakyThrows\n    public static void load(boolean terminal, FailableRunnable<Throwable>... r) {\n        var latch = new CountDownLatch(r.length);\n        for (var i = 0; i < r.length; i++) {\n            var runnable = r[i];\n            var thread = ThreadHelper.createPlatformThread(\"init-\" + i, false, () -> {\n                try {\n                    runnable.run();\n                    latch.countDown();\n                } catch (Throwable e) {\n                    ErrorEventFactory.fromThrowable(e).terminal(terminal).handle();\n                    latch.countDown();\n                }\n            });\n            thread.start();\n        }\n        latch.await();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/Translatable.java",
    "content": "package io.xpipe.app.util;\n\nimport javafx.beans.value.ObservableValue;\n\npublic interface Translatable {\n\n    ObservableValue<String> toTranslatedString();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/User32Ex.java",
    "content": "package io.xpipe.app.util;\n\nimport com.sun.jna.Native;\nimport com.sun.jna.Pointer;\nimport com.sun.jna.platform.win32.WinDef;\nimport com.sun.jna.win32.W32APIOptions;\nimport io.xpipe.app.core.AppWindowsLock;\n\npublic interface User32Ex extends W32APIOptions {\n\n    User32Ex INSTANCE = Native.load(\"user32\", User32Ex.class, DEFAULT_OPTIONS);\n\n    int SetWindowLongPtr(WinDef.HWND hWnd, int nIndex, AppWindowsLock.WinMsgProc callback);\n\n    int SetWindowLongPtr(WinDef.HWND hWnd, int nIndex, Pointer p);\n\n    int SetWindowLongPtr(WinDef.HWND hWnd, int nIndex, WinDef.HWND w);\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/Validators.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.ValidationException;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport java.util.List;\n\npublic class Validators {\n\n    public static <T extends DataStore> void isType(DataStoreEntryRef<? extends T> ref, Class<T> c)\n            throws ValidationException {\n        if (ref == null\n                || ref.getStore() == null\n                || !c.isAssignableFrom(ref.getStore().getClass())) {\n            throw new ValidationException(\"Value must be an instance of \" + c.getSimpleName());\n        }\n    }\n\n    public static void nonNull(Object object) throws ValidationException {\n        if (object == null) {\n            throw new ValidationException(AppI18n.get(\"valueMustNotBeEmpty\"));\n        }\n    }\n\n    public static void nonNull(Object object, String name) throws ValidationException {\n        if (object == null) {\n            throw new ValidationException(AppI18n.get(\"mustNotBeEmpty\", name));\n        }\n    }\n\n    public static void contentNonNull(List<?> object) throws ValidationException {\n        if (object.stream().anyMatch(o -> o == null)) {\n            throw new ValidationException(AppI18n.get(\"valueMustNotBeEmpty\"));\n        }\n    }\n\n    public static void notEmpty(String string) throws ValidationException {\n        if (string.strip().length() == 0) {\n            throw new ValidationException(AppI18n.get(\"valueMustNotBeEmpty\"));\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/util/WindowsRegistry.java",
    "content": "package io.xpipe.app.util;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.core.OsType;\n\nimport com.sun.jna.Native;\nimport com.sun.jna.platform.win32.Advapi32;\nimport com.sun.jna.platform.win32.Advapi32Util;\nimport com.sun.jna.platform.win32.Win32Exception;\nimport com.sun.jna.platform.win32.WinReg;\nimport com.sun.jna.win32.W32APIOptions;\nimport lombok.Value;\n\nimport java.util.*;\n\npublic abstract class WindowsRegistry {\n\n    public static final int HKEY_CURRENT_USER = 0x80000001;\n    public static final int HKEY_LOCAL_MACHINE = 0x80000002;\n\n    public static void init() {\n        if (OsType.ofLocal() != OsType.WINDOWS) {\n            return;\n        }\n\n        // Load lib\n        Local.isLibrarySupported();\n    }\n\n    public static WindowsRegistry.Local local() {\n        return new Local();\n    }\n\n    public static WindowsRegistry ofShell(ShellControl shellControl) {\n        return shellControl.isLocal() ? new Local() : new Remote(shellControl);\n    }\n\n    public abstract boolean keyExists(int hkey, String key) throws Exception;\n\n    public abstract List<String> listSubKeys(int hkey, String key) throws Exception;\n\n    public abstract boolean valueExists(int hkey, String key, String valueName) throws Exception;\n\n    public abstract OptionalInt readIntegerValueIfPresent(int hkey, String key, String valueName) throws Exception;\n\n    public abstract Optional<String> readStringValueIfPresent(int hkey, String key, String valueName) throws Exception;\n\n    public Optional<String> readStringValueIfPresent(int hkey, String key) throws Exception {\n        return readStringValueIfPresent(hkey, key, null);\n    }\n\n    public abstract Optional<String> findValuesRecursive(int hkey, String key, String valueName) throws Exception;\n\n    public abstract Optional<Key> findKeyForEqualValueMatchRecursive(int hkey, String key, String match)\n            throws Exception;\n\n    @Value\n    public static class Key {\n        int hkey;\n        String key;\n    }\n\n    public static class Local extends WindowsRegistry {\n\n        private WinReg.HKEY hkey(int hkey) {\n            return hkey == HKEY_LOCAL_MACHINE ? WinReg.HKEY_LOCAL_MACHINE : WinReg.HKEY_CURRENT_USER;\n        }\n\n        private static Boolean libraryLoaded;\n\n        private static synchronized boolean isLibrarySupported() {\n            if (libraryLoaded != null) {\n                return libraryLoaded;\n            }\n\n            try {\n                Native.load(\"Advapi32\", Advapi32.class, W32APIOptions.DEFAULT_OPTIONS);\n                return (libraryLoaded = true);\n            } catch (Throwable t) {\n                libraryLoaded = false;\n                ErrorEventFactory.fromThrowable(t)\n                        .description(\"Unable to load native library Advapi32.dll for registry queries.\"\n                                + \" Registry queries will fail and some functionality will be unavailable\")\n                        .handle();\n                return false;\n            }\n        }\n\n        public void setStringValue(int hkey, String key, String valueName, String value) {\n            if (!isLibrarySupported()) {\n                return;\n            }\n\n            try {\n                Advapi32Util.registryCreateKey(hkey(hkey), key);\n                Advapi32Util.registrySetStringValue(hkey(hkey), key, valueName, value);\n            } catch (Win32Exception ignored) {\n            }\n        }\n\n        public void setBinaryValue(int hkey, String key, String valueName, byte[] value) {\n            if (!isLibrarySupported()) {\n                return;\n            }\n\n            try {\n                Advapi32Util.registryCreateKey(hkey(hkey), key);\n                Advapi32Util.registrySetBinaryValue(hkey(hkey), key, valueName, value);\n            } catch (Win32Exception ignored) {\n            }\n        }\n\n        public void deleteKey(int hkey, String key) {\n            if (!isLibrarySupported()) {\n                return;\n            }\n\n            try {\n                Advapi32Util.registryDeleteKey(hkey(hkey), key);\n            } catch (Win32Exception ignored) {\n            }\n        }\n\n        public void deleteValue(int hkey, String key, String valueName) {\n            if (!isLibrarySupported()) {\n                return;\n            }\n\n            try {\n                Advapi32Util.registryDeleteValue(hkey(hkey), key, valueName);\n            } catch (Win32Exception ignored) {\n            }\n        }\n\n        public Optional<byte[]> readBinaryValueIfPresent(int hkey, String key, String valueName) {\n            if (!isLibrarySupported()) {\n                return Optional.empty();\n            }\n\n            try {\n                if (!Advapi32Util.registryValueExists(hkey(hkey), key, valueName)) {\n                    return Optional.empty();\n                }\n\n                return Optional.of(Advapi32Util.registryGetBinaryValue(hkey(hkey), key, valueName));\n            } catch (Win32Exception ignored) {\n                return Optional.empty();\n            }\n        }\n\n        @Override\n        public boolean keyExists(int hkey, String key) {\n            if (!isLibrarySupported()) {\n                return false;\n            }\n\n            try {\n                return Advapi32Util.registryKeyExists(hkey(hkey), key);\n            } catch (Win32Exception ignored) {\n                return false;\n            }\n        }\n\n        @Override\n        public List<String> listSubKeys(int hkey, String key) {\n            if (!isLibrarySupported()) {\n                return List.of();\n            }\n\n            try {\n                return Arrays.asList(Advapi32Util.registryGetKeys(hkey(hkey), key));\n            } catch (Win32Exception ignored) {\n                return List.of();\n            }\n        }\n\n        @Override\n        public boolean valueExists(int hkey, String key, String valueName) {\n            if (!isLibrarySupported()) {\n                return false;\n            }\n\n            try {\n                return Advapi32Util.registryValueExists(hkey(hkey), key, valueName);\n            } catch (Win32Exception ignored) {\n                return false;\n            }\n        }\n\n        @Override\n        public OptionalInt readIntegerValueIfPresent(int hkey, String key, String valueName) {\n            if (!isLibrarySupported()) {\n                return OptionalInt.empty();\n            }\n\n            try {\n                if (!Advapi32Util.registryValueExists(hkey(hkey), key, valueName)) {\n                    return OptionalInt.empty();\n                }\n\n                return OptionalInt.of(Advapi32Util.registryGetIntValue(hkey(hkey), key, valueName));\n            } catch (Win32Exception ignored) {\n                return OptionalInt.empty();\n            }\n        }\n\n        @Override\n        public Optional<String> readStringValueIfPresent(int hkey, String key, String valueName) {\n            if (!isLibrarySupported()) {\n                return Optional.empty();\n            }\n\n            try {\n                if (!Advapi32Util.registryValueExists(hkey(hkey), key, valueName)) {\n                    return Optional.empty();\n                }\n\n                return Optional.ofNullable(Advapi32Util.registryGetStringValue(hkey(hkey), key, valueName));\n            } catch (Win32Exception ignored) {\n                return Optional.empty();\n            }\n        }\n\n        @Override\n        public Optional<String> findValuesRecursive(int hkey, String key, String valueName) throws Exception {\n            if (!isLibrarySupported()) {\n                return Optional.empty();\n            }\n\n            try (var sc = LocalShell.getShell().start()) {\n                return new Remote(sc).findValuesRecursive(hkey, key, valueName);\n            }\n        }\n\n        @Override\n        public Optional<Key> findKeyForEqualValueMatchRecursive(int hkey, String key, String match) throws Exception {\n            if (!isLibrarySupported()) {\n                return Optional.empty();\n            }\n\n            try (var sc = LocalShell.getShell().start()) {\n                return new Remote(sc).findKeyForEqualValueMatchRecursive(hkey, key, match);\n            }\n        }\n    }\n\n    public static class Remote extends WindowsRegistry {\n\n        private final ShellControl shellControl;\n\n        public Remote(ShellControl shellControl) {\n            this.shellControl = shellControl;\n        }\n\n        public static Optional<String> readOutputValue(String original) {\n            // Output has the following format:\n            // \\n<Version information>\\n\\n<key>\\t<registry type>\\t<value>\n            if (original.contains(\"\\t\")) {\n                String[] parsed = original.split(\"\\t\");\n                if (parsed.length < 4) {\n                    return Optional.empty();\n                }\n                return Optional.of(parsed[parsed.length - 1]);\n            }\n\n            if (original.contains(\"    \")) {\n                String[] parsed = original.split(\" {4}\");\n                if (parsed.length < 4) {\n                    return Optional.empty();\n                }\n                return Optional.of(parsed[parsed.length - 1]);\n            }\n\n            return Optional.empty();\n        }\n\n        private String hkey(int hkey) {\n            return hkey == HKEY_LOCAL_MACHINE ? \"HKEY_LOCAL_MACHINE\" : \"HKEY_CURRENT_USER\";\n        }\n\n        @Override\n        public boolean keyExists(int hkey, String key) throws Exception {\n            var command = CommandBuilder.of()\n                    .add(\"reg\", \"query\")\n                    .addQuoted(hkey(hkey) + \"\\\\\" + key)\n                    .add(\"/ve\");\n            try (var c = shellControl.command(command).start()) {\n                return c.discardAndCheckExit();\n            }\n        }\n\n        @Override\n        public List<String> listSubKeys(int hkey, String key) throws Exception {\n            var prefix = hkey(hkey) + \"\\\\\" + key;\n            var command = CommandBuilder.of().add(\"reg\", \"query\").addQuoted(prefix);\n            var out = shellControl.command(command).readStdoutOrThrow();\n            return out.lines()\n                    .filter(s -> {\n                        return s.contains(prefix + \"\\\\\");\n                    })\n                    .map(s -> s.replace(prefix + \"\\\\\", \"\"))\n                    .toList();\n        }\n\n        @Override\n        public boolean valueExists(int hkey, String key, String valueName) throws Exception {\n            var command = CommandBuilder.of()\n                    .add(\"reg\", \"query\")\n                    .addQuoted(hkey(hkey) + \"\\\\\" + key)\n                    .add(\"/v\")\n                    .addQuoted(valueName);\n            try (var c = shellControl.command(command).start()) {\n                return c.discardAndCheckExit();\n            }\n        }\n\n        @Override\n        public OptionalInt readIntegerValueIfPresent(int hkey, String key, String valueName) throws Exception {\n            var r = readStringValueIfPresent(hkey, key, valueName);\n            if (r.isPresent()) {\n                return OptionalInt.of(Integer.decode(r.get()));\n            } else {\n                return OptionalInt.empty();\n            }\n        }\n\n        @Override\n        public Optional<String> readStringValueIfPresent(int hkey, String key, String valueName) throws Exception {\n            var command = CommandBuilder.of()\n                    .add(\"reg\", \"query\")\n                    .addQuoted(hkey(hkey) + \"\\\\\" + key)\n                    .add(\"/v\")\n                    .addQuoted(valueName);\n\n            var output = shellControl.command(command).readStdoutIfPossible();\n            if (output.isEmpty()) {\n                return Optional.empty();\n            }\n\n            return readOutputValue(output.get());\n        }\n\n        @Override\n        public Optional<String> findValuesRecursive(int hkey, String key, String valueName) throws Exception {\n            var command = CommandBuilder.of()\n                    .add(\"reg\", \"query\")\n                    .addQuoted(hkey(hkey) + \"\\\\\" + key)\n                    .add(\"/v\")\n                    .addQuoted(valueName)\n                    .add(\"/s\");\n            return shellControl.command(command).readStdoutIfPossible();\n        }\n\n        @Override\n        public Optional<Key> findKeyForEqualValueMatchRecursive(int hkey, String key, String match) throws Exception {\n            var command = CommandBuilder.of()\n                    .add(\"reg\", \"query\")\n                    .addQuoted(hkey(hkey) + \"\\\\\" + key)\n                    .add(\"/f\")\n                    .addQuoted(match)\n                    .add(\"/s\")\n                    .add(\"/e\")\n                    .add(\"/d\");\n            return shellControl.command(command).readStdoutIfPossible().flatMap(output -> {\n                return output.lines().findFirst().flatMap(s -> {\n                    if (s.startsWith(\"HKEY_CURRENT_USER\\\\\")) {\n                        return Optional.of(new Key(HKEY_CURRENT_USER, s.replace(\"HKEY_CURRENT_USER\\\\\", \"\")));\n                    }\n                    if (s.startsWith(\"HKEY_LOCAL_MACHINE\\\\\")) {\n                        return Optional.of(new Key(HKEY_LOCAL_MACHINE, s.replace(\"HKEY_LOCAL_MACHINE\\\\\", \"\")));\n                    }\n                    return Optional.empty();\n                });\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/vnc/CustomVncClient.java",
    "content": "package io.xpipe.app.vnc;\n\nimport io.xpipe.app.comp.base.TextFieldComp;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.prefs.ExternalApplicationHelper;\nimport io.xpipe.app.process.CommandBuilder;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleObjectProperty;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.Locale;\n\n@JsonTypeName(\"custom\")\n@Value\n@Jacksonized\n@Builder\npublic class CustomVncClient implements ExternalVncClient {\n\n    String command;\n\n    @SuppressWarnings(\"unused\")\n    static OptionsBuilder createOptions(Property<CustomVncClient> property) {\n        var command = new SimpleObjectProperty<>(property.getValue().getCommand());\n        return new OptionsBuilder()\n                .nameAndDescription(\"customVncCommand\")\n                .addComp(\n                        new TextFieldComp(command, false)\n                                .apply(struc -> struc.setPromptText(\"myvncclient $ADDRESS\"))\n                                .maxWidth(600),\n                        command)\n                .bind(() -> CustomVncClient.builder().command(command.get()).build(), property);\n    }\n\n    @Override\n    public void launch(VncLaunchConfig configuration) throws Exception {\n        if (command == null) {\n            return;\n        }\n\n        var address = configuration.getHost() + \":\" + configuration.getPort();\n        var format = command.toLowerCase(Locale.ROOT).contains(\"$address\") ? command : command + \" $ADDRESS\";\n        var toExecute = ExternalApplicationHelper.replaceVariableArgument(format, \"ADDRESS\", address);\n        ExternalApplicationHelper.startAsync(CommandBuilder.of().add(toExecute));\n    }\n\n    @Override\n    public boolean supportsPasswords() {\n        return false;\n    }\n\n    @Override\n    public String getWebsite() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/vnc/ExternalVncClient.java",
    "content": "package io.xpipe.app.vnc;\n\nimport io.xpipe.app.ext.PrefsValue;\nimport io.xpipe.app.platform.ClipboardHelper;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.rdp.*;\nimport io.xpipe.core.OsType;\n\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\npublic interface ExternalVncClient extends PrefsValue {\n\n    static void launchClient(VncLaunchConfig configuration) throws Exception {\n        var client = AppPrefs.get().vncClient.getValue();\n        if (client == null) {\n            return;\n        }\n\n        if (!client.supportsPasswords() && configuration.hasFixedPassword()) {\n            var pw = configuration.retrievePassword();\n            if (pw.isPresent()) {\n                ClipboardHelper.copyPassword(pw.get());\n            }\n        }\n\n        client.launch(configuration);\n    }\n\n    static List<Class<?>> getClasses() {\n        var l = new ArrayList<Class<?>>();\n        l.add(InternalVncClient.class);\n        switch (OsType.ofLocal()) {\n            case OsType.Linux ignored -> {\n                l.add(RemminaVncClient.class);\n                l.add(TigerVncClient.Linux.class);\n                l.add(RemoteViewerVncClient.Linux.class);\n                l.add(RealVncClient.Linux.class);\n            }\n            case OsType.MacOs ignored -> {\n                l.add(ScreenSharingVncClient.class);\n                l.add(TigerVncClient.MacOs.class);\n                l.add(RemoteViewerVncClient.MacOs.class);\n                l.add(RealVncClient.MacOs.class);\n            }\n            case OsType.Windows ignored -> {\n                l.add(TigerVncClient.Windows.class);\n                l.add(TightVncClient.class);\n                l.add(RemoteViewerVncClient.Windows.class);\n                l.add(RealVncClient.Windows.class);\n            }\n        }\n        l.add(CustomVncClient.class);\n        return l;\n    }\n\n\n    static ExternalVncClient determineDefault(ExternalVncClient existing) {\n        // Verify that our selection is still valid\n        if (existing != null && existing.isAvailable()) {\n            return existing;\n        }\n\n        var l = new ArrayList<ExternalVncClient>();\n        switch (OsType.ofLocal()) {\n            case OsType.Linux ignored -> {\n                l.add(new TigerVncClient.Linux());\n                l.add(new RealVncClient.Linux());\n            }\n            case OsType.MacOs ignored -> {}\n            case OsType.Windows ignored -> {\n                l.add(new TightVncClient());\n            }\n        }\n\n        var found = l.stream().filter(externalVncClient -> externalVncClient.isAvailable()).findFirst();\n        if (found.isPresent()) {\n            return found.get();\n        }\n\n        return new InternalVncClient();\n    }\n\n\n    void launch(VncLaunchConfig configuration) throws Exception;\n\n    boolean supportsPasswords();\n\n    String getWebsite();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/vnc/InternalVncClient.java",
    "content": "package io.xpipe.app.vnc;\n\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.browser.BrowserStoreSessionTab;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.util.DocumentationLink;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.extern.jackson.Jacksonized;\n\n@Builder\n@Jacksonized\n@JsonTypeName(\"integratedXPipeVncClient\")\npublic class InternalVncClient implements ExternalVncClient {\n\n    @Override\n    public void launch(VncLaunchConfig configuration) throws Exception {\n        var browserSession = BrowserFullSessionModel.DEFAULT;\n        var open = browserSession.getSessionEntriesSnapshot().stream()\n                .filter(browserSessionTab -> browserSessionTab instanceof BrowserStoreSessionTab<?> st\n                        && st.getEntry().get().equals(configuration.getEntry().get()))\n                .findFirst()\n                .orElse(null);\n        if (open != null) {\n            AppLayoutModel.get().selectBrowser();\n            browserSession.getSelectedEntry().setValue(open);\n            return;\n        }\n\n        browserSession.openSync(\n                ProcessControlProvider.get().createVncSession(browserSession, configuration.getEntry()),\n                browserSession.getBusy());\n        AppLayoutModel.get().selectBrowser();\n    }\n\n    @Override\n    public boolean supportsPasswords() {\n        return true;\n    }\n\n    @Override\n    public String getWebsite() {\n        return DocumentationLink.VNC_CLIENTS.getLink();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/vnc/RealVncClient.java",
    "content": "package io.xpipe.app.vnc;\n\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Optional;\n\npublic abstract class RealVncClient implements ExternalVncClient {\n\n    @Override\n    public boolean supportsPasswords() {\n        return false;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://www.realvnc.com/\";\n    }\n\n    protected CommandBuilder createBuilder(VncLaunchConfig configuration) throws Exception {\n        var builder = CommandBuilder.of()\n                .addQuoted(configuration.getHost() + \":\" + configuration.getPort())\n                .addQuotedKeyValue(\"-ColorLevel\", \"full\")\n                .addQuotedKeyValue(\"-SecurityNotificationTimeout\", \"0\")\n                .addQuotedKeyValue(\"-WarnUnencrypted\", configuration.isTunneled() ? \"0\" : \"1\");\n        configuration.retrieveUsername().ifPresent(s -> builder.addQuotedKeyValue(\"-UserName\", s));\n        return builder;\n    }\n\n    @Builder\n    @Jacksonized\n    @JsonTypeName(\"realVnc\")\n    public static class Windows extends RealVncClient implements ExternalApplicationType.WindowsType {\n\n        @Override\n        public boolean detach() {\n            return true;\n        }\n\n        @Override\n        public String getExecutable() {\n            return \"vncviewer.exe\";\n        }\n\n        @Override\n        public Optional<Path> determineInstallation() {\n            return Optional.of(AppSystemInfo.ofWindows()\n                            .getProgramFiles()\n                            .resolve(\"RealVNC\")\n                            .resolve(\"VNC Viewer\")\n                            .resolve(\"vncviewer.exe\"))\n                    .filter(path -> Files.exists(path));\n        }\n\n        @Override\n        public Optional<Path> determineFromPath() {\n            var found = WindowsType.super.determineFromPath();\n            return found.filter(path -> path.toString().contains(\"RealVNC\"));\n        }\n\n        @Override\n        public void launch(VncLaunchConfig configuration) throws Exception {\n            var builder = createBuilder(configuration);\n            launch(builder);\n        }\n    }\n\n    @Builder\n    @Jacksonized\n    @JsonTypeName(\"realVnc\")\n    public static class Linux extends RealVncClient implements ExternalApplicationType.PathApplication {\n\n        @Override\n        public String getExecutable() {\n            return \"vncviewer\";\n        }\n\n        @Override\n        public boolean detach() {\n            return true;\n        }\n\n        @Override\n        public void launch(VncLaunchConfig configuration) throws Exception {\n            var builder = createBuilder(configuration);\n            launch(builder);\n        }\n    }\n\n    @Builder\n    @Jacksonized\n    @JsonTypeName(\"realVnc\")\n    public static class MacOs extends RealVncClient implements ExternalApplicationType.MacApplication {\n\n        @Override\n        public void launch(VncLaunchConfig configuration) throws Exception {\n            var builder = createBuilder(configuration);\n            launchCommand(builder, true).execute();\n        }\n\n        @Override\n        public String getApplicationName() {\n            return \"VNC Viewer\";\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/vnc/RemminaVncClient.java",
    "content": "package io.xpipe.app.vnc;\n\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.util.LocalFileTracker;\nimport io.xpipe.app.util.RemminaHelper;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.Optional;\n\n@Builder\n@Jacksonized\n@JsonTypeName(\"remmina\")\npublic class RemminaVncClient implements ExternalApplicationType.LinuxApplication, ExternalVncClient {\n\n    @Override\n    public String getExecutable() {\n        return \"remmina\";\n    }\n\n    @Override\n    public boolean detach() {\n        return true;\n    }\n\n    @Override\n    public void launch(VncLaunchConfig configuration) throws Exception {\n        var pw = configuration.retrievePassword();\n        var encrypted = pw.isPresent() ? RemminaHelper.encryptPassword(pw.get()) : Optional.<String>empty();\n        var file = RemminaHelper.writeRemminaVncConfigFile(configuration, encrypted.orElse(null));\n        launch(CommandBuilder.of().add(\"-c\").addFile(file.toString()));\n        LocalFileTracker.deleteOnExit(file);\n    }\n\n    @Override\n    public boolean supportsPasswords() {\n        return true;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://remmina.org/\";\n    }\n\n    @Override\n    public String getFlatpakId() {\n        return \"org.remmina.Remmina\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/vnc/RemoteViewerVncClient.java",
    "content": "package io.xpipe.app.vnc;\n\nimport io.xpipe.app.core.AppLocalTemp;\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.OsFileSystem;\nimport io.xpipe.app.util.GlobalTimer;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.extern.jackson.Jacksonized;\nimport org.apache.commons.io.FileUtils;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Duration;\nimport java.util.Optional;\n\npublic abstract class RemoteViewerVncClient implements ExternalVncClient {\n\n    protected CommandBuilder createBuilder(VncLaunchConfig configuration) throws Exception {\n        var vv = \"\"\"\n                 [virt-viewer]\n                 type=vnc\n                 host=%s\n                 port=%s\n                 title=%s\n                 \"\"\".formatted(configuration.getHost(), configuration.getPort(), configuration.getTitle());\n\n        var user = configuration.retrieveUsername();\n        if (user.isPresent()) {\n            vv += \"username=\" + user.get() + \"\\n\";\n        }\n\n        var pass = configuration.retrievePassword();\n        if (pass.isPresent()) {\n            vv += \"password=\" + pass.get().getSecretValue() + \"\\n\";\n        }\n\n        var file = writeVncConfigFile(configuration.getTitle(), vv);\n        var builder = CommandBuilder.of().addFile(file);\n        return builder;\n    }\n\n    private Path writeVncConfigFile(String title, String content) throws Exception {\n        var file = getFilePath(title);\n        Files.createDirectories(file.getParent());\n        Files.writeString(file, content);\n        return file;\n    }\n\n    protected Path getFilePath(String title) {\n        var name = OsFileSystem.ofLocal().makeFileSystemCompatible(title);\n        var file = AppLocalTemp.getLocalTempDataDirectory(\"vnc\").resolve(name + \".vv\");\n        return file;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://virt-manager.org\";\n    }\n\n    @Override\n    public boolean supportsPasswords() {\n        return true;\n    }\n\n    @Builder\n    @Jacksonized\n    @JsonTypeName(\"remoteViewer\")\n    public static class Windows extends RemoteViewerVncClient implements ExternalApplicationType.WindowsType {\n\n        @Override\n        public boolean detach() {\n            return false;\n        }\n\n        @Override\n        public String getExecutable() {\n            return \"remote-viewer.exe\";\n        }\n\n        @Override\n        public Optional<Path> determineInstallation() {\n            try (var stream = Files.list(AppSystemInfo.ofWindows().getProgramFiles())) {\n                var l = stream.toList();\n                var found = l.stream()\n                        .filter(path -> path.toString().contains(\"VirtViewer\"))\n                        .findFirst();\n                if (found.isEmpty()) {\n                    return Optional.empty();\n                }\n\n                return Optional.ofNullable(found.get().resolve(\"bin\", \"remote-viewer.exe\"));\n            } catch (IOException e) {\n                ErrorEventFactory.fromThrowable(e).handle();\n                return Optional.empty();\n            }\n        }\n\n        @Override\n        public void launch(VncLaunchConfig configuration) throws Exception {\n            var builder = createBuilder(configuration);\n            launch(builder);\n            GlobalTimer.delay(\n                    () -> {\n                        FileUtils.deleteQuietly(\n                                getFilePath(configuration.getTitle()).toFile());\n                    },\n                    Duration.ofSeconds(5));\n        }\n    }\n\n    @Builder\n    @Jacksonized\n    @JsonTypeName(\"remoteViewer\")\n    public static class Linux extends RemoteViewerVncClient implements ExternalApplicationType.LinuxApplication {\n\n        @Override\n        public void launch(VncLaunchConfig configuration) throws Exception {\n            var builder = createBuilder(configuration);\n            launch(builder);\n            GlobalTimer.delay(\n                    () -> {\n                        FileUtils.deleteQuietly(\n                                getFilePath(configuration.getTitle()).toFile());\n                    },\n                    Duration.ofSeconds(5));\n        }\n\n        @Override\n        public String getExecutable() {\n            return \"remote-viewer\";\n        }\n\n        @Override\n        public boolean detach() {\n            return true;\n        }\n\n        @Override\n        public String getFlatpakId() {\n            return \"org.virt_manager.virt-viewer\";\n        }\n    }\n\n    @Builder\n    @Jacksonized\n    @JsonTypeName(\"remoteViewer\")\n    public static class MacOs extends RemoteViewerVncClient implements ExternalApplicationType.PathApplication {\n\n        @Override\n        public void launch(VncLaunchConfig configuration) throws Exception {\n            var builder = createBuilder(configuration);\n            launch(builder);\n            GlobalTimer.delay(\n                    () -> {\n                        FileUtils.deleteQuietly(\n                                getFilePath(configuration.getTitle()).toFile());\n                    },\n                    Duration.ofSeconds(5));\n        }\n\n        @Override\n        public String getExecutable() {\n            return \"remote-viewer\";\n        }\n\n        @Override\n        public boolean detach() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/vnc/ScreenSharingVncClient.java",
    "content": "package io.xpipe.app.vnc;\n\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.extern.jackson.Jacksonized;\n\n@Builder\n@Jacksonized\n@JsonTypeName(\"screenSharing\")\npublic class ScreenSharingVncClient implements ExternalApplicationType.MacApplication, ExternalVncClient {\n\n    @Override\n    public void launch(VncLaunchConfig configuration) throws Exception {\n        var pw = configuration.retrievePassword();\n        var credentials = (configuration.retrieveUsername().orElse(\"\")\n                + pw.map(secretValue -> \":\" + secretValue.getSecretValue()).orElse(\"\"));\n        var address = configuration.getHost() + \":\" + configuration.getPort();\n        var args = \"vnc://\" + credentials + \"@\" + address;\n        var command = launchCommand(CommandBuilder.of().add(args), false);\n        if (pw.isPresent()) {\n            command.sensitive();\n        }\n        command.execute();\n    }\n\n    @Override\n    public boolean supportsPasswords() {\n        return true;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://support.apple.com/en-is/guide/mac-help/mh14066/15.0/mac/15.0\";\n    }\n\n    @Override\n    public String getApplicationName() {\n        return \"Screen Sharing\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/vnc/TigerVncClient.java",
    "content": "package io.xpipe.app.vnc;\n\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Optional;\n\npublic abstract class TigerVncClient implements ExternalVncClient {\n\n    protected CommandBuilder createBuilder(VncLaunchConfig configuration) {\n        var builder = CommandBuilder.of().addQuoted(configuration.getHost() + \":\" + configuration.getPort());\n        builder.addQuotedKeyValue(\"-ReconnectOnError\", \"off\");\n        return builder;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://tigervnc.org/\";\n    }\n\n    @Builder\n    @Jacksonized\n    @JsonTypeName(\"tigerVnc\")\n    public static class Windows extends TigerVncClient implements ExternalApplicationType.WindowsType {\n\n        @Override\n        public boolean detach() {\n            return false;\n        }\n\n        @Override\n        public String getExecutable() {\n            return \"vncviewer.exe\";\n        }\n\n        @Override\n        public Optional<Path> determineInstallation() {\n            return Optional.of(AppSystemInfo.ofWindows()\n                            .getProgramFiles()\n                            .resolve(\"TigerVNC\")\n                            .resolve(\"vncviewer.exe\"))\n                    .filter(path -> Files.exists(path));\n        }\n\n        @Override\n        public Optional<Path> determineFromPath() {\n            var found = WindowsType.super.determineFromPath();\n            return found.filter(path -> path.toString().contains(\"TigerVNC\"));\n        }\n\n        @Override\n        public void launch(VncLaunchConfig configuration) throws Exception {\n            var builder = createBuilder(configuration);\n            launch(builder);\n        }\n\n        @Override\n        public boolean supportsPasswords() {\n            return false;\n        }\n    }\n\n    @Builder\n    @Jacksonized\n    @JsonTypeName(\"tigerVnc\")\n    public static class Linux extends TigerVncClient implements ExternalApplicationType.LinuxApplication {\n\n        @Override\n        public void launch(VncLaunchConfig configuration) throws Exception {\n            var builder = createBuilder(configuration);\n            if (configuration.hasFixedPassword()) {\n                var pw = configuration.retrievePassword();\n                if (pw.isPresent()) {\n                    builder.add(sc -> \"<(echo \"\n                            + sc.getShellDialect().literalArgument(pw.get().getSecretValue()) + \" | vncpasswd -f)\");\n                }\n            }\n            launch(builder);\n        }\n\n        @Override\n        public boolean supportsPasswords() {\n            try {\n                return LocalShell.getShell().view().findProgram(\"vncpasswd\").isPresent();\n            } catch (Exception e) {\n                ErrorEventFactory.fromThrowable(e).handle();\n                return false;\n            }\n        }\n\n        @Override\n        public String getExecutable() {\n            return \"xtigervncviewer\";\n        }\n\n        @Override\n        public boolean detach() {\n            return true;\n        }\n\n        @Override\n        public String getFlatpakId() {\n            return \"org.tigervnc.vncviewer\";\n        }\n    }\n\n    @Builder\n    @Jacksonized\n    @JsonTypeName(\"tigerVnc\")\n    public static class MacOs extends TigerVncClient implements ExternalApplicationType.InstallLocationType {\n\n        @Override\n        public void launch(VncLaunchConfig configuration) throws Exception {\n            var loc = findExecutable();\n            var builder = createBuilder(configuration);\n            var open = CommandBuilder.of().add(\"open\", \"-a\").addFile(loc).add(\"--args\");\n            builder.add(0, open);\n            LocalShell.getShell().command(builder).execute();\n        }\n\n        @Override\n        public boolean supportsPasswords() {\n            return false;\n        }\n\n        @Override\n        public String getExecutable() {\n            return \"VNCViewer\";\n        }\n\n        @Override\n        public Optional<Path> determineInstallation() {\n            try (var appsStream = Files.list(Path.of(\"/Applications\"))) {\n                var dirs = appsStream.toList();\n                return dirs.stream()\n                        .filter(path -> path.toString().contains(\"TigerVNC viewer\"))\n                        .findFirst();\n            } catch (IOException e) {\n                ErrorEventFactory.fromThrowable(e).handle();\n                return Optional.empty();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/vnc/TightVncClient.java",
    "content": "package io.xpipe.app.vnc;\n\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.prefs.ExternalApplicationType;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Optional;\n\n@Builder\n@Jacksonized\n@JsonTypeName(\"tightVnc\")\npublic class TightVncClient implements ExternalApplicationType.InstallLocationType, ExternalVncClient {\n\n    @Override\n    public String getExecutable() {\n        return \"tvnviewer.exe\";\n    }\n\n    @Override\n    public Optional<Path> determineInstallation() {\n        return Optional.of(AppSystemInfo.ofWindows()\n                        .getProgramFiles()\n                        .resolve(\"TightVNC\")\n                        .resolve(\"tvnviewer.exe\"))\n                .filter(path -> Files.exists(path));\n    }\n\n    @Override\n    public void launch(VncLaunchConfig configuration) throws Exception {\n        var builder = CommandBuilder.of()\n                .addFile(findExecutable())\n                .addQuotedKeyValue(\"-host\", configuration.getHost())\n                .addQuotedKeyValue(\"-port\", \"\" + configuration.getPort());\n        var pw = configuration.retrievePassword();\n        pw.ifPresent(secretValue -> builder.addQuotedKeyValue(\"-password\", secretValue.getSecretValue()));\n        var command = LocalShell.getShell().command(builder);\n        if (pw.isPresent()) {\n            command.sensitive();\n        }\n        command.execute();\n    }\n\n    @Override\n    public boolean supportsPasswords() {\n        return true;\n    }\n\n    @Override\n    public String getWebsite() {\n        return \"https://www.tightvnc.com\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/vnc/VncBaseStore.java",
    "content": "package io.xpipe.app.vnc;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.secret.SecretRetrievalStrategy;\n\npublic interface VncBaseStore extends DataStore {\n\n    String getEffectiveHost() throws Exception;\n\n    int getEffectivePort();\n\n    String retrieveUser() throws Exception;\n\n    SecretRetrievalStrategy getPassword();\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/vnc/VncCategory.java",
    "content": "package io.xpipe.app.vnc;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.base.ButtonComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.platform.OptionsChoiceBuilder;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.prefs.AppPrefsCategory;\nimport io.xpipe.app.util.*;\n\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Priority;\nimport javafx.scene.layout.Region;\n\nimport org.kordamp.ikonli.javafx.FontIcon;\n\npublic class VncCategory extends AppPrefsCategory {\n\n    @Override\n    protected String getId() {\n        return \"vnc\";\n    }\n\n    @Override\n    protected LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdral-desktop_windows\");\n    }\n\n    @Override\n    protected BaseRegionBuilder<?, ?> create() {\n        var prefs = AppPrefs.get();\n        var choiceBuilder = OptionsChoiceBuilder.builder()\n                .property(prefs.vncClient)\n                .available(ExternalVncClient.getClasses())\n                .allowNull(false)\n                .transformer(entryComboBox -> {\n                    var websiteLinkButton =\n                            new ButtonComp(AppI18n.observable(\"website\"), new FontIcon(\"mdi2w-web\"), () -> {\n                                var c = prefs.vncClient.getValue();\n                                if (c != null && c.getWebsite() != null) {\n                                    Hyperlinks.open(c.getWebsite());\n                                }\n                            });\n                    websiteLinkButton.minWidth(Region.USE_PREF_SIZE);\n\n                    var hbox = new HBox(entryComboBox, websiteLinkButton.build());\n                    HBox.setHgrow(entryComboBox, Priority.ALWAYS);\n                    hbox.setSpacing(10);\n                    return hbox;\n                })\n                .build();\n        var choice = choiceBuilder.build().buildComp().maxWidth(600);\n        return new OptionsBuilder()\n                .addTitle(\"vncClient\")\n                .sub(new OptionsBuilder().pref(prefs.vncClient).addComp(choice))\n                .buildComp();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/xpipe/app/vnc/VncLaunchConfig.java",
    "content": "package io.xpipe.app.vnc;\n\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.secret.SecretManager;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.core.SecretValue;\n\nimport lombok.Value;\n\nimport java.util.Optional;\n\n@Value\npublic class VncLaunchConfig {\n    String title;\n    String host;\n    int port;\n    DataStoreEntryRef<VncBaseStore> entry;\n    ShellControl shellControl;\n\n    public Optional<String> retrieveUsername() throws Exception {\n        return Optional.ofNullable(entry.getStore().retrieveUser());\n    }\n\n    public boolean hasFixedPassword() {\n        return entry.getStore().getPassword() != null\n                && entry.getStore().getPassword().expectsQuery()\n                && !entry.getStore().getPassword().query().requiresUserInteraction();\n    }\n\n    public boolean isTunneled() {\n        return shellControl != null;\n    }\n\n    public Optional<SecretValue> retrievePassword() {\n        var strat = entry.getStore().getPassword();\n        if (!strat.expectsQuery()) {\n            return Optional.empty();\n        }\n\n        var secret =\n                SecretManager.retrieve(strat, \"VNC login password\", entry.get().getUuid(), 1, true);\n        return Optional.ofNullable(secret);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/module-info.java",
    "content": "import io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.action.XPipeUrlProvider;\nimport io.xpipe.app.beacon.impl.*;\nimport io.xpipe.app.browser.action.BrowserActionProvider;\nimport io.xpipe.app.browser.action.impl.*;\nimport io.xpipe.app.browser.menu.impl.*;\nimport io.xpipe.app.browser.menu.impl.compress.*;\nimport io.xpipe.app.core.AppLogs;\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.hub.action.impl.*;\nimport io.xpipe.app.issue.EventHandler;\nimport io.xpipe.app.issue.EventHandlerImpl;\nimport io.xpipe.app.process.ShellDialect;\nimport io.xpipe.app.process.ShellDialects;\nimport io.xpipe.app.terminal.TerminalLauncher;\nimport io.xpipe.app.util.AppJacksonModule;\nimport io.xpipe.app.util.LicenseProvider;\nimport io.xpipe.beacon.BeaconInterface;\nimport io.xpipe.core.ModuleLayerLoader;\n\nimport com.fasterxml.jackson.databind.Module;\nimport org.slf4j.spi.SLF4JServiceProvider;\n\nopen module io.xpipe.app {\n    exports io.xpipe.app.beacon;\n    exports io.xpipe.app.core;\n    exports io.xpipe.app.util;\n    exports io.xpipe.app;\n    exports io.xpipe.app.issue;\n    exports io.xpipe.app.comp.base;\n    exports io.xpipe.app.core.mode;\n    exports io.xpipe.app.prefs;\n    exports io.xpipe.app.hub.comp;\n    exports io.xpipe.app.storage;\n    exports io.xpipe.app.update;\n    exports io.xpipe.app.ext;\n    exports io.xpipe.app.comp.augment;\n    exports io.xpipe.app.test;\n    exports io.xpipe.app.browser.action;\n    exports io.xpipe.app.browser;\n    exports io.xpipe.app.browser.icon;\n    exports io.xpipe.app.core.check;\n    exports io.xpipe.app.terminal;\n    exports io.xpipe.app.browser.file;\n    exports io.xpipe.app.core.window;\n    exports io.xpipe.app.comp;\n    exports io.xpipe.app.icon;\n    exports io.xpipe.app.pwman;\n    exports io.xpipe.app.rdp;\n    exports io.xpipe.app.vnc;\n    exports io.xpipe.app.action;\n    exports io.xpipe.app.browser.menu;\n    exports io.xpipe.app.browser.menu.impl;\n    exports io.xpipe.app.browser.action.impl;\n    exports io.xpipe.app.browser.menu.impl.compress;\n    exports io.xpipe.app.hub.action;\n    exports io.xpipe.app.hub.action.impl;\n    exports io.xpipe.app.process;\n    exports io.xpipe.app.secret;\n    exports io.xpipe.app.platform;\n    exports io.xpipe.app.spice;\n\n    requires com.sun.jna;\n    requires com.sun.jna.platform;\n    requires org.slf4j;\n    requires org.slf4j.jdk.platform.logging;\n    requires atlantafx.base;\n    requires com.vladsch.flexmark;\n    requires com.fasterxml.jackson.core;\n    requires com.fasterxml.jackson.databind;\n    requires com.fasterxml.jackson.annotation;\n    requires net.synedra.validatorfx;\n    requires io.xpipe.modulefs;\n    requires io.xpipe.core;\n    requires static lombok;\n    requires org.apache.commons.io;\n    requires org.apache.commons.lang3;\n    requires javafx.base;\n    requires static org.junit.jupiter.api;\n    requires javafx.controls;\n    requires javafx.media;\n    requires javafx.web;\n    requires javafx.graphics;\n    requires org.kordamp.ikonli.javafx;\n    requires io.sentry;\n    requires io.xpipe.beacon;\n    requires info.picocli;\n    requires java.instrument;\n    requires java.management;\n    requires jdk.management;\n    requires jdk.management.agent;\n    requires com.shinyhut.vernacular;\n    requires org.kordamp.ikonli.core;\n    requires jdk.httpserver;\n    requires com.github.weisj.jsvg;\n    requires java.net.http;\n    requires org.bouncycastle.provider;\n    requires org.jetbrains.annotations;\n    requires io.modelcontextprotocol.sdk.mcp;\n    requires reactor.core;\n    requires org.reactivestreams;\n\n    // Required runtime modules\n    requires jdk.charsets;\n    requires jdk.crypto.cryptoki;\n    requires jdk.localedata;\n    requires jdk.accessibility;\n    requires org.kordamp.ikonli.material2;\n    requires org.kordamp.ikonli.materialdesign2;\n    requires org.kordamp.ikonli.bootstrapicons;\n    requires jdk.zipfs;\n    requires org.int4.fx.builders;\n\n    uses TerminalLauncher;\n    uses ActionProvider;\n    uses EventHandler;\n    uses PrefsProvider;\n    uses DataStoreProvider;\n    uses ModuleLayerLoader;\n    uses ScanProvider;\n    uses BrowserActionProvider;\n    uses LicenseProvider;\n    uses io.xpipe.app.util.LicensedFeature;\n    uses io.xpipe.beacon.BeaconInterface;\n    uses DataStorageExtensionProvider;\n    uses ProcessControlProvider;\n    uses ShellDialect;\n    uses CloudSetupProvider;\n\n    provides ActionProvider with\n            GradleRunMenuProvider,\n            RefreshHubLeafProvider,\n            SetupToolActionProvider,\n            XPipeUrlProvider,\n            OpenHubMenuLeafProvider,\n            OpenSplitHubBatchProvider,\n            EditHubLeafProvider,\n            CloneHubLeafProvider,\n            DownloadMenuProvider,\n            RefreshChildrenHubLeafProvider,\n            ScanHubBatchProvider,\n            RunCommandInBrowserActionProvider,\n            RunCommandInBackgroundActionProvider,\n            RunCommandInTerminalActionProvider,\n            ComputeDirectorySizesMenuProvider,\n            FollowLinkMenuProvider,\n            BackMenuProvider,\n            ForwardMenuProvider,\n            RefreshDirectoryMenuProvider,\n            OpenFileDefaultMenuProvider,\n            OpenFileWithMenuProvider,\n            OpenDirectoryMenuProvider,\n            OpenDirectoryInNewTabMenuProvider,\n            ScanHubLeafProvider,\n            StartOnInitHubLeafProvider,\n            BrowseHubLeafProvider,\n            RefreshActionProvider,\n            ToggleActionProvider,\n            OpenTerminalInDirectoryMenuProvider,\n            OpenNativeFileDetailsMenuProvider,\n            BrowseInNativeManagerMenuProvider,\n            BrowseInNativeManagerActionProvider,\n            ApplyFileEditActionProvider,\n            TransferFilesActionProvider,\n            EditFileMenuProvider,\n            RunFileMenuProvider,\n            RenameMenuProvider,\n            ChmodMenuProvider,\n            ChownMenuProvider,\n            ChgrpActionProvider,\n            ChgrpMenuProvider,\n            CopyMenuProvider,\n            CopyPathMenuProvider,\n            PasteMenuProvider,\n            CompressMenuProvider,\n            NewItemMenuProvider,\n            DeleteActionProvider,\n            ComputeDirectorySizesActionProvider,\n            DeleteMenuProvider,\n            ChownActionProvider,\n            ChmodActionProvider,\n            TarActionProvider,\n            UntarActionProvider,\n            ZipActionProvider,\n            UnzipActionProvider,\n            UnzipHereUnixMenuProvider,\n            UnzipDirectoryUnixMenuProvider,\n            UnzipHereWindowsActionProvider,\n            UnzipDirectoryWindowsActionProvider,\n            UntarHereMenuProvider,\n            UntarGzHereMenuProvider,\n            UntarDirectoryMenuProvider,\n            UntarGzDirectoryMenuProvider,\n            JavapMenuProvider,\n            JarMenuProvider,\n            MoveFileActionProvider,\n            NewFileActionProvider,\n            NewDirectoryActionProvider,\n            NewLinkActionProvider,\n            OpenDirectoryActionProvider,\n            OpenFileDefaultActionProvider,\n            OpenFileNativeDetailsActionProvider,\n            OpenFileWithActionProvider;\n    provides Module with\n            AppJacksonModule;\n    provides ModuleLayerLoader with\n            DataStorageExtensionProvider.Loader,\n            DataStoreProviders.Loader,\n            ActionProvider.Loader,\n            PrefsProvider.Loader,\n            LicenseProvider.Loader,\n            ScanProvider.Loader,\n            ShellDialects.Loader,\n            CloudSetupProvider.Loader;\n    provides SLF4JServiceProvider with\n            AppLogs.Slf4jProvider;\n    provides EventHandler with\n            EventHandlerImpl;\n    provides BeaconInterface with\n            ShellStartExchangeImpl,\n            ShellStopExchangeImpl,\n            ShellExecExchangeImpl,\n            ConnectionQueryExchangeImpl,\n            ConnectionInfoExchangeImpl,\n            ConnectionRemoveExchangeImpl,\n            ConnectionAddExchangeImpl,\n            CategoryAddExchangeImpl,\n            CategoryQueryExchangeImpl,\n            CategoryInfoExchangeImpl,\n            CategoryRemoveExchangeImpl,\n            ActionExchangeImpl,\n            ConnectionRefreshExchangeImpl,\n            DaemonOpenExchangeImpl,\n            DaemonFocusExchangeImpl,\n            DaemonStatusExchangeImpl,\n            DaemonStopExchangeImpl,\n            HandshakeExchangeImpl,\n            DaemonModeExchangeImpl,\n            FsBlobExchangeImpl,\n            FsReadExchangeImpl,\n            FsScriptExchangeImpl,\n            FsWriteExchangeImpl,\n            AskpassExchangeImpl,\n            TerminalPrepareExchangeImpl,\n            TerminalRegisterExchangeImpl,\n            TerminalWaitExchangeImpl,\n            TerminalLaunchExchangeImpl,\n            TerminalExternalLaunchExchangeImpl,\n            SshLaunchExchangeImpl,\n            DaemonVersionExchangeImpl,\n            SecretEncryptExchangeImpl,\n            SecretDecryptExchangeImpl;\n}\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/file_list.txt",
    "content": "access | accdb, accdt, mdb, accda, accdc, accde, accdp, accdr, accdu, ade, adp, laccdb, ldb, mam, maq, mdw | file_type_access.svg\naccess2 | accdb, accdt, mdb, accda, accdc, accde, accdp, accdr, accdu, ade, adp, laccdb, ldb, mam, maq, mdw | file_type_access2.svg\nactionscript | actionscript | file_type_actionscript.svg\nactionscript2 | actionscript | file_type_actionscript2.svg | file_type_light_actionscript2.svg\nada | ada | file_type_ada.svg | file_type_light_ada.svg\nadvpl | advpl | file_type_advpl.svg\nai | ai | file_type_ai.svg\nai2 | ai | file_type_ai2.svg\nal | al | file_type_al.svg\nallcontributors | .all-contributorsrc | file_type_allcontributors.svg\naffinitydesigner | afdesign, affinitydesigner | file_type_affinitydesigner.svg\naffinityphoto | afphoto, affinityphoto | file_type_affinityphoto.svg\naffinitypublisher | afpub, affinitypublisher | file_type_affinitypublisher.svg\nappscript | gs | file_type_appscript.svg\nfitbit | fba | file_type_fitbit.svg\nangular | .angular-cli.json, angular-cli.json, angular.json, .angular.json | file_type_angular.svg\nng_component_dart | component.dart | file_type_ng_component_dart.svg\nng_component_ts | component.ts | file_type_ng_component_ts.svg\nng_component_js | component.js | file_type_ng_component_js.svg\nng_controller_ts | controller.ts | file_type_ng_controller_ts.svg\nng_controller_js | controller.js | file_type_ng_controller_js.svg\nng_directive_dart | directive.dart | file_type_ng_directive_dart.svg\nng_directive_ts | directive.ts | file_type_ng_directive_ts.svg\nng_directive_js | directive.js | file_type_ng_directive_js.svg\nng_guard_dart | guard.dart | file_type_ng_guard_dart.svg\nng_guard_ts | guard.ts | file_type_ng_guard_ts.svg\nng_guard_js | guard.js | file_type_ng_guard_js.svg\nng_module_dart | module.dart | file_type_ng_module_dart.svg\nng_module_ts | module.ts | file_type_ng_module_ts.svg\nng_module_js | module.js | file_type_ng_module_js.svg\nng_pipe_dart | pipe.dart | file_type_ng_pipe_dart.svg\nng_pipe_ts | pipe.ts | file_type_ng_pipe_ts.svg\nng_pipe_js | pipe.js | file_type_ng_pipe_js.svg\nng_routing_dart | routing.dart | file_type_ng_routing_dart.svg\nng_routing_ts | routing.ts | file_type_ng_routing_ts.svg\nng_routing_js | routing.js | file_type_ng_routing_js.svg\nng_routing_dart | app-routing.module.dart | file_type_ng_routing_dart.svg\nng_routing_ts | app-routing.module.ts | file_type_ng_routing_ts.svg\nng_routing_js | app-routing.module.js | file_type_ng_routing_js.svg\nng_smart_component_dart | page.dart, container.dart | file_type_ng_smart_component_dart.svg\nng_smart_component_ts | page.ts, container.ts | file_type_ng_smart_component_ts.svg\nng_smart_component_js | page.js, container.js | file_type_ng_smart_component_js.svg\nng_service_dart | service.dart | file_type_ng_service_dart.svg\nng_service_ts | service.ts | file_type_ng_service_ts.svg\nng_service_js | service.js | file_type_ng_service_js.svg\nng_interceptor_dart | interceptor.dart | file_type_ng_interceptor_dart.svg\nng_interceptor_ts | interceptor.ts | file_type_ng_interceptor_ts.svg\nng_interceptor_js | interceptor.js | file_type_ng_interceptor_js.svg\nng_component_ts2 | component.ts | file_type_ng_component_ts2.svg\nng_component_js2 | component.js | file_type_ng_component_js2.svg\nng_directive_ts2 | directive.ts | file_type_ng_directive_ts2.svg\nng_directive_js2 | directive.js | file_type_ng_directive_js2.svg\nng_module_ts2 | module.ts | file_type_ng_module_ts2.svg\nng_module_js2 | module.js | file_type_ng_module_js2.svg\nng_pipe_ts2 | pipe.ts | file_type_ng_pipe_ts2.svg\nng_pipe_js2 | pipe.js | file_type_ng_pipe_js2.svg\nng_routing_ts2 | routing.ts | file_type_ng_routing_ts2.svg\nng_routing_js2 | routing.js | file_type_ng_routing_js2.svg\nng_routing_ts2 | app-routing.module.ts | file_type_ng_routing_ts2.svg\nng_routing_js2 | app-routing.module.js | file_type_ng_routing_js2.svg\nng_smart_component_ts2 | page.ts, container.ts | file_type_ng_smart_component_ts2.svg\nng_smart_component_js2 | page.js, container.js | file_type_ng_smart_component_js2.svg\nng_service_ts2 | service.ts | file_type_ng_service_ts2.svg\nng_service_js2 | service.js | file_type_ng_service_js2.svg\nng_tailwind | ng-tailwind.js | file_type_ng_tailwind.svg\naffectscript | affectscript | file_type_affectscript.svg\nansible | ansible | file_type_ansible.svg\nantlr | antlr | file_type_antlr.svg\nanyscript | anyscript | file_type_anyscript.svg\napache | apacheconf | file_type_apache.svg\napex | apex | file_type_apex.svg\napib | apiblueprint | file_type_apib.svg\napi_extractor | api-extractor.json, api-extractor-base.json | file_type_api_extractor.svg\napl | apl | file_type_apl.svg | file_type_light_apl.svg\napplescript | applescript | file_type_applescript.svg\nappsemble | .appsemblerc.yaml, app-definition.yaml | file_type_appsemble.svg\nappveyor | appveyor.yml, .appveyor.yml | file_type_appveyor.svg\narduino | ino, pde | file_type_arduino.svg\nasciidoc | asciidoc | file_type_asciidoc.svg\nasp | asp, asp (html) | file_type_asp.svg\naspx | aspx, ascx | file_type_aspx.svg\nassembly | arm, asm | file_type_assembly.svg\nastro | astro | file_type_astro.svg\nastroconfig | astro.config.js, astro.config.cjs, astro.config.mjs, astro.config.ts | file_type_astroconfig.svg\nats | ats | file_type_ats.svg\naudio | aac, act, aiff, amr, ape, au, dct, dss, dvf, flac, gsm, iklax, ivs, m4a, m4b, m4p, mmf, mogg, mp3, mpc, msv, oga, ogg, opus, ra, raw, tta, vox, wav, wma | file_type_audio.svg\naurelia | aurelia.json | file_type_aurelia.svg\nautohotkey | ahk | file_type_autohotkey.svg\nautoit | autoit | file_type_autoit.svg\navif | avif | file_type_avif.svg\navro | avro | file_type_avro.svg\nawk | awk | file_type_awk.svg\naws | <sub></sub> | file_type_aws.svg\nazure | azcli | file_type_azure.svg\nazurepipelines | azure-pipelines.yml, .vsts-ci.yml, azure-pipelines | file_type_azurepipelines.svg\nbabel | .babelrc, .babelignore, .babelrc.js, .babelrc.cjs, .babelrc.mjs, .babelrc.json, babel.config.js, babel.config.cjs, babel.config.mjs, babel.config.json | file_type_babel.svg | file_type_light_babel.svg\nbabel2 | .babelrc, .babelignore, .babelrc.js, .babelrc.cjs, .babelrc.mjs, .babelrc.json, babel.config.js, babel.config.cjs, babel.config.mjs, babel.config.json | file_type_babel2.svg | file_type_light_babel2.svg\nballerina | ballerina | file_type_ballerina.svg\nbat | bat | file_type_bat.svg\nbats | bats | file_type_bats.svg\nbazaar | .bzrignore | file_type_bazaar.svg\nbazel | BUILD.bazel, .bazelrc, bazel.rc, bazel.bazelrc, bazel, starlark | file_type_bazel.svg\nbefunge | befunge, befunge98 | file_type_befunge.svg\nbicep | bicep | file_type_bicep.svg\nbiml | biml | file_type_biml.svg\nbinary | a, app, bin, cmo, cmx, cma, cmxa, cmi, dll, exe, hl, ilk, lib, n, ndll, o, obj, pyc, pyd, pyo, pdb, scpt, scptd, so | file_type_binary.svg\nbithound | .bithoundrc | file_type_bithound.svg\nbitbucketpipeline | bitbucket-pipelines.yml | file_type_bitbucketpipeline.svg\nblade | blade, laravel-blade | file_type_blade.svg\nblitzbasic | bb, blitzbasic | file_type_blitzbasic.svg\nbolt | bolt | file_type_bolt.svg\nbosque | bosque | file_type_bosque.svg\nbower | .bowerrc, bower.json | file_type_bower.svg\nbrowserslist | .browserslistrc, browserslist | file_type_browserslist.svg\nbuckbuild | .buckconfig | file_type_buckbuild.svg\nbundler | gemfile, gemfile.lock | file_type_bundler.svg\nbundler | gemfile, gemfile.lock | file_type_bundler.svg\nc | c | file_type_c.svg\nc2 | c | file_type_c2.svg\nc3 | c | file_type_c3.svg\nc_al | c-al | file_type_c_al.svg\ncabal | cabal | file_type_cabal.svg\ncaddy | caddyfile | file_type_caddy.svg\ncake | cake | file_type_cake.svg\ncakephp | <sub></sub> | file_type_cakephp.svg\ncapacitor | capacitor.config.json | file_type_capacitor.svg\ncargo | cargo.toml, cargo.lock | file_type_cargo.svg\ncasc | casc | file_type_casc.svg\ncddl | cddl | file_type_cddl.svg\ncert | csr, crt, cer, der, pfx, p12, p7b, p7r, src, crl, sst, stl | file_type_cert.svg\nceylon | ceylon | file_type_ceylon.svg\ncf | lucee, cfml, lang-cfml | file_type_cf.svg\ncf2 | lucee, cfml, lang-cfml | file_type_cf2.svg\ncfc | cfc | file_type_cfc.svg\ncfc2 | cfc | file_type_cfc2.svg\ncfm | cfmhtml | file_type_cfm.svg\ncfm2 | cfmhtml | file_type_cfm2.svg\ncheader | h | file_type_cheader.svg\nchef | chefignore, berksfile, berksfile.lock, policyfile.rb, policyfile.lock.json | file_type_chef.svg\nclass | class | file_type_class.svg\ncircleci | circle.yml | file_type_circleci.svg | file_type_light_circleci.svg\nclojure | cjm, cljc, clojure | file_type_clojure.svg\nclojurescript | cljs, clojurescript | file_type_clojurescript.svg\ncloudfoundry | .cfignore, manifest-yaml | file_type_cloudfoundry.svg | file_type_light_cloudfoundry.svg\ncmake | cmake, cmake-cache | file_type_cmake.svg\ncobol | cobol | file_type_cobol.svg\ncodeql | ql | file_type_codeql.svg\ncodeowners | codeowners | file_type_codeowners.svg | file_type_light_codeowners.svg\ncodacy | .codacy.yml, .codacy.yaml | file_type_codacy.svg | file_type_light_codacy.svg\ncodeclimate | .codeclimate.yml | file_type_codeclimate.svg | file_type_light_codeclimate.svg\ncodecov | codecov.yml, .codecov.yml | file_type_codecov.svg\ncodekit | kit | file_type_codekit.svg\ncodekit | config.codekit, config.codekit2, config.codekit3, .config.codekit, .config.codekit2, .config.codekit3 | file_type_codekit.svg\ncoffeelint | coffeelint.json, .coffeelintignore | file_type_coffeelint.svg\ncoffeescript | coffeescript | file_type_coffeescript.svg\nconan | conanfile.txt, conanfile.py | file_type_conan.svg\nconda | .condarc | file_type_conda.svg\nconfig | plist, properties, dotenv, env | file_type_config.svg | file_type_light_config.svg\nconfig | .tool-versions | file_type_config.svg | file_type_light_config.svg\ncommitizen | .czrc, .cz.json | file_type_commitizen.svg\ncommitlint | .commitlintrc | file_type_commitlint.svg\ncommitlint | commitlint.config.js, commitlint.config.cjs, commitlint.config.ts, .commitlintrc.json, .commitlintrc.yaml, .commitlintrc.yml, .commitlintrc.js, .commitlintrc.cjs, .commitlintrc.ts | file_type_commitlint.svg\ncompass | <sub></sub> | file_type_compass.svg\ncomposer | composer.json, composer.lock | file_type_composer.svg\nchef_cookbook | cookbook | file_type_chef_cookbook.svg\nconfluence | confluence | file_type_confluence.svg\ncoveralls | .coveralls.yml | file_type_coveralls.svg\ncpp | cpp | file_type_cpp.svg\ncpp2 | cpp | file_type_cpp2.svg\ncpp3 | cpp | file_type_cpp3.svg\ncppheader | hpp, hh, hxx, h++ | file_type_cppheader.svg\ncrowdin | crowdin.yml | file_type_crowdin.svg\ncrystal | crystal | file_type_crystal.svg | file_type_light_crystal.svg\ncsharp | csx, csharp | file_type_csharp.svg\ncsharp2 | csx, csharp | file_type_csharp2.svg\ncsproj | csproj | file_type_csproj.svg\ncss | css | file_type_css.svg\ncsscomb | .csscomb.json | file_type_csscomb.svg\ncsslint | .csslintrc | file_type_csslint.svg\ncssmap | css.map | file_type_cssmap.svg\ncucumber | feature | file_type_cucumber.svg\ncuda | cuda, cuda-cpp | file_type_cuda.svg\ncython | cython | file_type_cython.svg\ncypress | cypress.json, cypress.env.json, cypress.config.js, cypress.config.ts, cypress.config.cjs, cypress.config.mjs | file_type_cypress.svg | file_type_light_cypress.svg\ncypress_spec | cy.js, cy.mjs, cy.cjs, cy.coffee, cy.ts, cy.tsx, cy.jsx | file_type_cypress_spec.svg | file_type_light_cypress_spec.svg\ncvs | .cvsignore | file_type_cvs.svg\ndal | dal | file_type_dal.svg\ndarcs | .boringignore | file_type_darcs.svg\ndartlang | dart | file_type_dartlang.svg\ndartlang_generated | g.dart, freezed.dart | file_type_dartlang_generated.svg\ndartlang_ignore | .pubignore | file_type_dartlang_ignore.svg\ndb | db | file_type_db.svg | file_type_light_db.svg\ndependabot | dependabot.yml | file_type_dependabot.svg\ndependencies | dependencies.yml | file_type_dependencies.svg\ndelphi | pascal, objectpascal | file_type_delphi.svg\ndevcontainer | devcontainer.json, .devcontainer.json | file_type_devcontainer.svg\ndhall | dhall | file_type_dhall.svg | file_type_light_dhall.svg\ndjango | djt, django-html, django-txt | file_type_django.svg\ndlang | d, dscript, dml, diet | file_type_dlang.svg\ndiff | diff | file_type_diff.svg\ndocker | .dockerignore, compose.yaml, compose.yml, docker-compose.yaml, docker-compose.yml, docker-compose.ci-build.yaml, docker-compose.ci-build.yml, docker-compose.override.yaml, docker-compose.override.yml, docker-compose.vs.debug.yaml, docker-compose.vs.debug.yml, docker-compose.vs.release.yaml, docker-compose.vs.release.yml, docker-cloud.yaml, docker-cloud.yml, dockerfile | file_type_docker.svg\ndocker2 | .dockerignore, compose.yaml, compose.yml, docker-compose.yaml, docker-compose.yml, docker-compose.ci-build.yaml, docker-compose.ci-build.yml, docker-compose.override.yaml, docker-compose.override.yml, docker-compose.vs.debug.yaml, docker-compose.vs.debug.yml, docker-compose.vs.release.yaml, docker-compose.vs.release.yml, docker-cloud.yaml, docker-cloud.yml, dockerfile | file_type_docker2.svg\ndockertest | docker-compose.test.yml | file_type_dockertest.svg\ndockertest2 | docker-compose.test.yml | file_type_dockertest2.svg\ndocpad | eco | file_type_docpad.svg | file_type_light_docpad.svg\ndocz | .doczrc, docz.js, docz.json, .docz.js, .docz.json, doczrc.js, doczrc.json, docz.config.js, docz.config.json | file_type_docz.svg\ndojo | .dojorc | file_type_dojo.svg\ndoxygen | doxygen | file_type_doxygen.svg\ndrawio | drawio, dio .drawio.png, .drawio.svg, .dio.png, .dio.svg | file_type_drawio.svg\ndrone | .drone.yml, .drone.yml.sig | file_type_drone.svg | file_type_light_drone.svg\ndrools | drools | file_type_drools.svg\ndotjs | dotjs | file_type_dotjs.svg\ndustjs | dustjs | file_type_dustjs.svg\ndvc | .dvc | file_type_dvc.svg\ndylan | dylan, dylan-lid | file_type_dylan.svg\neditorconfig | .editorconfig | file_type_editorconfig.svg\nearthly | .earthlyignore, Earthfile, earthfile | file_type_earthly.svg\nedge | edge | file_type_edge.svg\nedge2 | edge | file_type_edge2.svg\neex | eex, html-eex | file_type_eex.svg\nejs | ejs | file_type_ejs.svg\nelastic | es | file_type_elastic.svg\nelasticbeanstalk | <sub></sub> | file_type_elasticbeanstalk.svg\nelixir | elixir | file_type_elixir.svg\nelm | elm-package.json, elm | file_type_elm.svg\nelm2 | elm-package.json, elm | file_type_elm2.svg\nemacs | el, elc | file_type_emacs.svg\nember | .ember-cli | file_type_ember.svg\nensime | ensime | file_type_ensime.svg\neps | eps | file_type_eps.svg\nerb | erb, html.erb | file_type_erb.svg\nerlang | emakefile, .emakerfile, erlang | file_type_erlang.svg\nerlang2 | emakefile, .emakerfile, erlang | file_type_erlang2.svg\neslint | .eslintrc, .eslintignore, .eslintcache, .eslintrc.js, .eslintrc.mjs, .eslintrc.cjs, .eslintrc.json, .eslintrc.yaml, .eslintrc.yml | file_type_eslint.svg\neslint2 | .eslintrc, .eslintignore, .eslintcache, .eslintrc.js, .eslintrc.mjs, .eslintrc.cjs, .eslintrc.json, .eslintrc.yaml, .eslintrc.yml | file_type_eslint2.svg\nexcel | xls, xlsx, xlsm, ods, fods, xlsb | file_type_excel.svg\nexcel2 | xls, xlsx, xlsm, ods, fods, xlsb | file_type_excel2.svg\nexpo | app.json, app.config.js, app.config.json, app.config.json5 | file_type_expo.svg | file_type_light_expo.svg\nfalcon | falcon | file_type_falcon.svg\nfauna | .faunarc, fql | file_type_fauna.svg\nfavicon | favicon.ico | file_type_favicon.svg\nfbx | fbx | file_type_fbx.svg\nfirebase | .firebaserc | file_type_firebase.svg\nfirebasehosting | firebase.json | file_type_firebasehosting.svg | file_type_light_firebasehosting.svg\nfirestore | firestore.rules, firestore.indexes.json | file_type_firestore.svg\nfla | fla | file_type_fla.svg | file_type_light_fla.svg\nflareact | flareact.config.js | file_type_flareact.svg\nflash | swf, swc | file_type_flash.svg\nfloobits | .flooignore | file_type_floobits.svg\nflow | js.flow | file_type_flow.svg\nflow | .flowconfig | file_type_flow.svg\nflutter | .flutter-plugins, .metadata | file_type_flutter.svg\nflutter_package | pubspec.lock, pubspec.yaml, .packages | file_type_flutter_package.svg\nfont | woff, woff2, ttf, otf, eot, pfa, pfb, sfd | file_type_font.svg | file_type_light_font.svg\nformkit | formkit.config.js, formkit.config.mjs, formkit.config.cjs, formkit.config.ts | file_type_formkit.svg\nfortran | fortran, fortran-modern, FortranFreeForm, FortranFixedForm, fortran_fixed-form | file_type_fortran.svg\nfossa | .fossaignore | file_type_fossa.svg\nfossil | ignore-glob | file_type_fossil.svg\nfsharp | fsharp | file_type_fsharp.svg\nfsproj | fsproj | file_type_fsproj.svg\nfreemarker | ftl | file_type_freemarker.svg\nfthtml | fthtml | file_type_fthtml.svg\nfunding | funding.yml | file_type_funding.svg\nfusebox | fuse.js | file_type_fusebox.svg\ngalen | galen | file_type_galen.svg\ngalen2 | galen | file_type_galen2.svg\ngit | .gitattributes, .gitconfig, .gitignore, .gitmodules, .gitkeep, .mailmap, .issuetracker, git-commit, git-rebase, ignore | file_type_git.svg\ngamemaker | gmx, gml-gms | file_type_gamemaker.svg\ngamemaker2 | yy, yyp, gml-gms2 | file_type_gamemaker2.svg | file_type_light_gamemaker2.svg\ngamemaker81 | gml-gm81 | file_type_gamemaker81.svg\ngatsby | gatsby-browser.js, gatsby-browser.ts, gatsby-browser.tsx, gatsby-ssr.js, gatsby-ssr.ts, gatsby-ssr.tsx | file_type_gatsby.svg\ngatsby | gatsby-config.js, gatsby-config.ts, gatsby-node.js, gatsby-node.ts | file_type_gatsby.svg\ngcode | gcode | file_type_gcode.svg\ngenstat | genstat | file_type_genstat.svg\ngitlab | .gitlab-ci.yml | file_type_gitlab.svg\ngitpod | .gitpod.yaml, .gitpod.yml, gitpod.yaml, gitpod.yml | file_type_gitpod.svg\nglide | glide.yml | file_type_glide.svg\nglitter | .glitterrc | file_type_glitter.svg\nglsl | glsl | file_type_glsl.svg\nglyphs | glyphs | file_type_glyphs.svg\ngnuplot | gnuplot | file_type_gnuplot.svg\ngo | go | file_type_go.svg\ngo_package | go.sum, go.mod | file_type_go_package.svg\ngoctl | goctl | file_type_goctl.svg\ngodot | gdscript | file_type_godot.svg\ngradle | gradle | file_type_gradle.svg | file_type_light_gradle.svg\ngradle2 | gradle | file_type_gradle2.svg\ngraphql | .gqlconfig, graphql | file_type_graphql.svg\ngraphql_config | .graphqlconfig, .graphqlconfig.yml, .graphqlconfig.yaml | file_type_graphql_config.svg\ngraphviz | dot | file_type_graphviz.svg\ngreenkeeper | greenkeeper.json | file_type_greenkeeper.svg\ngridsome | gridsome.config.js, gridsome.config.ts, gridsome.server.js, gridsome.server.ts, gridsome.client.js, gridsome.client.ts | file_type_gridsome.svg\ngroovy | groovy | file_type_groovy.svg\ngroovy2 | groovy | file_type_groovy2.svg\ngrunt | gruntfile.js, gruntfile.coffee, gruntfile.ts, gruntfile.babel.js, gruntfile.babel.coffee, gruntfile.babel.ts | file_type_grunt.svg\ngulp | gulpfile.js, gulpfile.coffee, gulpfile.ts, gulpfile.mjs, gulpfile.esm.js, gulpfile.esm.coffee, gulpfile.esm.ts, gulpfile.esm.mjs, gulpfile.babel.js, gulpfile.babel.coffee, gulpfile.babel.ts, gulpfile.babel.mjs | file_type_gulp.svg\nhaml | haml | file_type_haml.svg\nhandlebars | handlebars | file_type_handlebars.svg\nhandlebars2 | handlebars | file_type_handlebars2.svg\nharbour | harbour | file_type_harbour.svg\nhardhat | hardhat.config.js, hardhat.config.ts | file_type_hardhat.svg\nhaskell | haskell, literate haskell | file_type_haskell.svg\nhaskell2 | haskell, literate haskell | file_type_haskell2.svg\nhaxe | haxelib.json, haxe, hxml, Haxe AST dump | file_type_haxe.svg\nhaxecheckstyle | checkstyle.json | file_type_haxecheckstyle.svg\nhaxedevelop | hxproj | file_type_haxedevelop.svg\nhelix | .p4ignore | file_type_helix.svg\nhelm | chart.lock, chart.yaml, helm | file_type_helm.svg\nhjson | hjson | file_type_hjson.svg | file_type_light_hjson.svg\nhlsl | hlsl | file_type_hlsl.svg\nhomeassistant | home-assistant | file_type_homeassistant.svg\nhorusec | horusec-config.json | file_type_horusec.svg\nhost | hosts | file_type_host.svg\nhtml | html | file_type_html.svg\nhtmlhint | .htmlhintrc | file_type_htmlhint.svg\nhttp | http | file_type_http.svg\nhunspell | hunspell.aff, hunspell.dic | file_type_hunspell.svg\nhusky | .huskyrc, husky.config.js, .huskyrc.js, .huskyrc.json, .huskyrc.yaml, .huskyrc.yml | file_type_husky.svg\nhy | hy | file_type_hy.svg\nhygen | ejs.t | file_type_hygen.svg\nhypr | hypr | file_type_hypr.svg\nicl | icl | file_type_icl.svg\nidris | idr, lidr | file_type_idris.svg\nidrisbin | ibc | file_type_idrisbin.svg\nidrispkg | ipkg | file_type_idrispkg.svg\nimage | jpeg, jpg, gif, png, bmp, tiff, ico, webp | file_type_image.svg\nimba | imba, imba2, imba | file_type_imba.svg\ninc | inc, include | file_type_inc.svg\ninfopath | infopathxml, xsn, xsf, xtp2 | file_type_infopath.svg\ninformix | 4GL | file_type_informix.svg\nini | ini | file_type_ini.svg | file_type_light_ini.svg\nink | ink | file_type_ink.svg\ninnosetup | innosetup | file_type_innosetup.svg\nionic | ionic.project, ionic.config.json | file_type_ionic.svg\njake | jakefile, jakefile.js | file_type_jake.svg\njanet | janet | file_type_janet.svg\njar | jar | file_type_jar.svg\njasmine | jasmine.json | file_type_jasmine.svg\njava | java | file_type_java.svg\njbuilder | jbuilder | file_type_jbuilder.svg\njest | jest.config.json, jest.config.base.json, jest.config.common.json, jest.config.ts, jest.config.base.ts, jest.config.common.ts, jest.json, .jestrc, .jestrc.js, .jestrc.json, jest.config.js, jest.config.cjs, jest.config.mjs, jest.config.base.js, jest.config.base.cjs, jest.config.base.mjs, jest.config.common.js, jest.config.common.cjs, jest.config.common.mjs, jest.config.babel.js, jest.config.babel.cjs, jest.config.babel.mjs | file_type_jest.svg\njest_snapshot | js.snap, jsx.snap, ts.snap, tsx.snap | file_type_jest_snapshot.svg\njekyll | jekyll | file_type_jekyll.svg\njenkins | jenkins, declarative, jenkinsfile | file_type_jenkins.svg\njinja | jinja, jinja-html, jinja-xml, jinja-css, jinja-json, jinja-md, jinja-py, jinja-rb, jinja-js, jinja-yaml, jinja-toml, jinja-latex, jinja-lua, jinja-properties, jinja-shell, jinja-dockerfile, jinja-sql, jinja-terraform, jinja-nginx, jinja-groovy, jinja-systemd, jinja-cpp | file_type_jinja.svg\njpm | .jpmignore | file_type_jpm.svg\njs | javascript | file_type_js.svg | file_type_light_js.svg\njs_official | javascript | file_type_js_official.svg\njsbeautify | .jsbeautifyrc, jsbeautifyrc, .jsbeautify, jsbeautify | file_type_jsbeautify.svg\njsconfig | jsconfig.json | file_type_jsconfig.svg | file_type_light_jsconfig.svg\njscpd | .jscpd.json, jscpd-report.xml, jscpd-report.json, jscpd-report.html | file_type_jscpd.svg\njshint | .jshintrc, .jshintignore | file_type_jshint.svg\njsmap | js.map, cjs.map, mjs.map | file_type_jsmap.svg | file_type_light_jsmap.svg\njson | jsonl, ndjson, json, json-tmlanguage, jsonc | file_type_json.svg | file_type_light_json.svg\njson_official | jsonl, ndjson, json, json-tmlanguage, jsonc | file_type_json_official.svg\njson2 | jsonl, ndjson, json, json-tmlanguage, jsonc | file_type_json2.svg\njsonnet | jsonnet | file_type_jsonnet.svg\njson5 | json5, json5 | file_type_json5.svg | file_type_light_json5.svg\njsonld | jsonld, json-ld | file_type_jsonld.svg | file_type_light_jsonld.svg\njsp | jsp | file_type_jsp.svg\njss | jss | file_type_jss.svg\njulia | julia, juliamarkdown | file_type_julia.svg\njulia2 | julia, juliamarkdown | file_type_julia2.svg\njupyter | ipynb | file_type_jupyter.svg\nio | io | file_type_io.svg | file_type_light_io.svg\niodine | iodine | file_type_iodine.svg\nk | k | file_type_k.svg\nkarma | karma.conf.js, karma.conf.coffee, karma.conf.ts | file_type_karma.svg\nkey | key, pem | file_type_key.svg\nkite | .kiteignore | file_type_kite.svg | file_type_light_kite.svg\nkitchenci | .kitchen.yml, kitchen.yml | file_type_kitchenci.svg\nkivy | kivy | file_type_kivy.svg\nkos | kos | file_type_kos.svg\nkotlin | kotlin | file_type_kotlin.svg\nkusto | kusto | file_type_kusto.svg\nlatino | latino | file_type_latino.svg\nlayout | master, layout.html, layout.htm | file_type_layout.svg\nlayout | layout.html, layout.htm | file_type_layout.svg\nlerna | lerna.json | file_type_lerna.svg | file_type_light_lerna.svg\nless | less | file_type_less.svg\nlex | lex | file_type_lex.svg\nlicense | enc, license, lic | file_type_license.svg\nlicense | license, licence, copying, copying.lesser, license-mit, license-apache, license.md, license.txt, licence.md, licence.txt, copying.md, copying.txt, copying.lesser.md, copying.lesser.txt, license-mit.md, license-mit.txt, license-apache.md, license-apache.txt | file_type_license.svg\nlicensebat | .licrc | file_type_licensebat.svg\nlighthouse | .lighthouserc.js, .lighthouserc.json, .lighthouserc.yaml, .lighthouserc.yml | file_type_lighthouse.svg\nlisp | lisp, autolisp, autolispdcl | file_type_lisp.svg\nlime | hxp | file_type_lime.svg\nlime | include.xml | file_type_lime.svg\nlintstagedrc | .lintstagedrc, .lintstagedrc.json, .lintstagedrc.yaml, .lintstagedrc.yml, .lintstagedrc.mjs, .lintstagedrc.js, .lintstagedrc.cjs, lint-staged.config.mjs, lint-staged.config.js, lint-staged.config.cjs | file_type_lintstagedrc.svg\nliquid | liquid | file_type_liquid.svg\nlivescript | ls | file_type_livescript.svg\nlnk | url, lnk | file_type_lnk.svg\nlocale | <sub></sub> | file_type_locale.svg\nlog | log, tlg | file_type_log.svg\nlog | log, tlg, log | file_type_log.svg\nlolcode | lolcode | file_type_lolcode.svg\nlsl | lsl | file_type_lsl.svg\nlua | lua | file_type_lua.svg\nluau | luau | file_type_luau.svg\nlync | crec, ocrec | file_type_lync.svg\nmakefile | makefile, makefile | file_type_makefile.svg\nmanifest | manifest | file_type_manifest.svg\nmanifest_skip | manifest.skip | file_type_manifest_skip.svg\nmanifest_bak | manifest.bak | file_type_manifest_bak.svg\nmap | map | file_type_map.svg\nmarkdown | md, mdown, markdown, markdown | file_type_markdown.svg\nmarkdownlint | .markdownlint.json | file_type_markdownlint.svg\nmarkdownlint_ignore | .markdownlintignore | file_type_markdownlint_ignore.svg\nmarko | marko | file_type_marko.svg\nmarkojs | marko.js | file_type_markojs.svg\nmatlab | fig, mex, mexn, mexrs6, mn, mum, mx, mx3, rwd, slx, slddc, smv, xvc, matlab | file_type_matlab.svg\nmaxscript | maxscript | file_type_maxscript.svg\nmaven | maven.config, pom.xml, extensions.xml, settings.xml | file_type_maven.svg\nmaya | mel | file_type_maya.svg\nmdx | mdx | file_type_mdx.svg | file_type_light_mdx.svg\nmediawiki | mediawiki | file_type_mediawiki.svg\nmercurial | .hgignore | file_type_mercurial.svg\nmeson | meson | file_type_meson.svg\nmeteor | <sub></sub> | file_type_meteor.svg\nmjml | mjml | file_type_mjml.svg\nmlang | powerquery | file_type_mlang.svg | file_type_light_mlang.svg\nmocha | mocha.opts, .mocharc.js, .mocharc.json, .mocharc.jsonc, .mocharc.yaml, .mocharc.yml | file_type_mocha.svg\nmodernizr | modernizr, modernizr.js, modernizrrc.js, .modernizr.js, .modernizrrc.js | file_type_modernizr.svg\nmojolicious | mojolicious | file_type_mojolicious.svg\nmoleculer | moleculer.config.js, moleculer.config.json, moleculer.config.ts | file_type_moleculer.svg\nmongo | mongo | file_type_mongo.svg\nmonotone | .mtn-ignore | file_type_monotone.svg\nmson | mson | file_type_mson.svg\nmustache | mustache, mst | file_type_mustache.svg | file_type_light_mustache.svg\nndst | ndst.yaml, ndst.yml, ndst.json | file_type_ndst.svg\nnearly | nearley | file_type_nearly.svg\nnestjs | .nest-cli.json, nest-cli.json, nestconfig.json, .nestconfig.json | file_type_nestjs.svg\nnest_adapter_js | adapter.js | file_type_nest_adapter_js.svg\nnest_adapter_ts | adapter.ts | file_type_nest_adapter_ts.svg\nnest_controller_js | controller.js | file_type_nest_controller_js.svg\nnest_controller_ts | controller.ts | file_type_nest_controller_ts.svg\nnest_decorator_js | decorator.js | file_type_nest_decorator_js.svg\nnest_decorator_ts | decorator.ts | file_type_nest_decorator_ts.svg\nnest_filter_js | filter.js | file_type_nest_filter_js.svg\nnest_filter_ts | filter.ts | file_type_nest_filter_ts.svg\nnest_gateway_js | gateway.js | file_type_nest_gateway_js.svg\nnest_gateway_ts | gateway.ts | file_type_nest_gateway_ts.svg\nnest_guard_js | guard.js | file_type_nest_guard_js.svg\nnest_guard_ts | guard.ts | file_type_nest_guard_ts.svg\nnest_interceptor_js | interceptor.js | file_type_nest_interceptor_js.svg\nnest_interceptor_ts | interceptor.ts | file_type_nest_interceptor_ts.svg\nnest_middleware_js | middleware.js | file_type_nest_middleware_js.svg\nnest_middleware_ts | middleware.ts | file_type_nest_middleware_ts.svg\nnest_module_js | module.js | file_type_nest_module_js.svg\nnest_module_ts | module.ts | file_type_nest_module_ts.svg\nnest_pipe_js | pipe.js | file_type_nest_pipe_js.svg\nnest_pipe_ts | pipe.ts | file_type_nest_pipe_ts.svg\nnest_service_js | service.js | file_type_nest_service_js.svg\nnest_service_ts | service.ts | file_type_nest_service_ts.svg\nnetlify | netlify.toml | file_type_netlify.svg\nnext | next.config.js, next.config.mjs | file_type_next.svg | file_type_light_next.svg\nnginx | nginx.conf | file_type_nginx.svg\nnim | nim | file_type_nim.svg\nnimble | nimble | file_type_nimble.svg\nninja | build.ninja | file_type_ninja.svg\nnoc | noc | file_type_noc.svg\nnix | nix | file_type_nix.svg\nnjsproj | njsproj | file_type_njsproj.svg\nnode | .node-version, .nvmrc | file_type_node.svg\nnode2 | .node-version, .nvmrc | file_type_node2.svg\nnodemon | nodemon.json | file_type_nodemon.svg\nnpm | .npmignore, .npmrc, package.json, package-lock.json, npm-shrinkwrap.json | file_type_npm.svg\nnsi | nsis, nfl, nsl, bridlensis | file_type_nsi.svg\nnsri | .nsrirc, .nsriignore, nsri.config.js, .nsrirc.js, .nsrirc.json, .nsrirc.yaml, .nsrirc.yml | file_type_nsri.svg\nnsri-integrity | .integrity.json | file_type_nsri-integrity.svg\nnuget | nupkg, snupkg, nuspec, psmdcp | file_type_nuget.svg\nnumpy | npy, npz | file_type_numpy.svg\nnunjucks | nunj, njs, nunjucks | file_type_nunjucks.svg\nnuxt | nuxt.config.js, nuxt.config.ts | file_type_nuxt.svg\nnyc | .nycrc, .nycrc.json | file_type_nyc.svg\nobjectivec | objective-c | file_type_objectivec.svg\nobjectivecpp | objective-cpp | file_type_objectivecpp.svg\nobjidconfig | .objidconfig | file_type_objidconfig.svg | file_type_light_objidconfig.svg\nocaml | .merlin, ocaml, ocamllex, menhir | file_type_ocaml.svg\nogone | ogone | file_type_ogone.svg\nonenote | one, onepkg, onetoc, onetoc2, sig | file_type_onenote.svg\nopenscad | scad | file_type_openscad.svg\nopencl | cl, opencl | file_type_opencl.svg\nopenHAB | openhab | file_type_openHAB.svg | file_type_light_openHAB.svg\norg | org | file_type_org.svg\noutlook | pst, bcmx, otm, msg, oft | file_type_outlook.svg\novpn | ovpn | file_type_ovpn.svg\npackage | pkg | file_type_package.svg\npaket | paket.dependencies, paket.lock, paket.references, paket.template, paket.local | file_type_paket.svg\npatch | patch | file_type_patch.svg\npcl | pcd | file_type_pcl.svg | file_type_light_pcl.svg\npddl | pddl | file_type_pddl.svg\npddl_plan | plan | file_type_pddl_plan.svg\npddl_happenings | happenings | file_type_pddl_happenings.svg\npdf | pdf | file_type_pdf.svg\npdf2 | pdf | file_type_pdf2.svg\npeeky | peeky.config.ts, peeky.config.js, peeky.config.mjs | file_type_peeky.svg\nperl | perl | file_type_perl.svg\nperl2 | perl | file_type_perl2.svg\nperl6 | perl6 | file_type_perl6.svg\npgsql | pgsql | file_type_pgsql.svg\nphotoshop | psd | file_type_photoshop.svg\nphotoshop2 | psd | file_type_photoshop2.svg\nphp | php1, php2, php3, php4, php5, php6, phps, phpsa, phpt, phtml, phar, php | file_type_php.svg\nphp2 | php1, php2, php3, php4, php5, php6, phps, phpsa, phpt, phtml, phar, php | file_type_php2.svg\nphp3 | php1, php2, php3, php4, php5, php6, phps, phpsa, phpt, phtml, phar, php | file_type_php3.svg\nphpcsfixer | .php_cs, .php_cs.dist | file_type_phpcsfixer.svg\nphpunit | phpunit, phpunit.xml, phpunit.xml.dist | file_type_phpunit.svg\nphraseapp | .phraseapp.yml | file_type_phraseapp.svg\npine | pine, pinescript | file_type_pine.svg\npip | pipfile, pipfile.lock, pip-requirements | file_type_pip.svg\npipeline | pipeline | file_type_pipeline.svg\nplatformio | platformio.ini, platformio-debug.disassembly, platformio-debug.memoryview, platformio-debug.asm | file_type_platformio.svg\nplantuml | pu, plantuml, iuml, puml | file_type_plantuml.svg\nplaywright | playwright.config.js, playwright.config.ts | file_type_playwright.svg\nplsql | plsql, oracle | file_type_plsql.svg\nplsql_package | pck | file_type_plsql_package.svg\nplsql_package_body | pkb | file_type_plsql_package_body.svg\nplsql_package_header | pkh | file_type_plsql_package_header.svg\nplsql_package_spec | pks | file_type_plsql_package_spec.svg\npnpm | pnpmfile.js, pnpm-lock.yaml, pnpm-workspace.yaml | file_type_pnpm.svg | file_type_light_pnpm.svg\npoedit | po, mo | file_type_poedit.svg\npolymer | polymer | file_type_polymer.svg\npony | pony | file_type_pony.svg\npostcss | postcss | file_type_postcss.svg\npostcssconfig | .postcssrc, .postcssrc.json, .postcssrc.yaml, .postcssrc.yml, .postcssrc.ts, .postcssrc.js, .postcssrc.cjs, postcss.config.ts, postcss.config.js, postcss.config.cjs | file_type_postcssconfig.svg\npowerpoint | pot, potx, potm, pps, ppsx, ppsm, ppt, pptx, pptm, pa, ppa, ppam, sldm, sldx | file_type_powerpoint.svg\npowerpoint2 | pot, potx, potm, pps, ppsx, ppsm, ppt, pptx, pptm, pa, ppa, ppam, sldm, sldx | file_type_powerpoint2.svg\npowershell | ps1, powershell | file_type_powershell.svg\npowershell_psm | psm1 | file_type_powershell_psm.svg\npowershell_psd | psd1 | file_type_powershell_psd.svg\npowershell_format | format.ps1xml | file_type_powershell_format.svg\npowershell_types | types.ps1xml | file_type_powershell_types.svg\npowershell2 | powershell | file_type_powershell2.svg\npowershell_psm2 | psm1 | file_type_powershell_psm2.svg\npowershell_psd2 | psd1 | file_type_powershell_psd2.svg\npreact | preact.config.js | file_type_preact.svg\nprecommit | .pre-commit-config.yaml | file_type_precommit.svg\nprettier | .prettierrc, .prettierignore | file_type_prettier.svg | file_type_light_prettier.svg\nprettier | prettier.config.js, prettier.config.cjs, prettier.config.ts, prettier.config.coffee | file_type_prettier.svg | file_type_light_prettier.svg\nprettier | .prettierrc.js, .prettierrc.cjs, .prettierrc.json, .prettierrc.json5, .prettierrc.yml, .prettierrc.yaml, .prettierrc.toml | file_type_prettier.svg | file_type_light_prettier.svg\nprisma | prisma | file_type_prisma.svg | file_type_light_prisma.svg\nprocessinglang | pde | file_type_processinglang.svg\nprocfile | procfile | file_type_procfile.svg\nprogress | abl | file_type_progress.svg\nprolog | pro, P, prolog | file_type_prolog.svg\nprometheus | prometheus | file_type_prometheus.svg\nprotobuf | proto3, proto | file_type_protobuf.svg\nprotractor | protractor.conf.js, protractor.conf.coffee, protractor.conf.ts | file_type_protractor.svg\npublisher | pub, puz | file_type_publisher.svg\npuppet | puppet | file_type_puppet.svg\npug | .jade-lintrc, .pug-lintrc, .jade-lint.json, .pug-lintrc.js, .pug-lintrc.json, jade | file_type_pug.svg\npurescript | purescript | file_type_purescript.svg | file_type_light_purescript.svg\npyret | pyret | file_type_pyret.svg\npython | python | file_type_python.svg\npytyped | py.typed | file_type_pytyped.svg\npyup | .pyup, .pyup.yml | file_type_pyup.svg\nq | q | file_type_q.svg\nqbs | qbs | file_type_qbs.svg\nqlikview | qvd, qvw, qlik | file_type_qlikview.svg\nqml | qml | file_type_qml.svg\nqmldir | qmldir | file_type_qmldir.svg\nqsharp | qsharp | file_type_qsharp.svg\nquasar | quasar.config.js, quasar.conf.js | file_type_quasar.svg | file_type_light_quasar.svg\nr | r | file_type_r.svg\nracket | racket | file_type_racket.svg\nrails | <sub></sub> | file_type_rails.svg\nrake | rake | file_type_rake.svg\nrake | rakefile | file_type_rake.svg\nraml | raml | file_type_raml.svg\nrazor | razor, aspnetcorerazor | file_type_razor.svg\nrazzle | razzle.config.js | file_type_razzle.svg | file_type_light_razzle.svg\nreactjs | javascriptreact | file_type_reactjs.svg\nreacttemplate | rt | file_type_reacttemplate.svg\nreactts | typescriptreact | file_type_reactts.svg\nreason | reason | file_type_reason.svg\nred | red | file_type_red.svg\nregistry | reg | file_type_registry.svg\nrego | rego | file_type_rego.svg\nrehype | .rehyperc, .rehypeignore, .rehyperc.cjs, .rehyperc.js, .rehyperc.json, .rehyperc.mjs, .rehyperc.yml, .rehyperc.yaml | file_type_rehype.svg | file_type_light_rehype.svg\nremark | .remarkrc, .remarkignore, .remarkrc.cjs, .remarkrc.js, .remarkrc.json, .remarkrc.mjs, .remarkrc.yml, .remarkrc.yaml | file_type_remark.svg | file_type_light_remark.svg\nrenovate | .renovaterc, renovate.json, .renovaterc.json | file_type_renovate.svg\nreplit | .replit, replit.nix | file_type_replit.svg | file_type_light_replit.svg\nrescript | rescript | file_type_rescript.svg\nrest | restructuredtext | file_type_rest.svg\nretext | .retextrc, .retextignore, .retextrc.cjs, .retextrc.js, .retextrc.json, .retextrc.mjs, .retextrc.yml, .retextrc.yaml | file_type_retext.svg | file_type_light_retext.svg\nrexx | rexx | file_type_rexx.svg\nriot | riot | file_type_riot.svg\nrobotframework | robot | file_type_robotframework.svg\nrobots | robots.txt | file_type_robots.svg\nrollup | rollup.config.js, rollup.config.cjs, rollup.config.mjs, rollup.config.coffee, rollup.config.ts, rollup.config.common.js, rollup.config.common.cjs, rollup.config.common.mjs, rollup.config.common.coffee, rollup.config.common.ts, rollup.config.dev.js, rollup.config.dev.cjs, rollup.config.dev.mjs, rollup.config.dev.coffee, rollup.config.dev.ts, rollup.config.prod.js, rollup.config.prod.cjs, rollup.config.prod.mjs, rollup.config.prod.coffee, rollup.config.prod.ts | file_type_rollup.svg\nron | ron | file_type_ron.svg\nrmd | rmd | file_type_rmd.svg\nrproj | rproj | file_type_rproj.svg\nrspec | .rspec | file_type_rspec.svg\nrubocop | .rubocop.yml, .rubocop_todo.yml | file_type_rubocop.svg | file_type_light_rubocop.svg\nruby | ruby | file_type_ruby.svg\nrust | rust | file_type_rust.svg | file_type_light_rust.svg\nrust_toolchain | rust-toolchain | file_type_rust_toolchain.svg | file_type_light_rust_toolchain.svg\nsails | .sailsrc | file_type_sails.svg\nsaltstack | sls | file_type_saltstack.svg\nsan | san | file_type_san.svg\nsas | SAS | file_type_sas.svg\nsass | sass | file_type_sass.svg\nsbt | sbt | file_type_sbt.svg\nscala | scala | file_type_scala.svg\nscript | vbs, vbscript | file_type_script.svg\nscss | scssm, scss | file_type_scss.svg\nscilab | scilab | file_type_scilab.svg\nsdlang | sdl | file_type_sdlang.svg\nsentry | .sentryclirc | file_type_sentry.svg\nserverless | serverless.yml, serverless.json, serverless.js, serverless.ts | file_type_serverless.svg\nsequelize | .sequelizerc, .sequelizerc.js, .sequelizerc.json | file_type_sequelize.svg\nshaderlab | unity, shaderlab | file_type_shaderlab.svg | file_type_light_shaderlab.svg\nshell | sh, zsh, bat, fish, shellscript | file_type_shell.svg\nsiyuan | sy | file_type_siyuan.svg\nsketch | sketch | file_type_sketch.svg\nslang | slang | file_type_slang.svg\nslashup | slash-up.config.js | file_type_slashup.svg\nslice | slice | file_type_slice.svg\nslim | slim | file_type_slim.svg\nsln | sln | file_type_sln.svg\nsln2 | sln | file_type_sln2.svg\nsilverstripe | silverstripe | file_type_silverstripe.svg\nskipper | eskip, eskip | file_type_skipper.svg\nsmarty | smarty | file_type_smarty.svg\nsnapcraft | snapcraft.yaml | file_type_snapcraft.svg\nsnort | snort | file_type_snort.svg\nsnyk | .snyk | file_type_snyk.svg\nsolidarity | .solidarity, .solidarity.json | file_type_solidarity.svg\nsolidity | solidity | file_type_solidity.svg | file_type_light_solidity.svg\nsource | <sub></sub> | file_type_source.svg\nspacengine | spe | file_type_spacengine.svg\nsparql | sparql | file_type_sparql.svg\nsqf | sqf | file_type_sqf.svg\nsql | sql | file_type_sql.svg\nsqlite | sqlite, sqlite3, db3 | file_type_sqlite.svg\nsquirrel | squirrel | file_type_squirrel.svg\nsss | sss | file_type_sss.svg\nstan | stan | file_type_stan.svg\nstata | dta, stata | file_type_stata.svg\nstencil | stencil, stencil-html | file_type_stencil.svg\nstryker | stryker.conf.mjs, stryker.conf.cjs, stryker.conf.js, stryker.conf.conf, stryker.conf.json, .stryker.conf.mjs, .stryker.conf.cjs, .stryker.conf.js, .stryker.conf.conf, .stryker.conf.json, stryker-config.mjs, stryker-config.cjs, stryker-config.js, stryker-config.conf, stryker-config.json, stryker4s.mjs, stryker4s.cjs, stryker4s.js, stryker4s.conf, stryker4s.json | file_type_stryker.svg\nstyle | <sub></sub> | file_type_style.svg\nstylelint | .stylelintrc, .stylelintignore, .stylelintcache, stylelint.config.js, stylelint.config.json, stylelint.config.yaml, stylelint.config.yml, stylelint.config.ts, stylelint.config.cjs, .stylelintrc.js, .stylelintrc.json, .stylelintrc.yaml, .stylelintrc.yml, .stylelintrc.ts, .stylelintrc.cjs | file_type_stylelint.svg | file_type_light_stylelint.svg\nstylable | stylable | file_type_stylable.svg\nstyled | source.css.styled | file_type_styled.svg\nstylish_haskell | .stylish-haskell.yaml | file_type_stylish_haskell.svg\nstylus | stylus | file_type_stylus.svg | file_type_light_stylus.svg\nstoryboard | storyboard | file_type_storyboard.svg\nstorybook | story.js, story.jsx, story.ts, story.tsx, story.mdx, stories.js, stories.jsx, stories.ts, stories.tsx, stories.mdx | file_type_storybook.svg\nsubversion | .svnignore | file_type_subversion.svg\nsvelte | svelte | file_type_svelte.svg\nsvg | svg | file_type_svg.svg\nswagger | Swagger, swagger | file_type_swagger.svg\nswift | package.pins, swift | file_type_swift.svg\nswig | swig | file_type_swig.svg\nsymfony | symfony.lock | file_type_symfony.svg | file_type_light_symfony.svg\nsystemd | systemd-unit-file | file_type_systemd.svg | file_type_light_systemd.svg\nsystemverilog | systemverilog | file_type_systemverilog.svg | file_type_light_systemverilog.svg\nt4tt | t4 | file_type_t4tt.svg\ntailwind | tailwind.js, tailwind.cjs, tailwind.coffee, tailwind.ts, tailwind.json, tailwind.config.js, tailwind.config.cjs, tailwind.config.coffee, tailwind.config.ts, tailwind.config.json, .tailwind.js, .tailwind.cjs, .tailwind.coffee, .tailwind.ts, .tailwind.json, .tailwindrc.js, .tailwindrc.cjs, .tailwindrc.coffee, .tailwindrc.ts, .tailwindrc.json | file_type_tailwind.svg\ntauri | tauri.conf.json | file_type_tauri.svg\nteal | teal | file_type_teal.svg\ntt | tt2, tt | file_type_tt.svg\ntcl | tcl, exp | file_type_tcl.svg\ntera | tera | file_type_tera.svg\nterraform | tfstate, terraform | file_type_terraform.svg\ntest | tst | file_type_test.svg\ntestcafe | .testcaferc.json | file_type_testcafe.svg | file_type_light_testcafe.svg\ntestjs | test.js, test.jsx, test.mjs, spec.js, spec.jsx, spec.mjs | file_type_testjs.svg | file_type_light_testjs.svg\ntestts | test.ts, test.tsx, spec.ts, spec.tsx, e2e-test.ts, e2e-test.tsx, e2e-spec.ts, e2e-spec.tsx | file_type_testts.svg\ntex | texi, tikz, tex, latex, bibtex, doctex | file_type_tex.svg | file_type_light_tex.svg\ntext | txt, csv, tsv, plaintext | file_type_text.svg\ntextile | textile | file_type_textile.svg\ntiltfile | tiltfile | file_type_tiltfile.svg\ntfs | .tfignore | file_type_tfs.svg\ntodo | todo | file_type_todo.svg | file_type_light_todo.svg\ntoit | toit | file_type_toit.svg | file_type_light_toit.svg\ntoml | toml | file_type_toml.svg | file_type_light_toml.svg\ntox | tox.ini | file_type_tox.svg\ntravis | .travis.yml | file_type_travis.svg\ntrunk | trunk.yaml | file_type_trunk.svg\ntsconfig | tsconfig.json, tsconfig.app.json, tsconfig.base.json, tsconfig.common.json, tsconfig.dev.json, tsconfig.development.json, tsconfig.e2e.json, tsconfig.eslint.json, tsconfig.node.json, tsconfig.prod.json, tsconfig.production.json, tsconfig.server.json, tsconfig.spec.json, tsconfig.staging.json, tsconfig.test.json, tsconfig.lib.json, tsconfig.lib.prod.json | file_type_tsconfig.svg\ntsconfig_official | tsconfig.json, tsconfig.app.json, tsconfig.base.json, tsconfig.common.json, tsconfig.dev.json, tsconfig.development.json, tsconfig.e2e.json, tsconfig.node.json, tsconfig.prod.json, tsconfig.production.json, tsconfig.server.json, tsconfig.spec.json, tsconfig.staging.json, tsconfig.test.json, tsconfig.lib.json, tsconfig.lib.prod.json | file_type_tsconfig_official.svg\ntslint | tslint.json, tslint.yaml, tslint.yml | file_type_tslint.svg\nttcn | ttcn | file_type_ttcn.svg\ntuc | tuc | file_type_tuc.svg\ntwig | twig | file_type_twig.svg\ntypedoc | typedoc.js, typedoc.json | file_type_typedoc.svg\ntypescript | typescript | file_type_typescript.svg\ntypescript_official | typescript | file_type_typescript_official.svg\ntypescriptdef | d.ts, d.cts, d.mts | file_type_typescriptdef.svg\ntypescriptdef_official | d.ts, d.cts, d.mts | file_type_typescriptdef_official.svg\ntypo3 | typoscript | file_type_typo3.svg\nunibeautify | .unibeautifyrc, unibeautify.config.js, .unibeautifyrc.js, .unibeautifyrc.json, .unibeautifyrc.yaml, .unibeautifyrc.yml | file_type_unibeautify.svg | file_type_light_unibeautify.svg\nunlicense | unlicense, unlicence, unlicense.md, unlicense.txt, unlicence.md, unlicence.txt | file_type_unlicense.svg\nvagrant | vagrantfile | file_type_vagrant.svg\nvala | vala | file_type_vala.svg\nvanilla_extract | css.ts | file_type_vanilla_extract.svg\nvapi | vapi | file_type_vapi.svg\nvash | vash | file_type_vash.svg | file_type_light_vash.svg\nvapor | vapor.yml | file_type_vapor.svg\nvb | vb | file_type_vb.svg\nvba | vba | file_type_vba.svg\nvbhtml | vbhtml | file_type_vbhtml.svg\nvbproj | vbproj | file_type_vbproj.svg\nvcxproj | vcxproj | file_type_vcxproj.svg\nvelocity | velocity | file_type_velocity.svg\nverilog | verilog | file_type_verilog.svg\nvhdl | vhdl | file_type_vhdl.svg\nvideo | 3g2, 3gp, asf, amv, avi, divx, qt, f4a, f4b, f4p, f4v, flv, m2v, m4v, mkv, mk3d, mov, mp2, mp4, mpe, mpeg, mpeg2, mpg, mpv, nsv, ogv, rm, rmvb, svi, vob, webm, wmv | file_type_video.svg\nview | <sub></sub> | file_type_view.svg\nvim | .vimrc, .gvimrc, viml | file_type_vim.svg\nvite | vite.config.js, vite.config.ts | file_type_vite.svg\nvitest | vitest.config.ts, vitest.config.js, vitest.config.mjs | file_type_vitest.svg\nvlang | v | file_type_vlang.svg\nvolt | volt | file_type_volt.svg\nvscode | .vscodeignore, launch.json, tasks.json, vscodeignore.json | file_type_vscode.svg\nvscode2 | .vscodeignore, launch.json, tasks.json, vscodeignore.json | file_type_vscode2.svg\nvscode3 | .vscodeignore, launch.json, tasks.json, vscodeignore.json | file_type_vscode3.svg\nvscode-insiders | .vscodeignore, launch.json, tasks.json, vscodeignore.json | file_type_vscode-insiders.svg\nvsix | vsix | file_type_vsix.svg | file_type_light_vsix.svg\nvsixmanifest | vsixmanifest | file_type_vsixmanifest.svg | file_type_light_vsixmanifest.svg\nvue | vue | file_type_vue.svg\nvueconfig | .vuerc, vue.config.js, vue.config.cjs, vue.config.mjs | file_type_vueconfig.svg\nwallaby | wallaby.json, wallaby.js, wallaby.ts, wallaby.coffee, wallaby.conf.json, wallaby.conf.js, wallaby.conf.ts, wallaby.conf.coffee, .wallaby.json, .wallaby.js, .wallaby.ts, .wallaby.coffee, .wallaby.conf.json, .wallaby.conf.js, .wallaby.conf.ts, .wallaby.conf.coffee | file_type_wallaby.svg\nwatchmanconfig | .watchmanconfig | file_type_watchmanconfig.svg\nwasm | wasm, wasm, wat | file_type_wasm.svg\nwebpack | webpack.base.conf.js, webpack.base.conf.coffee, webpack.base.conf.ts, webpack.common.js, webpack.common.coffee, webpack.common.ts, webpack.config.js, webpack.config.coffee, webpack.config.ts, webpack.config.base.js, webpack.config.base.coffee, webpack.config.base.ts, webpack.config.common.js, webpack.config.common.coffee, webpack.config.common.ts, webpack.config.dev.js, webpack.config.dev.coffee, webpack.config.dev.ts, webpack.config.development.js, webpack.config.development.coffee, webpack.config.development.ts, webpack.config.staging.js, webpack.config.staging.coffee, webpack.config.staging.ts, webpack.config.test.js, webpack.config.test.coffee, webpack.config.test.ts, webpack.config.prod.js, webpack.config.prod.coffee, webpack.config.prod.ts, webpack.config.production.js, webpack.config.production.coffee, webpack.config.production.ts, webpack.config.babel.js, webpack.config.babel.coffee, webpack.config.babel.ts, webpack.config.base.babel.js, webpack.config.base.babel.coffee, webpack.config.base.babel.ts, webpack.config.common.babel.js, webpack.config.common.babel.coffee, webpack.config.common.babel.ts, webpack.config.dev.babel.js, webpack.config.dev.babel.coffee, webpack.config.dev.babel.ts, webpack.config.development.babel.js, webpack.config.development.babel.coffee, webpack.config.development.babel.ts, webpack.config.staging.babel.js, webpack.config.staging.babel.coffee, webpack.config.staging.babel.ts, webpack.config.test.babel.js, webpack.config.test.babel.coffee, webpack.config.test.babel.ts, webpack.config.prod.babel.js, webpack.config.prod.babel.coffee, webpack.config.prod.babel.ts, webpack.config.production.babel.js, webpack.config.production.babel.coffee, webpack.config.production.babel.ts, webpack.dev.js, webpack.dev.coffee, webpack.dev.ts, webpack.dev.conf.js, webpack.dev.conf.coffee, webpack.dev.conf.ts, webpack.prod.js, webpack.prod.coffee, webpack.prod.ts, webpack.prod.conf.js, webpack.prod.conf.coffee, webpack.prod.conf.ts, webpack.main.config.js, webpack.main.config.coffee, webpack.main.config.ts, webpack.mix.js, webpack.mix.coffee, webpack.mix.ts, webpack.plugins.js, webpack.plugins.coffee, webpack.plugins.ts, webpack.renderer.config.js, webpack.renderer.config.coffee, webpack.renderer.config.ts, webpack.rules.js, webpack.rules.coffee, webpack.rules.ts, webpack.test.conf.js, webpack.test.conf.coffee, webpack.test.conf.ts | file_type_webpack.svg\nwenyan | wenyan | file_type_wenyan.svg\nwercker | wercker.yml | file_type_wercker.svg\nwindi | windi.config.ts, windi.config.js | file_type_windi.svg\nwolfram | wolfram | file_type_wolfram.svg\nword | doc, docx, docm, dot, dotx, dotm, wll | file_type_word.svg\nword2 | doc, docx, docm, dot, dotx, dotm, wll | file_type_word2.svg\nwpml | wpml-config.xml | file_type_wpml.svg\nwurst | wurstlang, wurst | file_type_wurst.svg\nwxml | wxml | file_type_wxml.svg\nwxss | wxss | file_type_wxss.svg\nxcode | xcodeproj | file_type_xcode.svg\nxfl | xfl | file_type_xfl.svg | file_type_light_xfl.svg\nxib | xib | file_type_xib.svg\nxliff | xliff, xlf | file_type_xliff.svg\nxmake | xmake | file_type_xmake.svg\nxml | pex, tmlanguage, xml | file_type_xml.svg\nxquery | xquery | file_type_xquery.svg\nxsl | xsl | file_type_xsl.svg\nyacc | yacc | file_type_yacc.svg\nyaml | yml, yaml, yaml-tmlanguage | file_type_yaml.svg | file_type_light_yaml.svg\nyamllint | .yamllint | file_type_yamllint.svg\nyandex | .yaspellerrc, .yaspeller.json | file_type_yandex.svg\nyang | yang | file_type_yang.svg\nyarn | yarn.lock, .yarnrc, .yarnrc.yml, .yarnclean, .yarn-integrity, .yarn-metadata.json, .yarnignore | file_type_yarn.svg\nyeoman | .yo-rc.json | file_type_yeoman.svg\nzeit | now.json, .nowignore, vercel.json, .vercelignore | file_type_zeit.svg | file_type_light_zeit.svg\nzig | zig | file_type_zig.svg\nturbo | turbo.json | file_type_turbo.svg | file_type_light_turbo.svg\ndoppler | doppler.yaml, doppler-template.yaml | file_type_doppler.svg\nzip | zip, rar, 7z, tar, tgz, bz, gz, bzip2, xz, bz2, zipx | file_type_zip.svg\nzip2 | zip, rar, 7z, tar, tgz, bz, gz, bzip2, xz, bz2, zipx | file_type_zip2.svg\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/folder_list.txt",
    "content": "android | android | folder_type_android.svg | folder_type_android_opened.svg\napi | api, .api, apis, .apis | folder_type_api.svg | folder_type_api_opened.svg\napp | app, .app | folder_type_app.svg | folder_type_app_opened.svg\narangodb | arangodb, arango | folder_type_arangodb.svg | folder_type_arangodb_opened.svg\nasset | assets, .assets | folder_type_asset.svg | folder_type_asset_opened.svg\naurelia | aurelia_project | folder_type_aurelia.svg | folder_type_aurelia_opened.svg\naudio | audio, .audio, audios, .audios, sound, .sound, sounds, .sounds | folder_type_audio.svg | folder_type_audio_opened.svg\naws | aws, .aws | folder_type_aws.svg | folder_type_aws_opened.svg\nazure | azure, .azure | folder_type_azure.svg | folder_type_azure_opened.svg\nazurepipelines | azure-pipelines, .azure-pipelines | folder_type_azurepipelines.svg | folder_type_azurepipelines_opened.svg\nbinary | bin, .bin | folder_type_binary.svg | folder_type_binary_opened.svg\nbloc | blocs, bloc | folder_type_bloc.svg | folder_type_bloc_opened.svg\nblueprint | blueprint, .blueprint, blueprints, .blueprints | folder_type_blueprint.svg | folder_type_blueprint_opened.svg\nbower | bower_components | folder_type_bower.svg | folder_type_bower_opened.svg\nbuildkite | .buildkite | folder_type_buildkite.svg | folder_type_buildkite_opened.svg\ncake | cake, .cake | folder_type_cake.svg | folder_type_cake_opened.svg\ncertificate | certificates, .certificates, certs, certs. | folder_type_certificate.svg | folder_type_certificate_opened.svg\nchef | chef, .chef | folder_type_chef.svg | folder_type_chef_opened.svg\ncircleci | .circleci | folder_type_circleci.svg | folder_type_circleci_opened.svg\ncontroller | controller, controllers, .controllers, handlers, .handlers | folder_type_controller.svg | folder_type_controller_opened.svg\ncomponent | component, components, .components, gui, ui, widgets | folder_type_component.svg | folder_type_component_opened.svg\ncomposer | composer, .composer | folder_type_composer.svg | folder_type_composer_opened.svg\ncli | cli, cmd, command, commands, commandline, console | folder_type_cli.svg | folder_type_cli_opened.svg\nclient | client, clients | folder_type_client.svg | folder_type_client_opened.svg\ncmake | .cmake, cmake | folder_type_cmake.svg | folder_type_cmake_opened.svg\nconfig | conf, .conf, config, .config, configs, .configs, configuration, .configuration, configurations, .configurations, setting, .setting, settings, .settings, ini, .ini, initializers, .initializers | folder_type_config.svg | folder_type_config_opened.svg\ncoverage | coverage | folder_type_coverage.svg | folder_type_coverage_opened.svg\ncss | css, _css | folder_type_css.svg | folder_type_css_opened.svg\ncubit | cubits, cubit | folder_type_cubit.svg | folder_type_cubit_opened.svg\ncypress | cypress | folder_type_cypress.svg | folder_type_cypress_opened.svg | folder_type_light_cypress.svg | folder_type_light_cypress_opened.svg\ndapr | .dapr, dapr | folder_type_dapr.svg | folder_type_dapr_opened.svg\ndb | db, database, sql, data, repo, repository, repositories | folder_type_db.svg | folder_type_db_opened.svg\ndebian | debian, deb | folder_type_debian.svg | folder_type_debian_opened.svg\ndependabot | .dependabot | folder_type_dependabot.svg | folder_type_dependabot_opened.svg\ndevcontainer | .devcontainer | folder_type_devcontainer.svg | folder_type_devcontainer_opened.svg\ndist | dist, .dist, dists, out, outs, export, exports, build, .build, builds, release, releases, target, targets | folder_type_dist.svg | folder_type_dist_opened.svg\ndocker | docker, .docker | folder_type_docker.svg | folder_type_docker_opened.svg\ndocs | docs, doc | folder_type_docs.svg | folder_type_docs_opened.svg\ne2e | e2e | folder_type_e2e.svg | folder_type_e2e_opened.svg\nelasticbeanstalk | .elasticbeanstalk, .ebextensions | folder_type_elasticbeanstalk.svg | folder_type_elasticbeanstalk_opened.svg\nelectron | electron | folder_type_electron.svg | folder_type_electron_opened.svg | folder_type_light_electron.svg | folder_type_light_electron_opened.svg\nexpo | .expo, .expo-shared | folder_type_expo.svg | folder_type_expo_opened.svg | folder_type_light_expo.svg | folder_type_light_expo_opened.svg\nfavicon | favicon, favicons | folder_type_favicon.svg | folder_type_favicon_opened.svg\nflow | flow, flow-typed | folder_type_flow.svg | folder_type_flow_opened.svg\nfonts | fonts, font, fnt | folder_type_fonts.svg | folder_type_fonts_opened.svg | folder_type_light_fonts.svg | folder_type_light_fonts_opened.svg\ngcp | gcp, .gcp | folder_type_gcp.svg | folder_type_gcp_opened.svg\ngit | .git, submodules, .submodules | folder_type_git.svg | folder_type_git_opened.svg\ngithub | .github | folder_type_github.svg | folder_type_github_opened.svg\ngitlab | .gitlab | folder_type_gitlab.svg | folder_type_gitlab_opened.svg\ngradle | gradle, .gradle | folder_type_gradle.svg | folder_type_gradle_opened.svg | folder_type_light_gradle.svg | folder_type_light_gradle_opened.svg\ngraphql | graphql | folder_type_graphql.svg | folder_type_graphql_opened.svg\ngrunt | grunt | folder_type_grunt.svg | folder_type_grunt_opened.svg\ngulp | gulp, gulpfile.js, gulpfile.coffee, gulpfile.ts, gulpfile.babel.js, gulpfile.babel.coffee, gulpfile.babel.ts | folder_type_gulp.svg | folder_type_gulp_opened.svg\nhaxelib | .haxelib, haxe_libraries | folder_type_haxelib.svg | folder_type_haxelib_opened.svg\nhelper | helper, .helper, helpers, .helpers | folder_type_helper.svg | folder_type_helper_opened.svg\nhook | hook, .hook, hooks, .hooks | folder_type_hook.svg | folder_type_hook_opened.svg\nhusky | .husky | folder_type_husky.svg | folder_type_husky_opened.svg\nidea | .idea | folder_type_idea.svg | folder_type_idea_opened.svg\nimages | images, image, img, imgs, icons, icon, ico, screenshot, screenshots, svg | folder_type_images.svg | folder_type_images_opened.svg\ninclude | include, includes, incl, inc, .include, .includes, .incl, .inc, _include, _includes, _incl, _inc | folder_type_include.svg | folder_type_include_opened.svg\ninterfaces | interface, interfaces | folder_type_interfaces.svg | folder_type_interfaces_opened.svg\nios | ios | folder_type_ios.svg | folder_type_ios_opened.svg\njs | js | folder_type_js.svg | folder_type_js_opened.svg\njson | json | folder_type_json.svg | folder_type_json_opened.svg\njson_official | json | folder_type_json_official.svg | folder_type_json_official_opened.svg\nkubernetes | kubernetes, k8s, kube, kuber, .kubernetes, .k8s, .kube, .kuber | folder_type_kubernetes.svg | folder_type_kubernetes_opened.svg\nless | less, _less | folder_type_less.svg | folder_type_less_opened.svg\nlibrary | lib, libs, .lib, .libs, library, libraries | folder_type_library.svg | folder_type_library_opened.svg\nlinux | linux | folder_type_linux.svg | folder_type_linux_opened.svg\nlocale | lang, language, languages, locale, locales, _locale, _locales, internationalization, globalization, localization, i18n, g11n, l10n | folder_type_locale.svg | folder_type_locale_opened.svg\nlog | log, logs | folder_type_log.svg | folder_type_log_opened.svg\nmacos | macos, darwin | folder_type_macos.svg | folder_type_macos_opened.svg\nmariadb | mariadb, maria | folder_type_mariadb.svg | folder_type_mariadb_opened.svg\nmaven | .mvn | folder_type_maven.svg | folder_type_maven_opened.svg\nmemcached | memcached, .memcached | folder_type_memcached.svg | folder_type_memcached_opened.svg\nmiddleware | middleware, middlewares | folder_type_middleware.svg | folder_type_middleware_opened.svg\nmjml | mjml, .mjml | folder_type_mjml.svg | folder_type_mjml_opened.svg\nminikube | minikube, minik8s, minikuber | folder_type_minikube.svg | folder_type_minikube_opened.svg\nmock | mocks, .mocks, __mocks__ | folder_type_mock.svg | folder_type_mock_opened.svg\nmodel | model, .model, models, .models, entities, .entities | folder_type_model.svg | folder_type_model_opened.svg\nmodule | modules | folder_type_module.svg | folder_type_module_opened.svg\nmongodb | mongodb, mongo | folder_type_mongodb.svg | folder_type_mongodb_opened.svg\nmysql | mysqldb, mysql | folder_type_mysql.svg | folder_type_mysql_opened.svg | folder_type_light_mysql.svg | folder_type_light_mysql_opened.svg\nnext | .next | folder_type_next.svg | folder_type_next_opened.svg\nnginx | nginx, conf.d | folder_type_nginx.svg | folder_type_nginx_opened.svg\nnix | .niv, .nix, nix, niv | folder_type_nix.svg | folder_type_nix_opened.svg\nnode | node_modules | folder_type_node.svg | folder_type_node_opened.svg | folder_type_light_node.svg | folder_type_light_node_opened.svg\nnotification | notification, notifications, event, events | folder_type_notification.svg | folder_type_notification_opened.svg\nnuget | .nuget | folder_type_nuget.svg | folder_type_nuget_opened.svg\npackage | package, packages, .package, .packages, pkg | folder_type_package.svg | folder_type_package_opened.svg\npaket | .paket | folder_type_paket.svg | folder_type_paket_opened.svg\nphp | php | folder_type_php.svg | folder_type_php_opened.svg\nplatformio | .pio, .pioenvs | folder_type_platformio.svg | folder_type_platformio_opened.svg\nplugin | plugin, .plugin, plugins, .plugins, extension, .extension, extensions, .extensions | folder_type_plugin.svg | folder_type_plugin_opened.svg\nprisma | prisma | folder_type_prisma.svg | folder_type_prisma_opened.svg\nprivate | private, .private | folder_type_private.svg | folder_type_private_opened.svg\npublic | public, .public | folder_type_public.svg | folder_type_public_opened.svg\npython | .venv, .virtualenv | folder_type_python.svg | folder_type_python_opened.svg\nredis | redis | folder_type_redis.svg | folder_type_redis_opened.svg\nravendb | ravendb | folder_type_ravendb.svg | folder_type_ravendb_opened.svg\nroute | route, routes, _route, _routes, routers | folder_type_route.svg | folder_type_route_opened.svg\nredux | redux | folder_type_redux.svg | folder_type_redux_opened.svg | folder_type_light_redux.svg | folder_type_light_redux_opened.svg\nmeteor | .meteor | folder_type_meteor.svg | folder_type_meteor_opened.svg | folder_type_light_meteor.svg | folder_type_light_meteor_opened.svg\nnuxt | .nuxt | folder_type_nuxt.svg | folder_type_nuxt_opened.svg\nsass | sass, scss, _sass, _scss | folder_type_sass.svg | folder_type_sass_opened.svg | folder_type_light_sass.svg | folder_type_light_sass_opened.svg\nscript | script, scripts | folder_type_script.svg | folder_type_script_opened.svg\nserver | server | folder_type_server.svg | folder_type_server_opened.svg\nservices | service, services | folder_type_services.svg | folder_type_services_opened.svg\nsrc | src, source, sources | folder_type_src.svg | folder_type_src_opened.svg\nsso | sso | folder_type_sso.svg | folder_type_sso_opened.svg\nstory | story, stories, __stories__, .storybook | folder_type_story.svg | folder_type_story_opened.svg\nstyle | style, styles | folder_type_style.svg | folder_type_style_opened.svg\ntauri | src-tauri | folder_type_tauri.svg | folder_type_tauri_opened.svg\ntest | tests, .tests, test, .test, __tests__, __test__, spec, .spec, specs, .specs, integration | folder_type_test.svg | folder_type_test_opened.svg\ntemp | temp, .temp, tmp, .tmp | folder_type_temp.svg | folder_type_temp_opened.svg\ntemplate | template, .template, templates, .templates | folder_type_template.svg | folder_type_template_opened.svg\ntheme | theme, themes | folder_type_theme.svg | folder_type_theme_opened.svg\ntravis | .travis | folder_type_travis.svg | folder_type_travis_opened.svg\ntools | tool, tools, .tools, util, utils | folder_type_tools.svg | folder_type_tools_opened.svg\ntrunk | .trunk | folder_type_trunk.svg | folder_type_trunk_opened.svg\ntypescript | typescript, ts | folder_type_typescript.svg | folder_type_typescript_opened.svg\ntypings | typings, @types | folder_type_typings.svg | folder_type_typings_opened.svg\ntypings2 | typings, @types | folder_type_typings2.svg | folder_type_typings2_opened.svg\nvagrant | vagrant, .vagrant | folder_type_vagrant.svg | folder_type_vagrant_opened.svg\nvideo | video, .video, videos, .videos | folder_type_video.svg | folder_type_video_opened.svg\nview | html, view, views, layout, layouts, page, pages, _view, _views, _layout, _layouts, _page, _pages | folder_type_view.svg | folder_type_view_opened.svg\nvs | .vs | folder_type_vs.svg | folder_type_vs_opened.svg\nvs2 | .vs | folder_type_vs2.svg | folder_type_vs2_opened.svg\nvscode | .vscode, vscode | folder_type_vscode.svg | folder_type_vscode_opened.svg\nvscode2 | .vscode, vscode | folder_type_vscode2.svg | folder_type_vscode2_opened.svg\nvscode3 | .vscode, vscode | folder_type_vscode3.svg | folder_type_vscode3_opened.svg\nvscode_test | .vscode-test | folder_type_vscode_test.svg | folder_type_vscode_test_opened.svg\nvscode_test2 | .vscode-test | folder_type_vscode_test2.svg | folder_type_vscode_test2_opened.svg\nvscode_test3 | .vscode-test | folder_type_vscode_test3.svg | folder_type_vscode_test3_opened.svg\nwebpack | webpack | folder_type_webpack.svg | folder_type_webpack_opened.svg\nwindows | windows, win32 | folder_type_windows.svg | folder_type_windows_opened.svg\nwww | www, wwwroot | folder_type_www.svg | folder_type_www_opened.svg\nyarn | .yarn | folder_type_yarn.svg | folder_type_yarn_opened.svg\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/font-config/font.css",
    "content": ".root {\n    -fx-font-family: \"Inter\";\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/mcp/create_directory.json",
    "content": "{\n  \"name\": \"create_directory\",\n  \"description\": \"Creates a new directory\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"system\": {\n        \"type\": \"string\",\n        \"description\": \"The system identifier\"\n      },\n      \"path\": {\n        \"type\": \"string\",\n        \"description\": \"The file path\"\n      }\n    },\n    \"required\": [\n      \"system\",\n      \"path\"\n    ]\n  },\n  \"annotations\": {\n    \"readOnlyHint\": false,\n    \"destructiveHint\": true,\n    \"openWorldHint\": false\n  }\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/mcp/create_file.json",
    "content": "{\n  \"name\": \"create_file\",\n  \"description\": \"Creates a new file\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"system\": {\n        \"type\": \"string\",\n        \"description\": \"The system identifier\"\n      },\n      \"path\": {\n        \"type\": \"string\",\n        \"description\": \"The file path\"\n      },\n      \"content\": {\n        \"type\": \"string\",\n        \"description\": \"The optional file content as text\"\n      }\n    },\n    \"required\": [\n      \"system\",\n      \"path\"\n    ]\n  },\n  \"annotations\": {\n    \"readOnlyHint\": false,\n    \"destructiveHint\": true,\n    \"openWorldHint\": false\n  }\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/mcp/find_file.json",
    "content": "{\n  \"name\": \"find_file\",\n  \"description\": \"Finds files in a directory\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"system\": {\n        \"type\": \"string\",\n        \"description\": \"The system identifier\"\n      },\n      \"path\": {\n        \"type\": \"string\",\n        \"description\": \"The directory to search in\"\n      },\n      \"recursive\": {\n        \"type\": \"boolean\",\n        \"description\": \"Whether to traverse subdirectories recursively or not\"\n      },\n      \"name\": {\n        \"type\": \"boolean\",\n        \"description\": \"The name of the file. Supports globs\"\n      }\n    },\n    \"required\": [\n      \"system\",\n      \"path\"\n    ]\n  },\n  \"annotations\": {\n    \"readOnlyHint\": true,\n    \"destructiveHint\": false,\n    \"openWorldHint\": false\n  }\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/mcp/get_file_info.json",
    "content": "{\n  \"name\": \"get_file_info\",\n  \"description\": \"Retrieves information about a file\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"system\": {\n        \"type\": \"string\",\n        \"description\": \"The system identifier\"\n      },\n      \"path\": {\n        \"type\": \"string\",\n        \"description\": \"The file path\"\n      }\n    },\n    \"required\": [\n      \"system\",\n      \"path\"\n    ]\n  },\n  \"annotations\": {\n    \"readOnlyHint\": true,\n    \"destructiveHint\": false,\n    \"openWorldHint\": false\n  }\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/mcp/help.json",
    "content": "{\n  \"name\": \"help\",\n  \"description\": \"Information about the XPipe MCP server and what tools it provides\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {},\n    \"required\": [\n      \"path\",\n      \"system\"\n    ]\n  },\n  \"annotations\": {\n    \"readOnlyHint\": true,\n    \"destructiveHint\": false,\n    \"openWorldHint\": false\n  }\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/mcp/list_files.json",
    "content": "{\n  \"name\": \"list_files\",\n  \"description\": \"Lists files in a directory\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"system\": {\n        \"type\": \"string\",\n        \"description\": \"The system identifier\"\n      },\n      \"path\": {\n        \"type\": \"string\",\n        \"description\": \"The directory to list the contents of\"\n      },\n      \"recursive\": {\n        \"type\": \"boolean\",\n        \"description\": \"Whether to traverse subdirectories recursively or not\"\n      }\n    },\n    \"required\": [\n      \"system\",\n      \"path\"\n    ]\n  },\n  \"annotations\": {\n    \"readOnlyHint\": true,\n    \"destructiveHint\": false,\n    \"openWorldHint\": false\n  }\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/mcp/list_systems.json",
    "content": "{\n  \"name\": \"list_systems\",\n  \"description\": \"Lists all available systems in XPipe\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"filter\": {\n        \"type\": \"string\",\n        \"description\": \"The system name filter, also supports globs\"\n      }\n    },\n    \"required\": []\n  },\n  \"outputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"found\": {\n        \"type\": \"array\",\n        \"description\": \"The found list of systems\",\n        \"properties\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"name\": {\n              \"type\": \"string\",\n              \"description\": \"The name of the system\"\n            },\n            \"path\": {\n              \"type\": \"string\",\n              \"description\": \"The full path of the system\"\n            },\n            \"information\": {\n              \"type\": \"string\",\n              \"description\": \"Summary of the known system information\"\n            },\n            \"notes\": {\n              \"type\": \"string\",\n              \"description\": \"User-supplied notes for the individual system\"\n            }\n          },\n          \"required\": [\n            \"name\",\n            \"path\"\n          ]\n        }\n      }\n    },\n    \"required\": [\"found\"]\n  },\n  \"annotations\": {\n    \"readOnlyHint\": true,\n    \"destructiveHint\": false,\n    \"openWorldHint\": false\n  }\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/mcp/open_terminal.json",
    "content": "{\n  \"name\": \"open_terminal\",\n  \"description\": \"Open a new terminal session in a new window\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"system\": {\n        \"type\": \"string\",\n        \"description\": \"The system identifier\"\n      },\n      \"directory\": {\n        \"type\": \"string\",\n        \"description\": \"The working directory\"\n      }\n    },\n    \"required\": [\n      \"system\"\n    ]\n  },\n  \"annotations\": {\n    \"readOnlyHint\": false,\n    \"destructiveHint\": true,\n    \"openWorldHint\": false\n  }\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/mcp/open_terminal_inline.json",
    "content": "{\n  \"name\": \"open_terminal_inline\",\n  \"description\": \"Returns a command to open a new terminal session in the client\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"system\": {\n        \"type\": \"string\",\n        \"description\": \"The system identifier\"\n      },\n      \"directory\": {\n        \"type\": \"string\",\n        \"description\": \"The working directory\"\n      }\n    },\n    \"required\": [\n      \"system\"\n    ]\n  },\n  \"annotations\": {\n    \"readOnlyHint\": false,\n    \"destructiveHint\": true,\n    \"openWorldHint\": false\n  },\n  \"outputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"command\": {\n        \"type\": \"string\",\n        \"description\": \"Run this command in the MCP client's local shell directly\"\n      }\n    },\n    \"required\": [\n      \"command\"\n    ]\n  }\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/mcp/read_file.json",
    "content": "{\n  \"name\": \"read_file\",\n  \"description\": \"Reads file\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"path\": {\n        \"type\": \"string\",\n        \"description\": \"The file path to read\"\n      },\n      \"system\": {\n        \"type\": \"string\",\n        \"description\": \"The system identifier\"\n      }\n    },\n    \"required\": [\n      \"path\",\n      \"system\"\n    ]\n  },\n  \"annotations\": {\n    \"readOnlyHint\": true,\n    \"destructiveHint\": false,\n    \"openWorldHint\": false\n  }\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/mcp/run_command.json",
    "content": "{\n  \"name\": \"run_command\",\n  \"description\": \"Runs a shell command\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"system\": {\n        \"type\": \"string\",\n        \"description\": \"The system identifier\"\n      },\n      \"command\": {\n        \"type\": \"string\",\n        \"description\": \"The command to execute\"\n      }\n    },\n    \"required\": [\n      \"system\",\n      \"command\"\n    ]\n  },\n  \"annotations\": {\n    \"readOnlyHint\": false,\n    \"destructiveHint\": true,\n    \"openWorldHint\": false\n  }\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/mcp/run_script.json",
    "content": "{\n  \"name\": \"run_script\",\n  \"description\": \"Runs a predefined script\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"system\": {\n        \"type\": \"string\",\n        \"description\": \"The system identifier\"\n      },\n      \"script\": {\n        \"type\": \"string\",\n        \"description\": \"The script identifier\"\n      },\n      \"directory\": {\n        \"type\": \"string\",\n        \"description\": \"The working directory\"\n      },\n      \"arguments\": {\n        \"type\": \"string\",\n        \"description\": \"The optional argument list\"\n      }\n    },\n    \"required\": [\n      \"system\",\n      \"script\"\n    ]\n  },\n  \"annotations\": {\n    \"readOnlyHint\": false,\n    \"destructiveHint\": true,\n    \"openWorldHint\": false\n  }\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/mcp/toggle_state.json",
    "content": "{\n  \"name\": \"toggle_state\",\n  \"description\": \"Toggles the state of a connection\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"system\": {\n        \"type\": \"string\",\n        \"description\": \"The system identifier\"\n      },\n      \"state\": {\n        \"type\": \"boolean\",\n        \"description\": \"The new state\"\n      }\n    },\n    \"required\": [\n      \"system\",\n      \"state\"\n    ]\n  },\n  \"annotations\": {\n    \"readOnlyHint\": false,\n    \"destructiveHint\": true,\n    \"openWorldHint\": false\n  }\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/mcp/write_file.json",
    "content": "{\n  \"name\": \"write_file\",\n  \"description\": \"Writes text to a file\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"system\": {\n        \"type\": \"string\",\n        \"description\": \"The system identifier\"\n      },\n      \"path\": {\n        \"type\": \"string\",\n        \"description\": \"The file path\"\n      },\n      \"content\": {\n        \"type\": \"string\",\n        \"description\": \"The file content as text\"\n      }\n    },\n    \"required\": [\n      \"system\",\n      \"path\",\n      \"content\"\n    ]\n  },\n  \"annotations\": {\n    \"readOnlyHint\": false,\n    \"destructiveHint\": true,\n    \"openWorldHint\": false\n  }\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/misc/antivirus.md",
    "content": "### Information about antivirus programs\n\nXPipe detected that you are running %s. As a result, you might run into a few problems due to %s detecting suspicious activity when XPipe is interacting with your shell programs. This can range from severe slowdowns, notifications, up to full isolation of XPipe and your shell programs, effectively making the application unusable and causing errors.\n\n%s\n\n### Threat analysis\n\nAll artifacts of every release are automatically uploaded and analyzed on [VirusTotal](https://virustotal.com), so uploading the release you downloaded to VirusTotal should instantly show complete analysis results. From there you should be able to get a more accurate overview over the threat level of XPipe to you.\nYou can also find the analysis results listed at the bottom of every release on GitHub, i.e. at [https://github.com/xpipe-io/xpipe/releases/tag/%s](https://github.com/xpipe-io/xpipe/releases/tag/%s)\n\nFurthermore, you can find detailed information about the security model of XPipe at [https://docs.xpipe.io/security](https://docs.xpipe.io/security). From there you should be able to get a more accurate overview over the threat level of XPipe to you if you are feeling uneasy about turning any protection off.\n\n### What you can do\n\nIf such a protection kicks in or false-positive happens on your end, you might have to explicitly whitelist XPipe or disable certain protections in order for it to work correctly. Accessing shells is necessary for XPipe, there is no fallback alternative built in that does not launch shells.\n\nIf you choose to continue from here, XPipe will start calling shell programs to properly initialize, which might provoke %s."
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/misc/eula.md",
    "content": "# End User License Agreement\n\n## Preamble\n\nThis end user license agreement (hereinafter “**EULA**”) is a license agreement between you as the end user and **XPipe UG (haftungsbeschränkt)**, Reichertshalde 81, 71642 Ludwigsburg, (hereinafter “**XPipe**”) for the use of the products “XPipe”, a server connection and server management software (hereinafter “**Software**”).\n\nThis document can be viewed and printed out at any time in the XPipe desktop application or at http://docs.xpipe.io/end-user-license-agreement. The customer is independently responsible for saving the text of the contract at the time the contract is concluded. No copies will be provided by XPipe after the fact.\n\n## § 1 Subject matter and validity of the EULA\n\n(1) The EULA governs the terms under which you may use the Software. By accepting the EULA or using the Software, you agree to be bound by the terms of the EULA.\n\n(2) This EULA does not establish a service relationship between XPipe and you in return for payment. The payment of a license fee is solely the subject of the contractual relationship between you and the payment service provider Lemonsqueezy, to whom you pay license fees and in return we grant you a right to use additional software features. “Lemonsqueezy” collectively means Lemon Squeezy, LLC, 222 South Main Street Suite 500, Salt Lake City, Utah. Lemon Squeezy, LLC is a global payment service provider that facilitates online transactions using common online payment methods and acts as the seller of the product.\n\n## § 2 License\n\n(1) By entering into the EULA, you receive the non-exclusive, non-transferable and non-sublicensable right, unlimited in time, to run the Software installed on a computer in machine-readable format in accordance with the terms of the EULA (“License”).\n\n(2) The rights granted with the license include the right to obtain updates of the software provided by XPipe for the period of an active license. The use of updates is voluntary. Insofar as new software functions are introduced with an update, this does not justify any claim by you that these will also be maintained with future updates.\n\n## § 3 Provision of the Software, Activation of the License\n\n(1) The software is made available by you downloading it from the XPipe website and installing it independently on your device. XPipe itself owes you neither provision nor installation or integration of the software on a computer.\n\n(2) Any active license is checked for validity each time the program is started. For this purpose, an Internet connection is required at least once a week when the program is started. Any circumvention of a license activation and any unauthorized use of the software not authorized by XPipe and in violation of this EULA is prohibited.\n\n(3) Activation of a license is done individually for you as an end user, and the activated license is verified when the program is started. You may install the software on multiple computers.\n\n(4) Any circumvention of the aforementioned activation is prohibited.\n\n## § 4 Rights to the Software\n\n(1) Subject to the terms of any license granted and except as expressly provided in this EULA or otherwise agreed, you acquire no rights in the Software or Documentation. All Software and Documentation provided by XPipe, all copies, compilations, derivative products, programmatic enhancements, patches, revisions and updates to or relating to the Software, and all patent, utility model, trademark, design and copyright, trade secret, trade name and any other invention, design and information protected by law contained in any of the foregoing are and shall remain the property or ownership of XPipe.\n\n(2) You are prohibited from asserting the rights set forth in § 4 (1) or claims for the granting of any of the aforementioned rights to the Software or the Documentation and, in particular, from registering or claiming intellectual property rights to the Software against XPipe or third parties.\n\n(3) With the exception of §§ 69d, 69e UrhG, the customer is prohibited from attempting to reconstruct, decompile or decrypt any source code or underlying ideas or algorithms of any software or to permit a third party to do so. Except as otherwise provided in this EULA, or as otherwise expressly agreed between the parties, you may not modify the Software, make it available for use by third parties against payment, sublet it, or otherwise transfer any rights of use to third parties. Your right to execute the software installed on a specific computer in accordance with this EULA remains unaffected.\n\n(4) You are obligated to notify XPipe immediately of any violations of these provisions or any other violations of XPipe's rights to the software that are known to you or become known to you in the future.\n\n## § 5 No warranty\n\nXPipe does not warrant to you that the software has a certain quality or a certain scope of functions and is free of defects.\n\n## § 6 Liability\n\n(1) XPipe shall be liable without limitation in the event of (i) intent and gross negligence, (ii) for injury to life, limb and health, (iii) in accordance with the provisions of the Product Liability Act, from Art. 82 of Regulation 2016/670/EU (DSGVO) or other mandatory statutory liability provisions in accordance with the standards set forth therein, and (iv) to the extent of any warranty assumed.\n\n(2) XPipe shall further be liable for culpable breach of a material contractual obligation, the fulfillment of which is a prerequisite for proper performance of the contract and compliance with which the contractual partner may regularly rely on (“cardinal obligation”), but in the event of simple (minor) negligence limited to the damage reasonably to be expected and foreseeable at the time of conclusion of the contract.\n\n(3) XPipe shall have no further liability.\n\n(4) The above limitation of liability shall also apply to the personal liability of employees, representatives and members of XPipe's governing bodies.\n\n## § 7 Data protection\n\nXPipe is obligated to comply with the applicable data protection regulations. Information on data protection and the processing of personal data by XPipe can be found in the privacy policy for the Software: https://docs.xpipe.io/legal/privacy-policy\n\n## § 8 Final provisions\n\n(1) Neither party may transfer or assign this EULA in whole or any rights or obligations hereunder without the prior written consent of the other party. Any such transfer or assignment shall be void. Notwithstanding the foregoing, (i) a party may assign this agreement to a third party that assumes all or substantially all of its related business activities as a result of a merger, sale of shares or assets, or similar transaction, and (ii) XPipe may use third parties to perform its obligations under this agreement, provided that XPipe shall remain liable for any breach of such obligations.\n\n(2) This EULA shall be governed by the laws of the Federal Republic of Germany, excluding the United Nations Convention on Contracts for the International Sale of Goods (CISG) and conflict of law provisions. The place of performance and jurisdiction shall be Stuttgart, Germany.\n\n(3) If any provision of this EULA is invalid, void or unenforceable under any present or future law, the remainder of this EULA shall continue in full force and effect. To the extent that the invalid, void or unenforceable provision is a material term of the agreement, the parties agree to jointly negotiate a valid alternative provision.\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/misc/github-markdown-dark.css",
    "content": "body {\n    margin: 0;\n    padding: 0;\n}\n\nbody.standalone {\n    background-color: #0d1117;\n}\n\n.markdown-body {\n    color-scheme: dark;\n    -ms-text-size-adjust: 100%;\n    -webkit-text-size-adjust: 100%;\n    margin: 0;\n    padding: 0 8px 0 0;\n    color: #c9d1d9;\n    font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Noto Sans\", Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\";\n    font-size: 14px;\n    line-height: 1.5;\n    word-wrap: break-word;\n}\n\nbody.padded .markdown-body {\n    padding: 1.5em 8px 1.5em 1.5em;\n}\n\n.markdown-body .octicon {\n    display: inline-block;\n    fill: currentColor;\n    vertical-align: text-bottom;\n}\n\n.markdown-body h1:hover .anchor .octicon-link:before,\n.markdown-body h2:hover .anchor .octicon-link:before,\n.markdown-body h3:hover .anchor .octicon-link:before,\n.markdown-body h4:hover .anchor .octicon-link:before,\n.markdown-body h5:hover .anchor .octicon-link:before,\n.markdown-body h6:hover .anchor .octicon-link:before {\n    width: 16px;\n    height: 16px;\n    content: ' ';\n    display: inline-block;\n    background-color: currentColor;\n    -webkit-mask-image: url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>\");\n    mask-image: url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>\");\n}\n\n.markdown-body details,\n.markdown-body figcaption,\n.markdown-body figure {\n    display: block;\n}\n\n.markdown-body summary {\n    display: list-item;\n}\n\n.markdown-body [hidden] {\n    display: none !important;\n}\n\n.markdown-body a {\n    background-color: transparent;\n    color: #58a6ff;\n    text-decoration: none;\n}\n\n.markdown-body abbr[title] {\n    border-bottom: none;\n    text-decoration: underline dotted;\n}\n\n.markdown-body b,\n.markdown-body strong {\n    font-weight: 600;\n}\n\n.markdown-body dfn {\n    font-style: italic;\n}\n\n.markdown-body summary {\n    font-weight: 600;\n    padding-bottom: .3em;\n    font-size: 1.4em;\n}\n\n.markdown-body h1 {\n    margin: .67em 0;\n    font-weight: 600;\n    padding-bottom: .3em;\n    font-size: 2em;\n    border-bottom: 1px solid #21262d;\n}\n\n.markdown-body mark {\n    background-color: rgba(187, 128, 9, 0.15);\n    color: #c9d1d9;\n}\n\n.markdown-body small {\n    font-size: 90%;\n}\n\n.markdown-body sub,\n.markdown-body sup {\n    font-size: 75%;\n    line-height: 0;\n    position: relative;\n    vertical-align: baseline;\n}\n\n.markdown-body sub {\n    bottom: -0.25em;\n}\n\n.markdown-body sup {\n    top: -0.5em;\n}\n\n.markdown-body img {\n    border-style: none;\n    max-width: 100%;\n    box-sizing: content-box;\n    background-color: #0d1117;\n}\n\n.markdown-body code,\n.markdown-body kbd,\n.markdown-body pre,\n.markdown-body samp {\n    font-family: monospace;\n    font-size: 1em;\n}\n\n.markdown-body figure {\n    margin: 1em 40px;\n}\n\n.markdown-body hr {\n    box-sizing: content-box;\n    overflow: hidden;\n    background: transparent;\n    border-bottom: 1px solid #21262d;\n    height: .25em;\n    padding: 0;\n    margin: 24px 0;\n    background-color: #30363d;\n    border: 0;\n}\n\n.markdown-body input {\n    font: inherit;\n    margin: 0;\n    overflow: visible;\n    font-family: inherit;\n    font-size: inherit;\n    line-height: inherit;\n}\n\n.markdown-body [type=button],\n.markdown-body [type=reset],\n.markdown-body [type=submit] {\n    -webkit-appearance: button;\n}\n\n.markdown-body [type=checkbox],\n.markdown-body [type=radio] {\n    box-sizing: border-box;\n    padding: 0;\n}\n\n.markdown-body [type=number]::-webkit-inner-spin-button,\n.markdown-body [type=number]::-webkit-outer-spin-button {\n    height: auto;\n}\n\n.markdown-body [type=search]::-webkit-search-cancel-button,\n.markdown-body [type=search]::-webkit-search-decoration {\n    -webkit-appearance: none;\n}\n\n.markdown-body ::-webkit-input-placeholder {\n    color: inherit;\n    opacity: .54;\n}\n\n.markdown-body ::-webkit-file-upload-button {\n    -webkit-appearance: button;\n    font: inherit;\n}\n\n.markdown-body a:hover {\n    text-decoration: underline;\n}\n\n.markdown-body ::placeholder {\n    color: #6e7681;\n    opacity: 1;\n}\n\n.markdown-body hr::before {\n    display: table;\n    content: \"\";\n}\n\n.markdown-body hr::after {\n    display: table;\n    clear: both;\n    content: \"\";\n}\n\n.markdown-body table {\n    border-spacing: 0;\n    border-collapse: collapse;\n    display: block;\n    width: max-content;\n    max-width: 100%;\n    overflow: auto;\n}\n\n.markdown-body td,\n.markdown-body th {\n    padding: 0;\n}\n\n.markdown-body details summary {\n    cursor: pointer;\n}\n\n.markdown-body details:not([open]) > *:not(summary) {\n    display: none !important;\n}\n\n.markdown-body a:focus,\n.markdown-body [role=button]:focus,\n.markdown-body input[type=radio]:focus,\n.markdown-body input[type=checkbox]:focus {\n    outline: 2px solid #58a6ff;\n    outline-offset: -2px;\n    box-shadow: none;\n}\n\n.markdown-body a:focus:not(:focus-visible),\n.markdown-body [role=button]:focus:not(:focus-visible),\n.markdown-body input[type=radio]:focus:not(:focus-visible),\n.markdown-body input[type=checkbox]:focus:not(:focus-visible) {\n    outline: solid 1px transparent;\n}\n\n.markdown-body a:focus-visible,\n.markdown-body [role=button]:focus-visible,\n.markdown-body input[type=radio]:focus-visible,\n.markdown-body input[type=checkbox]:focus-visible {\n    outline: 2px solid #58a6ff;\n    outline-offset: -2px;\n    box-shadow: none;\n}\n\n.markdown-body a:not([class]):focus,\n.markdown-body a:not([class]):focus-visible,\n.markdown-body input[type=radio]:focus,\n.markdown-body input[type=radio]:focus-visible,\n.markdown-body input[type=checkbox]:focus,\n.markdown-body input[type=checkbox]:focus-visible {\n    outline-offset: 0;\n}\n\n.markdown-body kbd {\n    display: inline-block;\n    padding: 3px 5px;\n    font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;\n    line-height: 10px;\n    color: #c9d1d9;\n    vertical-align: middle;\n    background-color: #161b22;\n    border: solid 1px rgba(110, 118, 129, 0.4);\n    border-bottom-color: rgba(110, 118, 129, 0.4);\n    border-radius: 6px;\n    box-shadow: inset 0 -1px 0 rgba(110, 118, 129, 0.4);\n}\n\n.markdown-body h1,\n.markdown-body h2,\n.markdown-body h3,\n.markdown-body h4,\n.markdown-body h5,\n.markdown-body h6 {\n    margin-top: 24px;\n    margin-bottom: 16px;\n    font-weight: 600;\n    line-height: 1.25;\n}\n\n.markdown-body h2 {\n    font-weight: 600;\n    padding-bottom: .3em;\n    font-size: 1.5em;\n    border-bottom: 1px solid #21262d;\n}\n\n.markdown-body h3 {\n    font-weight: 600;\n    font-size: 1.25em;\n}\n\n.markdown-body h4 {\n    font-weight: 600;\n    font-size: 1em;\n}\n\n.markdown-body h5 {\n    font-weight: 600;\n    font-size: .875em;\n}\n\n.markdown-body h6 {\n    font-weight: 600;\n    font-size: .85em;\n    color: #8b949e;\n}\n\n.markdown-body p {\n    margin-top: 0;\n    margin-bottom: 10px;\n}\n\n.markdown-body blockquote {\n    margin: 0;\n    padding: 0 1em;\n    color: #8b949e;\n    border-left: .25em solid #30363d;\n}\n\n.markdown-body ul,\n.markdown-body ol {\n    margin-top: 0;\n    margin-bottom: 0;\n    padding-left: 2em;\n}\n\n.markdown-body ol ol,\n.markdown-body ul ol {\n    list-style-type: lower-roman;\n}\n\n.markdown-body ul ul ol,\n.markdown-body ul ol ol,\n.markdown-body ol ul ol,\n.markdown-body ol ol ol {\n    list-style-type: lower-alpha;\n}\n\n.markdown-body dd {\n    margin-left: 0;\n}\n\n.markdown-body tt,\n.markdown-body code,\n.markdown-body samp {\n    font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;\n    font-size: 12px;\n}\n\n.markdown-body pre {\n    margin-top: 0;\n    margin-bottom: 0;\n    font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;\n    font-size: 12px;\n    word-wrap: normal;\n}\n\n.markdown-body .octicon {\n    display: inline-block;\n    overflow: visible !important;\n    vertical-align: text-bottom;\n    fill: currentColor;\n}\n\n.markdown-body input::-webkit-outer-spin-button,\n.markdown-body input::-webkit-inner-spin-button {\n    margin: 0;\n    -webkit-appearance: none;\n    appearance: none;\n}\n\n.markdown-body::before {\n    display: table;\n    content: \"\";\n}\n\n.markdown-body::after {\n    display: table;\n    clear: both;\n    content: \"\";\n}\n\n.markdown-body > *:first-child {\n    margin-top: 0 !important;\n}\n\n.markdown-body > *:last-child {\n    margin-bottom: 0 !important;\n}\n\n.markdown-body a:not([href]) {\n    color: inherit;\n    text-decoration: none;\n}\n\n.markdown-body .absent {\n    color: #f85149;\n}\n\n.markdown-body .anchor {\n    float: left;\n    padding-right: 4px;\n    margin-left: -20px;\n    line-height: 1;\n}\n\n.markdown-body .anchor:focus {\n    outline: none;\n}\n\n.markdown-body p,\n.markdown-body blockquote,\n.markdown-body ul,\n.markdown-body ol,\n.markdown-body dl,\n.markdown-body table,\n.markdown-body pre,\n.markdown-body details {\n    margin-top: 16px;\n    margin-bottom: 16px;\n}\n\n.markdown-body blockquote > :first-child {\n    margin-top: 0;\n}\n\n.markdown-body blockquote > :last-child {\n    margin-bottom: 0;\n}\n\n.markdown-body h1 .octicon-link,\n.markdown-body h2 .octicon-link,\n.markdown-body h3 .octicon-link,\n.markdown-body h4 .octicon-link,\n.markdown-body h5 .octicon-link,\n.markdown-body h6 .octicon-link {\n    color: #c9d1d9;\n    vertical-align: middle;\n    visibility: hidden;\n}\n\n.markdown-body h1:hover .anchor,\n.markdown-body h2:hover .anchor,\n.markdown-body h3:hover .anchor,\n.markdown-body h4:hover .anchor,\n.markdown-body h5:hover .anchor,\n.markdown-body h6:hover .anchor {\n    text-decoration: none;\n}\n\n.markdown-body h1:hover .anchor .octicon-link,\n.markdown-body h2:hover .anchor .octicon-link,\n.markdown-body h3:hover .anchor .octicon-link,\n.markdown-body h4:hover .anchor .octicon-link,\n.markdown-body h5:hover .anchor .octicon-link,\n.markdown-body h6:hover .anchor .octicon-link {\n    visibility: visible;\n}\n\n.markdown-body h1 tt,\n.markdown-body h1 code,\n.markdown-body h2 tt,\n.markdown-body h2 code,\n.markdown-body h3 tt,\n.markdown-body h3 code,\n.markdown-body h4 tt,\n.markdown-body h4 code,\n.markdown-body h5 tt,\n.markdown-body h5 code,\n.markdown-body h6 tt,\n.markdown-body h6 code {\n    padding: 0 .2em;\n    font-size: inherit;\n}\n\n.markdown-body summary h1,\n.markdown-body summary h2,\n.markdown-body summary h3,\n.markdown-body summary h4,\n.markdown-body summary h5,\n.markdown-body summary h6 {\n    display: inline-block;\n}\n\n.markdown-body summary h1 .anchor,\n.markdown-body summary h2 .anchor,\n.markdown-body summary h3 .anchor,\n.markdown-body summary h4 .anchor,\n.markdown-body summary h5 .anchor,\n.markdown-body summary h6 .anchor {\n    margin-left: -40px;\n}\n\n.markdown-body summary h1,\n.markdown-body summary h2 {\n    padding-bottom: 0;\n    border-bottom: 0;\n}\n\n.markdown-body ul.no-list,\n.markdown-body ol.no-list {\n    padding: 0;\n    list-style-type: none;\n}\n\n.markdown-body ol[type=a] {\n    list-style-type: lower-alpha;\n}\n\n.markdown-body ol[type=A] {\n    list-style-type: upper-alpha;\n}\n\n.markdown-body ol[type=i] {\n    list-style-type: lower-roman;\n}\n\n.markdown-body ol[type=I] {\n    list-style-type: upper-roman;\n}\n\n.markdown-body ol[type=\"1\"] {\n    list-style-type: decimal;\n}\n\n.markdown-body div > ol:not([type]) {\n    list-style-type: decimal;\n}\n\n.markdown-body ul ul,\n.markdown-body ul ol,\n.markdown-body ol ol,\n.markdown-body ol ul {\n    margin-top: 0;\n    margin-bottom: 0;\n}\n\n.markdown-body li > p {\n    margin-top: 16px;\n}\n\n.markdown-body li + li {\n    margin-top: .25em;\n}\n\n.markdown-body dl {\n    padding: 0;\n}\n\n.markdown-body dl dt {\n    padding: 0;\n    margin-top: 16px;\n    font-size: 1em;\n    font-style: italic;\n    font-weight: 600;\n}\n\n.markdown-body dl dd {\n    padding: 0 16px;\n    margin-bottom: 16px;\n}\n\n.markdown-body table {\n    color: #c9d1d9;\n}\n\n.markdown-body table thead th {\n    font-weight: 600;\n}\n\n.markdown-body table thead th,\n.markdown-body table tbody td {\n    padding: 6px 13px !important;\n    border: 1px solid #30363d;\n}\n\n.markdown-body table thead tr .markdown-body table tbody tr {\n    background-color: #0d1117;\n    border-top: 1px solid #21262d;\n}\n\n.markdown-body table tr:nth-child(2n) {\n    background-color: #161b22;\n}\n\n.markdown-body table img {\n    background-color: transparent;\n}\n\n.markdown-body img[align=right] {\n    padding-left: 20px;\n}\n\n.markdown-body img[align=left] {\n    padding-right: 20px;\n}\n\n.markdown-body .emoji {\n    max-width: none;\n    vertical-align: text-top;\n    background-color: transparent;\n}\n\n.markdown-body span.frame {\n    display: block;\n    overflow: hidden;\n}\n\n.markdown-body span.frame > span {\n    display: block;\n    float: left;\n    width: auto;\n    padding: 7px;\n    margin: 13px 0 0;\n    overflow: hidden;\n    border: 1px solid #30363d;\n}\n\n.markdown-body span.frame span img {\n    display: block;\n    float: left;\n}\n\n.markdown-body span.frame span span {\n    display: block;\n    padding: 5px 0 0;\n    clear: both;\n    color: #c9d1d9;\n}\n\n.markdown-body span.align-center {\n    display: block;\n    overflow: hidden;\n    clear: both;\n}\n\n.markdown-body span.align-center > span {\n    display: block;\n    margin: 13px auto 0;\n    overflow: hidden;\n    text-align: center;\n}\n\n.markdown-body span.align-center span img {\n    margin: 0 auto;\n    text-align: center;\n}\n\n.markdown-body span.align-right {\n    display: block;\n    overflow: hidden;\n    clear: both;\n}\n\n.markdown-body span.align-right > span {\n    display: block;\n    margin: 13px 0 0;\n    overflow: hidden;\n    text-align: right;\n}\n\n.markdown-body span.align-right span img {\n    margin: 0;\n    text-align: right;\n}\n\n.markdown-body span.float-left {\n    display: block;\n    float: left;\n    margin-right: 13px;\n    overflow: hidden;\n}\n\n.markdown-body span.float-left span {\n    margin: 13px 0 0;\n}\n\n.markdown-body span.float-right {\n    display: block;\n    float: right;\n    margin-left: 13px;\n    overflow: hidden;\n}\n\n.markdown-body span.float-right > span {\n    display: block;\n    margin: 13px auto 0;\n    overflow: hidden;\n    text-align: right;\n}\n\n.markdown-body code,\n.markdown-body tt {\n    padding: .2em .4em;\n    margin: 0;\n    font-size: 85%;\n    white-space: break-spaces;\n    background-color: rgba(110, 118, 129, 0.4);\n    border-radius: 6px;\n}\n\n.markdown-body code br,\n.markdown-body tt br {\n    display: none;\n}\n\n.markdown-body del code {\n    text-decoration: inherit;\n}\n\n.markdown-body samp {\n    font-size: 85%;\n}\n\n.markdown-body pre code {\n    font-size: 100%;\n}\n\n.markdown-body pre > code {\n    padding: 0;\n    margin: 0;\n    word-break: normal;\n    white-space: pre;\n    background: transparent;\n    border: 0;\n}\n\n.markdown-body .highlight {\n    margin-bottom: 16px;\n}\n\n.markdown-body .highlight pre {\n    margin-bottom: 0;\n    word-break: normal;\n}\n\n.markdown-body .highlight pre,\n.markdown-body pre {\n    padding: 16px;\n    overflow: auto;\n    font-size: 85%;\n    line-height: 1.45;\n    background-color: #161b22;\n    border-radius: 6px;\n}\n\n.markdown-body pre code,\n.markdown-body pre tt {\n    display: inline;\n    max-width: auto;\n    padding: 0;\n    margin: 0;\n    overflow: visible;\n    line-height: inherit;\n    word-wrap: normal;\n    background-color: transparent;\n    border: 0;\n}\n\n.markdown-body .csv-data td,\n.markdown-body .csv-data th {\n    padding: 5px;\n    overflow: hidden;\n    font-size: 12px;\n    line-height: 1;\n    text-align: left;\n    white-space: nowrap;\n}\n\n.markdown-body .csv-data .blob-num {\n    padding: 10px 8px 9px;\n    text-align: right;\n    background: #0d1117;\n    border: 0;\n}\n\n.markdown-body .csv-data tr {\n    border-top: 0;\n}\n\n.markdown-body .csv-data th {\n    font-weight: 600;\n    background: #161b22;\n    border-top: 0;\n}\n\n.markdown-body [data-footnote-ref]::before {\n    content: \"[\";\n}\n\n.markdown-body [data-footnote-ref]::after {\n    content: \"]\";\n}\n\n.markdown-body .footnotes {\n    font-size: 12px;\n    color: #8b949e;\n    border-top: 1px solid #30363d;\n}\n\n.markdown-body .footnotes ol {\n    padding-left: 16px;\n}\n\n.markdown-body .footnotes ol ul {\n    display: inline-block;\n    padding-left: 16px;\n    margin-top: 16px;\n}\n\n.markdown-body .footnotes li {\n    position: relative;\n}\n\n.markdown-body .footnotes li:target::before {\n    position: absolute;\n    top: -8px;\n    right: -8px;\n    bottom: -8px;\n    left: -24px;\n    pointer-events: none;\n    content: \"\";\n    border: 2px solid #1f6feb;\n    border-radius: 6px;\n}\n\n.markdown-body .footnotes li:target {\n    color: #c9d1d9;\n}\n\n.markdown-body .footnotes .data-footnote-backref g-emoji {\n    font-family: monospace;\n}\n\n.markdown-body .pl-c {\n    color: #8b949e;\n}\n\n.markdown-body .pl-c1,\n.markdown-body .pl-s .pl-v {\n    color: #79c0ff;\n}\n\n.markdown-body .pl-e,\n.markdown-body .pl-en {\n    color: #d2a8ff;\n}\n\n.markdown-body .pl-smi,\n.markdown-body .pl-s .pl-s1 {\n    color: #c9d1d9;\n}\n\n.markdown-body .pl-ent {\n    color: #7ee787;\n}\n\n.markdown-body .pl-k {\n    color: #ff7b72;\n}\n\n.markdown-body .pl-s,\n.markdown-body .pl-pds,\n.markdown-body .pl-s .pl-pse .pl-s1,\n.markdown-body .pl-sr,\n.markdown-body .pl-sr .pl-cce,\n.markdown-body .pl-sr .pl-sre,\n.markdown-body .pl-sr .pl-sra {\n    color: #a5d6ff;\n}\n\n.markdown-body .pl-v,\n.markdown-body .pl-smw {\n    color: #ffa657;\n}\n\n.markdown-body .pl-bu {\n    color: #f85149;\n}\n\n.markdown-body .pl-ii {\n    color: #f0f6fc;\n    background-color: #8e1519;\n}\n\n.markdown-body .pl-c2 {\n    color: #f0f6fc;\n    background-color: #b62324;\n}\n\n.markdown-body .pl-sr .pl-cce {\n    font-weight: bold;\n    color: #7ee787;\n}\n\n.markdown-body .pl-ml {\n    color: #f2cc60;\n}\n\n.markdown-body .pl-mh,\n.markdown-body .pl-mh .pl-en,\n.markdown-body .pl-ms {\n    font-weight: bold;\n    color: #1f6feb;\n}\n\n.markdown-body .pl-mi {\n    font-style: italic;\n    color: #c9d1d9;\n}\n\n.markdown-body .pl-mb {\n    font-weight: bold;\n    color: #c9d1d9;\n}\n\n.markdown-body .pl-md {\n    color: #ffdcd7;\n    background-color: #67060c;\n}\n\n.markdown-body .pl-mi1 {\n    color: #aff5b4;\n    background-color: #033a16;\n}\n\n.markdown-body .pl-mc {\n    color: #ffdfb6;\n    background-color: #5a1e02;\n}\n\n.markdown-body .pl-mi2 {\n    color: #c9d1d9;\n    background-color: #1158c7;\n}\n\n.markdown-body .pl-mdr {\n    font-weight: bold;\n    color: #d2a8ff;\n}\n\n.markdown-body .pl-ba {\n    color: #8b949e;\n}\n\n.markdown-body .pl-sg {\n    color: #484f58;\n}\n\n.markdown-body .pl-corl {\n    text-decoration: underline;\n    color: #a5d6ff;\n}\n\n.markdown-body g-emoji {\n    display: inline-block;\n    min-width: 1ch;\n    font-family: \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n    font-size: 1em;\n    font-style: normal !important;\n    font-weight: 400;\n    line-height: 1;\n    vertical-align: -0.075em;\n}\n\n.markdown-body g-emoji img {\n    width: 1em;\n    height: 1em;\n}\n\n.markdown-body .task-list-item {\n    list-style-type: none;\n}\n\n.markdown-body .task-list-item label {\n    font-weight: 400;\n}\n\n.markdown-body .task-list-item.enabled label {\n    cursor: pointer;\n}\n\n.markdown-body .task-list-item + .task-list-item {\n    margin-top: 4px;\n}\n\n.markdown-body .task-list-item .handle {\n    display: none;\n}\n\n.markdown-body .task-list-item-checkbox {\n    margin: 0 .2em .25em -1.4em;\n    vertical-align: middle;\n}\n\n.markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox {\n    margin: 0 -1.6em .25em .2em;\n}\n\n.markdown-body .contains-task-list {\n    position: relative;\n}\n\n.markdown-body .contains-task-list:hover .task-list-item-convert-container,\n.markdown-body .contains-task-list:focus-within .task-list-item-convert-container {\n    display: block;\n    width: auto;\n    height: 24px;\n    overflow: visible;\n    clip: auto;\n}\n\n.markdown-body ::-webkit-calendar-picker-indicator {\n    filter: invert(50%);\n}\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/misc/github-markdown-light.css",
    "content": "body {\n    margin: 0;\n    padding: 0;\n}\n\nbody.standalone {\n    background-color: #ffffff;\n}\n\n.markdown-body {\n    -ms-text-size-adjust: 100%;\n    -webkit-text-size-adjust: 100%;\n    margin: 0;\n    padding: 0 8px 0 0;\n    color: #24292f;\n    font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\";\n    font-size: 14px;\n    line-height: 1.5;\n    word-wrap: break-word;\n}\n\nbody.padded .markdown-body {\n    padding: 1.5em 8px 1.5em 1.5em;\n}\n\n.markdown-body .octicon {\n    display: inline-block;\n    fill: currentColor;\n    vertical-align: text-bottom;\n}\n\n.markdown-body h1:hover .anchor .octicon-link:before,\n.markdown-body h2:hover .anchor .octicon-link:before,\n.markdown-body h3:hover .anchor .octicon-link:before,\n.markdown-body h4:hover .anchor .octicon-link:before,\n.markdown-body h5:hover .anchor .octicon-link:before,\n.markdown-body h6:hover .anchor .octicon-link:before {\n    width: 16px;\n    height: 16px;\n    content: ' ';\n    display: inline-block;\n    background-color: currentColor;\n    -webkit-mask-image: url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>\");\n    mask-image: url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>\");\n}\n\n.markdown-body details,\n.markdown-body figcaption,\n.markdown-body figure {\n    display: block;\n}\n\n.markdown-body summary {\n    display: list-item;\n}\n\n.markdown-body [hidden] {\n    display: none !important;\n}\n\n.markdown-body a {\n    background-color: transparent;\n    color: #0969da;\n    text-decoration: none;\n}\n\n.markdown-body a:active,\n.markdown-body a:hover {\n    outline-width: 0;\n}\n\n.markdown-body abbr[title] {\n    border-bottom: none;\n    text-decoration: underline dotted;\n}\n\n.markdown-body b,\n.markdown-body strong {\n    font-weight: 600;\n}\n\n.markdown-body dfn {\n    font-style: italic;\n}\n\n.markdown-body h1 {\n    margin: .67em 0;\n    font-weight: 600;\n    padding-bottom: .3em;\n    font-size: 2em;\n    border-bottom: 1px solid hsla(210, 18%, 87%, 1);\n}\n\n.markdown-body mark {\n    background-color: #fff8c5;\n    color: #24292f;\n}\n\n.markdown-body small {\n    font-size: 90%;\n}\n\n.markdown-body sub,\n.markdown-body sup {\n    font-size: 75%;\n    line-height: 0;\n    position: relative;\n    vertical-align: baseline;\n}\n\n.markdown-body sub {\n    bottom: -0.25em;\n}\n\n.markdown-body sup {\n    top: -0.5em;\n}\n\n.markdown-body img {\n    border-style: none;\n    max-width: 100%;\n    box-sizing: content-box;\n    background-color: #ffffff;\n}\n\n.markdown-body code,\n.markdown-body kbd,\n.markdown-body pre,\n.markdown-body samp {\n    font-family: monospace, monospace;\n    font-size: 1em;\n}\n\n.markdown-body figure {\n    margin: 1em 40px;\n}\n\n.markdown-body hr {\n    box-sizing: content-box;\n    overflow: hidden;\n    background: transparent;\n    border-bottom: 1px solid hsla(210, 18%, 87%, 1);\n    height: .25em;\n    padding: 0;\n    margin: 24px 0;\n    background-color: #d0d7de;\n    border: 0;\n}\n\n.markdown-body input {\n    font: inherit;\n    margin: 0;\n    overflow: visible;\n    font-family: inherit;\n    font-size: inherit;\n    line-height: inherit;\n}\n\n.markdown-body [type=button],\n.markdown-body [type=reset],\n.markdown-body [type=submit] {\n    -webkit-appearance: button;\n}\n\n.markdown-body [type=button]::-moz-focus-inner,\n.markdown-body [type=reset]::-moz-focus-inner,\n.markdown-body [type=submit]::-moz-focus-inner {\n    border-style: none;\n    padding: 0;\n}\n\n.markdown-body [type=button]:-moz-focusring,\n.markdown-body [type=reset]:-moz-focusring,\n.markdown-body [type=submit]:-moz-focusring {\n    outline: 1px dotted ButtonText;\n}\n\n.markdown-body [type=checkbox],\n.markdown-body [type=radio] {\n    box-sizing: border-box;\n    padding: 0;\n}\n\n.markdown-body [type=number]::-webkit-inner-spin-button,\n.markdown-body [type=number]::-webkit-outer-spin-button {\n    height: auto;\n}\n\n.markdown-body [type=search] {\n    -webkit-appearance: textfield;\n    outline-offset: -2px;\n}\n\n.markdown-body [type=search]::-webkit-search-cancel-button,\n.markdown-body [type=search]::-webkit-search-decoration {\n    -webkit-appearance: none;\n}\n\n.markdown-body ::-webkit-input-placeholder {\n    color: inherit;\n    opacity: .54;\n}\n\n.markdown-body ::-webkit-file-upload-button {\n    -webkit-appearance: button;\n    font: inherit;\n}\n\n.markdown-body a:hover {\n    text-decoration: underline;\n}\n\n.markdown-body hr::before {\n    display: table;\n    content: \"\";\n}\n\n.markdown-body hr::after {\n    display: table;\n    clear: both;\n    content: \"\";\n}\n\n.markdown-body table {\n    border-spacing: 0;\n    border-collapse: collapse;\n    display: block;\n    width: max-content;\n    max-width: 100%;\n    overflow: auto;\n}\n\n.markdown-body td,\n.markdown-body th {\n    padding: 0;\n}\n\n.markdown-body details summary {\n    cursor: pointer;\n}\n\n.markdown-body details:not([open]) > *:not(summary) {\n    display: none !important;\n}\n\n.markdown-body kbd {\n    display: inline-block;\n    padding: 3px 5px;\n    font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;\n    line-height: 10px;\n    color: #24292f;\n    vertical-align: middle;\n    background-color: #f6f8fa;\n    border: solid 1px rgba(175, 184, 193, 0.2);\n    border-bottom-color: rgba(175, 184, 193, 0.2);\n    border-radius: 6px;\n    box-shadow: inset 0 -1px 0 rgba(175, 184, 193, 0.2);\n}\n\n.markdown-body h1,\n.markdown-body h2,\n.markdown-body h3,\n.markdown-body h4,\n.markdown-body h5,\n.markdown-body h6 {\n    margin-top: 24px;\n    margin-bottom: 16px;\n    font-weight: 600;\n    line-height: 1.25;\n}\n\n.markdown-body h2 {\n    font-weight: 600;\n    padding-bottom: .3em;\n    font-size: 1.5em;\n    border-bottom: 1px solid hsla(210, 18%, 87%, 1);\n}\n\n.markdown-body h3 {\n    font-weight: 600;\n    font-size: 1.25em;\n}\n\n.markdown-body h4 {\n    font-weight: 600;\n    font-size: 1em;\n}\n\n.markdown-body h5 {\n    font-weight: 600;\n    font-size: .875em;\n}\n\n.markdown-body h6 {\n    font-weight: 600;\n    font-size: .85em;\n    color: #57606a;\n}\n\n.markdown-body p {\n    margin-top: 0;\n    margin-bottom: 10px;\n}\n\n.markdown-body blockquote {\n    margin: 0;\n    padding: 0 1em;\n    color: #57606a;\n    border-left: .25em solid #d0d7de;\n}\n\n.markdown-body ul,\n.markdown-body ol {\n    margin-top: 0;\n    margin-bottom: 0;\n    padding-left: 2em;\n}\n\n.markdown-body ol ol,\n.markdown-body ul ol {\n    list-style-type: lower-roman;\n}\n\n.markdown-body ul ul ol,\n.markdown-body ul ol ol,\n.markdown-body ol ul ol,\n.markdown-body ol ol ol {\n    list-style-type: lower-alpha;\n}\n\n.markdown-body dd {\n    margin-left: 0;\n}\n\n.markdown-body tt,\n.markdown-body code {\n    font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;\n    font-size: 12px;\n}\n\n.markdown-body pre {\n    margin-top: 0;\n    margin-bottom: 0;\n    font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;\n    font-size: 12px;\n    word-wrap: normal;\n}\n\n.markdown-body .octicon {\n    display: inline-block;\n    overflow: visible !important;\n    vertical-align: text-bottom;\n    fill: currentColor;\n}\n\n.markdown-body ::placeholder {\n    color: #6e7781;\n    opacity: 1;\n}\n\n.markdown-body input::-webkit-outer-spin-button,\n.markdown-body input::-webkit-inner-spin-button {\n    margin: 0;\n    -webkit-appearance: none;\n    appearance: none;\n}\n\n.markdown-body .pl-c {\n    color: #6e7781;\n}\n\n.markdown-body .pl-c1,\n.markdown-body .pl-s .pl-v {\n    color: #0550ae;\n}\n\n.markdown-body .pl-e,\n.markdown-body .pl-en {\n    color: #8250df;\n}\n\n.markdown-body .pl-smi,\n.markdown-body .pl-s .pl-s1 {\n    color: #24292f;\n}\n\n.markdown-body .pl-ent {\n    color: #116329;\n}\n\n.markdown-body .pl-k {\n    color: #cf222e;\n}\n\n.markdown-body .pl-s,\n.markdown-body .pl-pds,\n.markdown-body .pl-s .pl-pse .pl-s1,\n.markdown-body .pl-sr,\n.markdown-body .pl-sr .pl-cce,\n.markdown-body .pl-sr .pl-sre,\n.markdown-body .pl-sr .pl-sra {\n    color: #0a3069;\n}\n\n.markdown-body .pl-v,\n.markdown-body .pl-smw {\n    color: #953800;\n}\n\n.markdown-body .pl-bu {\n    color: #82071e;\n}\n\n.markdown-body .pl-ii {\n    color: #f6f8fa;\n    background-color: #82071e;\n}\n\n.markdown-body .pl-c2 {\n    color: #f6f8fa;\n    background-color: #cf222e;\n}\n\n.markdown-body .pl-sr .pl-cce {\n    font-weight: bold;\n    color: #116329;\n}\n\n.markdown-body .pl-ml {\n    color: #3b2300;\n}\n\n.markdown-body .pl-mh,\n.markdown-body .pl-mh .pl-en,\n.markdown-body .pl-ms {\n    font-weight: bold;\n    color: #0550ae;\n}\n\n.markdown-body .pl-mi {\n    font-style: italic;\n    color: #24292f;\n}\n\n.markdown-body .pl-mb {\n    font-weight: bold;\n    color: #24292f;\n}\n\n.markdown-body .pl-md {\n    color: #82071e;\n    background-color: #FFEBE9;\n}\n\n.markdown-body .pl-mi1 {\n    color: #116329;\n    background-color: #dafbe1;\n}\n\n.markdown-body .pl-mc {\n    color: #953800;\n    background-color: #ffd8b5;\n}\n\n.markdown-body .pl-mi2 {\n    color: #eaeef2;\n    background-color: #0550ae;\n}\n\n.markdown-body .pl-mdr {\n    font-weight: bold;\n    color: #8250df;\n}\n\n.markdown-body .pl-ba {\n    color: #57606a;\n}\n\n.markdown-body .pl-sg {\n    color: #8c959f;\n}\n\n.markdown-body .pl-corl {\n    text-decoration: underline;\n    color: #0a3069;\n}\n\n.markdown-body [data-catalyst] {\n    display: block;\n}\n\n.markdown-body g-emoji {\n    font-family: \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n    font-size: 1em;\n    font-style: normal !important;\n    font-weight: 400;\n    line-height: 1;\n    vertical-align: -0.075em;\n}\n\n.markdown-body g-emoji img {\n    width: 1em;\n    height: 1em;\n}\n\n.markdown-body::before {\n    display: table;\n    content: \"\";\n}\n\n.markdown-body::after {\n    display: table;\n    clear: both;\n    content: \"\";\n}\n\n.markdown-body > *:first-child {\n    margin-top: 0 !important;\n}\n\n.markdown-body > *:last-child {\n    margin-bottom: 0 !important;\n}\n\n.markdown-body a:not([href]) {\n    color: inherit;\n    text-decoration: none;\n}\n\n.markdown-body .absent {\n    color: #cf222e;\n}\n\n.markdown-body .anchor {\n    float: left;\n    padding-right: 4px;\n    margin-left: -20px;\n    line-height: 1;\n}\n\n.markdown-body .anchor:focus {\n    outline: none;\n}\n\n.markdown-body p,\n.markdown-body blockquote,\n.markdown-body ul,\n.markdown-body ol,\n.markdown-body dl,\n.markdown-body table,\n.markdown-body pre,\n.markdown-body details {\n    margin-top: 16px;\n    margin-bottom: 16px;\n}\n\n.markdown-body summary {\n    font-weight: 600;\n    padding-bottom: .3em;\n    font-size: 1.4em;\n}\n\n.markdown-body blockquote > :first-child {\n    margin-top: 0;\n}\n\n.markdown-body blockquote > :last-child {\n    margin-bottom: 0;\n}\n\n.markdown-body sup > a::before {\n    content: \"[\";\n}\n\n.markdown-body sup > a::after {\n    content: \"]\";\n}\n\n.markdown-body h1 .octicon-link,\n.markdown-body h2 .octicon-link,\n.markdown-body h3 .octicon-link,\n.markdown-body h4 .octicon-link,\n.markdown-body h5 .octicon-link,\n.markdown-body h6 .octicon-link {\n    color: #24292f;\n    vertical-align: middle;\n    visibility: hidden;\n}\n\n.markdown-body h1:hover .anchor,\n.markdown-body h2:hover .anchor,\n.markdown-body h3:hover .anchor,\n.markdown-body h4:hover .anchor,\n.markdown-body h5:hover .anchor,\n.markdown-body h6:hover .anchor {\n    text-decoration: none;\n}\n\n.markdown-body h1:hover .anchor .octicon-link,\n.markdown-body h2:hover .anchor .octicon-link,\n.markdown-body h3:hover .anchor .octicon-link,\n.markdown-body h4:hover .anchor .octicon-link,\n.markdown-body h5:hover .anchor .octicon-link,\n.markdown-body h6:hover .anchor .octicon-link {\n    visibility: visible;\n}\n\n.markdown-body h1 tt,\n.markdown-body h1 code,\n.markdown-body h2 tt,\n.markdown-body h2 code,\n.markdown-body h3 tt,\n.markdown-body h3 code,\n.markdown-body h4 tt,\n.markdown-body h4 code,\n.markdown-body h5 tt,\n.markdown-body h5 code,\n.markdown-body h6 tt,\n.markdown-body h6 code {\n    padding: 0 .2em;\n    font-size: inherit;\n}\n\n.markdown-body ul.no-list,\n.markdown-body ol.no-list {\n    padding: 0;\n    list-style-type: none;\n}\n\n.markdown-body ol[type=\"1\"] {\n    list-style-type: decimal;\n}\n\n.markdown-body ol[type=a] {\n    list-style-type: lower-alpha;\n}\n\n.markdown-body ol[type=i] {\n    list-style-type: lower-roman;\n}\n\n.markdown-body div > ol:not([type]) {\n    list-style-type: decimal;\n}\n\n.markdown-body ul ul,\n.markdown-body ul ol,\n.markdown-body ol ol,\n.markdown-body ol ul {\n    margin-top: 0;\n    margin-bottom: 0;\n}\n\n.markdown-body li > p {\n    margin-top: 16px;\n}\n\n.markdown-body li + li {\n    margin-top: .25em;\n}\n\n.markdown-body dl {\n    padding: 0;\n}\n\n.markdown-body dl dt {\n    padding: 0;\n    margin-top: 16px;\n    font-size: 1em;\n    font-style: italic;\n    font-weight: 600;\n}\n\n.markdown-body dl dd {\n    padding: 0 16px;\n    margin-bottom: 16px;\n}\n\n.markdown-body table {\n    color: #24292f;\n}\n\n.markdown-body table thead th {\n    font-weight: 600;\n}\n\n.markdown-body table thead th,\n.markdown-body table tbody td {\n    padding: 6px 13px !important;\n    border: 1px solid #d0d7de;\n}\n\n.markdown-body table thead tr .markdown-body table tbody tr {\n    background-color: #ffffff;\n    border-top: 1px solid hsla(210, 18%, 87%, 1);\n}\n\n.markdown-body table tr:nth-child(2n) {\n    background-color: #f6f8fa;\n}\n\n.markdown-body table img {\n    background-color: transparent;\n}\n\n.markdown-body img[align=right] {\n    padding-left: 20px;\n}\n\n.markdown-body img[align=left] {\n    padding-right: 20px;\n}\n\n.markdown-body .emoji {\n    max-width: none;\n    vertical-align: text-top;\n    background-color: transparent;\n}\n\n.markdown-body span.frame {\n    display: block;\n    overflow: hidden;\n}\n\n.markdown-body span.frame > span {\n    display: block;\n    float: left;\n    width: auto;\n    padding: 7px;\n    margin: 13px 0 0;\n    overflow: hidden;\n    border: 1px solid #d0d7de;\n}\n\n.markdown-body span.frame span img {\n    display: block;\n    float: left;\n}\n\n.markdown-body span.frame span span {\n    display: block;\n    padding: 5px 0 0;\n    clear: both;\n    color: #24292f;\n}\n\n.markdown-body span.align-center {\n    display: block;\n    overflow: hidden;\n    clear: both;\n}\n\n.markdown-body span.align-center > span {\n    display: block;\n    margin: 13px auto 0;\n    overflow: hidden;\n    text-align: center;\n}\n\n.markdown-body span.align-center span img {\n    margin: 0 auto;\n    text-align: center;\n}\n\n.markdown-body span.align-right {\n    display: block;\n    overflow: hidden;\n    clear: both;\n}\n\n.markdown-body span.align-right > span {\n    display: block;\n    margin: 13px 0 0;\n    overflow: hidden;\n    text-align: right;\n}\n\n.markdown-body span.align-right span img {\n    margin: 0;\n    text-align: right;\n}\n\n.markdown-body span.float-left {\n    display: block;\n    float: left;\n    margin-right: 13px;\n    overflow: hidden;\n}\n\n.markdown-body span.float-left span {\n    margin: 13px 0 0;\n}\n\n.markdown-body span.float-right {\n    display: block;\n    float: right;\n    margin-left: 13px;\n    overflow: hidden;\n}\n\n.markdown-body span.float-right > span {\n    display: block;\n    margin: 13px auto 0;\n    overflow: hidden;\n    text-align: right;\n}\n\n.markdown-body code,\n.markdown-body tt {\n    padding: .2em .4em;\n    margin: 0;\n    font-size: 85%;\n    background-color: rgba(175, 184, 193, 0.2);\n    border-radius: 6px;\n}\n\n.markdown-body code br,\n.markdown-body tt br {\n    display: none;\n}\n\n.markdown-body del code {\n    text-decoration: inherit;\n}\n\n.markdown-body pre code {\n    font-size: 100%;\n}\n\n.markdown-body pre > code {\n    padding: 0;\n    margin: 0;\n    word-break: normal;\n    white-space: pre;\n    background: transparent;\n    border: 0;\n}\n\n.markdown-body .highlight {\n    margin-bottom: 16px;\n}\n\n.markdown-body .highlight pre {\n    margin-bottom: 0;\n    word-break: normal;\n}\n\n.markdown-body .highlight pre,\n.markdown-body pre {\n    padding: 16px;\n    overflow: auto;\n    font-size: 85%;\n    line-height: 1.45;\n    background-color: #f6f8fa;\n    border-radius: 6px;\n}\n\n.markdown-body pre code,\n.markdown-body pre tt {\n    display: inline;\n    max-width: auto;\n    padding: 0;\n    margin: 0;\n    overflow: visible;\n    line-height: inherit;\n    word-wrap: normal;\n    background-color: transparent;\n    border: 0;\n}\n\n.markdown-body .csv-data td,\n.markdown-body .csv-data th {\n    padding: 5px;\n    overflow: hidden;\n    font-size: 12px;\n    line-height: 1;\n    text-align: left;\n    white-space: nowrap;\n}\n\n.markdown-body .csv-data .blob-num {\n    padding: 10px 8px 9px;\n    text-align: right;\n    background: #ffffff;\n    border: 0;\n}\n\n.markdown-body .csv-data tr {\n    border-top: 0;\n}\n\n.markdown-body .csv-data th {\n    font-weight: 600;\n    background: #f6f8fa;\n    border-top: 0;\n}\n\n.markdown-body .footnotes {\n    font-size: 12px;\n    color: #57606a;\n    border-top: 1px solid #d0d7de;\n}\n\n.markdown-body .footnotes ol {\n    padding-left: 16px;\n}\n\n.markdown-body .footnotes li {\n    position: relative;\n}\n\n.markdown-body .footnotes li:target::before {\n    position: absolute;\n    top: -8px;\n    right: -8px;\n    bottom: -8px;\n    left: -24px;\n    pointer-events: none;\n    content: \"\";\n    border: 2px solid #0969da;\n    border-radius: 6px;\n}\n\n.markdown-body .footnotes li:target {\n    color: #24292f;\n}\n\n.markdown-body .footnotes .data-footnote-backref g-emoji {\n    font-family: monospace;\n}\n\n.markdown-body .task-list-item {\n    list-style-type: none;\n}\n\n.markdown-body .task-list-item label {\n    font-weight: 400;\n}\n\n.markdown-body .task-list-item.enabled label {\n    cursor: pointer;\n}\n\n.markdown-body .task-list-item + .task-list-item {\n    margin-top: 3px;\n}\n\n.markdown-body .task-list-item .handle {\n    display: none;\n}\n\n.markdown-body .task-list-item-checkbox {\n    margin: 0 .2em .25em -1.6em;\n    vertical-align: middle;\n}\n\n.markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox {\n    margin: 0 -1.6em .25em .2em;\n}\n\n.markdown-body ::-webkit-calendar-picker-indicator {\n    filter: invert(50%);\n}\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/misc/notes_default.md",
    "content": "An h1 header\n============\n\nParagraphs are separated by a blank line.\n\n2nd paragraph. *Italic*, **bold**, and `monospace`. Itemized lists\nlook like:\n\n* this one\n* that one\n* the other one\n\n> Block quotes are\n> written like so.\n>\n> They can span multiple paragraphs,\n> if you like.\n\nUnicode is supported. ☺\n\n\n\nAn h2 header\n------------\n\nHere's a numbered list:\n\n1. first item\n2. second item\n3. third item\n\nNote again how the actual text starts at 4 columns in (4 characters\nfrom the left side). Here's a code sample:\n\n    # Let me re-iterate ...\n    for i in 1 .. 10 { do-something(i) }\n\nAs you probably guessed, indented 4 spaces. By the way, instead of\nindenting the block, you can use delimited blocks, if you like:\n\n~~~\ndefine foobar() {\n    print \"Welcome to flavor country!\";\n}\n~~~\n\n\n\n### An h3 header ###\n\nNow a nested list:\n\n1. First, get these ingredients:\n\n    * carrots\n    * celery\n    * lentils\n\n2. Boil some water.\n\n3. Dump everything in the pot and follow\n   this algorithm:\n\n       find wooden spoon\n       uncover pot\n       stir\n       cover pot\n       balance wooden spoon precariously on pot handle\n       wait 10 minutes\n       goto first step (or shut off burner when done)\n\n   Do not bump wooden spoon or it will fall.\n\nNotice again how text always lines up on 4-space indents (including\nthat last line which continues item 3 above).\n\nHere's a link to [a website](http://foo.bar) and to a [section heading in the current\ndoc](#an-h2-header). Here's a footnote [^1].\n\n[^1]: Footnote text goes here.\n\nTables can look like this:\n\n| size | material    | color       |\n|------|-------------|-------------|\n| 9    | leather     | brown       |\n| 10   | hemp canvas | natural     |\n| 11   | glass       | transparent |\n\nTable: Shoes, their sizes, and what they're made of\n\nA horizontal rule follows.\n\n***\n\nHere's a definition list:\n\napples\n: Good for making applesauce.\noranges\n: Citrus!\ntomatoes\n: There's no \"e\" in tomatoe.\n\nAgain, text is indented 4 spaces. (Put a blank line between each\nterm/definition pair to spread things out more.)\n\nAnd note that you can backslash-escape any punctuation characters\nwhich you wish to be displayed literally, ex.: \\`foo\\`, \\*bar\\*, etc.\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/misc/report_privacy_policy.md",
    "content": "**PRIVACY NOTICE**\n\n**Last updated September 19, 2023**\n\nThis privacy notice for the XPipe error reporter, in which (\"**we**,\" \"**us**,\" or \"**our**\") refers to XPipe UG (haftungsbeschränkt), describes how\nand why we\nmight collect, store, use, and/or share (\"**process**\") your information when you use our services (\"**Services**\"),\nsuch as when you:\n\n* Use the error reporter of our application (XPipe)\n\n**Questions or concerns?** Reading this privacy notice will help you understand your privacy rights and choices. If you\ndo not agree with our policies and practices, please do not use our Services. If you still have any questions or\nconcerns, please contact us at hello@xpipe.io.\n\n<a name=\"toc\"></a> **TABLE OF CONTENTS**\n\n[1\\. WHAT INFORMATION DO WE COLLECT?](#infocollect)\n\n[2\\. HOW DO WE PROCESS YOUR INFORMATION?](#infouse)\n\n[3\\. WHAT LEGAL BASES DO WE RELY ON TO PROCESS YOUR PERSONAL INFORMATION?](#legalbases)\n\n[4\\. WHEN AND WITH WHOM DO WE SHARE YOUR PERSONAL INFORMATION?](#whoshare)\n\n[5\\. IS YOUR INFORMATION TRANSFERRED INTERNATIONALLY?](#intltransfers)\n\n[6\\. HOW LONG DO WE KEEP YOUR INFORMATION?](#inforetain)\n\n[7\\. HOW DO WE KEEP YOUR INFORMATION SAFE?](#infosafe)\n\n[8\\. WHAT ARE YOUR PRIVACY RIGHTS?](#privacyrights)\n\n[9\\. DO CALIFORNIA RESIDENTS HAVE SPECIFIC PRIVACY RIGHTS?](#caresidents)\n\n[10\\. DO WE MAKE UPDATES TO THIS NOTICE?](#policyupdates)\n\n[11\\. HOW CAN YOU CONTACT US ABOUT THIS NOTICE?](#contact)\n\n[12\\. HOW CAN YOU REVIEW, UPDATE, OR DELETE THE DATA WE COLLECT FROM YOU?](#request)\n\n<a name=\"infocollect\"></a> **1\\. WHAT INFORMATION DO WE COLLECT?**\n\n**Error reports.** If you encounter and send application error reports using the issue reporter,\nwe also may collect the following information:\n\n* _Error Messages._ An error message can contain a variety of information, some of which might be personal information. You can review the exact data in the shown error message before reporting.\n\n* _Email address. (Optional choice)_ You can choose to provide your email address to be contacted about any states updates for your reported issue.\n\n* _Log Data. (Optional choice)_ Log and usage data is service-related, diagnostic, usage, and performance information our\n  application automatically collect when you use it and which we record in log files. Depending on how\n  you interact with it, this log data may include your used IP addresses, device information, and settings and\n  information about your activity in the services (such as the date/time stamps associated with your usage, actions and\n  files viewed, searches, and other actions you take such as which features you use), device event information (such as\n  system activity, error reports, and hardware settings).\n\nThis information is primarily needed to maintain the security and operation of our application(s), for troubleshooting,\nand for our internal analytics and reporting purposes.\n\n<a name=\"infouse\"></a> **2\\. HOW DO WE PROCESS YOUR INFORMATION?**\n\n**We process your personal information for a variety of reasons, depending on how you interact with our Services,\nincluding:**\n\n* **To protect our Services.** We may process your information as part of our efforts to keep our Services safe and\n  secure, including error and exploit monitoring and prevention.\n\n* **To save or protect an individual's vital interest.** We may process your information when necessary to save or\n  protect an individual’s vital interest, such as to prevent harm.\n\n<a name=\"legalbases\"></a> **3\\. WHAT LEGAL BASES DO WE RELY ON TO PROCESS YOUR INFORMATION?**\n\n_**In Short:** We only process your personal information when we believe it is necessary and we have a valid legal\nreason (i.e., legal basis) to do so under applicable law, like with your consent, to comply with laws, to provide you\nwith services to enter into or fulfill our contractual obligations, to protect your rights, or to fulfill our legitimate\nbusiness interests._\n\n_**If you are located in the EU or UK, this section applies to you.**_\n\nThe General Data Protection Regulation (GDPR) and UK GDPR require us to explain the valid legal bases we rely on in\norder to process your personal information. As such, we may rely on the following legal bases to process your personal\ninformation:\n\n* **Legitimate Interests.** We may process your information when we believe it is reasonably necessary to achieve our\n  legitimate business interests and those interests do not outweigh your interests and fundamental rights and freedoms.\n  For example, we may process your personal information for some of the purposes described in order to:\n\n* Diagnose problems and/or prevent errors and exploits\n\n* Understand how our users use our products and services so we can improve user experience\n\nIn legal terms, we are generally the \"data controller\" under European data protection laws of the personal information\ndescribed in this privacy notice, since we determine the means and/or purposes of the data processing we perform. This\nprivacy notice does not apply to the personal information we process as a \"data processor\" on behalf of our customers.\nIn those situations, the customer that we provide services to and with whom we have entered into a data processing\nagreement is the \"data controller\" responsible for your personal information, and we merely process your information on\ntheir behalf in accordance with your instructions. If you want to know more about our customers' privacy practices, you\nshould read their privacy policies and direct any questions you have to them.\n\n**_If you are located in Canada, this section applies to you._**\n\nWe may process your information if you have given us specific permission (i.e., express consent) to use your personal\ninformation for a specific purpose, or in situations where your permission can be inferred (i.e., implied consent). You\ncan withdraw your consent at any time. Click [here](#request) to learn more.\n\nIn some exceptional cases, we may be legally permitted under applicable law to process your information without your\nconsent, including, for example:\n\n* If collection is clearly in the interests of an individual and consent cannot be obtained in a timely way\n\n* For investigations and fraud detection and prevention\n\n* For business transactions provided certain conditions are met\n\n* If it is contained in a witness statement and the collection is necessary to assess, process, or settle an insurance\n  claim\n\n* For identifying injured, ill, or deceased persons and communicating with next of kin\n\n* If we have reasonable grounds to believe an individual has been, is, or may be victim of financial abuse\n\n* If it is reasonable to expect collection and use with consent would compromise the availability or the accuracy of the\n  information and the collection is reasonable for purposes related to investigating a breach of an agreement or a\n  contravention of the laws of Canada or a province\n\n* If disclosure is required to comply with a subpoena, warrant, court order, or rules of the court relating to the\n  production of records\n\n* If it was produced by an individual in the course of their employment, business, or profession and the collection is\n  consistent with the purposes for which the information was produced\n\n* If the collection is solely for journalistic, artistic, or literary purposes\n\n* If the information is publicly available and is specified by the regulations\n\n<a name=\"whoshare\"></a> **4\\. WHEN AND WITH WHOM DO WE SHARE YOUR PERSONAL INFORMATION?**\n\n**_In Short:_** _We may share information in specific situations described in this section and/or with the following\nthird parties._\n\n* **Error reporting and usage tracking**\n\n**_Sentry_**:\nFunctional Software, Inc. dba Sentry, 45 Fremont Street, 8th Floor, San Francisco, CA 94105.\nYou can find their privacy policy here: [https://sentry.io/privacy/](https://sentry.io/privacy/)\n\n<a name=\"intltransfers\"></a> **5\\. IS YOUR INFORMATION TRANSFERRED INTERNATIONALLY?**\n\n**_In Short:_** _We may transfer, store, and process your information in countries other than your own._\n\nPlease be aware that your information may be transferred to, stored, and processed by us in our\nfacilities and by those third parties with whom we may share your personal information (\nsee \"[WHEN AND WITH WHOM DO WE SHARE YOUR PERSONAL INFORMATION?](#whoshare)\" above), in the United States, and other\ncountries.\n\nIf you are a resident in the European Economic Area (EEA) or United Kingdom (UK), then these countries may not\nnecessarily have data protection laws or other similar laws as comprehensive as those in your country. However, we will\ntake all necessary measures to protect your personal information in accordance with this privacy notice and applicable\nlaw.\n\nEuropean Commission's Standard Contractual Clauses:\n\nWe have implemented measures to protect your personal information, including by using the European Commission's Standard\nContractual Clauses for transfers of personal information between our group companies and between us and our third-party\nproviders. These clauses require all recipients to protect all personal information that they process originating from\nthe EEA or UK in accordance with European data protection laws and regulations. Our Data Processing Agreements that\ninclude Standard Contractual Clauses are available here: [https://sentry.io/legal/dpa/](https://sentry.io/legal/dpa/).\nWe have implemented similar appropriate safeguards with our third-party service providers and partners and further\ndetails can be provided upon request.\n\n<a name=\"inforetain\"></a> **6\\. HOW LONG DO WE KEEP YOUR INFORMATION?**\n\n**_In Short:_** _We keep your information for as long as necessary to fulfill the purposes outlined in this privacy\nnotice unless otherwise required by law._\n\nWe will only keep your personal information for as long as it is necessary for the purposes set out in this privacy\nnotice, unless a longer retention period is required or permitted by law (such as tax, accounting, or other legal\nrequirements).\n\nWhen we have no ongoing legitimate business need to process your personal information, we will either delete or\nanonymize such information, or, if this is not possible (for example, because your personal information has been stored\nin backup archives), then we will securely store your personal information and isolate it from any further processing\nuntil deletion is possible.\n\n<a name=\"infosafe\"></a> **7\\. HOW DO WE KEEP YOUR INFORMATION SAFE?**\n\n**_In Short:_** _We aim to protect your personal information through a system of organizational and technical security\nmeasures._\n\nWe have implemented appropriate and reasonable technical and organizational security measures designed to protect the\nsecurity of any personal information we process. However, despite our safeguards and efforts to secure your information,\nno electronic transmission over the Internet or information storage technology can be guaranteed to be 100% secure, so\nwe cannot promise or guarantee that hackers, cybercriminals, or other unauthorized third parties will not be able to\ndefeat our security and improperly collect, access, steal, or modify your information. Although we will do our best to\nprotect your personal information, transmission of personal information to and from our Services is at your own risk.\nYou should only access the Services within a secure environment.\n\n<a name=\"privacyrights\"></a> **8\\. WHAT ARE YOUR PRIVACY RIGHTS?**\n\n**_In Short:_** _In some regions, such as the European Economic Area (EEA), United Kingdom (UK), and Canada, you have\nrights that allow you greater access to and control over your personal information. You may review, change, or terminate\nyour account at any time._\n\nIn some regions (like the EEA, UK, and Canada), you have certain rights under applicable data protection laws. These may\ninclude the right (i) to request access and obtain a copy of your personal information, (ii) to request rectification or\nerasure; (iii) to restrict the processing of your personal information; and (iv) if applicable, to data portability. In\ncertain circumstances, you may also have the right to object to the processing of your personal information. You can\nmake such a request by contacting us by using the contact details provided in the\nsection \"[HOW CAN YOU CONTACT US ABOUT THIS NOTICE?](#contact)\" below.\n\nWe will consider and act upon any request in accordance with applicable data protection laws.\n\nIf you are located in the EEA or UK and you believe we are unlawfully processing your personal information, you also\nhave the right to complain to your local data protection supervisory authority. You can find their contact details\nhere: [https://ec.europa.eu/justice/data-protection/bodies/authorities/index\\_en.htm](https://ec.europa.eu/justice/data-protection/bodies/authorities/index_en.htm)\n.\n\nIf you are located in Switzerland, the contact details for the data protection authorities are available\nhere: [https://www.edoeb.admin.ch/edoeb/en/home.html](https://www.edoeb.admin.ch/edoeb/en/home.html).\n\n**Withdrawing your consent:** If we are relying on your consent to process your personal information, which may be\nexpress and/or implied consent depending on the applicable law, you have the right to withdraw your consent at any time.\nYou can withdraw your consent at any time by contacting us by using the contact details provided in the\nsection \"[HOW CAN YOU CONTACT US ABOUT THIS NOTICE?](#contact)\" below.\n\nHowever, please note that this will not affect the lawfulness of the processing before its withdrawal nor, when\napplicable law allows, will it affect the processing of your personal information conducted in reliance on lawful\nprocessing grounds other than consent.\n\nIf you have questions or comments about your privacy rights, you may email us at hello@xpipe.io.\n\n<a name=\"caresidents\"></a> **9\\. DO CALIFORNIA RESIDENTS HAVE SPECIFIC PRIVACY RIGHTS?**\n\n**_In Short:_** _Yes, if you are a resident of California, you are granted specific rights regarding access to your\npersonal information._\n\nCalifornia Civil Code Section 1798.83, also known as the \"Shine The Light\" law, permits our users who are California\nresidents to request and obtain from us, once a year and free of charge, information about categories of personal\ninformation (if any) we disclosed to third parties for direct marketing purposes and the names and addresses of all\nthird parties with which we shared personal information in the immediately preceding calendar year. If you are a\nCalifornia resident and would like to make such a request, please submit your request in writing to us using the contact\ninformation provided below.\n\nIf you are under 18 years of age, reside in California, and have a registered account with Services, you have the right\nto request removal of unwanted data that you publicly post on the Services. To request removal of such data, please\ncontact us using the contact information provided below and include the email address associated with your account and a\nstatement that you reside in California. We will make sure the data is not publicly displayed on the Services, but\nplease be aware that the data may not be completely or comprehensively removed from all our systems (e.g., backups,\netc.).\n\n<a name=\"policyupdates\"></a> **10\\. DO WE MAKE UPDATES TO THIS NOTICE?**\n\n_**In Short:** Yes, we will update this notice as necessary to stay compliant with relevant laws._\n\nWe may update this privacy notice from time to time. The updated version will be indicated by an updated \"Revised\" date\nand the updated version will be effective as soon as it is accessible. If we make material changes to this privacy\nnotice, we may notify you either by prominently posting a notice of such changes or by directly sending you a\nnotification. We encourage you to review this privacy notice frequently to be informed of how we are protecting your\ninformation.\n\n<a name=\"contact\"></a> **11\\. HOW CAN YOU CONTACT US ABOUT THIS NOTICE?**\n\nIf you have questions or comments about this notice, you may contact us by email at hello@xpipe.io.\n\n<a name=\"request\"></a> **12\\. HOW CAN YOU REVIEW, UPDATE, OR DELETE THE DATA WE COLLECT FROM YOU?**\n\nBased on the applicable laws of your country, you may have the right to request access to the personal information we\ncollect from you, change that information, or delete it. To request to review, update, or delete your personal\ninformation, please submit a request form by writing to hello@xpipe.io.\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/misc/vault.md",
    "content": "# XPipe Vault (Keep this repository private!)\n\nThis repository contains all connection information that is designated to be shared.\n\nYou can sync with this repository in all XPipe application instances the same way, every change you make in one instance will be reflected in the repository. \n\n## Category list\n\n%s\n\n## Connection list\n\n%s\n\n## Secret encryption\n\nYou have the option to fetch any sensitive information like passwords from outside sources like password managers or enter them at connection time through a prompt window. In that case, XPipe doesn't have to store any secrets itself.\n\nIn case you choose to store passwords and other secrets within XPipe, all sensitive information is encrypted when it is saved using AES with either:\n\n- A dynamically generated key file `vaultkey` (The data can then only be decrypted with that file present)\n- A custom passphrase that can be set for your user in the vault settings menu (This option can only as secure as the password you choose)\n\nBy default, general connection data is not encrypted, only secrets are.\nSo things like hostnames and usernames are stored without encryption, which is in line with many other tools.\nThere is an available setting in the vault settings menu to encrypt all connection data if you want to do that.\n\n## Cloning the repository on other systems\n\nNowadays, most providers require a personal access token (PAT) to authenticate from the command-line instead of traditional passwords.\nYou can find common (PAT) pages here:\n- **GitHub**: [Personal access tokens (classic)](https://github.com/settings/tokens)\n- **GitLab**: [Personal access token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html)\n- **BitBucket**: [Personal access token](https://support.atlassian.com/bitbucket-cloud/docs/access-tokens/)\n- **Gitea**: `Settings -> Applications -> Manage Access Tokens section`\nSet the token permission for repository to Read and Write. The rest of the token permissions can be set as Read.\n\nEven if your git client prompts you for a password, you should enter your token unless your provider still uses passwords.\n\nIf you don't want to enter your credentials every time, you can use any git credentials manager for that.\nFor more information, see for example:\n- https://git-scm.com/doc/credential-helpers\n- https://docs.github.com/en/get-started/getting-started-with-git/caching-your-github-credentials-in-git\n\nSome modern git clients also take care of storing credentials automatically.\n\n## Troubleshooting\n\n### Adding connections to the repository\n\nBy default, no connection categories are set to sync so that you have explicit control on what connections to commit.\n\nTo have your connections of a category put inside your git repository, you first need to change its sync configuration.\nIn your `Connections` tab under the category overview on the left side, you can open the category configuration menu either by right-clicking the category or click on the `⚙️` icon when hovering over the category, and then clicking on the `🔧` configure button.\n\nThen, set the `Sync with git repository` value to `Yes` to sync the category and connections to your git repository.\nThis will add all syncable connections in that category to the git repository.\nThe sync settings for a category are inherited by default from its parent if not explicitly set.\n\n### Local connections are not synced\n\nAny connection located under the local machine can not be shared as it refers to connections and data that are only available on the local system.\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/misc/vault_empty.md",
    "content": "# XPipe Vault (Keep this repository private!)\n\nIt works! The git remote push succeeded. However, no connections have been pushed to this git repository yet.\n\n## Adding connections to the repository\n\nBy default, no connection categories are set to sync so that you have explicit control on what connections to commit.\n\nTo have your connections of a category put inside your git repository, you first need to change its sync configuration.\nIn your `Connections` tab under the category overview on the left side, you can open the category configuration menu either by right-clicking the category or click on the `⚙️` icon when hovering over the category, and then clicking on the `🔧` configure button.\n\nThen, set the `Sync with git repository` value to `Yes` to sync the category and connections to your git repository.\nThis will add all syncable connections in that category to the git repository.\nThe sync settings for a category are inherited by default from its parent if not explicitly set.\n\n## Local connections are not synced\n\nAny connection located under the local machine can not be shared as it refers to connections and data that are only available on the local system.\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/misc/welcome.md",
    "content": "## Welcome\n\nWelcome to XPipe!\n\nYou can view the development status, report issues, and more at the following places:\n\n- [GitHub Repository](https://github.com/xpipe-io/xpipe/)\n- [Email us](mailto://hello@xpipe.io)\n- [Discord Server](https://discord.gg/8y89vS8cRb)\n- [XPipe subreddit](https://reddit.com/r/xpipe)\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/about.css",
    "content": ".tile-button-comp.update-button {\n    -fx-border-width: 1;\n    -fx-border-radius: 4px;\n    -fx-background-radius: 4px;\n    -fx-border-color: -color-border-default;\n}\n\n.open-source-notices .open-source-header {\n    -fx-padding: 0.2em 0.3em 0.35em 0.3em;\n    -fx-graphic-text-gap: 6px;\n}\n\n.open-source-notices {\n\n}\n\n.properties-comp .header {\n    -fx-graphic-text-gap: 0.8em;\n}\n\n.properties-comp .tile > * {\n    -fx-padding: 0.6em 0 0.6em 0;\n}\n\n\n.troubleshoot-tab .separator {\n    -fx-padding: 0.3em 0 0.3em 0;\n}\n\n.about-tab .update-check {\n    -fx-spacing: 0.6em;\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/alert.css",
    "content": ".dialog-pane:header {\n    -fx-pref-height: 2em;\n}\n\n.dialog-pane .button {\n    -fx-font-size: 0.9em;\n    -fx-border-width: 1px;\n    -fx-border-radius: 2px;\n    -fx-background-radius: 2px;\n    -fx-padding: 6;\n}\n\n.dialog-pane > *.button-bar > *.container {\n    -fx-padding: 12;\n    -fx-border-color: -color-border-default;\n    -fx-background-color: -color-bg-subtle;\n    -fx-border-width: 1 0 0 0;\n}\n\n.root:seamless-frame.dialog-pane > *.button-bar > *.container {\n    -fx-background-color: transparent;\n}\n\n.dialog-pane:header .header-panel {\n    -fx-background-color: -color-bg-default;\n    -fx-padding: 1.5em;\n    -fx-font-size: 1.15em;\n}\n\n.dialog-pane:header .header-panel .graphic-container {\n    -fx-font-size: 0.5em;\n}\n\n.dialog-pane:header > .content {\n    -fx-border-width: 2px 0 0 0;\n    -fx-border-color: -color-accent-fg;\n    -fx-background-color: -color-bg-default;\n    -fx-border-insets: 0 1.5em 1.5em 1.5em;\n    -fx-padding: 1.5em 0 0 0;\n}\n\n.content-text {\n    -fx-text-fill: -color-fg-default;\n}\n\n.dialog-pane:header .content.label {\n    -fx-text-fill: -color-fg-default;\n}\n\n.dialog-pane:header .header-panel .label {\n    -fx-font-size: 1.0em;\n    -fx-wrap-text: true;\n    -fx-text-fill: -color-fg-default;\n}\n\n.dialog-pane:header > *.label.content {\n    -fx-font-size: 0.8em;\n}\n\n.dialog-pane:no-header .content {\n    -fx-background-color: -color-bg-default;\n}\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/bookmark.css",
    "content": "\n\n.root:nord .bookmarks-header .filter-comp {\n    -fx-border-radius: 0;\n    -fx-background-radius: 0;\n}\n\n.bookmarks-header .filter-comp {\n    -fx-border-width: 1;\n    -fx-border-radius: 0 4px 4px 0;\n    -fx-background-radius: 0 4px 4px 0;\n    -fx-background-color: -color-bg-default-transparent;\n    -fx-border-color: -color-border-default;\n}\n\n.bookmark-list .store-section-mini-comp .item:selected {\n    -fx-font-weight: BOLD;\n}\n\n.bookmark-list .store-section-mini-comp:top {\n    -fx-padding: 0 0 0 2px;\n}\n\n.bookmark-list.store-section-mini-comp > .children-content > .scroll-pane {\n    -fx-vbar-policy: always;\n}\n\n.bookmarks-container {\n    -fx-background-radius: 4 2 2 4;\n    -fx-background-insets: 0 7 4 4, 1 8 5 5;\n    -fx-padding: 1 0 5 5;\n    -fx-background-color: -color-border-default, -color-bg-default-transparent;\n}\n\n.bookmarks-header {\n    -fx-min-height: 2.8em;\n    -fx-pref-height: 2.8em;\n    -fx-max-height: 2.8em;\n    -fx-padding: 6 6 6 4;\n}\n\n.bookmarks-header .ikonli-font-icon {\n    -fx-icon-color: -color-fg-default;\n}\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/browser.css",
    "content": ".root:macos .browser .browser-content, .root:macos .browser .top-bar {\n    -fx-font-family: \"Inter\";\n}\n\n.browser .visual-display {\n    -fx-border-color: -color-border-default;\n    -fx-border-width: 2 1 1 1;\n}\n\n.browser .visual-display:focused {\n    -fx-border-color: -color-accent-emphasis;\n}\n\n.download-background {\n    -fx-padding: 1em;\n}\n\n.transfer {\n    -fx-padding: 0 6 4 4;\n}\n\n.transfer > .download-background.color-box.gray {\n    -fx-border-radius: 4;\n    -fx-background-radius: 4;\n}\n\n.transfer:highlighted > .download-background.color-box.gray {\n    -fx-border-color: -color-accent-emphasis;\n    -fx-background-color: derive(-color-bg-subtle, 5%);\n}\n\n.transfer .button {\n    -fx-border-color: -color-border-default;\n    -fx-border-width: 1px;\n    -fx-background-color: -color-bg-default-transparent;\n    -fx-background-radius: 4;\n    -fx-border-radius: 4;\n    -fx-padding: 0.1em 0.2em;\n}\n\n.root:nord .transfer > * {\n    -fx-background-radius: 0;\n    -fx-border-radius: 0;\n}\n\n.transfer .button:hover {\n    -fx-background-color: -color-accent-subtle;\n    -fx-opacity: 1.0;\n}\n\n.browser .top-spacer {\n    -fx-background-color: transparent;\n    -fx-border-width: 0 0 1 0;\n    -fx-border-color: -color-border-default;\n}\n\n.root:seamless-frame .browser .top-spacer {\n    -fx-background-radius: 0 6 0 0;\n}\n\n.browser .welcome .button:hover {\n    -fx-background-color: -color-neutral-muted;\n}\n\n.browser .tile > * {\n    -fx-padding: 0.6em 0 0.6em 0;\n}\n\n.browser .browser-content.overview {\n    -fx-spacing: 1.5em;\n    -fx-padding: 1.5em;\n    -fx-background-color: -color-bg-default-transparent;\n}\n\n.browser .browser-overview{\n    -fx-spacing: 7;\n}\n\n.browser .terminal-dock-comp {\n    -fx-padding: 7 7 7 3;\n}\n\n.browser .terminal-dock-comp:empty {\n    -fx-border-insets: 6;\n    -fx-border-width: 1;\n    -fx-border-color: -color-border-default;\n    -fx-border-radius: 4;\n    -fx-background-color: -color-bg-default-transparent;\n    -fx-background-insets: 6;\n    -fx-background-radius: 4;\n}\n\n.root:nord .browser .terminal-dock-comp:empty {\n    -fx-border-radius: 0;\n    -fx-background-radius: 0;\n}\n\n.selected-file-list {\n    -fx-spacing: 5px;\n    -fx-padding: 8px;\n}\n\n.selected-file-list * {\n    -fx-spacing: 5px;\n}\n\n.selected-file-list.drag {\n    -fx-border-width: 1px;\n    -fx-border-color: -color-border-default;\n    -fx-background-color: -color-neutral-muted;\n    -fx-background-radius: 1px;\n    -fx-border-radius: 1px;\n}\n\n.transfer:drag-over > .download-background.color-box.gray {\n    -fx-background-color: -color-success-muted;\n}\n\n.browser .top-bar {\n    -fx-min-height: 2.8em;\n    -fx-pref-height: 2.8em;\n    -fx-max-height: 2.8em;\n    -fx-padding: 6 4 6px 0;\n}\n\n.browser .top-bar > .button-bar {\n    -fx-border-color: -color-border-default;\n    -fx-border-width: 1px;\n    -fx-background-color: -color-bg-default-transparent;\n    -fx-background-radius: 4;\n    -fx-border-radius: 4;\n}\n\n.root:nord .browser .top-bar > .button-bar {\n    -fx-background-radius: 0;\n    -fx-border-radius: 0;\n}\n\n.browser .top-bar > .button-bar > .button {\n    -fx-background-insets: 0;\n    -fx-background-color: transparent;\n}\n\n.browser .top-bar > .button-bar > .menu-button {\n    -fx-padding: 0;\n    -fx-background-insets: 0;\n    -fx-background-color: transparent;\n}\n\n.browser .top-bar > .button-bar > .button:hover, .root:key-navigation .browser .top-bar > .button-bar > .button:focused {\n    -fx-background-color: -color-neutral-muted;\n}\n\n.browser .top-bar > .button-bar > .menu-button:hover, .root:key-navigation .browser .top-bar > .button-bar > .menu-button:focused {\n    -fx-background-color: -color-neutral-muted;\n}\n\n.browser .top-bar > .button-bar > .browser-filter .button:hover {\n    -fx-background-color: -color-neutral-muted;\n}\n\n.browser .status-bar {\n    -fx-background-radius: 0 0 4 4;\n    -fx-border-width: 1 0 0 0;\n    -fx-border-color: -color-border-default;\n    -fx-padding: 5 8;\n}\n\n.browser .status-bar .progress {\n    -fx-font-family: Roboto;\n}\n\n.browser .breadcrumbs > .divider {\n    -fx-padding: 0;\n}\n\n.browser .breadcrumbs {\n    -fx-padding: 0px 10px 0px 10px;\n}\n\n.browser .breadcrumbs {\n    -fx-background-color: transparent;\n}\n\n.browser .breadcrumbs .button {\n    -fx-padding: 3px 1px 3px 1px;\n    -fx-background-color: transparent;\n}\n\n.browser .breadcrumbs .button:hover {\n    -fx-background-color: -color-neutral-muted;\n}\n\n.browser .path-text {\n    -fx-padding: 3 12;\n}\n\n.browser .browser-filter .text-field {\n    -fx-padding: 3 12;\n    -fx-background-radius: 4 0 0 4;\n    -fx-background-insets: 0;\n}\n\n.root:nord .browser .browser-filter .text-field {\n    -fx-background-radius: 0;\n}\n\n.browser .path-text:invisible {\n    -fx-text-fill: transparent;\n}\n\n.browser .overview .simple-titled-pane-comp {\n    -fx-background-radius: 4;\n}\n\n.root:nord .browser .overview .simple-titled-pane-comp {\n    -fx-background-radius: 0;\n}\n\n.browser .overview-file-list {\n    -fx-border-width: 0;\n}\n\n.browser .overview-file-list .button {\n    -fx-border-width: 0;\n    -fx-background-radius: 0;\n    -fx-background-insets: 0;\n    -fx-effect: none;\n}\n\n.browser .tab-pane {\n    -fx-border-width: 0 0 0 0px;\n    -fx-border-color: -color-border-default;\n}\n\n.browser.chooser {\n    -fx-padding: -11 -3 -10 -3;\n}\n\n.chooser-selection {\n    -fx-background-color: -color-bg-subtle;\n    -fx-background-radius: 2;\n}\n\n.browser .singular {\n    -fx-tab-max-height: 0;\n}\n\n.browser .singular .tab-header-area {\n    visibility: hidden;\n}\n\n.browser .tab-header-area, .browser .headers-region {\n\n}\n\n.browser .tab-header-area .control-buttons-tab {\n    -fx-opacity: 0;\n}\n\n.browser .tab-loading-indicator {\n    -fx-min-width: 2.5em;\n    -fx-pref-width: 2.5em;\n    -fx-max-width: 2.5em;\n    -fx-min-height: 2.5em;\n    -fx-pref-height: 2.5em;\n    -fx-max-height: 2.5em;\n}\n\n.browser.chooser .left {\n    -fx-border-width: 0;\n}\n\n.browser .tab-header-area {\n    -fx-background-color:  transparent;\n}\n\n.root:seamless-frame .browser .tab-header-area {\n    -fx-background-radius: 0 6 0 0;\n}\n\n.browser .browser-content {\n    -fx-padding: 6 0 0 0;\n    -fx-border-radius: 4;\n    -fx-background-radius: 4;\n    -fx-background-insets: 0, 7 0 0 0;\n}\n\n.browser .browser-content-container {\n    -fx-padding: 0 4 4 0;\n}\n\n.root:nord .browser .browser-content {\n    -fx-border-radius: 0;\n    -fx-background-radius: 0;\n}\n\n.browser .table-view {\n    -fx-border-width: 0 0 0 0px;\n    -fx-border-color: -color-border-default;\n}\n\n.browser .split-pane-divider {\n    -fx-padding: 0 2 0 3;\n    -fx-opacity: 1.0;\n    -fx-background-color: transparent;\n}\n\n.browser.chooser .split-pane-divider {\n    -fx-border-width: 0;\n}\n\n.table-view .column-header {\n    -fx-pref-height: 2em;\n    -fx-background-color: transparent;\n}\n\n.table-view .column-header-background {\n    -fx-background-color: transparent;\n}\n\n.table-view .column-header-background .label {\n    -fx-font-weight: bolder;\n    -fx-opacity: 0.9;\n    -fx-padding: 0 0 0 7;\n}\n\n.browser .table-row-cell:empty {\n    -fx-opacity: 0.7;\n}\n\n/* setting opacity directly to the .table-cell or .table-row-cell\n   leads to incorrect table row height calculation #javafx-bug */\n/*.browser .table-row-cell:hidden > .table-cell > * {*/\n/*    -fx-opacity: 0.75;*/\n/*}*/\n\n.browser .table-view:drag-into-current .table-row-cell {\n    -fx-opacity: 0.65;\n}\n\n.browser .table-row-cell:file:hover, .table-row-cell:folder:hover {\n    -fx-background-color: -color-accent-subtle;\n}\n\n.browser .table-row-cell:selected, .browser .table-row-cell:hover:selected, .root:key-navigation .browser .table-view:focus-within .table-row-cell:focused:selected {\n    -fx-background-color: -color-success-subtle;\n}\n\n.root:key-navigation .browser .browser-content:focus-within .table-view:focus-within .table-row-cell:focus-visible {\n    -fx-background-color: -color-warning-subtle;\n}\n\n.root:dark:nord .browser .table-row-cell:selected, .root:dark:nord .browser .table-row-cell:hover:selected {\n    -fx-background-color: -color-success-7;\n}\n\n.root:light:nord .browser .table-row-cell:selected, .root:light:nord .browser .table-row-cell:hover:selected {\n    -fx-background-color: -color-success-1;\n}\n\n.browser .table-row-cell:drag-over {\n    -fx-background-color: -color-success-muted;\n}\n\n.browser .table-row-cell:drag {\n    -fx-background-color: -color-accent-muted;\n}\n\n.browser .tab-container {\n    -fx-border-radius: 4;\n    -fx-background-radius: 4;\n}\n\n.browser .tab {\n    -fx-opacity: 0.6;\n}\n\n.browser .tab:hover {\n    -fx-opacity: 0.8;\n}\n\n.browser .tab:selected {\n    -fx-opacity: 1.0;\n}\n\n.root:nord .browser .tab-container {\n    -fx-border-radius: 0;\n    -fx-background-radius: 0;\n}\n\n.browser .quick-access-button {\n    -fx-border-radius: 0;\n    -fx-background-radius: 0;\n}\n\n.browser .quick-access-button .context-menu .leaf > * > .arrow {\n    -fx-pref-width: 0;\n    -fx-opacity: 0;\n}\n\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/category.css",
    "content": ".category-button {\n    -fx-background-color: transparent;\n    -fx-background-radius: 4px;\n    -fx-border-radius: 4px;\n    -fx-border-width: 0;\n    -fx-padding: 0 0 0 2;\n    -fx-background-insets: 0;\n}\n\n.category-button .name .text-field {\n    -fx-padding: 0.25em 0.1em;\n    -fx-background-color: transparent;\n}\n\n.category-button:hover, .root:key-navigation .category-button:focused {\n    -fx-background-color: -color-bg-default-transparent;\n}\n\n.category:selected .category-button {\n    -fx-border-color: -color-border-default;\n    -fx-background-color: -color-bg-default-transparent;\n}\n\n.category:selected .category-button .name {\n    -fx-font-weight: BOLD;\n}\n\n.category.gray > .category-button .expand-button:disabled .ikonli-font-icon {\n    -fx-opacity: 0.3;\n}\n\n.root:light .category.yellow > .category-button .expand-button .ikonli-font-icon {\n    -fx-icon-color: #bc9925;\n}\n\n.root:light .category.yellow > .category-button .expand-button:disabled .ikonli-font-icon {\n    -fx-icon-color: #887028;\n}\n\n.root:light .category.green > .category-button .expand-button .ikonli-font-icon {\n    -fx-icon-color: #0d770d;\n}\n\n.root:light .category.green > .category-button .expand-button:disabled .ikonli-font-icon {\n    -fx-icon-color: #085908;\n}\n\n.root:light .category.blue > .category-button  .expand-button .ikonli-font-icon {\n    -fx-icon-color: #1c62be;\n}\n\n.root:light .category.blue > .category-button  .expand-button:disabled .ikonli-font-icon {\n    -fx-icon-color: #124d98;\n}\n\n\n.root:light .category.cyan > .category-button  .expand-button .ikonli-font-icon {\n    -fx-icon-color: #00c5e1;\n}\n\n.root:light .category.cyan > .category-button  .expand-button:disabled .ikonli-font-icon {\n    -fx-icon-color: #108d91;\n}\n\n.root:light .category.purple > .category-button  .expand-button .ikonli-font-icon {\n    -fx-icon-color: #8353f3;\n}\n\n.root:light .category.purple > .category-button  .expand-button:disabled .ikonli-font-icon {\n    -fx-icon-color: #6436c5;\n}\n\n.root:light .category.red > .category-button  .expand-button .ikonli-font-icon {\n    -fx-icon-color: #a40000;\n}\n\n.root:light .category.red > .category-button  .expand-button:disabled .ikonli-font-icon {\n    -fx-icon-color: #800000;\n}\n\n.root:dark .category.yellow > .category-button .expand-button .ikonli-font-icon {\n    -fx-icon-color: yellow;\n}\n\n.root:dark .category.yellow > .category-button .expand-button:disabled .ikonli-font-icon {\n    -fx-icon-color: #a1a116;\n}\n\n.root:dark .category.green > .category-button .expand-button .ikonli-font-icon {\n    -fx-icon-color: #00a300;\n}\n\n.root:dark .category.green > .category-button .expand-button:disabled .ikonli-font-icon {\n    -fx-icon-color: #007000;\n}\n\n.root:dark .category.blue > .category-button  .expand-button .ikonli-font-icon {\n    -fx-icon-color: #2a78da;\n}\n\n.root:dark .category.blue > .category-button  .expand-button:disabled .ikonli-font-icon {\n    -fx-icon-color: #2459a1;\n}\n\n.root:dark .category.cyan > .category-button  .expand-button .ikonli-font-icon {\n    -fx-icon-color: #25c9d8;\n}\n\n.root:dark .category.cyan > .category-button  .expand-button:disabled .ikonli-font-icon {\n    -fx-icon-color: #21afaf;\n}\n\n.root:dark .category.purple > .category-button  .expand-button .ikonli-font-icon {\n    -fx-icon-color: #8f61f6;\n}\n\n.root:dark .category.purple > .category-button  .expand-button:disabled .ikonli-font-icon {\n    -fx-icon-color: #7447d5;\n}\n\n.root:dark .category.red > .category-button  .expand-button .ikonli-font-icon {\n    -fx-icon-color: red;\n}\n\n.root:dark .category.red > .category-button  .expand-button:disabled .ikonli-font-icon {\n    -fx-icon-color: darkred;\n}\n\n.category-button  .expand-button:disabled {\n    -fx-opacity: 1.0;\n}\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/choice-comp.css",
    "content": ".combo-box {\n    -fx-focus-color: transparent;\n}\n\n.combo-box-base:hover .arrow-button:hover .arrow {\n    -fx-background-color: -color-accent-fg;\n}\n\n.clear-button:hover .ikonli-font-icon {\n    -fx-icon-color: -color-accent-fg;\n}\n\n.clear-button {\n    -fx-background-color: transparent;\n}\n\n.combo-box-popup .list-cell:hover, .combo-box-popup .list-cell:focused {\n    -fx-background-color: -color-context-menu;\n    -fx-text-fill: -color-fg-default;\n}\n\n.combo-box-popup .list-cell:hover .graphic, .combo-box-popup .list-cell .graphic {\n    -fx-icon-color: -color-fg-default;\n}\n\n.store-choice-comp.left-pill .choice-comp {\n    -fx-background-radius: 4 0 0 4;\n    -fx-border-radius: 4 0 0 4;\n}\n\n.choice-comp-content > .top {\n    -fx-padding: 0.4em;\n    -fx-background-color: -color-neutral-subtle;\n    -fx-border-width: 1 0 1 0;\n    -fx-border-color: -color-border-default;\n}\n\n.choice-comp .filter-comp {\n    -fx-border-width: 1px;\n    -fx-background-color: -color-bg-default-transparent;\n    -fx-border-color: -color-border-default;\n}\n\n.host-address-choice-comp:empty .arrow-button .arrow, .host-address-choice-comp:empty:hover .arrow-button .arrow {\n    -fx-shape: \"\";\n}\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/color-box.css",
    "content": ".root:dark .color-box.gray {\n    -fx-background-color: -color-foreground-base;\n    -fx-fill: -color-foreground-base;\n    -fx-border-color: -color-border-default;\n}\n\n.root:light .color-box.gray {\n    -fx-background-color: -color-foreground-base;\n    -fx-fill: -color-foreground-base;\n    -fx-border-color: derive(-color-border-default, -10%);\n}\n\n.color-box > .separator .line {\n    -fx-border-color: -color-border-default;\n}\n\n\n.root:pretty:light .color-box.blue {\n    -fx-background-color: linear-gradient(from 100% 0% to 0% 100%, rgb(170, 130, 250, 0.05) 40%, rgb(87, 57, 200, 0.05) 50%, rgb(167, 137, 250, 0.05) 100%);\n    -fx-fill: linear-gradient(from 100% 0% to 0% 100%, rgb(170, 130, 250, 0.25) 40%, rgb(87, 57, 200, 0.25) 50%, rgb(167, 137, 250, 0.25) 100%);\n    -fx-border-color: rgba(80, 100, 200, 0.6);\n}\n\n.root:performance:light .color-box.blue {\n    -fx-background-color: rgb(90, 90, 250, 0.06);\n    -fx-fill: rgb(90, 90, 250, 0.25);\n    -fx-border-color: rgba(80, 100, 200, 0.6);\n}\n\n.root:light .color-box.blue > .separator .line {\n    -fx-border-color: rgba(80, 100, 150, 0.4);\n}\n\n.root:pretty:dark .color-box.blue {\n    -fx-background-color: linear-gradient(from 100% 0% to 0% 100%, rgb(30, 30, 80, 0.8) 40%, rgb(27, 27, 65, 0.8) 50%, rgb(50, 37, 100, 0.8) 100%);\n    -fx-fill: linear-gradient(from 100% 0% to 0% 100%, rgb(30, 30, 80, 0.8) 40%, rgb(27, 27, 65, 0.8) 50%, rgb(50, 37, 100, 0.8) 100%);\n    -fx-border-color: rgba(80, 100, 150, 0.7);\n}\n\n.root:performance:dark .color-box.blue {\n    -fx-background-color: rgb(30, 30, 80, 0.8);\n    -fx-fill: rgb(30, 30, 80, 0.8);\n    -fx-border-color: rgba(80, 100, 150, 0.7);\n}\n\n.root:dark .color-box.blue > .separator .line {\n    -fx-border-color: rgba(80, 100, 150, 0.7);\n}\n\n\n\n.root:pretty:light .color-box.cyan {\n    -fx-background-color: linear-gradient(from 100% 0% to 0% 100%, rgb(70, 230, 250, 0.1) 40%, rgb(87, 230, 250, 0.1) 50%, rgb(147, 230, 250, 0.1) 100%);\n    -fx-fill: linear-gradient(from 100% 0% to 0% 100%, rgb(70, 230, 250, 0.25) 40%, rgb(87, 230, 250, 0.25) 50%, rgb(147, 230, 250, 0.25) 100%);\n    -fx-border-color: rgba(80, 230, 230, 0.6);\n}\n\n.root:performance:light .color-box.cyan {\n    -fx-background-color: rgb(90, 250, 250, 0.06);\n    -fx-fill: rgb(90, 250, 250, 0.25);\n    -fx-border-color: rgba(80, 230, 230, 0.6);\n}\n\n.root:light .color-box.cyan > .separator .line {\n    -fx-border-color: rgba(80, 150, 200, 0.4);\n}\n\n.root:pretty:dark .color-box.cyan {\n    -fx-background-color: linear-gradient(from 100% 0% to 0% 100%, rgb(30, 70, 90, 0.5) 40%, rgb(27, 77, 105, 0.5) 50%, rgb(50, 82, 105, 0.45) 100%);\n    -fx-fill: linear-gradient(from 100% 0% to 0% 100%, rgb(30, 70, 90, 0.5) 40%, rgb(27, 77, 105, 0.5) 50%, rgb(50, 82, 105, 0.45) 100%);\n    -fx-border-color: rgba(80, 150, 200, 0.5);\n}\n\n.root:performance:dark .color-box.cyan {\n    -fx-background-color: rgb(30, 70, 90, 0.45);\n    -fx-fill: rgb(30, 70, 90, 0.45);\n    -fx-border-color: rgba(80, 150, 200, 0.5);\n}\n\n.root:dark .color-box.cyan > .separator .line {\n    -fx-border-color: rgba(80, 200, 250, 0.2);\n}\n\n\n\n.root:pretty:light .color-box.purple {\n    -fx-background-color: linear-gradient(from 100% 0% to 0% 100%, rgb(230, 70, 250, 0.05) 40%, rgb(230,87,  250, 0.05) 50%, rgb(230, 147, 250, 0.05) 100%);\n    -fx-fill: linear-gradient(from 100% 0% to 0% 100%, rgb(230, 70, 250, 0.25) 40%, rgb(230,87,  250, 0.25) 50%, rgb(230, 147, 250, 0.25) 100%);\n    -fx-border-color: rgba(230, 80, 230, 0.4);\n}\n\n.root:performance:light .color-box.purple {\n    -fx-background-color: rgb(250, 90, 250, 0.06);\n    -fx-fill: rgb(250, 90, 250, 0.26);\n    -fx-border-color: rgba(230, 80, 230, 0.4);\n}\n\n.root:light .color-box.purple > .separator .line {\n    -fx-border-color: rgba(150, 80, 200, 0.4);\n}\n\n.root:pretty:dark .color-box.purple {\n    -fx-background-color: linear-gradient(from 100% 0% to 0% 100%, rgb(70, 30, 90, 0.4) 40%, rgb(77, 27, 105, 0.4) 50%, rgb(82, 50, 105, 0.35) 100%);\n    -fx-fill: linear-gradient(from 100% 0% to 0% 100%, rgb(70, 30, 90, 0.4) 40%, rgb(77, 27, 105, 0.4) 50%, rgb(82, 50, 105, 0.35) 100%);\n    -fx-border-color: rgba(150, 80, 200, 0.4);\n}\n\n.root:performance:dark .color-box.purple {\n    -fx-background-color: rgb(70, 30, 90, 0.4);\n    -fx-fill: rgb(70, 30, 90, 0.4);\n    -fx-border-color: rgba(150, 80, 200, 0.4);\n}\n\n.root:dark .color-box.purple > .separator .line {\n    -fx-border-color: rgba(200, 80, 250, 0.3);\n}\n\n\n\n.root:pretty:light .color-box.red {\n    -fx-background-color: linear-gradient(from 100% 0% to 0% 100%, rgb(250, 100, 100, 0.03) 40%, rgb(245, 50, 50, 0.03) 50%, rgb(240, 90, 90, 0.03) 100%);\n    -fx-fill: linear-gradient(from 100% 0% to 0% 100%, rgb(250, 100, 100, 0.25) 40%, rgb(245, 50, 50, 0.25) 50%, rgb(240, 90, 90, 0.25) 100%);\n    -fx-border-color: rgba(200, 100, 80, 0.6);\n}\n\n.root:performance:light .color-box.red {\n    -fx-background-color: rgb(250, 80, 80, 0.03);\n    -fx-fill: rgb(250, 80, 80, 0.25);\n    -fx-border-color: rgba(200, 100, 80, 0.6);\n}\n\n.root:light .color-box.red > .separator .line {\n    -fx-border-color: rgba(150, 100, 80, 0.4);\n}\n\n.root:pretty:dark .color-box.red {\n    -fx-background-color: linear-gradient(from 100% 0% to 0% 100%, rgb(80, 30, 40, 0.4) 40%, rgb(65, 30, 43, 0.4) 50%, rgb(100, 37, 57, 0.4) 100%);\n    -fx-fill: linear-gradient(from 100% 0% to 0% 100%, rgb(80, 30, 40, 0.4) 40%, rgb(65, 30, 43, 0.4) 50%, rgb(100, 37, 57, 0.4) 100%);\n    -fx-border-color: rgba(150, 100, 80, 0.4);\n}\n\n.root:performance:dark .color-box.red {\n    -fx-background-color: rgb(80, 30, 30, 0.4);\n    -fx-fill: rgb(80, 30, 30, 0.4);\n    -fx-border-color: rgba(150, 100, 80, 0.4);\n}\n\n.root:dark .color-box.red > .separator .line {\n    -fx-border-color: rgba(150, 100, 80, 0.4);\n}\n\n\n.root:pretty:light .color-box.yellow {\n    -fx-background-color: linear-gradient(from 100% 0% to 0% 100%, rgb(200, 200, 30, 0.03) 40%, rgb(135, 135, 27, 0.03) 50%, rgb(220, 220, 37, 0.03) 100%);\n    -fx-fill: linear-gradient(from 100% 0% to 0% 100%, rgb(200, 200, 30, 0.6) 40%, rgb(135, 135, 27, 0.6) 50%, rgb(220, 220, 37, 0.6) 100%);\n    -fx-border-color: rgba(110, 110, 20, 0.6);\n}\n\n.root:performance:light .color-box.yellow {\n    -fx-background-color: rgb(220, 220, 50, 0.03);\n    -fx-fill: rgb(220, 220, 50, 0.6);\n    -fx-border-color: rgba(110, 110, 20, 0.6);\n}\n\n.root:light .color-box.yellow > .separator .line {\n    -fx-border-color: rgba(170, 170, 80, 0.5);\n}\n\n.root:pretty:dark .color-box.yellow {\n    -fx-background-color: linear-gradient(from 100% 0% to 0% 100%, rgb(80, 80, 60, 0.4) 40%, rgb(65, 65, 57, 0.4) 50%, rgb(100, 100, 67, 0.4) 100%);\n    -fx-fill: linear-gradient(from 100% 0% to 0% 100%, rgb(80, 80, 60, 0.4) 40%, rgb(65, 65, 57, 0.4) 50%, rgb(100, 100, 67, 0.4) 100%);\n    -fx-border-color: rgba(150, 150, 80, 0.4);\n}\n\n.root:performance:dark .color-box.yellow {\n    -fx-background-color: rgb(80, 80, 30, 0.4);\n    -fx-fill: rgb(80, 80, 30, 0.4);\n    -fx-border-color: rgba(150, 150, 80, 0.4);\n}\n\n.root:dark .color-box.yellow > .separator .line {\n    -fx-border-color: rgba(170, 170, 80, 0.3);\n}\n\n\n.root:pretty:light .color-box.green {\n    -fx-background-color: linear-gradient(from 100% 0% to 0% 100%, rgb(30, 180, 30, 0.05) 40%, rgb(20, 120, 20, 0.05) 50%, rgb(37, 200, 37, 0.05) 100%);\n    -fx-fill: linear-gradient(from 100% 0% to 0% 100%, rgb(30, 180, 30, 0.25) 40%, rgb(20, 120, 20, 0.25) 50%, rgb(37, 200, 37, 0.25) 100%);\n    -fx-border-color: rgba(100, 210, 80, 0.6);\n}\n\n.root:performance:light .color-box.green {\n    -fx-background-color: rgb(30, 180, 30, 0.05);\n    -fx-fill: rgb(30, 180, 30, 0.25);\n    -fx-border-color: rgba(100, 210, 80, 0.6);\n}\n\n.root:light .color-box.green > .separator .line {\n    -fx-border-color: rgba(100, 150, 80, 0.4);\n}\n\n.root:pretty:dark .color-box.green {\n    -fx-background-color: linear-gradient(from 100% 0% to 0% 100%, rgb(30, 80, 60, 0.3) 40%, rgb(20, 60, 50, 0.3) 50%, rgb(37, 100, 57, 0.3) 100%);\n    -fx-fill: linear-gradient(from 100% 0% to 0% 100%, rgb(30, 80, 60, 0.3) 40%, rgb(20, 60, 50, 0.3) 50%, rgb(37, 100, 57, 0.3) 100%);\n    -fx-border-color: rgba(100, 190, 80, 0.3);\n}\n\n.root:performance:dark .color-box.green {\n    -fx-background-color: rgb(30, 80, 30, 0.3);\n    -fx-fill: rgb(30, 80, 30, 0.3);\n    -fx-border-color: rgba(100, 190, 80, 0.3);\n}\n\n.root:dark .color-box.green > .separator .line {\n    -fx-border-color: rgba(100, 190, 80, 0.2);\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/data-store-list-choice-comp.css",
    "content": ".data-store-list-choice-comp .entry {\n    -fx-border-width: 1px;\n    -fx-background-color: -color-bg-default-transparent;\n    -fx-border-color: -color-border-default;\n    -fx-padding: 0.5em;\n    -fx-border-radius: 4px;\n    -fx-background-radius: 4px;\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/dialog-comp.css",
    "content": ".dialog-comp .dialog-content {\n    -fx-background-color: -color-bg-default-transparent;\n}\n\n.dialog-comp .scroll-pane {\n    -fx-padding: 0;\n}\n\n.dialog-comp .buttons .button {\n    -fx-padding: 6 12 6 12;\n    -fx-border-width: 1px;\n    -fx-border-radius: 2px;\n    -fx-background-radius: 2px;\n}\n\n.dialog-comp .buttons {\n    -fx-padding: 10;\n    -fx-border-color: -color-border-default;\n    -fx-background-color: -color-bg-subtle;\n    -fx-border-width: 1 0 0 0;\n}\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/dropdown-comp.css",
    "content": ".dropdown-comp {\n    -fx-padding: 6px 7px;\n}\n\n.dropdown-comp .arrow {\n    -fx-alignment: center;\n}\n\n.dropdown-comp .arrow-button {\n    -fx-alignment: center;\n}\n\n.dropdown-comp .menu-item:hover:focused {\n    -fx-background-color: -color-neutral-subtle;\n}\n\n.dropdown-comp .menu-item:focused {\n    -fx-background-color: transparent;\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/error-handler-comp.css",
    "content": ".error-handler-comp .top {\n    -fx-spacing: 0.7em;\n}\n\n.error-handler-comp .actions {\n    -fx-spacing: 0.3em;\n}\n\n.error-handler-comp .button:hover {\n    -fx-border-width: 1;\n    -fx-background-color: -color-context-menu;\n}\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/file-drop-augment.css",
    "content": ".file-drop-comp {\n    -fx-background-color: #FFFFFF88;\n    -fx-font-size: 10em;\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/frame.css",
    "content": ".root:macos:seamless-frame {\n    -fx-padding: 0 0 27 0;\n}\n\n.root:windows:seamless-frame:maximized {\n    -fx-padding: 5 0 0 0;\n}\n\n.root:dark:separate-frame .background {\n    -fx-background-color: derive(-color-bg-default, -3%);\n}\n\n.root:light:separate-frame .background {\n    -fx-background-color: derive(-color-bg-default, -9%);\n}\n\n.root:dark:separate-frame.background {\n    -fx-background-color: derive(-color-bg-default, -3%);\n}\n\n.root:light:separate-frame.background {\n    -fx-background-color: derive(-color-bg-default, -9%);\n}\n\n\n\n.root:dark:seamless-frame .background {\n    -fx-background-color: derive(-color-bg-default-transparent, 1%);\n}\n\n.root:light:seamless-frame .background {\n    -fx-background-color: derive(-color-bg-default-transparent, -9%);\n}\n\n.root:dark:seamless-frame.background {\n    -fx-background-color: derive(-color-bg-default-transparent, 1%);\n}\n\n.root:light:seamless-frame.background {\n    -fx-background-color: derive(-color-bg-default-transparent, -9%);\n}\n\n.root:seamless-frame .layout > .background {\n    -fx-background-radius: 0 6 0 0;\n    -fx-border-radius: 0 6 0 0;\n    -fx-border-width: 1 1 0 0;\n    -fx-padding: 0 0 0 0;\n}\n\n.root:seamless-frame:nord .layout > .background {\n    -fx-background-radius: 0 0 0 0;\n    -fx-border-radius: 0 0 0 0;\n    -fx-border-width: 1 1 0 0;\n    -fx-padding: 0 0 0 0;\n}\n\n.root:light:seamless-frame .layout > .background {\n    -fx-border-color: #999;\n}\n\n.root:dark:seamless-frame .layout > .background {\n    -fx-border-color: -color-border-default;\n}\n\n.root:dark:seamless-frame:primer .layout > .background, .root:dark:seamless-frame:mocha .layout > .background {\n    -fx-border-color: #444;\n}\n\n.root:macos:seamless-frame .layout > .background {\n    -fx-background-insets: 0;\n    -fx-border-insets: 0;\n}\n\n.root:seamless-frame .layout > .background > * {\n    -fx-background-radius: 0 6 0 0;\n    -fx-border-radius: 0 6 0 0;\n}\n\n.root:seamless-frame:nord .layout > .background > * {\n    -fx-background-radius: 0 0 0 0;\n    -fx-border-radius: 0 0 0 0;\n}\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/header-bars.css",
    "content": "\n.store-header-bar {\n    -fx-padding: 0.8em 1.0em 0.8em 1.0em;\n}\n\n.bar {\n    -fx-background-radius: 4;\n    -fx-border-radius: 4;\n    -fx-border-width: 1;\n    -fx-border-color: -color-border-default;\n    -fx-background-color: -color-bg-subtle;\n}\n\n.store-header-bar {\n    -fx-spacing: 0.45em;\n}\n\n.root:nord .store-header-bar {\n    -fx-background-radius: 0;\n    -fx-border-radius: 0;\n}\n\n.root:nord .bar {\n    -fx-background-radius: 0;\n    -fx-border-radius: 0;\n}\n\n.bar .filter-bar .text-field {\n    -fx-padding: 0.35em;\n    -fx-text-fill: -color-fg-default;\n}\n\n.store-header-bar .menu-button {\n    -fx-background-radius: 3px;\n    -fx-border-radius: 3px;\n    -fx-border-width: 1px;\n    -fx-background-color: linear-gradient(from 100% 0% to 0% 100%, rgb(208, 220, 252) 40%, rgba(219, 219, 255, 1) 50%, rgb(250, 242, 242) 100%);\n    -fx-border-color: transparent;\n}\n\n.root:nord .store-header-bar .menu-button {\n    -fx-background-radius: 0;\n    -fx-border-radius: 0;\n}\n\n.root:macos .store-header-bar .menu-button {\n    -fx-padding: -3 0 -3 0;\n}\n\n.store-header-bar .menu-button {\n    -fx-padding: -5 -2 -5 -2;\n}\n\n.root:primer:macos .store-header-bar .menu-button, .root:nord:macos .store-header-bar .menu-button, .root:dracula:macos .store-header-bar .menu-button, .root:mocha:macos .store-header-bar .menu-button {\n    -fx-padding: -5 -2 -5 -2;\n}\n\n.root:light .store-header-bar .menu-button {\n    -fx-background-color: linear-gradient(from 100% 0% to 0% 100%, rgb(12, 11, 11) 40%, rgb(32, 32, 40) 50%, rgb(35, 29, 29) 100%);\n}\n\n.store-header-bar .menu-button > * {\n    -fx-text-fill: -color-bg-default-transparent;\n}\n\n.store-header-bar .menu-button > * > .ikonli-font-icon {\n    -fx-icon-color: -color-bg-default-transparent;\n}\n\n.store-header-bar .menu-button .arrow {\n    -fx-border-color: -color-bg-default-transparent;\n    -fx-border-width: 4;\n}\n\n.root .store-header-bar .menu-button:hover, .root:key-navigation .store-header-bar .menu-button:focused {\n    -fx-background-color: -color-bg-default-transparent;\n    -fx-border-color: -color-fg-default;\n}\n\n.root .store-header-bar .menu-button:hover > *, .root:key-navigation .store-header-bar .menu-button:focused > * {\n    -fx-text-fill: -color-fg-default;\n}\n\n.root .store-header-bar .menu-button:hover > * > .ikonli-font-icon, .root:key-navigation .store-header-bar .menu-button:focused > * > .ikonli-font-icon {\n    -fx-icon-color: -color-fg-default;\n}\n\n.root .store-header-bar .menu-button:hover .arrow, .root:key-navigation .store-header-bar .menu-button:focused .arrow {\n    -fx-border-color: -color-fg-default;\n    -fx-border-width: 4;\n}\n\n.store-creation-bar, .store-sort-bar, .store-category-bar {\n    -fx-spacing: 0.2em;\n}\n\n.root:nord .store-creation-bar, .root:nord .store-sort-bar, .root:nord .store-category-bar {\n    -fx-background-radius: 0;\n    -fx-border-radius: 0;\n}\n\n.store-category-bar > .viewport > .view > .scroll-comp-content {\n    -fx-padding: 0.5em 0.1em 0.2em 0.5em;\n}\n\n.store-category-bar * {\n    -fx-icon-color: -color-fg-default;\n}\n\n.store-sort-bar {\n    -fx-spacing: 0.2em;\n}\n\n.filler-bar {\n}\n\n.sidebar {\n    -fx-padding: 4 0 4 4;\n    -fx-spacing: 4;\n    -fx-background-color: transparent;\n}\n\n.store-header-bar .button-comp {\n    -fx-padding: 0.2em 0em 0.2em 0em;\n}\n\n.store-header-bar .icon-button-comp {\n    -fx-text-fill: -color-fg-default;\n}\n\n.store-header-bar .name {\n    -fx-font-weight: BOLD;\n    -fx-text-fill: -color-fg-default;\n}\n\n.store-header-bar .count-comp {\n    -fx-font-weight: BOLD;\n    -fx-padding: 0.0em 0em 0.0em 0.4em;\n    -fx-text-fill: -color-fg-muted;\n}\n\n.store-header-bar .filter-bar .text-field {\n    -fx-text-fill: -color-fg-default;\n}\n\n.store-header-bar .filter-bar .ikonli-font-icon {\n    -fx-icon-color: -color-fg-default;\n}\n\n.store-header-bar .filter-bar {\n    -fx-background-radius: 3px;\n    -fx-background-color: -color-bg-default-transparent;\n    -fx-border-color: -color-fg-muted;\n    -fx-border-width: 1;\n    -fx-border-radius: 3px;\n}\n\n.root:nord .store-header-bar .filter-bar {\n    -fx-background-radius: 0;\n    -fx-border-radius: 0;\n}\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/intro.css",
    "content": ".intro .label {\n    -fx-line-spacing: -1px;\n}\n\n.intro .title {\n    -fx-padding: 0 0 0.2em 0;\n    -fx-text-fill: -color-fg-emphasis;\n}\n\n.intro .separator {\n    -fx-padding: 0.75em 0 0.75em 0;\n}\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/lazy-text-field-comp.css",
    "content": ".lazy-text-field-comp {\n    -fx-padding: 5px;\n}\n\n.lazy-text-field-comp:disabled {\n    -fx-opacity: 1.0;\n}\n\n.lazy-text-field-comp:disabled.text-field .input-line {\n    -fx-opacity: 0;\n}\n\n.lazy-text-field-comp .input-line {\n    -fx-opacity: 0.0;\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/modal-overlay-comp.css",
    "content": ".modal-overlay-comp .modal-box {\n    -fx-border-width: 1;\n    -fx-border-color: -color-border-default;\n    -fx-border-radius: 4;\n    -fx-background-radius: 4;\n}\n\n.root:nord .modal-overlay-comp .modal-box {\n    -fx-border-width: 1;\n    -fx-border-radius: 0;\n    -fx-background-radius: 0;\n}\n\n.modal-overlay-comp .modal-box > .content {\n    -fx-padding: 15 27 10 27;\n}\n\n.modal-overlay-comp .modal-box .button-bar .button {\n    -fx-padding: 0.45em 0.8em 0.45em 0.8em;\n    -fx-border-width: 1px;\n    -fx-border-radius: 2px;\n    -fx-background-radius: 2px;\n}\n\n.root:nord .modal-overlay-comp .modal-box .button-bar .button {\n    -fx-border-radius: 0;\n    -fx-background-radius: 0;\n}\n\n.root:dark .modal-overlay-comp.modal-pane {\n    -color-modal-pane-overlay: rgba(0, 0, 0, 0.4);\n}\n\n.modal-overlay-stack-element:loaded > .modal-overlay-comp.modal-pane > .scroll-pane > .viewport > * > .scrollable-content {\n    -fx-background-insets: 0 3.2em 0 0;\n}\n\n.root:seamless-frame .modal-overlay-stack-element:loaded > .modal-overlay-comp.modal-pane > .scroll-pane > .viewport > * > .scrollable-content {\n    -fx-background-radius: 0 6 0 0;\n}\n\n.modal-overlay-stack-element > .modal-overlay-comp.modal-pane > .scroll-pane > .viewport > * > .scrollable-content {\n    -color-modal-pane-overlay: transparent\n}\n\n.root:light .modal-overlay-stack-element:loaded > .modal-overlay-comp.modal-pane > .scroll-pane > .viewport > * > .scrollable-content {\n    -color-modal-pane-overlay: rgba(0, 0, 0, 0.3);\n}\n\n.root:dark .modal-overlay-stack-element:loaded > .modal-overlay-comp.modal-pane > .scroll-pane > .viewport > * > .scrollable-content {\n    -color-modal-pane-overlay: rgba(0, 0, 0, 0.4);\n}\n\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/options-comp.css",
    "content": ".title-header {\n    -fx-font-size: 1.2em;\n    -fx-padding: 15px 0 3px 0;\n}\n\n.choice-pane-comp {\n    -fx-spacing: 7px;\n}\n\n.options-comp .name {\n    -fx-padding: 9px 0 0 0;\n    -fx-font-size: 1.0em;\n}\n\n.options-comp .description {\n    -fx-opacity: 0.8;\n    -fx-font-size: 0.95em;\n}\n\n.options-comp .options-content {\n    -fx-font-size: 0.95em;\n}\n\n.options-comp .long-description {\n    -fx-padding: 0 6 0 6;\n}\n\n.options-comp .simple-titled-pane-comp > .content {\n    -fx-padding: 0 9 6 9;\n}\n\n.options-comp .titled-pane > .title {\n    -fx-padding: 8 20 8 10;\n}\n\n.options-comp .titled-pane > .title .text {\n    -fx-font-size: 0.95em;\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/popover.css",
    "content": ".popover {\n    -fx-background-radius: 5;\n}\n\n.popover > .content {\n    -fx-padding: 1;\n    -fx-background-radius: 5px;\n}\n\n.popover > .content > .title > .icon {\n    -fx-padding: 10 10 0 0;\n    -fx-background-color: transparent;\n}\n\n.popover > .content > .title > .text {\n}\n\n.popover > .content > .title > .icon:hover > .graphics * {\n    -fx-fill: -color-neutral-muted;\n}\n\n.popover > .content > .title  {\n    -fx-background-color: -color-bg-subtle;\n    -fx-background-radius: 5 5 0 0;\n    -fx-border-width: 0 0 1 0;\n    -fx-border-color: -color-border-default;\n}\n\n.popover > .content > .title > .text {\n    -fx-padding: 10 0 0 0;\n}\n\n.popover .dialog-comp .buttons {\n    -fx-background-color: -color-bg-subtle;\n}\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/popup-menu.css",
    "content": ".store-header-bar .menu-button .context-menu > * > * {\n    -fx-padding: 5px 10px 5px 10px;\n}\n\n.store-header-bar .menu-button .context-menu > * {\n    -fx-padding: 0;\n}\n\n.store-header-bar .menu-button .context-menu {\n    -fx-padding: 0;\n    -fx-background-radius: 0;\n    -fx-border-radius: 0;\n    -fx-border-color: -color-border-default;\n}\n\n\n.context-menu > * > * {\n    -fx-padding: 3px 10px 3px 10px;\n    -fx-background-radius: 1px;\n    -fx-spacing: 20px;\n}\n\n.context-menu .menu-up-arrow, .context-menu .menu-down-arrow {\n    -fx-padding: 5;\n    -fx-background-color: -color-bg-default-transparent;\n}\n\n.context-menu .scroll-arrow {\n    -fx-padding: 5;\n    -fx-background-color: -color-fg-muted;\n    -fx-border-color: -color-border-default;\n    -fx-border-width: 1 0 1 0;\n}\n\n.context-menu .separator {\n    -fx-padding: 0;\n}\n\n.context-menu .separator .line {\n    -fx-padding: 0;\n    -fx-border-insets: 0px;\n}\n\n\n.context-menu .accelerator-text {\n    -fx-padding: 0px 0px 0px 50px;\n}\n\n.context-menu > * {\n    -fx-padding: 0;\n}\n\n.context-menu {\n    -fx-padding: 4 0 4 0;\n    -fx-background-radius: 4px;\n    -fx-border-radius: 4px;\n    -fx-border-color: -color-border-default;\n}\n\n.root:nord .context-menu {\n    -fx-padding: 0;\n    -fx-background-radius:0;\n    -fx-border-radius: 0;\n}\n\n.context-menu * .context-menu, .context-menu.condensed {\n    -fx-padding: 0;\n    -fx-background-radius: 0;\n    -fx-border-radius: 0;\n}\n\n.root:performance .context-menu, .root:performance .combo-box-popup .list-view {\n    -fx-effect: NONE;\n    -fx-background-radius: 0;\n    -fx-border-radius: 0;\n}\n\n.root .menu-item:focused{\n    -fx-background-color: -color-context-menu;\n}\n\n.root .menu-item:focused > .label {\n    -fx-text-fill: -color-fg-default;\n}\n\n.root .menu-item:focused .graphic {\n    -fx-icon-color: -color-fg-default;\n}\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/prefs.css",
    "content": ".prefs-box {\n    -fx-padding: 0 40 0 80;\n}\n\n.prefs-container .options-comp .name {\n    -fx-padding: 1.8em 0 0 0;\n    -fx-font-weight: BOLD;\n    -fx-font-size: 1.1em;\n}\n\n.initial-setup.prefs-container {\n    -fx-padding: -1.8em 25 0 0;\n}\n\n.prefs .prefs-container.options-comp > .options-comp {\n    -fx-padding: 0 0 0 1em;\n}\n\n.prefs-container .options-comp .description-box {\n    -fx-padding: 0.5em 0 0.5em 0;\n}\n\n.prefs-container .options-comp .description {\n    -fx-opacity: 0.9;\n    -fx-font-size: 1.0em;\n}\n\n\n.root:light .prefs-container .options-comp .description {\n    -fx-opacity: 0.95;\n}\n\n.prefs-container .options-comp .options-content {\n    -fx-font-size: 1.0em;\n}\n\n.prefs-container > .title-header {\n    -fx-padding: 2em 0 -0.5em 0;\n    -fx-font-weight: BOLD;\n    -fx-font-size: 1.5em;\n}\n\n.prefs {\n    -fx-background-color: transparent;\n}\n\n.prefs .sidebar {\n    -fx-spacing: 0;\n    -fx-padding: 0.2em 0 0 0;\n    -fx-border-width: 1;\n    -fx-border-radius: 4;\n    -fx-background-radius: 4;\n}\n\n.root:nord .prefs .sidebar {\n    -fx-border-radius: 0;\n    -fx-background-radius: 0;\n}\n\n.prefs .sidebar .button {\n    -fx-background-color: transparent;\n    -fx-padding: 0.29em 1em 0.29em 1.0em;\n    -fx-background-radius: 0, 4, 4;\n    -fx-background-insets: 0, 1 4 1 4;\n}\n\n.root:nord .prefs .sidebar .button {\n    -fx-background-radius: 0, 0, 0;\n}\n\n.prefs .sidebar .button:selected {\n    -fx-background-color: transparent, -color-border-default, -color-bg-default-transparent;\n    -fx-font-weight: BOLD;\n}\n\n.prefs .sidebar .button:armed {\n    -fx-background-color: transparent, -color-accent-muted, derive(-color-neutral-muted, 25%);\n}\n\n.prefs .sidebar .button:hover, .root:key-navigation .prefs .sidebar .button:focused {\n    -fx-background-color: transparent, -color-border-default, -color-bg-overlay;\n}\n\n.prefs .theme-switcher .combo-box-popup .list-view {\n    -fx-effect: NONE;\n}\n\n\n.root:seamless-frame .prefs .scroll-bar:vertical {\n    -fx-padding: 9 1 5 1;\n}\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/scrollbar.css",
    "content": ".scroll-bar {\n    -fx-opacity: 1.0;\n}\n\n.scroll-bar .thumb {\n    -fx-opacity: 0.5;\n}\n\n.scroll-bar:vertical {\n    -fx-min-width: 7px;\n    -fx-pref-width: 7px;\n    -fx-max-width: 7px;\n    -fx-padding: 1;\n    -fx-background-color: transparent;\n}\n\n.scroll-bar:horizontal {\n    -fx-pref-height: 0.3em;\n}\n\n.scroll-pane {\n    -fx-background-insets: 0;\n    -fx-padding: 0;\n    -fx-background-color: transparent;\n    -fx-border-insets: 0;\n}\n\n.scroll-pane:focused {\n    -fx-background-insets: 0;\n}\n\n.scroll-pane .corner {\n    -fx-background-insets: 0;\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/side-split-pane-comp.css",
    "content": ".side-split-pane-comp.split-pane-divider {\n    -fx-padding: 1;\n}\n\n.side-split-pane-comp .split-pane-divider .horizontal-grabber {\n    -fx-padding: 0;\n    -fx-background-color: transparent;\n    -fx-background-insets: 0;\n    -fx-shape: \" \";\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/sidebar-comp.css",
    "content": ".sidebar-comp {\n    -fx-padding: 0;\n    -fx-background-insets: 0;\n    -fx-border-width: 0 0 0 1px;\n    -fx-border-color: -color-border-default;\n}\n\n.root:dark:separate-frame .sidebar-comp {\n    -fx-background-color: derive(-color-bg-default, 10%);\n}\n\n.root:light:separate-frame .sidebar-comp {\n    -fx-background-color: derive(-color-bg-default, -1%);\n}\n\n.root:seamless-frame .sidebar-comp {\n    -fx-border-width: 0;\n    -fx-border-color: transparent;\n    -fx-background-color: transparent;\n}\n\n.sidebar-comp .icon-button-comp, .sidebar-comp .button {\n    -fx-background-radius: 0;\n    -fx-background-insets: 0;\n    -fx-background-color: transparent;\n}\n\n.sidebar-comp .button:disabled {\n    -fx-opacity: 1.0;\n}\n\n.sidebar-comp .icon-button-comp {\n    -fx-padding: 1.1em 0.85em;\n}\n\n.sidebar-comp .icon-button-comp .vbox {\n    -fx-spacing: 0.5em;\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css",
    "content": ".store-entry-list-status-bar {\n-fx-padding: 0 8 0 8;\n    -fx-border-radius: 0 0 4 4;\n    -fx-background-radius: 0 0 4 4;\n}\n\n.store-entry-list-status-bar .button {\n-fx-padding: 4 8;\n    -fx-background-color: transparent;\n    -fx-border-width: 1;\n    -fx-border-color: transparent;\n    -fx-border-radius: 4;\n}\n\n.root:nord .store-entry-list-status-bar .button {\n    -fx-border-radius: 0;\n}\n\n.store-entry-list-status-bar .button:hover {\n    -fx-border-color: -color-border-default;\n    -fx-border-width: 1;\n}\n\n.store-list-comp.scroll-pane * {\n    -fx-icon-color: -color-fg-default;\n}\n\n.store-list-comp.scroll-pane > .viewport .list-box-content {\n    -fx-spacing: 4;\n    -fx-padding: 4 0 4 0;\n}\n\n.store-list-comp.scroll-pane:dense > .viewport .list-box-content {\n    -fx-spacing: 2;\n}\n\n.store-list-comp.scroll-pane  {\n    -fx-padding: 0 0 0 2;\n}\n\n.store-list-comp.scroll-pane .scroll-bar:vertical {\n    -fx-padding: 9 1 5 1;\n    -fx-min-width: 6px;\n    -fx-pref-width: 6px;\n    -fx-max-width: 6px;\n}\n\n/* Grid */\n\n.store-entry-grid .name .text-field {\n    -fx-padding: 0;\n}\n\n.store-entry-grid .text-field {\n    -fx-background-color: transparent;\n}\n\n.store-entry-grid .date, .store-entry-grid .summary {\n    -fx-text-fill: -color-fg-muted;\n}\n\n.root:dark .store-entry-grid:incomplete .name > .text-field, .root:dark .store-entry-grid:failed .name > .text-field {\n    -fx-text-fill: #aa473c;\n}\n\n.root:light .store-entry-grid:incomplete .name > .text-field, .root:light .store-entry-grid:failed .name > .text-field {\n    -fx-text-fill: #88352b;\n}\n\n.store-entry-grid .user-icon .ikonli-font-icon {\n    -fx-icon-color: #5b2cffff;\n}\n\n.store-entry-grid .icon > .background {\n    -fx-background-radius: 5px;\n    -fx-background-color: -color-bg-overlay;\n    -fx-opacity: 0.5;\n}\n\n.store-entry-grid .icon:hover {\n    -fx-background-color: -color-bg-inset;\n    -fx-background-radius: 5px;\n}\n\n.root:nord .store-entry-grid .icon {\n    -fx-background-radius: 3;\n}\n\n.store-entry-grid {\n    -fx-padding: 0.15em 6px 0.15em 6px;\n}\n\n.store-entry-grid.dense {\n    -fx-padding: 0px 6px 0px 6px;\n}\n\n/* Entry */\n\n.store-entry-comp {\n    -fx-border-color: transparent;\n    -fx-background-color: transparent;\n    -fx-background-radius: 4px;\n}\n\n.root:nord .store-entry-comp {\n    -fx-background-radius: 0;\n}\n\n.store-entry-comp:hover:armed {\n    -fx-background-color: derive(-color-neutral-muted, 25%);\n}\n\n.store-entry-comp:hover, .root:key-navigation .store-entry-comp:focused {\n    -fx-background-color: -color-neutral-muted;\n}\n\n.store-entry-comp .button-bar, .store-entry-comp .dropdown-comp, .store-entry-comp .toggle-switch-comp {\n    -fx-opacity: 0.65;\n}\n\n.store-entry-comp:hover .button-bar, .store-entry-comp:hover .dropdown-comp, .store-entry-comp:hover .toggle-switch-comp {\n    -fx-opacity: 1.0;\n}\n\n.notes-button:hover, .root:key-navigation .notes-button:focused, .expand-button:hover, .root:key-navigation .expand-button:focused, .quick-access-button:hover, .root:key-navigation .quick-access-button:focused {\n    -fx-background-color: -color-neutral-muted;\n}\n\n.expand-button:disabled, .quick-access-button:disabled {\n    -fx-opacity: 0.2;\n}\n\n.store-entry-comp .button-bar {\n    -fx-padding: 5;\n}\n\n.store-entry-grid.dense .button-bar {\n    -fx-padding: 2;\n}\n\n.store-entry-comp .button-bar .button {\n    -fx-padding: 6px;\n}\n\n/* Section */\n\n.store-entry-section-comp > .separator {\n    -fx-padding: 0 12px 0 35px;\n    -fx-border-insets: 0px;\n}\n\n.store-entry-section-comp > .children-content {\n    -fx-padding: 5px 0 0 17px;\n}\n\n.store-entry-section-comp > .separator .line {\n    -fx-padding: 0;\n    -fx-border-insets: 0px;\n    -fx-background-color: -color-border-subtle;\n    -fx-pref-height: 1;\n}\n\n.root:pretty .top > .store-entry-section-comp {\n    -fx-effect: dropshadow(three-pass-box, -color-shadow-default, 2, 0.5, 0, 1);\n}\n\n.store-entry-section-comp:top {\n    -fx-border-radius: 4px;\n    -fx-background-radius: 4px;\n}\n\n.store-entry-section-comp:sub:expanded {\n    -fx-border-radius: 4 0 0 4;\n    -fx-border-width: 1 0 1 1;\n    -fx-background-radius: 4 0 0 4;\n}\n\n.store-entry-section-comp:last:sub:expanded {\n    -fx-border-width: 1 0 0 1;\n    -fx-background-radius: 4 0 0 0;\n    -fx-border-radius: 4 0 0 0;\n}\n\n.store-entry-section-comp:last:sub {\n    -fx-padding: 0 0 1 0;\n}\n\n.store-entry-section-comp:last:sub:expanded {\n    -fx-padding: 0;\n}\n\n.root:nord .store-entry-section-comp {\n    -fx-border-radius: 0;\n    -fx-background-radius: 0;\n}\n\n.store-entry-section-comp .list-box-view-comp .list-box-content {\n    -fx-spacing: 0.2em;\n}\n\n.root .store-entry-section-comp.color-box.vertical-comp:per-user {\n    -fx-border-color: #5b2cffcc;\n}\n\n/* Light sub backgrounds */\n\n.root:light .store-entry-section-comp:sub:expanded {\n    -fx-border-color: #9999;\n}\n\n.root:light .store-entry-section-comp.none .store-entry-section-comp:expanded:even-depth {\n    -fx-background-color: derive(-color-bg-default-transparent, -3%);\n}\n\n.root:light .store-entry-section-comp.none .store-entry-section-comp:expanded:odd-depth {\n    -fx-background-color: -color-bg-default-transparent;\n}\n\n.root:light .store-entry-section-comp:sub:expanded:even-depth {\n    -fx-background-color: #ddd5;\n}\n\n.root:light .store-entry-section-comp:sub:expanded:odd-depth {\n    -fx-background-color: #aaa3;\n}\n\n/* Dark sub backgrounds */\n\n.root:dark .store-entry-section-comp:sub:expanded {\n    -fx-border-color: #4449;\n}\n\n.root:dark .store-entry-section-comp.none .store-entry-section-comp:expanded:even-depth {\n    -fx-background-color: derive(-color-bg-default-transparent, 5%);\n}\n\n.root:dark .store-entry-section-comp.none .store-entry-section-comp:expanded:odd-depth {\n    -fx-background-color: -color-bg-default-transparent;\n}\n\n.root:dark .store-entry-section-comp:sub:expanded:even-depth {\n    -fx-background-color: #1114;\n}\n\n.root:dark .store-entry-section-comp:sub:expanded:odd-depth {\n    -fx-background-color: #2224;\n}\n\n\n\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/store-mini-section.css",
    "content": ".store-mini-list-comp:root {\n    -fx-border-color: transparent;\n    -fx-background-color: transparent;\n}\n\n.store-section-mini-comp .item:disabled {\n    -fx-opacity: 0.8;\n}\n\n.store-section-mini-comp .item {\n    -fx-padding: 0.25em 0.4em 0.25em 0.4em;\n    -fx-border-color: transparent;\n    -fx-background-color: transparent;\n    -fx-background-radius: 0;\n}\n\n.root:nord .store-section-mini-comp .item:hover, .root:nord:key-navigation .store-section-mini-comp .item:focused {\n    -fx-background-color: -color-neutral-muted;\n}\n\n.store-section-mini-comp .item:hover, .root:key-navigation .store-section-mini-comp .item:focused {\n    -fx-background-color: -color-neutral-muted;\n}\n\n.root:light .store-section-mini-comp:sub:expanded:even-depth {\n    -fx-background-color: #9991;\n    -fx-border-color: -color-border-muted;\n}\n\n.root:dark .store-section-mini-comp:sub:expanded:even-depth {\n    -fx-background-color: #0002;\n    -fx-border-color: -color-border-muted;\n}\n\n.store-section-mini-comp .expand-button:hover, .root:key-navigation .store-section-mini-comp .expand-button:focused {\n    -fx-background-color: -color-neutral-muted;\n}\n\n.store-section-mini-comp .expand-button:disabled {\n    -fx-opacity: 0.2;\n}\n\n.store-section-mini-comp .expand-button {\n    -fx-background-radius: 0;\n}\n\n.store-section-mini-comp .quick-access-button:hover, .root:key-navigation .store-section-mini-comp .quick-access-button:focused {\n    -fx-background-color: -color-neutral-muted;\n}\n\n.store-section-mini-comp .quick-access-button:disabled {\n    -fx-opacity: 0.2;\n}\n\n.store-section-mini-comp .quick-access-button {\n    -fx-background-radius: 0;\n}\n\n.store-section-mini-comp .separator {\n    -fx-padding: 0 0.75em 0 0.75em;\n    -fx-border-insets: 0px;\n}\n\n.store-section-mini-comp > .children-content {\n    -fx-padding: 0 0 0px 15px;\n}\n\n.store-section-mini-comp:root > .children-content {\n    -fx-padding: 0;\n}\n\n.store-section-mini-comp .separator .line {\n    -fx-padding: 0;\n    -fx-border-insets: 0px;\n    -fx-background-color: -color-border-subtle;\n    -fx-opacity: 0.5;\n    -fx-pref-height: 1;\n}\n\n.root:light .store-section-mini-comp:sub:expanded {\n    -fx-border-color: #bbb9;\n}\n\n.root:dark .store-section-mini-comp:sub:expanded {\n    -fx-border-color: #3339;\n}\n\n.root:nord .store-section-mini-comp:sub:expanded {\n    -fx-border-radius: 0;\n}\n\n.store-section-mini-comp:last:sub:expanded {\n    -fx-border-width: 1 0 0 1;\n    -fx-background-radius: 4 0 0 0;\n    -fx-border-radius: 4 0 0 0;\n}\n\n.store-section-mini-comp:last:sub:expanded {\n    -fx-padding: 0;\n}\n\n.store-section-mini-comp:sub:expanded {\n    -fx-border-radius: 4 0 0 4;\n    -fx-border-width: 1px 0 1px 1px;\n}\n\n.store-section-mini-comp:root {\n    -fx-border-width: 0;\n}\n\n.store-section-mini-comp:top {\n    -fx-border-radius: 0;\n    -fx-border-width: 1px 1 1px 0px;\n    -fx-border-color: -color-border-default;\n}\n\n.store-section-mini-comp .list-box-view-comp .list-box-content {\n    -fx-spacing: 0.2em;\n}\n\n.store-section-mini-comp:root .list-box-view-comp .list-box-content {\n    -fx-spacing: 0.4em;\n}\n\n\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/style.css",
    "content": "/* For development\n.root *:focused {\n    -fx-border-width: 1;\n    -fx-border-color: red;\n}\n*/\n\n.root:light, .root:dark {\n    -fx-background-color: transparent;\n}\n\n.store-layout .split-pane-divider {\n    -fx-background-color: transparent;\n}\n\n.store-layout .terminal-dock-comp {\n    -fx-padding: 0;\n}\n\n.edit-button.icon-button-comp {\n    -fx-background-radius: 4px;\n    -fx-border-width: 1px;\n    -fx-border-radius: 4px;\n    -fx-padding: 5px;\n    -fx-background-color: -color-bg-default-transparent;\n    -fx-border-color: -color-neutral-emphasis;\n}\n\n.scan-list {\n    -fx-background-radius: 4px;\n    -fx-border-width: 1;\n    -fx-border-radius: 4px;\n    -fx-padding: 1px;\n    -fx-background-color: -color-bg-default-transparent;\n    -fx-border-color: -color-neutral-emphasis;\n}\n\n.scan-list .list-content {\n    -fx-padding: 0.7em 1px 1em 1em;\n}\n\n.root:windows .text {\n    -fx-font-smoothing-type: gray;\n}\n\n.root:linux .text {\n    -fx-font-smoothing-type: lcd;\n}\n\n.radio-button {\n    -fx-background-color: transparent;\n}\n\n.icon-button-comp {\n    -fx-padding: 0.05em;\n}\n\n.icon-button-comp {\n    -fx-padding: 0.05em;\n}\n\n.root:light .icon-button-comp:hover .ikonli-font-icon {\n    -fx-icon-color: -color-fg-muted;\n}\n\n.root:dark .icon-button-comp:hover .ikonli-font-icon {\n    -fx-opacity: 0.85;\n}\n\n.root:key-navigation .icon-button-comp:focused {\n    -fx-background-color: -color-accent-muted;\n}\n\n.scroll-pane {\n    -fx-background-color: transparent;\n}\n\n.root:light .loading-comp {\n    -fx-background-color: rgba(100, 100, 100, 0.3);\n}\n\n.root:dark .loading-comp {\n    -fx-background-color: rgba(0, 0, 0, 0.4);\n}\n\n.icon-browser {\n    -fx-border-color: transparent;\n    -color-cell-border: transparent;\n}\n\n.icon-browser .column-header-background {\n    -fx-max-height: 0;\n    -fx-pref-height: 0;\n    -fx-min-height: 0;\n}\n\n.icon-browser .table-row-cell {\n    -fx-cell-size: 80px;\n    -fx-background-color: transparent;\n}\n\n.icon-browser .table-row-cell .icon-label {\n    -fx-padding: 15 0;\n    -fx-cursor: hand;\n}\n\n.intro .button {\n    -fx-padding: 6 10;\n}\n\n.error-overlay-comp {\n    -fx-font-size: 0.9em;\n}\n\n.store-creator > .choice-comp {\n    -fx-opacity: 0.9;\n}\n\n.root:light .store-creator-busy {\n    -fx-text-fill: -color-bg-default;\n}\n\n.store-creator > .choice-comp * {\n    -fx-opacity: 1.0;\n}\n\n.store-creator .combo-box:disabled .arrow {\n    -fx-background-color: transparent;\n}\n\n.store-creator .store-creator-options {\n    -fx-padding: -5 4 0 5;\n}\n\n.icon-button-comp.batch-mode-button {\n    -fx-border-radius: 3;\n    -fx-background-radius: 3;\n    -fx-focus-color: transparent;\n    -fx-background-insets: 0;\n    -fx-border-width: 1;\n    -fx-border-color: -color-fg-subtle;\n}\n\n.root:nord .icon-button-comp.batch-mode-button {\n    -fx-border-radius: 0;\n    -fx-background-radius: 0;\n}\n\n.batch-mode-button .ikonli-font-icon {\n    -fx-icon-color: -color-fg-default;\n}\n\n.batch-mode-button:active .ikonli-font-icon {\n    -fx-icon-color: -color-accent-emphasis;\n}\n\n.root:pretty:light .store-active-comp .dot {\n    -fx-fill: radial-gradient(radius 180%, rgb(30, 180, 30, 0.6), rgb(20, 120, 20, 0.65), rgb(37, 200, 37, 0.6));\n}\n\n.root:performance:light .store-active-comp .dot {\n    -fx-fill: rgb(30, 180, 30, 0.6);\n}\n\n.root:pretty:dark .store-active-comp .dot {\n    -fx-fill: radial-gradient(radius 180%, rgb(30, 180, 30, 0.8), rgb(20, 120, 20, 0.85), rgb(37, 200, 37, 0.8));\n}\n\n.root:performance:dark .store-active-comp .dot {\n    -fx-fill: rgb(30, 180, 30, 0.7);\n}\n\n.toggle-group-comp .toggle-button {\n    -fx-padding: 3 10;\n}\n\n.root .toggle-group-comp:disabled .toggle-button:disabled {\n    -fx-opacity: 0.6;\n}\n\n.toggle-group-comp .toggle-button:disabled {\n    -fx-opacity: 1.0;\n}\n\n.root .ikonli-font-icon.graphic.supported {\n    -fx-icon-color: -color-success-fg;\n}\n\n.root .ikonli-font-icon.graphic.unsupported {\n    -fx-icon-color: -color-warning-fg;\n}\n\n.root .ikonli-font-icon.graphic.error {\n    -fx-icon-color: -color-danger-fg;\n}\n\n.ikonli-font-icon {\n    -fx-icon-color: -color-fg-default;\n    -fx-fill: -color-fg-default;\n}\n\n.button.accent .ikonli-font-icon {\n    -fx-icon-color: -color-button-fg;\n}\n\n.button:default, .button.accent {\n    -color-button-bg-focused: -color-accent-subtle;\n}\n\n.creation-menu.menu-button .separator {\n    -fx-padding: 2 0;\n}\n\n.loading-text {\n    -fx-font-family: Roboto;\n}\n\n.root .label.success .ikonli-font-icon.graphic {\n    -fx-icon-color: -color-success-fg;\n}\n\n.root .label.warning .ikonli-font-icon.graphic {\n    -fx-icon-color: -color-warning-fg;\n}\n\n.root .label.danger .ikonli-font-icon.graphic {\n    -fx-icon-color: -color-danger-fg;\n}\n\n.monospace {\n    -fx-font-family: Monospace;\n}\n\n.ikonli-font-icon.graphic.terminal-dock-button {\n    -fx-icon-color: -color-accent-fg;\n}\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/third-party.css",
    "content": ".third-party-dependency-list-comp .titled-pane .content * {\n    -fx-background-color: transparent;\n}\n\n.third-party-dependency-list-comp .titled-pane {\n    -fx-background-radius: 5;\n}\n\n.third-party-dependency-list-comp .titled-pane .content {\n    -fx-padding: 4;\n}\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/tile-button-comp.css",
    "content": ".tile-button-comp {\n    -fx-border-color: transparent;\n    -fx-background-color: transparent;\n    -fx-background-radius: 4px;\n}\n\n.root:nord .tile-button-comp {\n    -fx-background-radius: 0;\n}\n\n.tile-button-comp:hover:armed {\n    -fx-background-color: derive(-color-neutral-muted, 25%);\n}\n\n.tile-button-comp:hover, .root:key-navigation .tile-button-comp:focused {\n    -fx-background-color: -color-neutral-muted;\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/toggle-switch-comp.css",
    "content": ".root:key-navigation .toggle-switch-comp:focused > .thumb-area {\n    -fx-background-color: -color-neutral-emphasis;\n}\n\n.toggle-switch-comp:has-graphic .label {\n    -fx-font-size: 1.7em;\n}\n\n.toggle-switch-comp:has-graphic {\n    -fx-font-size: 0.75em;\n}\n\n.toggle-switch-comp .label-container {\n    -fx-padding: -0.2em 0 0 0;\n}\n\n.root:linux .toggle-switch-comp .label-container {\n    -fx-padding: -0.05em 0 0 0;\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/style/tooltip.css",
    "content": ".fancy-tooltip {\n    -fx-opacity: 1;\n}\n\n.root:performance .fancy-tooltip {\n    -fx-effect: NONE;\n    -fx-background-radius: 0;\n    -fx-border-radius: 0;\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/theme/cupertinoDark.css",
    "content": ".root:windows { -color-bg-default-transparent: #1C1C1ED5; }\n\n.root:linux { -color-bg-default-transparent: #1C1C1ED5; }\n\n.root:macos { -color-bg-default-transparent: #0d0d10E6; }\n\n.root .button, .root .toggle-button {\n    -fx-effect: NONE;\n}\n\n.root .table-view {\n    -color-cell-bg-odd: derive(-color-bg-subtle, -45%);\n    -color-cell-bg: derive(-color-bg-subtle, -30%);\n}\n\n.root:dark * {\n    -color-foreground-base: derive(-color-bg-default-transparent, 9%);\n}\n\n.root:light * {\n    -color-foreground-base: derive(-color-bg-default-transparent, -3%);\n}\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/theme/cupertinoLight.css",
    "content": ".root:windows { -color-bg-default-transparent: #FFFFFFAF; }\n\n.root:linux { -color-bg-default-transparent: #FFFFFFAF; }\n\n.root:macos { -color-bg-default-transparent: #FFFFFFBF; }\n\n.root .button, .root .toggle-button {\n    -fx-effect: NONE;\n}\n\n.root .table-view {\n    -color-cell-bg-odd: derive(-color-bg-subtle, 35%);\n}\n\n.root:dark * {\n    -color-foreground-base: derive(-color-bg-default-transparent, 13%);\n}\n\n.root:light * {\n    -color-foreground-base: derive(-color-bg-default-transparent, -3%);\n}\n\n.root * {\n    -color-fg-default: #24292f;\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/theme/custom.css",
    "content": ".root {\n    -color-dark: rgb(255, 255, 255);\n    -color-light: rgb(0, 0, 0);\n    -color-base-0: #f2f2f7;\n    -color-base-1: #e5e5ea;\n    -color-base-2: #d1d1d6;\n    -color-base-3: #aeaeb2;\n    -color-base-4: rgb(142, 142, 147);\n    -color-base-5: rgb(99, 99, 102);\n    -color-base-6: rgb(72, 72, 74);\n    -color-base-7: rgb(58, 58, 60);\n    -color-base-8: rgb(44, 44, 46);\n    -color-base-9: rgb(28, 28, 30);\n    -color-accent-0: #c2e0ff;\n    -color-accent-1: #9dceff;\n    -color-accent-2: #78bbff;\n    -color-accent-3: #54a9ff;\n    -color-accent-4: #2f96ff;\n    -color-accent-5: rgb(10, 132, 255);\n    -color-accent-6: #0970d9;\n    -color-accent-7: #075cb3;\n    -color-accent-8: #06498c;\n    -color-accent-9: #043566;\n    -color-success-0: #ccf5d2;\n    -color-success-1: #adefb7;\n    -color-success-2: #8ee99c;\n    -color-success-3: #70e381;\n    -color-success-4: #51dd66;\n    -color-success-5: rgb(50, 215, 75);\n    -color-success-6: #2bb740;\n    -color-success-7: #239735;\n    -color-success-8: #1c7629;\n    -color-success-9: #14561e;\n    -color-warning-0: #ffe7c2;\n    -color-warning-1: #ffd99d;\n    -color-warning-2: #ffca78;\n    -color-warning-3: #ffbc54;\n    -color-warning-4: #ffad2f;\n    -color-warning-5: rgb(255, 159, 10);\n    -color-warning-6: #d98709;\n    -color-warning-7: #b36f07;\n    -color-warning-8: #8c5706;\n    -color-warning-9: #664004;\n    -color-danger-0: #ffd1ce;\n    -color-danger-1: #ffb5b0;\n    -color-danger-2: #ff9993;\n    -color-danger-3: #ff7d75;\n    -color-danger-4: #ff6158;\n    -color-danger-5: rgb(255, 69, 58);\n    -color-danger-6: #d93b31;\n    -color-danger-7: #b33029;\n    -color-danger-8: #8c2620;\n    -color-danger-9: #661c17;\n    -color-fg-default: rgb(255, 255, 255);\n    -color-fg-muted: #aeaeb2;\n    -color-fg-subtle: rgb(141, 141, 147);\n    -color-fg-emphasis: rgb(255, 255, 255);\n    -color-bg-default: rgb(28, 28, 30);\n    -color-bg-overlay: rgb(28, 28, 30);\n    -color-bg-subtle: rgb(44, 44, 46);\n    -color-bg-inset: #0b0b0c;\n    -color-border-default: #30363d;\n    -color-border-muted: rgb(58, 58, 60);\n    -color-border-subtle: rgb(49, 49, 52);\n    -color-shadow-default: rgb(0, 0, 0);\n    -color-neutral-emphasis-plus: rgb(142, 142, 147);\n    -color-neutral-emphasis: rgb(142, 142, 147);\n    -color-neutral-muted: rgba(99, 99, 102, 0.4);\n    -color-neutral-subtle: rgba(99, 99, 102, 0.1);\n    -color-accent-fg: #2f96ff;\n    -color-accent-emphasis: rgb(10, 132, 255);\n    -color-accent-muted: rgba(10, 132, 255, 0.4);\n    -color-accent-subtle: rgba(10, 132, 255, 0.15);\n    -color-warning-fg: rgb(255, 159, 10);\n    -color-warning-emphasis: #d98709;\n    -color-warning-muted: rgba(255, 159, 10, 0.4);\n    -color-warning-subtle: rgba(255, 159, 10, 0.15);\n    -color-success-fg: rgb(50, 215, 75);\n    -color-success-emphasis: #2bb740;\n    -color-success-muted: rgba(50, 215, 75, 0.4);\n    -color-success-subtle: rgba(50, 215, 75, 0.15);\n    -color-danger-fg: rgb(255, 69, 58);\n    -color-danger-emphasis: rgb(255, 69, 58);\n    -color-danger-muted: rgba(255, 69, 58, 0.4);\n    -color-danger-subtle: rgba(255, 69, 58, 0.15);\n    -color-chart-1: #f3622d;\n    -color-chart-2: #fba71b;\n    -color-chart-3: #57b757;\n    -color-chart-4: #41a9c9;\n    -color-chart-5: #4258c9;\n    -color-chart-6: #9a42c8;\n    -color-chart-7: #c84164;\n    -color-chart-8: #888888;\n    -color-chart-1-alpha70: rgba(243, 98, 45, 0.7);\n    -color-chart-2-alpha70: rgba(251, 167, 27, 0.7);\n    -color-chart-3-alpha70: rgba(87, 183, 87, 0.7);\n    -color-chart-4-alpha70: rgba(65, 169, 201, 0.7);\n    -color-chart-5-alpha70: rgba(66, 88, 201, 0.7);\n    -color-chart-6-alpha70: rgba(154, 66, 200, 0.7);\n    -color-chart-7-alpha70: rgba(200, 65, 100, 0.7);\n    -color-chart-8-alpha70: rgba(136, 136, 136, 0.7);\n    -color-chart-1-alpha20: rgba(243, 98, 45, 0.2);\n    -color-chart-2-alpha20: rgba(251, 167, 27, 0.2);\n    -color-chart-3-alpha20: rgba(87, 183, 87, 0.2);\n    -color-chart-4-alpha20: rgba(65, 169, 201, 0.2);\n    -color-chart-5-alpha20: rgba(66, 88, 201, 0.2);\n    -color-chart-6-alpha20: rgba(154, 66, 200, 0.2);\n    -color-chart-7-alpha20: rgba(200, 65, 100, 0.2);\n    -color-chart-8-alpha20: rgba(136, 136, 136, 0.2);\n    -fx-background-color: -color-bg-default;\n    -fx-background-radius: inherit;\n    -fx-background-insets: inherit;\n    -fx-padding: inherit;\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/theme/dark.css",
    "content": ".root:windows { -color-bg-default-transparent: #0d1117c3; }\n\n.root:linux { -color-bg-default-transparent: #0d1117c3; }\n\n.root:macos { -color-bg-default-transparent: #080d13e3; }\n\n.root .table-view {\n    -color-cell-bg-odd: derive(-color-bg-subtle, -45%);\n    -color-cell-bg: derive(-color-bg-subtle, -30%);\n}\n\n.root:dark * {\n    -color-foreground-base: derive(-color-bg-default, 10%);\n}\n\n.root:light * {\n    -color-foreground-base: derive(-color-bg-default, -3%);\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/theme/dracula.css",
    "content": ".root:windows { -color-bg-default-transparent: #282a36c2; }\n\n.root:linux { -color-bg-default-transparent: #282a36c2; }\n\n.root:macos { -color-bg-default-transparent: #20212ce3; }\n\n.root .table-view {\n    -color-cell-bg-odd: derive(-color-bg-subtle, -35%);\n    -color-cell-bg: derive(-color-bg-subtle, -28%);\n}\n\n.root:dark * {\n    -color-foreground-base: derive(-color-bg-default-transparent, 9%);\n}\n\n.root:light * {\n    -color-foreground-base: derive(-color-bg-default-transparent, -3%);\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/theme/light.css",
    "content": ".root:windows { -color-bg-default-transparent: #FFFFFFAF; }\n\n.root:linux { -color-bg-default-transparent: #FFFFFFAF; }\n\n.root:macos { -color-bg-default-transparent: #FFFFFFBF; }\n\n.root .table-view {\n    -color-cell-bg-odd: derive(-color-bg-subtle, 35%);\n}\n\n.root:dark * {\n    -color-foreground-base: derive(-color-bg-default-transparent, 13%);\n}\n\n.root:light * {\n    -color-foreground-base: derive(-color-bg-default-transparent, -3%);\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/theme/mocha.css",
    "content": ".root {\n    -color-dark: #010409;\n    -color-light: #ffffff;\n    -color-base-0: #f0f6fc;\n    -color-base-1: #c9d1d9;\n    -color-base-2: #b1bac4;\n    -color-base-3: #8b949e;\n    -color-base-4: #6e7681;\n    -color-base-5: #484f58;\n    -color-base-6: #30363d;\n    -color-base-7: #21262d;\n    -color-base-8: #161b22;\n    -color-base-9: #0d1117;\n    -color-accent-0: #c2e0ff;\n    -color-accent-1: #9dceff;\n    -color-accent-2: #78bbff;\n    -color-accent-3: #54a9ff;\n    -color-accent-4: #2f96ff;\n    -color-accent-5: rgb(10, 132, 255);\n    -color-accent-6: #0970d9;\n    -color-accent-7: #075cb3;\n    -color-accent-8: #06498c;\n    -color-accent-9: #043566;\n    -color-success-0: #ccf5d2;\n    -color-success-1: #adefb7;\n    -color-success-2: #8ee99c;\n    -color-success-3: #70e381;\n    -color-success-4: #51dd66;\n    -color-success-5: rgb(50, 215, 75);\n    -color-success-6: #2bb740;\n    -color-success-7: #239735;\n    -color-success-8: #1c7629;\n    -color-success-9: #14561e;\n    -color-warning-0: #ffe7c2;\n    -color-warning-1: #ffd99d;\n    -color-warning-2: #ffca78;\n    -color-warning-3: #ffbc54;\n    -color-warning-4: #ffad2f;\n    -color-warning-5: rgb(255, 159, 10);\n    -color-warning-6: #d98709;\n    -color-warning-7: #b36f07;\n    -color-warning-8: #8c5706;\n    -color-warning-9: #664004;\n    -color-danger-0: #ffd1ce;\n    -color-danger-1: #ffb5b0;\n    -color-danger-2: #ff9993;\n    -color-danger-3: #ff7d75;\n    -color-danger-4: #ff6158;\n    -color-danger-5: rgb(255, 69, 58);\n    -color-danger-6: #d93b31;\n    -color-danger-7: #b33029;\n    -color-danger-8: #8c2620;\n    -color-danger-9: #661c17;\n    -color-fg-default: rgb(205, 214, 244);\n    -color-fg-muted: rgb(186, 194, 222);\n    -color-fg-subtle: rgb(166, 173, 200);\n    -color-fg-emphasis: rgb(180, 190, 254);\n    -color-bg-default: rgb(30, 30, 46);\n    -color-bg-default-transparent: #1E1E2ED2;\n    -color-bg-overlay: rgb(24, 24, 37);\n    -color-bg-subtle: rgb(49, 50, 68);\n    -color-bg-inset: rgb(17, 17, 27);\n    -color-border-default: #30363d;\n    -color-border-muted: rgb(58, 58, 60);\n    -color-border-subtle: rgb(49, 49, 52);\n    -color-shadow-default: rgb(0, 0, 0);\n    -color-neutral-emphasis-plus: rgb(142, 142, 147);\n    -color-neutral-emphasis: rgb(142, 142, 147);\n    -color-neutral-muted: rgba(99, 99, 102, 0.4);\n    -color-neutral-subtle: rgba(99, 99, 102, 0.1);\n    -color-accent-fg: #2f96ff;\n    -color-accent-emphasis: rgb(10, 132, 255);\n    -color-accent-muted: rgba(10, 132, 255, 0.4);\n    -color-accent-subtle: rgba(10, 132, 255, 0.15);\n    -color-warning-fg: rgb(255, 159, 10);\n    -color-warning-emphasis: #d98709;\n    -color-warning-muted: rgba(255, 159, 10, 0.4);\n    -color-warning-subtle: rgba(255, 159, 10, 0.15);\n    -color-success-fg: rgb(50, 215, 75);\n    -color-success-emphasis: #2bb740;\n    -color-success-muted: rgba(50, 215, 75, 0.4);\n    -color-success-subtle: rgba(50, 215, 75, 0.15);\n    -color-danger-fg: rgb(255, 69, 58);\n    -color-danger-emphasis: rgb(255, 69, 58);\n    -color-danger-muted: rgba(255, 69, 58, 0.4);\n    -color-danger-subtle: rgba(255, 69, 58, 0.15);\n    -fx-background-color: -color-bg-default;\n}\n\n.root:macos { -color-bg-default-transparent: #13171de6; }\n\n.root:linux { -color-bg-default-transparent: #191c23b8; }\n\n.root:windows { -color-bg-default-transparent: #191c23b8; }\n\n.root .table-view {\n    -color-cell-bg-odd: derive(-color-bg-subtle, -35%);\n    -color-cell-bg: derive(-color-bg-subtle, -30%);\n}\n\n.root:dark * {\n    -color-foreground-base: derive(-color-bg-default-transparent, 9%);\n}\n\n.root:light * {\n    -color-foreground-base: derive(-color-bg-default-transparent, -3%);\n}\n"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/theme/nordDark.css",
    "content": ".root:macos { -color-bg-default-transparent: #2E3440e6; }\n\n.root:windows { -color-bg-default-transparent: #2E3440c0; }\n\n.root:linux { -color-bg-default-transparent: #2E3440c0; }\n\n.root .table-view {\n    -color-cell-bg-odd: derive(-color-bg-subtle, -15%);\n    -color-cell-bg: derive(-color-bg-subtle, -10%);\n}\n\n.root:dark * {\n    -color-foreground-base: derive(-color-bg-default-transparent, 9%);\n}\n\n.root:light * {\n    -color-foreground-base: derive(-color-bg-default-transparent, -3%);\n}"
  },
  {
    "path": "app/src/main/resources/io/xpipe/app/resources/theme/nordLight.css",
    "content": ".root { -color-bg-default-transparent: #fafafcAF; }\n\n.root .table-view {\n    -color-cell-bg-odd: derive(-color-bg-subtle, 20%);\n}\n\n.root:dark * {\n    -color-foreground-base: derive(-color-bg-default-transparent, 13%);\n}\n\n.root:light * {\n    -color-foreground-base: derive(-color-bg-default-transparent, -3%);\n}"
  },
  {
    "path": "app/src/test/java/Test.java",
    "content": "import io.xpipe.app.util.ThreadHelper;\n\npublic class Test {\n\n    @org.junit.jupiter.api.Test\n    public void test() {\n        System.out.println(\"a\");\n        ThreadHelper.sleep(1000);\n    }\n}\n"
  },
  {
    "path": "beacon/README.md",
    "content": "[![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.xpipe/xpipe-beacon/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.xpipe/xpipe-beacon)\n[![javadoc](https://javadoc.io/badge2/io.xpipe/xpipe-beacon/javadoc.svg)](https://javadoc.io/doc/io.xpipe/xpipe-beacon)\n\n## XPipe Beacon\n\nThe XPipe beacon component is responsible for handling all communications between the XPipe daemon\nand the APIs and the CLI. It provides an API that supports all kinds\nof different operations.\n\nFor a full documentation, see the [OpenAPI spec](https://docs.xpipe.io/api)\n\n### Inner Workings\n\n- The underlying communication is realized through an HTTP server on port `21721`\n\n- The data structures and exchange protocols are specified in the\n  [io.xpipe.beacon.api package](src/main/java/io/xpipe/beacon/api).\n\n- Every exchange is initiated from the outside by sending a request message to the XPipe daemon.\n  The daemon then always sends a response message.\n\n- The body of a message is usually formatted in the json format.\n  As a result, all data structures exchanged must be serializable/deserializable with jackson.\n\n## Configuration\n\n#### Custom port\n\nThe default port can be changed by passing the property `io.xpipe.beacon.port=<port>` to the daemon.\nNote that if both sides do not have the same port setting, they won't be able to reach each other.\n\n#### Verbose output\n\nBy passing the property `io.xpipe.beacon.printMessages=true`, it is possible to print debug information\nabout the underlying communications.\n\n"
  },
  {
    "path": "beacon/build.gradle",
    "content": "plugins {\n    id 'java-library'\n    id 'maven-publish'\n    id 'signing'\n}\n\napply from: \"$rootDir/gradle/gradle_scripts/java.gradle\"\napply from: \"$rootDir/gradle/gradle_scripts/lombok.gradle\"\n\nversion = versionString\ngroup = groupName\nbase.archivesName = 'xpipe-beacon'\n\nrepositories {\n    mavenCentral()\n}\n\ndependencies {\n    compileOnly 'org.hamcrest:hamcrest:3.0'\n    compileOnly 'org.junit.jupiter:junit-jupiter-api:5.14.2'\n    compileOnly 'org.junit.jupiter:junit-jupiter-params:5.14.2'\n    api project(':core')\n}\n\ntasks.register('dist', Copy) {\n    from jar.archiveFile\n    into \"${project(':dist').buildDir}/dist/libraries\"\n}\n\napply from: 'publish.gradle'\napply from: \"$rootDir/gradle/gradle_scripts/publish-base.gradle\"\n"
  },
  {
    "path": "beacon/publish.gradle",
    "content": "publishing {\n    publications {\n        mavenJava(MavenPublication) {\n            artifactId = project.base.archivesName\n\n            from components.java\n\n            pom.withXml {\n                def pomNode = asNode()\n                pomNode.dependencies.'*'.findAll().each() {\n                    it.scope*.value = 'compile'\n                }\n            }\n\n\n            pom {\n                name = 'XPipe Beacon'\n                description = 'The socket-based implementation used for the communication with the XPipe daemon.'\n                url = 'https://github.com/xpipe-io/xpipe/beacon'\n                licenses {\n                    license {\n                        name = 'Apache License 2.0'\n                        url = 'https://github.com/xpipe-io/xpipe/LICENSE.md'\n                    }\n                }\n                developers {\n                    developer {\n                        id = 'crschnick'\n                        name = 'Christopher Schnick'\n                        email = 'crschnick@xpipe.io'\n                    }\n                }\n                scm {\n                    connection = 'scm:git:git://github.com/xpipe-io/xpipe.git'\n                    developerConnection = 'scm:git:ssh://github.com/xpipe-io/xpipe.git'\n                    url = 'https://github.com/xpipe-io/xpipe'\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/BeaconAuthMethod.java",
    "content": "package io.xpipe.beacon;\n\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\npublic interface BeaconAuthMethod {\n\n    @JsonTypeName(\"Local\")\n    @Value\n    @Builder\n    @Jacksonized\n    class Local implements BeaconAuthMethod {\n\n        @NonNull\n        String authFileContent;\n    }\n\n    @JsonTypeName(\"ApiKey\")\n    @Value\n    @Builder\n    @Jacksonized\n    class ApiKey implements BeaconAuthMethod {\n\n        @NonNull\n        String key;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/BeaconClient.java",
    "content": "package io.xpipe.beacon;\n\nimport io.xpipe.beacon.api.HandshakeExchange;\nimport io.xpipe.core.JacksonMapper;\n\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport lombok.SneakyThrows;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.file.Files;\nimport java.util.Optional;\n\npublic class BeaconClient {\n\n    private final int port;\n    private String token;\n\n    public BeaconClient(int port) {\n        this.port = port;\n    }\n\n    public static BeaconClient establishConnection(int port, BeaconClientInformation information) throws Exception {\n        var client = new BeaconClient(port);\n        var auth = Files.readString(BeaconConfig.getLocalBeaconAuthFile());\n        HandshakeExchange.Response response = client.performRequest(HandshakeExchange.Request.builder()\n                .client(information)\n                .auth(BeaconAuthMethod.Local.builder().authFileContent(auth).build())\n                .build());\n        client.token = response.getSessionToken();\n        return client;\n    }\n\n    public static Optional<BeaconClient> tryEstablishConnection(int port, BeaconClientInformation information) {\n        try {\n            return Optional.of(establishConnection(port, information));\n        } catch (Exception ex) {\n            return Optional.empty();\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public <RES> RES performRequest(BeaconInterface<?> prov, String rawNode)\n            throws BeaconConnectorException, BeaconClientException, BeaconServerException {\n        var content = rawNode;\n        if (BeaconConfig.printMessages()) {\n            System.out.println(\"Sending raw request:\");\n            System.out.println(content);\n        }\n\n        var client = HttpClient.newBuilder()\n                .followRedirects(HttpClient.Redirect.NORMAL)\n                .build();\n        HttpResponse<String> response;\n        try {\n            // Use direct IP to prevent DNS lookups and potential blocks (e.g. portmaster)\n            var uri = URI.create(\"http://127.0.0.1:\" + port + prov.getPath());\n            var builder = HttpRequest.newBuilder();\n            if (token != null) {\n                builder.header(\"Authorization\", \"Bearer \" + token);\n            }\n            var httpRequest = builder.uri(uri)\n                    .POST(HttpRequest.BodyPublishers.ofString(content))\n                    .build();\n            response = client.send(httpRequest, HttpResponse.BodyHandlers.ofString());\n        } catch (Exception ex) {\n            throw new BeaconConnectorException(\"Couldn't send request\", ex);\n        }\n\n        if (BeaconConfig.printMessages()) {\n            System.out.println(\"Received raw response:\");\n            System.out.println(response.body());\n        }\n\n        var se = parseServerError(response);\n        if (se.isPresent()) {\n            se.get().throwError();\n        }\n\n        var ce = parseClientError(response);\n        if (ce.isPresent()) {\n            throw ce.get().throwException();\n        }\n\n        try {\n            var reader = JacksonMapper.getDefault().readerFor(prov.getResponseClass());\n            var emptyResponseClass = prov.getResponseClass().getDeclaredFields().length == 0;\n            var body = response.body();\n            if (emptyResponseClass && body.isBlank()) {\n                return createDefaultResponse(prov);\n            }\n            var v = (RES) reader.readValue(body);\n            return v;\n        } catch (IOException ex) {\n            throw new BeaconConnectorException(\"Couldn't parse response\", ex);\n        }\n    }\n\n    @SneakyThrows\n    @SuppressWarnings(\"unchecked\")\n    private <REQ> REQ createDefaultResponse(BeaconInterface<?> beaconInterface) {\n        var c = beaconInterface.getResponseClass().getDeclaredMethod(\"builder\");\n        c.setAccessible(true);\n        var b = c.invoke(null);\n        var m = b.getClass().getDeclaredMethod(\"build\");\n        m.setAccessible(true);\n        return (REQ) beaconInterface.getResponseClass().cast(m.invoke(b));\n    }\n\n    public <REQ, RES> RES performRequest(REQ req)\n            throws BeaconConnectorException, BeaconClientException, BeaconServerException {\n        ObjectNode node = JacksonMapper.getDefault().valueToTree(req);\n        var prov = BeaconInterface.byRequest(req);\n        if (prov.isEmpty()) {\n            throw new IllegalArgumentException(\"Unknown request class \" + req.getClass());\n        }\n        if (BeaconConfig.printMessages()) {\n            System.out.println(\n                    \"Sending request to server of type \" + req.getClass().getName());\n        }\n\n        return performRequest(prov.get(), node.toPrettyString());\n    }\n\n    private Optional<BeaconClientErrorResponse> parseClientError(HttpResponse<String> response)\n            throws BeaconConnectorException {\n        if (response.statusCode() < 400 || response.statusCode() > 499) {\n            return Optional.empty();\n        }\n\n        try {\n            var v = JacksonMapper.getDefault().readValue(response.body(), BeaconClientErrorResponse.class);\n            return Optional.of(v);\n        } catch (IOException ex) {\n            throw new BeaconConnectorException(\"Couldn't parse client error message\", ex);\n        }\n    }\n\n    private Optional<BeaconServerErrorResponse> parseServerError(HttpResponse<String> response)\n            throws BeaconConnectorException {\n        if (response.statusCode() < 500 || response.statusCode() > 599) {\n            return Optional.empty();\n        }\n\n        try {\n            var v = JacksonMapper.getDefault().readValue(response.body(), BeaconServerErrorResponse.class);\n            return Optional.of(v);\n        } catch (IOException ex) {\n            throw new BeaconConnectorException(\"Couldn't parse client error message\", ex);\n        }\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/BeaconClientErrorResponse.java",
    "content": "package io.xpipe.beacon;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\n@SuppressWarnings(\"ClassCanBeRecord\")\n@Value\n@Builder\n@Jacksonized\n@AllArgsConstructor\npublic class BeaconClientErrorResponse {\n\n    String message;\n\n    public BeaconClientException throwException() {\n        return new BeaconClientException(message);\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/BeaconClientException.java",
    "content": "package io.xpipe.beacon;\n\n/**\n * Indicates that a client request was invalid.\n */\npublic class BeaconClientException extends Exception {\n\n    public BeaconClientException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/BeaconClientInformation.java",
    "content": "package io.xpipe.beacon;\n\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.EqualsAndHashCode;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\npublic abstract class BeaconClientInformation {\n\n    public abstract String toDisplayString();\n\n    @JsonTypeName(\"Cli\")\n    @Value\n    @Builder\n    @Jacksonized\n    @EqualsAndHashCode(callSuper = false)\n    public static class Cli extends BeaconClientInformation {\n\n        @Override\n        public String toDisplayString() {\n            return \"XPipe CLI\";\n        }\n    }\n\n    @JsonTypeName(\"Daemon\")\n    @Value\n    @Builder\n    @Jacksonized\n    @EqualsAndHashCode(callSuper = false)\n    public static class Daemon extends BeaconClientInformation {\n\n        @Override\n        public String toDisplayString() {\n            return \"Daemon\";\n        }\n    }\n\n    @JsonTypeName(\"Api\")\n    @Value\n    @Builder\n    @Jacksonized\n    @EqualsAndHashCode(callSuper = false)\n    public static class Api extends BeaconClientInformation {\n\n        @NonNull\n        String name;\n\n        @Override\n        public String toDisplayString() {\n            return name;\n        }\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/BeaconConfig.java",
    "content": "package io.xpipe.beacon;\n\nimport io.xpipe.core.OsType;\n\nimport lombok.experimental.UtilityClass;\n\nimport java.nio.file.Path;\nimport java.util.Optional;\n\n@UtilityClass\npublic class BeaconConfig {\n\n    public static final String BEACON_PORT_PROP = \"io.xpipe.beacon.port\";\n    private static final String PRINT_MESSAGES_PROPERTY = \"io.xpipe.beacon.printMessages\";\n\n    public static boolean printMessages() {\n        if (System.getProperty(PRINT_MESSAGES_PROPERTY) != null) {\n            return Boolean.parseBoolean(System.getProperty(PRINT_MESSAGES_PROPERTY));\n        }\n        return false;\n    }\n\n    public static int getUsedPort() {\n        var beaconPort = System.getenv(\"BEACON_PORT\");\n        if (beaconPort != null && !beaconPort.isBlank()) {\n            return Integer.parseInt(beaconPort);\n        }\n\n        if (System.getProperty(BEACON_PORT_PROP) != null) {\n            return Integer.parseInt(System.getProperty(BEACON_PORT_PROP));\n        }\n\n        return getDefaultBeaconPort();\n    }\n\n    public static int getDefaultBeaconPort() {\n        var customPortVar = System.getenv(\"XPIPE_BEACON_PORT\");\n        Integer customPort = null;\n        if (customPortVar != null) {\n            try {\n                customPort = Integer.parseInt(customPortVar);\n            } catch (NumberFormatException ignored) {\n            }\n        }\n\n        var effectivePortBase = customPort != null ? customPort : 21721;\n\n        var staging = Optional.ofNullable(System.getProperty(\"io.xpipe.app.staging\"))\n                .map(Boolean::parseBoolean)\n                .orElse(false);\n        var offset = staging ? 1 : 0;\n\n        return effectivePortBase + offset;\n    }\n\n    public static Path getLocalBeaconAuthFile() {\n        var staging = Optional.ofNullable(System.getProperty(\"io.xpipe.app.staging\"))\n                .map(Boolean::parseBoolean)\n                .orElse(false);\n        if (OsType.ofLocal() == OsType.LINUX) {\n            var name = System.getenv(\"USER\") != null ? System.getenv(\"USER\") : System.getProperty(\"user.name\");\n            return Path.of(System.getProperty(\"java.io.tmpdir\"), staging ? \"xpipe-ptb\" : \"xpipe\", name, \"beacon-auth\");\n        } else {\n            var path = Path.of(System.getProperty(\"java.io.tmpdir\"), staging ? \"xpipe-ptb\" : \"xpipe\", \"beacon-auth\");\n            if (path.startsWith(Path.of(\"C:\\\\Windows\"))) {\n                path = Path.of(System.getenv(\"LOCALAPPDATA\")).resolve(\"Temp\", staging ? \"xpipe-ptb\" : \"xpipe\", \"beacon-auth\");\n            }\n            return path;\n        }\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/BeaconConnectorException.java",
    "content": "package io.xpipe.beacon;\n\n/**\n * Indicates that a connection error occurred.\n */\npublic class BeaconConnectorException extends Exception {\n\n    public BeaconConnectorException() {}\n\n    public BeaconConnectorException(String message) {\n        super(message);\n    }\n\n    public BeaconConnectorException(String message, Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/BeaconInterface.java",
    "content": "package io.xpipe.beacon;\n\nimport io.xpipe.core.ModuleLayerLoader;\n\nimport com.sun.net.httpserver.HttpExchange;\nimport lombok.SneakyThrows;\n\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.ServiceLoader;\nimport java.util.stream.Collectors;\n\npublic abstract class BeaconInterface<T> {\n\n    private static List<BeaconInterface<?>> ALL;\n\n    public static List<BeaconInterface<?>> getAll() {\n        return ALL;\n    }\n\n    public static Optional<BeaconInterface<?>> byPath(String path) {\n        return ALL.stream().filter(d -> d.getPath().equals(path)).findAny();\n    }\n\n    public static <RQ> Optional<BeaconInterface<?>> byRequest(RQ req) {\n        return ALL.stream()\n                .filter(d -> d.getRequestClass().equals(req.getClass()))\n                .findAny();\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    @SneakyThrows\n    public Class<T> getRequestClass() {\n        var c = getClass().getSuperclass();\n        var name = (c.getSuperclass().equals(BeaconInterface.class) ? c : getClass()).getName() + \"$Request\";\n        return (Class<T>) Class.forName(name);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    @SneakyThrows\n    public Class<T> getResponseClass() {\n        var c = getClass().getSuperclass();\n        var name = (c.getSuperclass().equals(BeaconInterface.class) ? c : getClass()).getName() + \"$Response\";\n        return (Class<T>) Class.forName(name);\n    }\n\n    public boolean acceptInShutdown() {\n        return false;\n    }\n\n    public boolean requiresCompletedStartup() {\n        return true;\n    }\n\n    public boolean requiresAuthentication() {\n        return true;\n    }\n\n    public abstract String getPath();\n\n    public Object handle(HttpExchange exchange, T body) throws Throwable {\n        throw new UnsupportedOperationException();\n    }\n\n    public boolean readRawRequestBody() {\n        return false;\n    }\n\n    public boolean requiresEnabledApi() {\n        return true;\n    }\n\n    public Object getSynchronizationObject() {\n        return null;\n    }\n\n    public static class Loader implements ModuleLayerLoader {\n\n        @Override\n        public void init(ModuleLayer layer) {\n            var services = layer != null\n                    ? ServiceLoader.load(layer, BeaconInterface.class)\n                    : ServiceLoader.load(BeaconInterface.class);\n            ALL = services.stream()\n                    .map(ServiceLoader.Provider::get)\n                    .map(beaconInterface -> (BeaconInterface<?>) beaconInterface)\n                    .collect(Collectors.toList());\n            // Remove parent classes\n            ALL.removeIf(beaconInterface -> ALL.stream()\n                    .anyMatch(other -> !other.equals(beaconInterface)\n                            && beaconInterface.getClass().isAssignableFrom(other.getClass())));\n        }\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/BeaconJacksonModule.java",
    "content": "package io.xpipe.beacon;\n\nimport com.fasterxml.jackson.databind.jsontype.NamedType;\nimport com.fasterxml.jackson.databind.module.SimpleModule;\n\npublic class BeaconJacksonModule extends SimpleModule {\n\n    @Override\n    public void setupModule(SetupContext context) {\n        context.registerSubtypes(\n                new NamedType(BeaconClientInformation.Api.class),\n                new NamedType(BeaconClientInformation.Cli.class),\n                new NamedType(BeaconClientInformation.Daemon.class));\n        context.registerSubtypes(\n                new NamedType(BeaconAuthMethod.Local.class), new NamedType(BeaconAuthMethod.ApiKey.class));\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/BeaconServer.java",
    "content": "package io.xpipe.beacon;\n\nimport io.xpipe.beacon.api.DaemonStopExchange;\n\nimport lombok.SneakyThrows;\n\nimport java.net.Inet4Address;\nimport java.net.InetSocketAddress;\nimport java.net.ServerSocket;\nimport java.net.Socket;\n\npublic class BeaconServer {\n\n    @SneakyThrows\n    public static boolean isReachable(int port) {\n        var local = Inet4Address.getByAddress(new byte[] {0x7f, 0x00, 0x00, 0x01});\n\n        try (var socket = new Socket()) {\n            InetSocketAddress adress = new InetSocketAddress(local, port);\n            socket.connect(adress, 5000);\n        } catch (Exception e) {\n            return false;\n        }\n\n        // If there's some kind of networking tool interfering with sockets by for example proxying socket connections\n        // The previous connect might succeed even though nothing is running.\n        // To be sure, check that the socket is indeed occupied\n        try (var ignored = new ServerSocket(port, 0, local)) {\n            return false;\n        } catch (Exception e) {\n            return true;\n        }\n    }\n\n    @SuppressWarnings(\"unused\")\n    public static boolean tryStop(BeaconClient client) throws Exception {\n        DaemonStopExchange.Response res =\n                client.performRequest(DaemonStopExchange.Request.builder().build());\n        return res.isSuccess();\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/BeaconServerErrorResponse.java",
    "content": "package io.xpipe.beacon;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\n@SuppressWarnings(\"ClassCanBeRecord\")\n@Value\n@Builder\n@Jacksonized\n@AllArgsConstructor\npublic class BeaconServerErrorResponse {\n\n    Throwable error;\n    String documentationLink;\n\n    public void throwError() throws BeaconServerException {\n        var message = error.getMessage();\n        if (documentationLink != null) {\n            message = message + \"\\n\\nFor more information, see: \" + documentationLink;\n        }\n        throw new BeaconServerException(message, error);\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/BeaconServerException.java",
    "content": "package io.xpipe.beacon;\n\n/**\n * Indicates that an internal server error occurred.\n */\npublic class BeaconServerException extends Exception {\n\n    public BeaconServerException(String message) {\n        super(message);\n    }\n\n    public BeaconServerException(String message, Throwable cause) {\n        super(message, cause);\n    }\n\n    public BeaconServerException(Throwable cause) {\n        super(cause);\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/ActionExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class ActionExchange extends BeaconInterface<ActionExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/action\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        JsonNode action;\n\n        boolean confirm;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {}\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/AskpassExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\nimport io.xpipe.core.SecretValue;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.UUID;\n\npublic class AskpassExchange extends BeaconInterface<AskpassExchange.Request> {\n\n    @Override\n    public boolean acceptInShutdown() {\n        return true;\n    }\n\n    @Override\n    public String getPath() {\n        return \"/askpass\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        long pid;\n\n        UUID secretId;\n\n        UUID request;\n\n        String prompt;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n        @NonNull\n        SecretValue value;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/CategoryAddExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.UUID;\n\npublic class CategoryAddExchange extends BeaconInterface<CategoryAddExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/category/add\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        String name;\n\n        @NonNull\n        UUID parent;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n        @NonNull\n        UUID category;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/CategoryInfoExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\nimport io.xpipe.core.StorePath;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.time.Instant;\nimport java.util.List;\nimport java.util.UUID;\n\npublic class CategoryInfoExchange extends BeaconInterface<CategoryInfoExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/category/info\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        List<UUID> categories;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n        @NonNull\n        List<@NonNull InfoResponse> infos;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class InfoResponse {\n        @NonNull\n        UUID category;\n\n        @NonNull\n        UUID parentCategory;\n\n        @NonNull\n        StorePath name;\n\n        @NonNull\n        Instant lastUsed;\n\n        @NonNull\n        Instant lastModified;\n\n        @NonNull\n        JsonNode config;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/CategoryQueryExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\nimport java.util.UUID;\n\npublic class CategoryQueryExchange extends BeaconInterface<CategoryQueryExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/category/query\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        String filter;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n        @NonNull\n        List<@NonNull UUID> found;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/CategoryRemoveExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\nimport java.util.UUID;\n\npublic class CategoryRemoveExchange extends BeaconInterface<CategoryRemoveExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/category/remove\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        List<UUID> categories;\n\n        boolean removeChildrenCategories;\n\n        boolean removeContents;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {}\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/ConnectionAddExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.UUID;\n\npublic class ConnectionAddExchange extends BeaconInterface<ConnectionAddExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/connection/add\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        String name;\n\n        @NonNull\n        JsonNode data;\n\n        @NonNull\n        Boolean validate;\n\n        UUID category;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n        @NonNull\n        UUID connection;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/ConnectionInfoExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\nimport io.xpipe.core.StorePath;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.time.Instant;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic class ConnectionInfoExchange extends BeaconInterface<ConnectionInfoExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/connection/info\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        List<UUID> connections;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n        @NonNull\n        List<@NonNull InfoResponse> infos;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class InfoResponse {\n        @NonNull\n        UUID connection;\n\n        @NonNull\n        StorePath category;\n\n        @NonNull\n        StorePath name;\n\n        @NonNull\n        String type;\n\n        @NonNull\n        Object rawData;\n\n        @NonNull\n        Object usageCategory;\n\n        @NonNull\n        Instant lastUsed;\n\n        @NonNull\n        Instant lastModified;\n\n        @NonNull\n        Object state;\n\n        @NonNull\n        Map<String, Object> cache;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/ConnectionQueryExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\nimport java.util.UUID;\n\npublic class ConnectionQueryExchange extends BeaconInterface<ConnectionQueryExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/connection/query\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        String categoryFilter;\n\n        @NonNull\n        String connectionFilter;\n\n        @NonNull\n        String typeFilter;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n        @NonNull\n        List<@NonNull UUID> found;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/ConnectionRefreshExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.UUID;\n\npublic class ConnectionRefreshExchange extends BeaconInterface<ConnectionRefreshExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/connection/refresh\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        UUID connection;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {}\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/ConnectionRemoveExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\nimport java.util.UUID;\n\npublic class ConnectionRemoveExchange extends BeaconInterface<ConnectionRemoveExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/connection/remove\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        List<UUID> connections;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {}\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/DaemonFocusExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class DaemonFocusExchange extends BeaconInterface<DaemonFocusExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/daemon/focus\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {}\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {}\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/DaemonModeExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\nimport io.xpipe.core.XPipeDaemonMode;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class DaemonModeExchange extends BeaconInterface<DaemonModeExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/daemon/mode\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        XPipeDaemonMode mode;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n        @NonNull\n        XPipeDaemonMode usedMode;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/DaemonOpenExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\n\npublic class DaemonOpenExchange extends BeaconInterface<DaemonOpenExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/daemon/open\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        List<String> arguments;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {}\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/DaemonStatusExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class DaemonStatusExchange extends BeaconInterface<DaemonStatusExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/daemon/status\";\n    }\n\n    @Value\n    @Jacksonized\n    @Builder\n    public static class Request {}\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n        String mode;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/DaemonStopExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\n/**\n * Requests the daemon to stop.\n */\npublic class DaemonStopExchange extends BeaconInterface<DaemonStopExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/daemon/stop\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {}\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n        boolean success;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/DaemonVersionExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class DaemonVersionExchange extends BeaconInterface<DaemonVersionExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/daemon/version\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {}\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n\n        @NonNull\n        String version;\n\n        @NonNull\n        String canonicalVersion;\n\n        @NonNull\n        String buildVersion;\n\n        @NonNull\n        String jvmVersion;\n\n        @NonNull\n        String plan;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/FsBlobExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.UUID;\n\npublic class FsBlobExchange extends BeaconInterface<FsBlobExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/fs/blob\";\n    }\n\n    @Override\n    public boolean readRawRequestBody() {\n        return true;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {}\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n        @NonNull\n        UUID blob;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/FsReadExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\nimport io.xpipe.core.FilePath;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.UUID;\n\npublic class FsReadExchange extends BeaconInterface<FsReadExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/fs/read\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        UUID connection;\n\n        @NonNull\n        FilePath path;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {}\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/FsScriptExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\nimport io.xpipe.core.FilePath;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.UUID;\n\npublic class FsScriptExchange extends BeaconInterface<FsScriptExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/fs/script\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        UUID connection;\n\n        @NonNull\n        UUID blob;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n        @NonNull\n        FilePath path;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/FsWriteExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\nimport io.xpipe.core.FilePath;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.UUID;\n\npublic class FsWriteExchange extends BeaconInterface<FsWriteExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/fs/write\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        UUID connection;\n\n        @NonNull\n        UUID blob;\n\n        @NonNull\n        FilePath path;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {}\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/HandshakeExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconAuthMethod;\nimport io.xpipe.beacon.BeaconClientInformation;\nimport io.xpipe.beacon.BeaconInterface;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class HandshakeExchange extends BeaconInterface<HandshakeExchange.Request> {\n\n    @Override\n    public boolean acceptInShutdown() {\n        return true;\n    }\n\n    @Override\n    public boolean requiresAuthentication() {\n        return false;\n    }\n\n    @Override\n    public String getPath() {\n        return \"/handshake\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        BeaconAuthMethod auth;\n\n        @NonNull\n        BeaconClientInformation client;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n        @NonNull\n        String sessionToken;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/SecretDecryptExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class SecretDecryptExchange extends BeaconInterface<SecretDecryptExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/secret/decrypt\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        JsonNode encrypted;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n        @NonNull\n        String decrypted;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/SecretEncryptExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class SecretEncryptExchange extends BeaconInterface<SecretEncryptExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/secret/encrypt\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        String value;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n        @NonNull\n        JsonNode encrypted;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/ShellExecExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.UUID;\n\npublic class ShellExecExchange extends BeaconInterface<ShellExecExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/shell/exec\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        UUID connection;\n\n        @NonNull\n        String command;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n        long exitCode;\n\n        @NonNull\n        String stdout;\n\n        @NonNull\n        String stderr;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/ShellStartExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.UUID;\n\npublic class ShellStartExchange extends BeaconInterface<ShellStartExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/shell/start\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        UUID connection;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n        @NonNull\n        String shellDialect;\n\n        @NonNull\n        OsType.Any osType;\n\n        @NonNull\n        String osName;\n\n        @NonNull\n        String ttyState;\n\n        @NonNull\n        FilePath temp;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/ShellStopExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.UUID;\n\npublic class ShellStopExchange extends BeaconInterface<ShellStopExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/shell/stop\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        UUID connection;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {}\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/SshLaunchExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\n\npublic class SshLaunchExchange extends BeaconInterface<SshLaunchExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/sshLaunch\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        String arguments;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n        @NonNull\n        List<String> command;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/TerminalExternalLaunchExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\n\npublic class TerminalExternalLaunchExchange extends BeaconInterface<TerminalExternalLaunchExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/terminal/externalLaunch\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        String connection;\n\n        @NonNull\n        List<String> arguments;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n        @NonNull\n        List<String> command;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/TerminalLaunchExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.nio.file.Path;\nimport java.util.UUID;\n\npublic class TerminalLaunchExchange extends BeaconInterface<TerminalLaunchExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/terminal/launch\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        UUID request;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n        @NonNull\n        Path targetFile;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/TerminalPrepareExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.UUID;\n\npublic class TerminalPrepareExchange extends BeaconInterface<TerminalPrepareExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/terminal/prepare\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        UUID request;\n\n        long pid;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {\n        boolean supportsUnicode;\n        boolean supportsEscapeSequences;\n    }\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/TerminalRegisterExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.UUID;\n\npublic class TerminalRegisterExchange extends BeaconInterface<TerminalRegisterExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/terminal/register\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        UUID request;\n\n        long pid;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {}\n}\n"
  },
  {
    "path": "beacon/src/main/java/io/xpipe/beacon/api/TerminalWaitExchange.java",
    "content": "package io.xpipe.beacon.api;\n\nimport io.xpipe.beacon.BeaconInterface;\n\nimport lombok.Builder;\nimport lombok.NonNull;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.UUID;\n\npublic class TerminalWaitExchange extends BeaconInterface<TerminalWaitExchange.Request> {\n\n    @Override\n    public String getPath() {\n        return \"/terminal/wait\";\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Request {\n        @NonNull\n        UUID request;\n    }\n\n    @Jacksonized\n    @Builder\n    @Value\n    public static class Response {}\n}\n"
  },
  {
    "path": "beacon/src/main/java/module-info.java",
    "content": "import io.xpipe.beacon.BeaconInterface;\nimport io.xpipe.beacon.BeaconJacksonModule;\nimport io.xpipe.beacon.api.*;\nimport io.xpipe.core.ModuleLayerLoader;\n\nimport com.fasterxml.jackson.databind.Module;\n\nopen module io.xpipe.beacon {\n    exports io.xpipe.beacon;\n    exports io.xpipe.beacon.api;\n\n    requires com.fasterxml.jackson.core;\n    requires com.fasterxml.jackson.annotation;\n    requires com.fasterxml.jackson.databind;\n    requires transitive io.xpipe.core;\n    requires static lombok;\n    requires static org.junit.jupiter.api;\n    requires jdk.httpserver;\n    requires java.net.http;\n    requires java.desktop;\n\n    uses io.xpipe.beacon.BeaconInterface;\n\n    provides ModuleLayerLoader with\n            BeaconInterface.Loader;\n    provides Module with\n            BeaconJacksonModule;\n    provides BeaconInterface with\n            ShellStartExchange,\n            ShellStopExchange,\n            ShellExecExchange,\n            DaemonModeExchange,\n            DaemonStatusExchange,\n            DaemonFocusExchange,\n            DaemonOpenExchange,\n            DaemonStopExchange,\n            HandshakeExchange,\n            ConnectionQueryExchange,\n            ConnectionInfoExchange,\n            ConnectionRemoveExchange,\n            ConnectionAddExchange,\n            CategoryAddExchange,\n            CategoryQueryExchange,\n            CategoryInfoExchange,\n            CategoryRemoveExchange,\n            ActionExchange,\n            ConnectionRefreshExchange,\n            AskpassExchange,\n            TerminalPrepareExchange,\n            TerminalRegisterExchange,\n            TerminalWaitExchange,\n            TerminalLaunchExchange,\n            TerminalExternalLaunchExchange,\n            SshLaunchExchange,\n            FsReadExchange,\n            FsBlobExchange,\n            FsWriteExchange,\n            FsScriptExchange,\n            DaemonVersionExchange,\n            SecretEncryptExchange,\n            SecretDecryptExchange;\n}\n"
  },
  {
    "path": "beacon/src/main/resources/META-INF/services/io.xpipe.core.ModuleLayerLoader",
    "content": "io.xpipe.beacon.BeaconInterface$Loader"
  },
  {
    "path": "build.gradle",
    "content": "import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform\n\nimport java.lang.module.ModuleFinder\nimport java.util.stream.Stream\n\nplugins {\n    id(\"io.github.gradle-nexus.publish-plugin\") version \"2.0.0\"\n    id 'org.gradlex.extra-java-module-info' version '1.14' apply false\n    id(\"com.diffplug.spotless\") version \"8.2.1\" apply false\n}\n\nallprojects { subproject ->\n    apply plugin: 'org.gradlex.extra-java-module-info'\n    extraJavaModuleInfo {\n        failOnMissingModuleInfo.set(false)\n        skipLocalJars.set(true)\n    }\n    apply from: \"$rootDir/gradle/gradle_scripts/modules.gradle\"\n\n    // https://docs.gradle.org/9.0.0/userguide/upgrading_major_version_9.html#reproducible_archives_by_default\n    tasks.withType(AbstractArchiveTask).configureEach {\n        reproducibleFileOrder = false\n        preserveFileTimestamps = true\n        useFileSystemPermissions()\n    }\n}\n\nsubprojects {subproject ->\n    if (subproject.name == 'dist') {\n        return\n    }\n\n    apply plugin: 'com.diffplug.spotless'\n    spotless {\n        java {\n            palantirJavaFormat()\n            trimTrailingWhitespace()\n            endWithNewline()\n            importOrder('io.xpipe', 'javafx', '', 'java', '\\\\#')\n        }\n    }\n}\n\n// Publishing\n\ndef sonatypeUser = project.hasProperty('sonatypeUsername') ? project.property('sonatypeUsername') : System.getenv('SONATYPE_USERNAME')\ndef sonatypePass = project.hasProperty('sonatypePassword') ? project.property('sonatypePassword') : System.getenv('SONATYPE_PASSWORD')\n\ntasks.withType(GenerateModuleMetadata).configureEach {\n    enabled = false\n}\n\nnexusPublishing  {\n    repositories {\n        sonatype  {\n            nexusUrl.set(uri('https://s01.oss.sonatype.org/service/local/'))\n            snapshotRepositoryUrl.set(uri('https://s01.oss.sonatype.org/content/repositories/snapshots/'))\n            username = sonatypeUser\n            password = sonatypePass\n        }\n    }\n    useStaging = true\n}\n\n// Developer config file setup\n\nvar devProps = file(\"$rootDir/app/dev.properties\")\nif (!devProps.exists()) {\n    devProps.text = file(\"$rootDir/gradle/gradle_scripts/dev_default.properties\").text\n}\n\n// Project variable functions\n\nstatic def getArchName() {\n    var arch = System.getProperty(\"os.arch\").toLowerCase(Locale.ROOT)\n    if (arch == 'amd64' || arch == 'x86_64') {\n        return 'x86_64'\n    }\n\n    if (arch == 'arm' || arch == 'aarch64') {\n        return 'arm64'\n    }\n\n    if (arch == 'x86') {\n        return 'x86'\n    }\n\n    return arch\n}\n\nstatic def getPlatformName() {\n    def currentOS = DefaultNativePlatform.currentOperatingSystem\n    def platform\n    if (currentOS.isWindows()) {\n        platform = 'windows'\n    }  else if (currentOS.isMacOsX()) {\n        platform = 'osx'\n    } else {\n        platform = 'linux'\n    }\n    return platform\n}\n\ndef getJvmArgs() {\n    def os = DefaultNativePlatform.currentOperatingSystem\n    def jvmRunArgs = [\n            \"-Dfile.encoding=UTF-8\",\n            \"-Dvisualvm.display.name=$productName\",\n            \"-Djavafx.preloader=\" + packageName(\"core.AppPreloader\"),\n            \"-Djdk.virtualThreadScheduler.parallelism=8\"\n    ]\n\n    // Virtual threads cause crashes on Windows ARM\n    if (os.isWindows() && arch == \"arm64\") {\n        jvmRunArgs += [\n                \"-D\" + propertyName(\"useVirtualThreads\") + \"=false\"\n        ]\n    }\n\n    // Disable JDK24 warnings\n    jvmRunArgs += [\n            \"--enable-native-access=com.sun.jna\",\n            \"--enable-native-access=javafx.graphics\",\n            \"--enable-native-access=javafx.web\",\n            \"--sun-misc-unsafe-memory-access=allow\",\n    ]\n\n    // Module access fixes\n    def appPackage = packageName(null)\n    jvmRunArgs += [\n            \"--add-opens\", \"java.base/java.lang=$appPackage\",\n            \"--add-opens\", \"java.base/java.net=$appPackage\",\n            \"--add-opens\", \"net.synedra.validatorfx/net.synedra.validatorfx=$appPackage\",\n            \"--add-opens\", \"java.base/java.nio.file=$appPackage\",\n            \"--add-exports\", \"javafx.graphics/com.sun.javafx.tk=$appPackage\",\n            \"--add-exports\", \"jdk.zipfs/jdk.nio.zipfs=io.xpipe.modulefs\",\n            \"--add-opens\", \"javafx.graphics/com.sun.glass.ui=$appPackage\",\n            \"--add-opens\", \"javafx.graphics/javafx.stage=$appPackage\",\n            \"--add-opens\", \"javafx.controls/javafx.scene.control.skin=$appPackage\",\n            \"--add-opens\", \"javafx.graphics/com.sun.javafx.tk=$appPackage\",\n            \"--add-opens\", \"javafx.graphics/com.sun.javafx.tk.quantum=$appPackage\"\n    ]\n\n    if (fullVersion) {\n        jvmRunArgs += [\n                \"--add-opens\", \"java.base/java.io=\" + packageName(\"ext.proc\", null),\n                \"--add-opens\", \"org.apache.commons.io/org.apache.commons.io.input=\" + packageName(\"ext.proc\", null),\n        ]\n    }\n\n    // Use project liliput\n    jvmRunArgs += ['-XX:+UseCompactObjectHeaders']\n\n    // Reduce heap usage with deduplication\n    jvmRunArgs += ['-XX:+UseStringDeduplication']\n\n    // GC config\n    jvmRunArgs += [\n            '-XX:+UseG1GC',\n            '-Xms200m',\n            '-Xmx4G',\n            '-XX:MinHeapFreeRatio=15',\n            '-XX:MaxHeapFreeRatio=25',\n            '-XX:GCTimeRatio=9',\n            // The default makes GC pauses longer for some reason\n            '-XX:G1HeapRegionSize=4m'\n    ]\n\n    // Why is this not on by default? ...\n    // https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/net/doc-files/net-properties.html\n    jvmRunArgs += [\n            '-Djava.net.useSystemProxies=true'\n    ]\n\n    // Fix platform theme detection on macOS\n    if (os.isMacOsX()) {\n        jvmRunArgs += [\"-Dapple.awt.application.appearance=system\"]\n    }\n\n    // Use new metal pipeline on macOS\n    if (os.isMacOsX()) {\n        jvmRunArgs += [\"-Dprism.order=mtl,es2,sw\"]\n    }\n\n    if (os.isLinux()) {\n        jvmRunArgs.addAll(\"--add-opens\", \"java.desktop/sun.awt.X11=\" + packageName(null))\n    }\n\n    return jvmRunArgs\n}\n\ndef getWindowsSchemaCanonicalVersion() {\n    def v = canonicalVersionString\n    def last = isStage ? versionReleaseNumber : 0\n    if (v.split(\"\\\\.\").length == 2) {\n        v = v + \".0\"\n    }\n    return v + \".\" + last\n}\n\n// https://stackoverflow.com/questions/79720795/replacing-deprecated-projectexec-in-dofirst-dolast\ninterface InjectedExecOps {\n    @javax.inject.Inject\n    ExecOperations getExecOps()\n}\n\n\n// Project variables\n\nproject.ext {\n    // Release pipeline config\n    isFullRelease = System.getenv('RELEASE') != null && Boolean.parseBoolean(System.getenv('RELEASE'))\n    isStage = System.getenv('STAGE') != null && Boolean.parseBoolean(System.getenv('STAGE'))\n    ci = System.getenv('CI') != null\n    obfuscate = true\n    fullVersion = file(\"$rootDir/private_files.txt\").exists()\n    bundleCds = ci && fullVersion\n\n    // Names\n    productName = isStage ? 'XPipe PTB' : 'XPipe'\n    kebapProductName = isStage ? 'xpipe-ptb' : 'xpipe'\n    flatcaseProductName = isStage ? 'xpipeptb' : 'xpipe'\n    snakeProductName = isStage ? 'xpipe_ptb' : 'xpipe'\n    artifactBaseName = \"xpipe\"\n\n    // Info\n    publisher = 'XPipe UG (haftungsbeschränkt)'\n    shortDescription = isStage ? 'XPipe PTB Public Test Build' : 'Your entire server infrastructure at your fingertips'\n    longDescription = 'XPipe is a new type of shell connection hub and remote file manager that allows you to access your entire server infrastructure from your local machine. It works on top of your installed command-line programs that you normally use to connect and does not require any setup on your remote systems.'\n    website = 'https://xpipe.io'\n    sourceWebsite = isStage ? 'https://github.com/xpipe-io/xpipe-ptb' : 'https://github.com/xpipe-io/xpipe'\n    authors = 'Christopher Schnick'\n\n    // Version info\n    rawVersion = file('version').text.strip()\n    versionString = rawVersion + (isFullRelease || isStage ? '' : '-SNAPSHOT')\n    versionReleaseNumber = rawVersion.split('-').length == 2 ? Integer.parseInt(rawVersion.split('-')[1]) : 1\n    canonicalVersionString = rawVersion.split('-').length == 2 ? rawVersion.split('-')[0] : rawVersion\n    buildId = UUID.nameUUIDFromBytes(versionString.getBytes())\n    windowsSchemaCanonicalVersion = getWindowsSchemaCanonicalVersion()\n\n    // Changelog info\n    changelog = file(\"dist/changelog/${canonicalVersionString}.md\").exists() ? file(\"dist/changelog/${canonicalVersionString}.md\").text.strip() + '\\n' : \"\"\n    changelogFile = file(\"$rootDir/dist/changelog/${versionString}.md\").exists() ?\n            file(\"$rootDir/dist/changelog/${versionString}.md\") :\n            file(\"$rootDir/dist/changelog/${canonicalVersionString}.md\")\n    incrementalChangelogFile = file(\"$rootDir/dist/changelog/${canonicalVersionString}_incremental.md\")\n    announce = System.getenv('SKIP_ANNOUNCEMENT') == null || !Boolean.parseBoolean(System.getenv('SKIP_ANNOUNCEMENT'))\n\n    // Signing config\n    signingKeyId = project.hasProperty('signingKeyId') ? project.property(\"signingKeyId\") : (System.getenv('GPG_KEY_ID') != null ? System.getenv('GPG_KEY_ID') : \"\")\n    signingKey = project.hasProperty('signingKeyFile') ? file(project.property(\"signingKeyFile\")).text : (System.getenv('GPG_KEY') != null ? System.getenv('GPG_KEY') : \"\")\n    signingPassword = project.hasProperty('signingKeyPassword') ? project.property(\"signingKeyPassword\") : (System.getenv('GPG_KEY_PASSWORD') != null ? System.getenv('GPG_KEY_PASSWORD') : \"\")\n\n    // Extension config\n    allExtensions = Stream.concat(Stream.of(project(':base')), Arrays.stream(file(\"$rootDir/ext\").list())\n            .filter(s -> file(\"$rootDir/ext/$s/build.gradle\").exists())\n            .filter(s -> s != 'base')\n            .map(l -> project(\":$l\"))).toList()\n    privateExtensions = file(\"$rootDir/private_extensions.txt\").exists() ? file(\"$rootDir/private_extensions.txt\").readLines() : []\n\n    // Build config\n    os = org.gradle.internal.os.OperatingSystem.current()\n    groupName = 'io.xpipe'\n    artifactName = 'app'\n    arch = getArchName()\n    jvmRunArgs = getJvmArgs()\n    useBundledJna = fullVersion\n    sentryUrl = \"https://fd5f67ff10764b7e8a704bec9558c8fe@o1084459.ingest.sentry.io/6094279\"\n\n    // JPackage config\n    jpackageExecutableName = \"xpiped\"\n    jpackageMacOsBundleName = isStage ? groupName + '.ptb-app' : groupName + '.app'\n    jpackageReleaseArguments = jvmRunArgs + [\n            \"-D\" + propertyName(\"version\") + \"=\" + versionString,\n            \"-D\" + propertyName(\"build\") + \"=$versionString/${new Date().format('yyyy-MM-dd-HH-mm')}\",\n            \"-D\" + propertyName(\"buildId\") + \"=\" + buildId,\n            \"-D\" + propertyName(\"fullVersion\") + \"=\" + fullVersion,\n            \"-D\" + propertyName(\"staging\") + \"=\" + isStage,\n            \"-D\" + propertyName(\"sentryUrl\") + \"=\" + sentryUrl,\n            '-Djna.nosys=false',\n            '-Djna.nounpack=true',\n            '-Djna.noclasspath=true'\n    ]\n    if (os.isMacOsX()) {\n        jpackageReleaseArguments += \"-Xdock:name=$productName\"\n    }\n    if (isFullRelease || isStage) {\n        jpackageReleaseArguments += \"-XX:+DisableAttachMechanism\"\n    }\n\n    // JavaFX config\n    devJavafxVersion = '27-ea+4'\n    platformName = getPlatformName()\n    useBundledJavaFx = fullVersion\n    bundledJdkJavaFx = ModuleFinder.ofSystem().find(\"javafx.base\").isPresent()\n    // Define a custom JavaFX SDK location\n    customJavaFxLibsPath = null; // file(\"C:\\\\Projects\\\\jfx\\\\build\\\\sdk\\\\lib\")\n    customJavaFxJmodsPath = null; // file(\"C:\\\\Projects\\\\jfx\\\\build\\\\jmods\")\n\n    // Other\n    deeplApiKey = findProperty('DEEPL_API_KEY') != null ? findProperty('DEEPL_API_KEY') : \"\"\n\n    def injected = project.objects.newInstance(InjectedExecOps)\n    execOps = injected.execOps\n}\n\ngroup = groupName\nversion = versionString\n\n// Global helper functions\n\ndef propertyName(String s) {\n    return groupName + \".\" + artifactName + \".\" + s\n}\n\ndef propertyName(String module, String s) {\n    return groupName + \".\" + module + \".\" + s\n}\n\ndef packageName(String s) {\n    return groupName + \".\" + artifactName + (s != null ? \".\" + s : \"\")\n}\n\ndef packageName(String module, String s) {\n    return groupName + \".\" + module + (s != null ? \".\" + s : \"\")\n}\n\ndef replaceVariablesInFileAsString(String f, Map<String, String> replacements) {\n    def text = file(f).text\n    def replaced = text.replace(replacements)\n    return replaced\n}\n\ndef replaceVariablesInFile(String f, Map<String, String> replacements) {\n    def fileName = file(f).getName()\n    def text = file(f).text\n    def replaced = text.replace(replacements)\n    def build = \"${project.layout.buildDirectory.get()}/${UUID.randomUUID()}\"\n    file(build).mkdirs()\n    def temp = \"$build/$fileName\"\n    file(temp).text = replaced\n    return file(temp)\n}\n\n// Test results\n\ndef testTasks = [\n        project(':core').getTasksByName('test', true),\n        project(':app').getTasksByName('test', true),\n        project(':base').getTasksByName('localTest', true),\n        project(':proc').getTasksByName('localTest', true),\n]\n\n\nif (file(\"cli\").exists()) {\n    testTasks += [project(':cli').getTasksByName('remoteTest', true)]\n}\n\ntasks.register('testReport', TestReport) {\n    getDestinationDirectory().set(file(\"$rootProject.buildDir/reports/all\"))\n    getTestResults().from(testTasks.stream().filter {!it.isEmpty()}.map {\n        file(\"${it.project.buildDir.get(0)}/test-results/${it.name.get(0)}/binary\")\n    }.toList())\n}\n\ntasks.register('testAll', DefaultTask) {\n    for (final def t in testTasks) {\n        t.forEach { dependsOn(it.getTaskDependencies()) }\n    }\n    doFirst {\n        for (final def t in testTasks) {\n            t.forEach { it.executeTests() }\n        }\n    }\n    finalizedBy(testReport)\n}\n\n// Checks\n\nif (System.getProperty(\"java.home\").contains(\" \")) {\n    throw new IllegalArgumentException(\"Your JDK home path contains spaces. This will break several gradle plugins\")\n}\n\nif (isFullRelease && rawVersion.contains(\"-\")) {\n    throw new IllegalArgumentException(\"Releases must have canonical versions\")\n}\n\nif (isStage && !rawVersion.contains(\"-\")) {\n    throw new IllegalArgumentException(\"Stage releases must have release numbers\")\n}\n\n"
  },
  {
    "path": "core/README.md",
    "content": "[![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.xpipe/xpipe-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.xpipe/xpipe-core)\n[![javadoc](https://javadoc.io/badge2/io.xpipe/xpipe-core/javadoc.svg)](https://javadoc.io/doc/io.xpipe/xpipe-core)\n\n## XPipe Core\n\nThe XPipe core module contains all the shared core classes used by the API, beacon, and daemon implementation.\nIt contains the following packages:\n\n- [dialog](src/main/java/io/xpipe/core/dialog): In API to create server/daemon side CLI dialogs.\n\n- [store](src/main/java/io/xpipe/core/store): The basic data store classes that are used by every data store implementation.\n\n- [process](src/main/java/io/xpipe/core/process): Base classes for the shell process handling implementation.\n\n- [util](src/main/java/io/xpipe/core/source): A few utility classes for serialization and more.\n\nEvery class is expected to be potentially used in the context of files and message exchanges.\nAs a result, essentially all objects must be serializable/deserializable with jackson.\n\n\n"
  },
  {
    "path": "core/build.gradle",
    "content": "plugins {\n    id 'java-library'\n    id 'maven-publish'\n    id 'signing'\n}\n\napply from: \"$rootDir/gradle/gradle_scripts/java.gradle\"\napply from: \"$rootDir/gradle/gradle_scripts/lombok.gradle\"\napply from: \"$rootDir/gradle/gradle_scripts/junit.gradle\"\n\ncompileJava {\n    options.compilerArgs << '-parameters'\n}\n\ndependencies {\n    api \"com.fasterxml.jackson.core:jackson-databind:2.21.0\"\n    implementation \"com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.21.0\"\n}\n\nversion = versionString\ngroup = groupName\nbase.archivesName = 'xpipe-core'\n\nrepositories {\n    mavenCentral()\n}\n\ndependencies {\n    testImplementation project(':core')\n}\n\ntasks.register('dist', Copy) {\n    from jar.archiveFile\n    into \"${project(':dist').buildDir}/dist/libraries\"\n}\n\napply from: 'publish.gradle'\napply from: \"$rootDir/gradle/gradle_scripts/publish-base.gradle\""
  },
  {
    "path": "core/publish.gradle",
    "content": "publishing {\n    publications {\n        mavenJava(MavenPublication) {\n            artifactId = project.base.archivesName\n\n            from components.java\n\n            pom {\n                name = 'XPipe Core'\n                description = 'Core classes used by all XPipe components.'\n                url = 'https://github.com/xpipe-io/xpipe/core'\n                licenses {\n                    license {\n                        name = 'Apache License 2.0'\n                        url = 'https://github.com/xpipe-io/xpipe/LICENSE.md'\n                    }\n                }\n                developers {\n                    developer {\n                        id = 'crschnick'\n                        name = 'Christopher Schnick'\n                        email = 'crschnick@xpipe.io'\n                    }\n                }\n                scm {\n                    connection = 'scm:git:git://github.com/xpipe-io/xpipe.git'\n                    developerConnection = 'scm:git:ssh://github.com/xpipe-io/xpipe.git'\n                    url = 'https://github.com/xpipe-io/xpipe'\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/io/xpipe/core/AesSecretValue.java",
    "content": "package io.xpipe.core;\n\nimport lombok.EqualsAndHashCode;\nimport lombok.SneakyThrows;\nimport lombok.experimental.SuperBuilder;\n\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.security.SecureRandom;\nimport javax.crypto.Cipher;\nimport javax.crypto.SecretKey;\nimport javax.crypto.spec.GCMParameterSpec;\n\n@SuperBuilder\n@EqualsAndHashCode(callSuper = true)\npublic abstract class AesSecretValue extends EncryptedSecretValue {\n\n    private static final String ENCRYPT_ALGO = \"AES/GCM/NoPadding\";\n    private static final int TAG_LENGTH_BIT = 128;\n    private static final int IV_LENGTH_BYTE = 12;\n\n    public AesSecretValue(String encryptedValue) {\n        super(encryptedValue);\n    }\n\n    public AesSecretValue(char[] secret) {\n        super(secret);\n    }\n\n    public AesSecretValue(byte[] b) {\n        super(b);\n    }\n\n    protected byte[] getNonce(int numBytes) {\n        byte[] nonce = new byte[numBytes];\n        new SecureRandom().nextBytes(nonce);\n        return nonce;\n    }\n\n    protected abstract SecretKey getSecretKey();\n\n    @Override\n    @SneakyThrows\n    public byte[] encrypt(byte[] c) {\n        SecretKey secretKey = getSecretKey();\n        if (secretKey == null) {\n            throw new IllegalStateException(\"Missing secret key\");\n        }\n\n        Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);\n        var iv = getNonce(IV_LENGTH_BYTE);\n        cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(TAG_LENGTH_BIT, iv));\n        var bytes = cipher.doFinal(c);\n        bytes = ByteBuffer.allocate(iv.length + bytes.length)\n                .order(ByteOrder.LITTLE_ENDIAN)\n                .put(iv)\n                .put(bytes)\n                .array();\n        return bytes;\n    }\n\n    @Override\n    @SneakyThrows\n    public byte[] decrypt(byte[] c) {\n        ByteBuffer bb = ByteBuffer.wrap(c).order(ByteOrder.LITTLE_ENDIAN);\n        byte[] iv = new byte[IV_LENGTH_BYTE];\n        bb.get(iv);\n        byte[] cipherText = new byte[bb.remaining()];\n        bb.get(cipherText);\n\n        SecretKey secretKey = getSecretKey();\n        if (secretKey == null) {\n            throw new IllegalStateException(\"Missing secret key\");\n        }\n\n        Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);\n        cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(TAG_LENGTH_BIT, iv));\n        return cipher.doFinal(cipherText);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/io/xpipe/core/CoreJacksonModule.java",
    "content": "package io.xpipe.core;\n\nimport com.fasterxml.jackson.annotation.JsonIdentityInfo;\nimport com.fasterxml.jackson.annotation.JsonPropertyOrder;\nimport com.fasterxml.jackson.annotation.ObjectIdGenerators;\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.databind.*;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.fasterxml.jackson.databind.jsontype.NamedType;\nimport com.fasterxml.jackson.databind.module.SimpleModule;\n\nimport java.io.IOException;\nimport java.nio.charset.Charset;\nimport java.nio.file.InvalidPathException;\nimport java.nio.file.Path;\nimport java.util.List;\n\npublic class CoreJacksonModule extends SimpleModule {\n\n    @Override\n    public void setupModule(SetupContext context) {\n        context.registerSubtypes(new NamedType(InPlaceSecretValue.class));\n\n        addSerializer(FilePath.class, new FilePathSerializer());\n        addDeserializer(FilePath.class, new FilePathDeserializer());\n\n        addSerializer(StorePath.class, new StorePathSerializer());\n        addDeserializer(StorePath.class, new StorePathDeserializer());\n\n        addSerializer(Charset.class, new CharsetSerializer());\n        addDeserializer(Charset.class, new CharsetDeserializer());\n\n        addSerializer(Path.class, new LocalPathSerializer());\n        addDeserializer(Path.class, new LocalPathDeserializer());\n\n        context.setMixInAnnotations(Throwable.class, ThrowableTypeMixIn.class);\n\n        context.addSerializers(_serializers);\n        context.addDeserializers(_deserializers);\n    }\n\n    public static class StorePathSerializer extends JsonSerializer<StorePath> {\n\n        @Override\n        public void serialize(StorePath value, JsonGenerator jgen, SerializerProvider provider) throws IOException {\n            var ar = value.getNames().toArray(String[]::new);\n            jgen.writeArray(ar, 0, ar.length);\n        }\n    }\n\n    public static class StorePathDeserializer extends JsonDeserializer<StorePath> {\n\n        @Override\n        public StorePath deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {\n            JavaType javaType =\n                    JacksonMapper.getDefault().getTypeFactory().constructCollectionLikeType(List.class, String.class);\n            List<String> list = JacksonMapper.getDefault().readValue(p, javaType);\n            return new StorePath(list);\n        }\n    }\n\n    public static class FilePathSerializer extends JsonSerializer<FilePath> {\n\n        @Override\n        public void serialize(FilePath value, JsonGenerator jgen, SerializerProvider provider) throws IOException {\n            jgen.writeString(value.toString());\n        }\n    }\n\n    public static class FilePathDeserializer extends JsonDeserializer<FilePath> {\n\n        @Override\n        public FilePath deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {\n            return FilePath.of(p.getValueAsString());\n        }\n    }\n\n    public static class CharsetSerializer extends JsonSerializer<Charset> {\n\n        @Override\n        public void serialize(Charset value, JsonGenerator jgen, SerializerProvider provider) throws IOException {\n            jgen.writeString(value.name());\n        }\n    }\n\n    public static class CharsetDeserializer extends JsonDeserializer<Charset> {\n\n        @Override\n        public Charset deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {\n            return Charset.forName(p.getValueAsString());\n        }\n    }\n\n    public static class LocalPathSerializer extends JsonSerializer<Path> {\n\n        @Override\n        public void serialize(Path value, JsonGenerator jgen, SerializerProvider provider) throws IOException {\n            jgen.writeString(value.toString());\n        }\n    }\n\n    public static class LocalPathDeserializer extends JsonDeserializer<Path> {\n\n        @Override\n        public Path deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {\n            try {\n                return Path.of(p.getValueAsString());\n            } catch (InvalidPathException ignored) {\n                return null;\n            }\n        }\n    }\n\n    @JsonSerialize(as = Throwable.class)\n    @JsonPropertyOrder(alphabetic = true)\n    public abstract static class ThrowableTypeMixIn {\n\n        @SuppressWarnings(\"unused\")\n        @JsonIdentityInfo(generator = ObjectIdGenerators.StringIdGenerator.class, property = \"$id\")\n        private Throwable cause;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/io/xpipe/core/EncryptedSecretValue.java",
    "content": "package io.xpipe.core;\n\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.experimental.SuperBuilder;\n\nimport java.nio.ByteBuffer;\nimport java.nio.CharBuffer;\nimport java.nio.charset.StandardCharsets;\n\n@Getter\n@SuperBuilder\n@EqualsAndHashCode\npublic abstract class EncryptedSecretValue implements SecretValue {\n\n    String encryptedValue;\n\n    public EncryptedSecretValue(String encryptedValue) {\n        this.encryptedValue = encryptedValue;\n    }\n\n    public EncryptedSecretValue(byte[] b) {\n        encryptedValue = SecretValue.toBase64e(encrypt(b));\n    }\n\n    public EncryptedSecretValue(char[] c) {\n        var utf8 = StandardCharsets.UTF_8.encode(CharBuffer.wrap(c));\n        var bytes = new byte[utf8.limit()];\n        utf8.get(bytes);\n        encryptedValue = SecretValue.toBase64e(encrypt(bytes));\n    }\n\n    @Override\n    public String toString() {\n        return \"<encrypted secret>\";\n    }\n\n    @Override\n    public byte[] getSecretRaw() {\n        try {\n            var bytes = SecretValue.fromBase64e(getEncryptedValue());\n            bytes = decrypt(bytes);\n            return bytes;\n        } catch (Exception ex) {\n            return new byte[0];\n        }\n    }\n\n    @Override\n    public char[] getSecret() {\n        try {\n            var bytes = SecretValue.fromBase64e(getEncryptedValue());\n            bytes = decrypt(bytes);\n            var charBuffer = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(bytes));\n            var chars = new char[charBuffer.limit()];\n            charBuffer.get(chars);\n            return chars;\n        } catch (Exception ex) {\n            return new char[0];\n        }\n    }\n\n    public byte[] encrypt(byte[] c) {\n        throw new UnsupportedOperationException();\n    }\n\n    public byte[] decrypt(byte[] c) {\n        throw new UnsupportedOperationException();\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/io/xpipe/core/FailableBiFunction.java",
    "content": "package io.xpipe.core;\n\n@FunctionalInterface\npublic interface FailableBiFunction<T1, T2, R, E extends Throwable> {\n\n    R apply(T1 var1, T2 var2) throws E;\n}\n"
  },
  {
    "path": "core/src/main/java/io/xpipe/core/FailableConsumer.java",
    "content": "package io.xpipe.core;\n\n@FunctionalInterface\npublic interface FailableConsumer<T, E extends Throwable> {\n\n    void accept(T var1) throws E;\n}\n"
  },
  {
    "path": "core/src/main/java/io/xpipe/core/FailableFunction.java",
    "content": "package io.xpipe.core;\n\n@FunctionalInterface\npublic interface FailableFunction<T, R, E extends Throwable> {\n\n    R apply(T var1) throws E;\n}\n"
  },
  {
    "path": "core/src/main/java/io/xpipe/core/FailableRunnable.java",
    "content": "package io.xpipe.core;\n\n@FunctionalInterface\npublic interface FailableRunnable<E extends Throwable> {\n\n    void run() throws E;\n}\n"
  },
  {
    "path": "core/src/main/java/io/xpipe/core/FailableSupplier.java",
    "content": "package io.xpipe.core;\n\npublic interface FailableSupplier<T> {\n\n    T get() throws Exception;\n}\n"
  },
  {
    "path": "core/src/main/java/io/xpipe/core/FilePath.java",
    "content": "package io.xpipe.core;\n\nimport lombok.NonNull;\n\nimport java.nio.file.InvalidPathException;\nimport java.nio.file.Path;\nimport java.util.*;\n\npublic final class FilePath {\n\n    @NonNull\n    private final String value;\n\n    private FilePath normalized;\n    private List<String> split;\n\n    private FilePath(@NonNull String value) {\n        this.value = value;\n        if (value.isEmpty()) {\n            throw new IllegalArgumentException(\"File path is empty\");\n        }\n    }\n\n    public static FilePath parse(String path) {\n        return path != null && path.equals(path.strip()) && !path.isBlank() ? new FilePath(path) : null;\n    }\n\n    public static FilePath of(String path) {\n        return path != null ? new FilePath(path) : null;\n    }\n\n    public static FilePath of(String... path) {\n        if (path == null || path.length == 0) {\n            return null;\n        }\n\n        var cp = Arrays.stream(path).skip(1).toArray(String[]::new);\n        return path.length > 1 ? new FilePath(path[0]).join(cp) : new FilePath(path[0]);\n    }\n\n    public static FilePath of(Path path) {\n        return path != null ? new FilePath(path.toString()) : null;\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hashCode(normalize().removeTrailingSlash().value);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n        FilePath filePath = (FilePath) o;\n        return Objects.equals(\n                normalize().removeTrailingSlash().value, filePath.normalize().removeTrailingSlash().value);\n    }\n\n    public String toString() {\n        return value;\n    }\n\n    public FilePath getRoot() {\n        if (value.startsWith(\"/\")) {\n            return FilePath.of(\"/\");\n        } else if (value.length() >= 2 && value.charAt(1) == ':') {\n            // Without the trailing slash, many programs struggle with this\n            return FilePath.of(value.substring(0, 2) + \"\\\\\");\n        } else if (value.startsWith(\"\\\\\\\\\")) {\n            var split = split();\n            if (split.size() > 0) {\n                return FilePath.of(\"\\\\\\\\\" + split.getFirst());\n            }\n        }\n\n        return FilePath.of(\"/\");\n    }\n\n    public Path toLocalPath() {\n        return Path.of(value);\n    }\n\n    public FilePath toDirectory() {\n        if (value.endsWith(\"/\") || value.endsWith(\"\\\\\")) {\n            return FilePath.of(value);\n        }\n\n        if (value.contains(\"\\\\\")) {\n            return FilePath.of(value + \"\\\\\");\n        }\n\n        return FilePath.of(value + \"/\");\n    }\n\n    public FilePath removeTrailingSlash() {\n        if (value.equals(\"/\") || value.equals(\"\\\\\")) {\n            return FilePath.of(value);\n        }\n\n        if (value.endsWith(\"/\") || value.endsWith(\"\\\\\")) {\n            return FilePath.of(value.substring(0, value.length() - 1));\n        }\n        return FilePath.of(value);\n    }\n\n    public String getFileName() {\n        var split = split();\n        if (split.size() == 0) {\n            return \"\";\n        }\n        var components = split.stream().filter(s -> !s.isEmpty()).toList();\n        if (components.size() == 0) {\n            return \"\";\n        }\n\n        return components.getLast();\n    }\n\n    public FilePath getBaseName() {\n        if (!getFileName().contains(\".\")) {\n            return this;\n        }\n\n        var split = value.lastIndexOf(\".\");\n        return FilePath.of(value.substring(0, split));\n    }\n\n    public Optional<String> getExtension() {\n        var name = getFileName();\n        var split = name.split(\"\\\\.\");\n        if (split.length < 2) {\n            return Optional.empty();\n        }\n        return Optional.of(split[split.length - 1]);\n    }\n\n    public FilePath join(String... parts) {\n        var joined = String.join(\"/\", parts);\n        return FilePath.of(value + \"/\" + joined).normalize();\n    }\n\n    public boolean isAbsolute() {\n        if (!value.contains(\"/\") && !value.contains(\"\\\\\")) {\n            return false;\n        }\n\n        if (!value.startsWith(\"\\\\\") && !value.startsWith(\"/\") && !value.startsWith(\"~\") && !value.matches(\"^\\\\w:.*\")) {\n            return false;\n        }\n\n        return true;\n    }\n\n    public FilePath getParent() {\n        var split = split();\n        if (split.size() == 0) {\n            return this;\n        }\n\n        if (split.size() == 1) {\n            return value.startsWith(\"/\") && !value.equals(\"/\") ? FilePath.of(\"/\") : this;\n        }\n\n        return FilePath.of(value.substring(0, value.length() - getFileName().length() - 1));\n    }\n\n    public boolean startsWith(String start) {\n        return startsWith(FilePath.of(start));\n    }\n\n    public boolean startsWith(FilePath start) {\n        return normalize()\n                .toString()\n                .startsWith(start.normalize().removeTrailingSlash().toString());\n    }\n\n    public FilePath relativize(FilePath base) {\n        return FilePath.of(normalize()\n                .toString()\n                .substring(base.normalize().toDirectory().toString().length()));\n    }\n\n    public FilePath normalize() {\n        if (normalized != null) {\n            return normalized;\n        }\n\n        var backslash = value.contains(\"\\\\\");\n        var r = backslash ? toWindows() : toUnix();\n        normalized = r;\n        return r;\n    }\n\n    public FilePath resolveTildeHome(FilePath dir) {\n        return value.startsWith(\"~\") ? FilePath.of(value.replace(\"~\", dir.toString())) : this;\n    }\n\n    public List<String> split() {\n        if (split != null) {\n            return split;\n        }\n\n        var ar = value.split(\"[\\\\\\\\/]\");\n        var l = Arrays.stream(ar).filter(s -> !s.isEmpty()).toList();\n        split = l;\n        return l;\n    }\n\n    public FilePath toUnix() {\n        if (value.equals(\"/\")) {\n            return this;\n        }\n\n        var joined = String.join(\"/\", split());\n        var prefix = value.startsWith(\"/\") ? \"/\" : \"\";\n        var suffix = value.endsWith(\"/\") || value.endsWith(\"\\\\\") ? \"/\" : \"\";\n        return FilePath.of(prefix + joined + suffix);\n    }\n\n    public FilePath toWindows() {\n        var suffix = value.endsWith(\"/\") || value.endsWith(\"\\\\\") ? \"\\\\\" : \"\";\n        return FilePath.of(String.join(\"\\\\\", split()) + suffix);\n    }\n\n    public boolean isRoot() {\n        return getRoot().equals(this);\n    }\n\n    public Path asLocalPath() {\n        return Path.of(value);\n    }\n\n    public Optional<Path> asLocalPathIfPossible() {\n        try {\n            return Optional.of(Path.of(value));\n        } catch (InvalidPathException ignored) {\n            return Optional.empty();\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/io/xpipe/core/InPlaceSecretValue.java",
    "content": "package io.xpipe.core;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.EqualsAndHashCode;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.security.NoSuchAlgorithmException;\nimport java.security.spec.InvalidKeySpecException;\nimport java.security.spec.KeySpec;\nimport java.util.Random;\nimport javax.crypto.SecretKey;\nimport javax.crypto.SecretKeyFactory;\nimport javax.crypto.spec.PBEKeySpec;\nimport javax.crypto.spec.SecretKeySpec;\n\n@JsonTypeName(\"default\")\n@SuperBuilder\n@Jacksonized\n@EqualsAndHashCode(callSuper = true)\npublic class InPlaceSecretValue extends AesSecretValue {\n\n    private static final int AES_KEY_BIT = 128;\n    private static final int SALT_BIT = 16;\n    private static final int ITERATION_COUNT = 2048;\n    private static final SecretKeyFactory SECRET_FACTORY;\n    private static final SecretKey SECRET_KEY;\n\n    static {\n        try {\n            SECRET_FACTORY = SecretKeyFactory.getInstance(\"PBKDF2WithHmacSHA256\");\n\n            var salt = new byte[SALT_BIT];\n            new Random(AES_KEY_BIT).nextBytes(salt);\n            KeySpec spec = new PBEKeySpec(new char[] {'X', 'P', 'E' << 1}, salt, ITERATION_COUNT, AES_KEY_BIT);\n            SECRET_KEY = new SecretKeySpec(SECRET_FACTORY.generateSecret(spec).getEncoded(), \"AES\");\n        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {\n            throw new IllegalStateException(e);\n        }\n    }\n\n    public InPlaceSecretValue(byte[] b) {\n        super(b);\n    }\n\n    public InPlaceSecretValue(char[] secret) {\n        super(secret);\n    }\n\n    public static InPlaceSecretValue of(String s) {\n        return new InPlaceSecretValue(s.toCharArray());\n    }\n\n    public static InPlaceSecretValue of(char[] c) {\n        return new InPlaceSecretValue(c);\n    }\n\n    public static InPlaceSecretValue of(byte[] b) {\n        return new InPlaceSecretValue(b);\n    }\n\n    protected byte[] getNonce(int numBytes) {\n        byte[] nonce = new byte[numBytes];\n        new Random(1 - 28 + 213213).nextBytes(nonce);\n        return nonce;\n    }\n\n    @Override\n    protected SecretKey getSecretKey() {\n        return SECRET_KEY;\n    }\n\n    @Override\n    public InPlaceSecretValue inPlace() {\n        return this;\n    }\n\n    @Override\n    public String toString() {\n        return \"<in place secret>\";\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/io/xpipe/core/JacksonMapper.java",
    "content": "package io.xpipe.core;\n\nimport com.fasterxml.jackson.annotation.JsonAutoDetect;\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.databind.*;\nimport com.fasterxml.jackson.databind.Module;\nimport com.fasterxml.jackson.databind.jsontype.TypeSerializer;\nimport com.fasterxml.jackson.databind.module.SimpleModule;\nimport lombok.Getter;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.ServiceLoader;\nimport java.util.function.Consumer;\n\npublic class JacksonMapper {\n\n    private static final ObjectMapper BASE = new ObjectMapper();\n    private static final ObjectMapper INSTANCE;\n\n    @Getter\n    private static boolean init = false;\n\n    static {\n        configureBase(BASE);\n        INSTANCE = BASE.copy();\n    }\n\n    @SuppressWarnings(\"deprecation\")\n    private static void configureBase(ObjectMapper objectMapper) {\n        objectMapper.enable(SerializationFeature.INDENT_OUTPUT);\n        objectMapper.enable(JsonParser.Feature.ALLOW_COMMENTS);\n        objectMapper.enable(JsonParser.Feature.ALLOW_TRAILING_COMMA);\n        objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);\n        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);\n        objectMapper.disable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE);\n        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);\n        objectMapper.setVisibility(objectMapper\n                .getSerializationConfig()\n                .getDefaultVisibilityChecker()\n                .withFieldVisibility(JsonAutoDetect.Visibility.ANY)\n                .withGetterVisibility(JsonAutoDetect.Visibility.NONE)\n                .withSetterVisibility(JsonAutoDetect.Visibility.NONE)\n                .withCreatorVisibility(JsonAutoDetect.Visibility.NONE)\n                .withIsGetterVisibility(JsonAutoDetect.Visibility.NONE));\n    }\n\n    public static synchronized void configure(Consumer<ObjectMapper> mapper) {\n        mapper.accept(INSTANCE);\n    }\n\n    private static List<Module> findModules(ModuleLayer layer) {\n        ArrayList<Module> modules = new ArrayList<>();\n        ServiceLoader<Module> loader =\n                layer != null ? ServiceLoader.load(layer, Module.class) : ServiceLoader.load(Module.class);\n        for (Module module : loader) {\n            modules.add(module);\n        }\n        return modules;\n    }\n\n    public static ObjectMapper newMapper() {\n        if (!JacksonMapper.isInit()) {\n            return BASE;\n        }\n\n        return INSTANCE.copy();\n    }\n\n    public static ObjectMapper getDefault() {\n        if (!JacksonMapper.isInit()) {\n            return BASE;\n        }\n\n        return INSTANCE;\n    }\n\n    public static ObjectMapper getCensored() {\n        if (!JacksonMapper.isInit()) {\n            return BASE;\n        }\n\n        var c = INSTANCE.copy();\n        c.registerModule(new SimpleModule() {\n            @Override\n            public void setupModule(SetupContext context) {\n                addSerializer(SecretValue.class, new JsonSerializer<>() {\n                    @Override\n                    public void serialize(SecretValue value, JsonGenerator gen, SerializerProvider serializers)\n                            throws IOException {\n                        gen.writeString(\"<secret>\");\n                    }\n\n                    @Override\n                    public void serializeWithType(\n                            SecretValue value,\n                            JsonGenerator gen,\n                            SerializerProvider serializers,\n                            TypeSerializer typeSer)\n                            throws IOException {\n                        gen.writeString(\"<secret>\");\n                    }\n                });\n                super.setupModule(context);\n            }\n        });\n        return c;\n    }\n\n    public static class Loader implements ModuleLayerLoader {\n\n        @Override\n        public void init(ModuleLayer layer) {\n            List<Module> modules = findModules(layer);\n            INSTANCE.registerModules(modules);\n            init = true;\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/io/xpipe/core/KeyValue.java",
    "content": "package io.xpipe.core;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\n@Value\n@Builder\n@Jacksonized\n@AllArgsConstructor\npublic class KeyValue {\n    String key;\n    String value;\n}\n"
  },
  {
    "path": "core/src/main/java/io/xpipe/core/ModuleLayerLoader.java",
    "content": "package io.xpipe.core;\n\nimport java.util.ServiceLoader;\nimport java.util.function.Consumer;\n\npublic interface ModuleLayerLoader {\n\n    static void loadAll(ModuleLayer layer, Consumer<Throwable> errorHandler) {\n        var loaded = layer != null\n                ? ServiceLoader.load(layer, ModuleLayerLoader.class)\n                : ServiceLoader.load(ModuleLayerLoader.class);\n        loaded.stream().forEach(moduleLayerLoaderProvider -> {\n            var instance = moduleLayerLoaderProvider.get();\n            try {\n                instance.init(layer);\n            } catch (Throwable t) {\n                errorHandler.accept(t);\n            }\n        });\n    }\n\n    default void init(ModuleLayer layer) {}\n}\n"
  },
  {
    "path": "core/src/main/java/io/xpipe/core/OsType.java",
    "content": "package io.xpipe.core;\n\nimport java.util.Locale;\n\npublic interface OsType {\n\n    Windows WINDOWS = new Windows();\n    Linux LINUX = new Linux();\n    MacOs MACOS = new MacOs();\n    Bsd BSD = new Bsd();\n    Solaris SOLARIS = new Solaris();\n    Aix AIX = new Aix();\n    OtherUnix UNIX = new OtherUnix();\n\n    static Local ofLocal() {\n        String osName = System.getProperty(\"os.name\", \"generic\").toLowerCase(Locale.ENGLISH);\n        if ((osName.contains(\"mac\")) || (osName.contains(\"darwin\"))) {\n            return MACOS;\n        } else if (osName.contains(\"win\")) {\n            return WINDOWS;\n        } else {\n            return LINUX;\n        }\n    }\n\n    String getId();\n\n    String getName();\n\n    sealed interface Local extends OsType permits OsType.Windows, OsType.Linux, OsType.MacOs {\n\n        default Any toAny() {\n            return (Any) this;\n        }\n    }\n\n    sealed interface Any extends OsType\n            permits OsType.Windows,\n                    OsType.Linux,\n                    OsType.MacOs,\n                    OsType.Solaris,\n                    OsType.Bsd,\n                    OsType.Aix,\n                    OsType.OtherUnix {}\n\n    final class Windows implements OsType, Local, Any {\n\n        @Override\n        public String getName() {\n            return \"Windows\";\n        }\n\n        @Override\n        public String getId() {\n            return \"windows\";\n        }\n    }\n\n    final class Linux implements OsType, Local, Any {\n\n        @Override\n        public String getName() {\n            return \"Linux\";\n        }\n\n        @Override\n        public String getId() {\n            return \"linux\";\n        }\n    }\n\n    final class Solaris implements Any {\n\n        @Override\n        public String getId() {\n            return \"solaris\";\n        }\n\n        @Override\n        public String getName() {\n            return \"Solaris\";\n        }\n    }\n\n    final class Aix implements Any {\n\n        @Override\n        public String getId() {\n            return \"aix\";\n        }\n\n        @Override\n        public String getName() {\n            return \"AIX\";\n        }\n    }\n\n    final class OtherUnix implements Any {\n\n        @Override\n        public String getId() {\n            return \"unix\";\n        }\n\n        @Override\n        public String getName() {\n            return \"Unix\";\n        }\n    }\n\n    final class Bsd implements Any {\n\n        @Override\n        public String getId() {\n            return \"bsd\";\n        }\n\n        @Override\n        public String getName() {\n            return \"Bsd\";\n        }\n    }\n\n    final class MacOs implements OsType, Local, Any {\n\n        @Override\n        public String getId() {\n            return \"macos\";\n        }\n\n        @Override\n        public String getName() {\n            return \"Mac\";\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/io/xpipe/core/SecretValue.java",
    "content": "package io.xpipe.core;\n\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\n\nimport java.util.Arrays;\nimport java.util.Base64;\nimport java.util.function.Consumer;\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\npublic interface SecretValue {\n\n    static String toBase64e(byte[] b) {\n        var base64 = Base64.getEncoder().encodeToString(b);\n        return base64.replace(\"/\", \"-\").replace(\"+\", \"_\");\n    }\n\n    static byte[] fromBase64e(String s) {\n        return Base64.getDecoder().decode(s.replace(\"_\", \"+\").replace(\"-\", \"/\"));\n    }\n\n    InPlaceSecretValue inPlace();\n\n    default void withSecretValue(Consumer<char[]> con) {\n        var chars = getSecret();\n        con.accept(chars);\n        Arrays.fill(chars, (char) 0);\n    }\n\n    default <T> T mapSecretValueFailable(FailableFunction<char[], T, Exception> con) throws Exception {\n        var chars = getSecret();\n        var r = con.apply(chars);\n        Arrays.fill(chars, (char) 0);\n        return r;\n    }\n\n    byte[] getSecretRaw();\n\n    char[] getSecret();\n\n    default String getSecretValue() {\n        return new String(getSecret());\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/io/xpipe/core/StorePath.java",
    "content": "package io.xpipe.core;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * Represents a reference to an XPipe storage location.\n * <p>\n * To allow for a simple usage, the names are trimmed and\n * converted to lower case names when creating them.\n * The names are separated by a slash and are therefore not allowed to contain slashes themselves.\n *\n * @see #fromString(String)\n */\n@EqualsAndHashCode\n@Getter\npublic class StorePath {\n\n    public static final char SEPARATOR = '/';\n\n    private final List<String> names;\n\n    @JsonCreator\n    public StorePath(List<String> names) {\n        this.names = names;\n    }\n\n    /**\n     * Creates a new store path.\n     *\n     * @throws IllegalArgumentException if any name is not valid\n     */\n    public static StorePath create(String... names) {\n        if (names == null) {\n            throw new IllegalArgumentException(\"Names are null\");\n        }\n\n        if (Arrays.stream(names).anyMatch(s -> s == null)) {\n            throw new IllegalArgumentException(\"Name is null\");\n        }\n\n        if (Arrays.stream(names).anyMatch(s -> s.contains(\"\" + SEPARATOR))) {\n            throw new IllegalArgumentException(\"Separator character \" + SEPARATOR + \" is not allowed in the names\");\n        }\n\n        if (Arrays.stream(names).anyMatch(s -> s.strip().length() == 0)) {\n            throw new IllegalArgumentException(\"Trimmed entry name is empty\");\n        }\n\n        return new StorePath(Arrays.stream(names).toList());\n    }\n\n    /**\n     * Creates a new store path from a string representation.\n     *\n     * @param s the string representation, must be not null and fulfill certain requirements\n     * @throws IllegalArgumentException if the string is not valid\n     */\n    public static StorePath fromString(String s) {\n        if (s == null) {\n            throw new IllegalArgumentException(\"String is null\");\n        }\n\n        var split = s.split(String.valueOf(SEPARATOR), -1);\n\n        var names =\n                Arrays.stream(split).map(String::trim).map(String::toLowerCase).toList();\n        if (names.stream().anyMatch(s1 -> s1.isEmpty())) {\n            throw new IllegalArgumentException(\"Name must not be empty\");\n        }\n\n        return new StorePath(names);\n    }\n\n    @Override\n    public String toString() {\n        return names.stream().map(String::toLowerCase).collect(Collectors.joining(\"\" + SEPARATOR));\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/io/xpipe/core/StreamCharset.java",
    "content": "package io.xpipe.core;\n\nimport lombok.Value;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.stream.Stream;\n\n@Value\npublic class StreamCharset {\n\n    public static final StreamCharset UTF8 = new StreamCharset(StandardCharsets.UTF_8, null);\n\n    public static final StreamCharset UTF8_BOM =\n            new StreamCharset(StandardCharsets.UTF_8, new byte[] {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF});\n\n    // ======\n    // UTF-16\n    // ======\n\n    public static final StreamCharset UTF16_BE = new StreamCharset(StandardCharsets.UTF_16BE, null);\n\n    public static final StreamCharset UTF16_BE_BOM =\n            new StreamCharset(StandardCharsets.UTF_16BE, new byte[] {(byte) 0xFE, (byte) 0xFF});\n\n    public static final StreamCharset UTF16_LE = new StreamCharset(StandardCharsets.UTF_16LE, null);\n\n    public static final StreamCharset UTF16_LE_BOM =\n            new StreamCharset(StandardCharsets.UTF_16LE, new byte[] {(byte) 0xFF, (byte) 0xFE});\n\n    public static final StreamCharset UTF16 = UTF16_LE;\n\n    public static final StreamCharset UTF16_BOM = UTF16_LE_BOM;\n\n    public static final List<StreamCharset> COMMON = List.of(\n            UTF8,\n            UTF8_BOM,\n            UTF16,\n            UTF16_BOM,\n            new StreamCharset(StandardCharsets.US_ASCII, null),\n            new StreamCharset(StandardCharsets.ISO_8859_1, null),\n            new StreamCharset(Charset.forName(\"Windows-1251\"), null),\n            new StreamCharset(Charset.forName(\"Windows-1252\"), null));\n\n    // ======\n    // UTF-32\n    // ======\n    public static final StreamCharset UTF32_LE = new StreamCharset(Charset.forName(\"utf-32le\"), null);\n    public static final StreamCharset UTF32_LE_BOM =\n            new StreamCharset(Charset.forName(\"utf-32le\"), new byte[] {0x00, 0x00, (byte) 0xFE, (byte) 0xFF});\n    public static final StreamCharset UTF32_BE = new StreamCharset(Charset.forName(\"utf-32be\"), null);\n    public static final StreamCharset UTF32_BE_BOM = new StreamCharset(Charset.forName(\"utf-32be\"), new byte[] {\n        (byte) 0xFF, (byte) 0xFE, 0x00, 0x00,\n    });\n    private static final List<StreamCharset> RARE =\n            List.of(UTF16_LE, UTF16_LE_BOM, UTF16_BE, UTF16_BE_BOM, UTF32_LE, UTF32_LE_BOM, UTF32_BE, UTF32_BE_BOM);\n\n    public static final List<StreamCharset> ALL =\n            Stream.concat(COMMON.stream(), RARE.stream()).toList();\n\n    Charset charset;\n    byte[] byteOrderMark;\n\n    public static InputStreamReader detectedReader(InputStream inputStream) throws Exception {\n        StreamCharset detected = null;\n        for (var charset : StreamCharset.COMMON) {\n            if (charset.hasByteOrderMark()) {\n                inputStream.mark(charset.getByteOrderMark().length);\n                var bom = inputStream.readNBytes(charset.getByteOrderMark().length);\n                inputStream.reset();\n                if (Arrays.equals(bom, charset.getByteOrderMark())) {\n                    detected = charset;\n                    break;\n                }\n            }\n        }\n\n        if (detected == null) {\n            detected = StreamCharset.UTF8;\n        }\n\n        return detected.reader(inputStream);\n    }\n\n    public String read(byte[] b) throws Exception {\n        return read(new ByteArrayInputStream(b));\n    }\n\n    public String read(InputStream inputStream) throws Exception {\n        if (hasByteOrderMark()) {\n            var bom = inputStream.readNBytes(getByteOrderMark().length);\n            if (bom.length != 0 && !Arrays.equals(bom, getByteOrderMark())) {\n                throw new IllegalStateException(\"Charset does not match: \" + charset.toString());\n            }\n        }\n        return new String(inputStream.readAllBytes(), charset);\n    }\n\n    public InputStreamReader reader(InputStream stream) throws Exception {\n        if (hasByteOrderMark()) {\n            var bom = stream.readNBytes(getByteOrderMark().length);\n            if (bom.length != 0 && !Arrays.equals(bom, getByteOrderMark())) {\n                throw new IllegalStateException(\"Charset does not match: \" + charset.toString());\n            }\n        }\n\n        return new InputStreamReader(stream, charset);\n    }\n\n    public byte[] toBytes(String s) {\n        var raw = s.getBytes(charset);\n        if (hasByteOrderMark()) {\n            var bom = getByteOrderMark();\n            var r = new byte[raw.length + bom.length];\n            System.arraycopy(bom, 0, r, 0, bom.length);\n            System.arraycopy(raw, 0, r, bom.length, raw.length);\n            return r;\n        } else {\n            return raw;\n        }\n    }\n\n    @Override\n    public int hashCode() {\n        int result = Objects.hash(charset);\n        result = 31 * result + Arrays.hashCode(byteOrderMark);\n        return result;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (!(o instanceof StreamCharset that)) {\n            return false;\n        }\n        return charset.equals(that.charset) && Arrays.equals(byteOrderMark, that.byteOrderMark);\n    }\n\n    public boolean hasByteOrderMark() {\n        return byteOrderMark != null;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/io/xpipe/core/UuidHelper.java",
    "content": "package io.xpipe.core;\n\nimport java.nio.charset.StandardCharsets;\nimport java.util.Arrays;\nimport java.util.Optional;\nimport java.util.UUID;\n\npublic class UuidHelper {\n\n    public static UUID generateFromObject(Object... o) {\n        return UUID.nameUUIDFromBytes(Arrays.toString(o).getBytes(StandardCharsets.UTF_8));\n    }\n\n    public static Optional<UUID> parse(String s) {\n        try {\n            return Optional.of(UUID.fromString(s));\n        } catch (IllegalArgumentException ex) {\n            return Optional.empty();\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/io/xpipe/core/XPipeDaemonMode.java",
    "content": "package io.xpipe.core;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport lombok.Getter;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\n\n@Getter\npublic enum XPipeDaemonMode {\n    @JsonProperty(\"background\")\n    BACKGROUND(\"background\", List.of(\"base\", \"background\")),\n\n    @JsonProperty(\"tray\")\n    TRAY(\"tray\", List.of(\"tray\", \"taskbar\")),\n\n    @JsonProperty(\"gui\")\n    GUI(\"gui\", List.of(\"gui\", \"desktop\", \"interface\"));\n\n    private final String displayName;\n    private final List<String> nameAlternatives;\n\n    XPipeDaemonMode(String displayName, List<String> nameAlternatives) {\n        this.displayName = displayName;\n        this.nameAlternatives = nameAlternatives;\n    }\n\n    public static Optional<XPipeDaemonMode> getIfPresent(String name) {\n        if (name == null) {\n            return Optional.empty();\n        }\n\n        return Arrays.stream(XPipeDaemonMode.values())\n                .filter(xPipeDaemonMode ->\n                        xPipeDaemonMode.getNameAlternatives().contains(name.toLowerCase(Locale.ROOT)))\n                .findAny();\n    }\n\n    public static XPipeDaemonMode get(String name) {\n        return Arrays.stream(XPipeDaemonMode.values())\n                .filter(xPipeDaemonMode ->\n                        xPipeDaemonMode.getNameAlternatives().contains(name.toLowerCase(Locale.ROOT)))\n                .findAny()\n                .orElseThrow(() -> new IllegalArgumentException(\"Unknown mode: \" + name + \". Possible values: \"\n                        + Arrays.stream(values())\n                                .map(XPipeDaemonMode::getDisplayName)\n                                .collect(Collectors.joining(\", \"))));\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/module-info.java",
    "content": "import io.xpipe.core.CoreJacksonModule;\nimport io.xpipe.core.JacksonMapper;\nimport io.xpipe.core.ModuleLayerLoader;\n\nopen module io.xpipe.core {\n    exports io.xpipe.core;\n\n    requires com.fasterxml.jackson.datatype.jsr310;\n    requires com.fasterxml.jackson.core;\n    requires com.fasterxml.jackson.annotation;\n    requires com.fasterxml.jackson.databind;\n    requires java.net.http;\n    requires static lombok;\n\n    uses com.fasterxml.jackson.databind.Module;\n    uses ModuleLayerLoader;\n\n    provides ModuleLayerLoader with\n            JacksonMapper.Loader;\n    provides com.fasterxml.jackson.databind.Module with\n            CoreJacksonModule;\n}\n"
  },
  {
    "path": "core/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module",
    "content": "io.xpipe.core.CoreJacksonModule"
  },
  {
    "path": "core/src/main/resources/META-INF/services/io.xpipe.core.ModuleLayerLoader",
    "content": "io.xpipe.core.JacksonMapper$Loader"
  },
  {
    "path": "core/src/test/java/io/xpipe/core/test/StorePathTest.java",
    "content": "package io.xpipe.core.test;\n\nimport io.xpipe.core.StorePath;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.ValueSource;\n\npublic class StorePathTest {\n\n    @Test\n    public void testCreateInvalidParameters() {\n        Assertions.assertThrows(IllegalArgumentException.class, () -> {\n            StorePath.create(\"a/bc\", \"abc\");\n        });\n        Assertions.assertThrows(IllegalArgumentException.class, () -> {\n            StorePath.create(\"  \\t\", \"abc\");\n        });\n        Assertions.assertThrows(IllegalArgumentException.class, () -> {\n            StorePath.create(\"\", \"abc\");\n        });\n\n        Assertions.assertThrows(IllegalArgumentException.class, () -> {\n            StorePath.create(\"abc\", null);\n        });\n        Assertions.assertThrows(IllegalArgumentException.class, () -> {\n            StorePath.create(\"abc\", \"a/bc\");\n        });\n        Assertions.assertThrows(IllegalArgumentException.class, () -> {\n            StorePath.create(\"abc\", \"  \\t\");\n        });\n        Assertions.assertThrows(IllegalArgumentException.class, () -> {\n            StorePath.create(\"abc\", \"\");\n        });\n    }\n\n    @Test\n    public void testFromStringNullParameters() {\n        Assertions.assertThrows(IllegalArgumentException.class, () -> {\n            StorePath.fromString(null);\n        });\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = {\"abc/\", \"ab//c\", \"//abc\", \"////\", \"\", \" \"})\n    public void testFromStringInvalidParameters(String arg) {\n        Assertions.assertThrows(IllegalArgumentException.class, () -> {\n            StorePath.fromString(arg);\n        });\n    }\n\n    @Test\n    public void testFromStringValidParameters() {\n        Assertions.assertEquals(StorePath.fromString(\"ab/c\"), StorePath.fromString(\" ab/ c \"));\n        Assertions.assertEquals(StorePath.fromString(\"ab/c\"), StorePath.fromString(\" AB/ C \"));\n        Assertions.assertEquals(StorePath.fromString(\"ab/c\"), StorePath.fromString(\"ab/c \"));\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/module-info.java",
    "content": "module io.xpipe.core.test {\n    exports io.xpipe.core.test;\n\n    requires org.junit.jupiter.api;\n    requires org.junit.jupiter.params;\n    requires io.xpipe.core;\n    requires static lombok;\n}\n"
  },
  {
    "path": "dist/base.gradle",
    "content": "import java.util.stream.Collectors\n\ndef distDir = \"${project.layout.buildDirectory.get()}/dist\"\n\ntasks.register('licenses', DefaultTask) {\n    doLast {\n        copy {\n            from \"$projectDir/licenses/\"\n            into \"$distDir/licenses/\"\n            include '*.license'\n            rename { String name ->\n                name.replace(\"license\", \"txt\")\n            }\n        }\n    }\n}\n\ndef debugArguments = file(\"$projectDir/debug/debug_arguments.txt\").text.lines().map(s -> '\"' + s + '\"').collect(Collectors.joining(' '))\n\nif (os.isWindows()) {\n    tasks.register('baseDist', DefaultTask) {\n        doLast {\n            copy {\n                from \"$distDir/jpackage/${jpackageExecutableName}\"\n                into \"$distDir/base\"\n            }\n            copy {\n                from \"$projectDir/logo/logo.ico\"\n                into \"$distDir/base\"\n            }\n            copy {\n                from \"$rootDir/lang\"\n                into \"$distDir/base/lang\"\n            }\n\n            file(\"$distDir/base/app/.jpackage.xml\").delete()\n\n            // Don't launch multiple exe instances: https://bugs.openjdk.org/browse/JDK-8340311\n            // We don't need the /app directory to be in the library path\n            def configFile = file(\"$distDir/base/app/${jpackageExecutableName}.cfg\")\n            configFile.text = configFile.text.replace(\"[Application]\", \"[Application]\\nwin.norestart=true\")\n\n            def batLauncherFile = file(\"$distDir/base/runtime/bin/${jpackageExecutableName}.bat\")\n            def batLauncherContent = batLauncherFile.text\n            batLauncherContent = batLauncherContent.replace(\" -p \\\"%~dp0/../app\\\"\", \"\")\n            batLauncherFile.text = batLauncherContent\n            file(\"$distDir/base/runtime/bin/${jpackageExecutableName}\").delete()\n\n            file(\"$distDir/base/scripts\").mkdirs()\n            def debug = file(\"$distDir/base/scripts/${jpackageExecutableName}_debug.bat\")\n            debug.text = file(\"$projectDir/debug/windows/${jpackageExecutableName}_debug.bat\").text.replace(\n                    'JVM-ARGS',\n                    debugArguments)\n            debug.setExecutable(true)\n\n            copy {\n                from \"$distDir/licenses\"\n                into \"$distDir/base/licenses\"\n            }\n        }\n    }\n} else if (os.isLinux()) {\n    tasks.register('baseDist', DefaultTask) {\n        doLast {\n            copy {\n                from \"$distDir/jpackage/${jpackageExecutableName}\"\n                into \"$distDir/base/\"\n            }\n            copy {\n                from \"$projectDir/logo/logo.png\"\n                into \"$distDir/base/\"\n            }\n            copy {\n                from \"$projectDir/fonts\"\n                into \"$distDir/base/fonts\"\n            }\n            copy {\n                from \"$rootDir/lang\"\n                into \"$distDir/base/lang\"\n            }\n\n            def shLauncherFile = file(\"$distDir/base/lib/runtime/bin/${jpackageExecutableName}\")\n            def shLauncherContent = shLauncherFile.text\n            shLauncherContent = shLauncherContent.replace(\" -p \\\"\\$DIR/../app\\\"\", \"\")\n            shLauncherFile.text = shLauncherContent\n            file(\"$distDir/base/lib/runtime/bin/${jpackageExecutableName}.bat\").delete()\n\n            file(\"$distDir/base/scripts\").mkdirs()\n            def debug = file(\"$distDir/base/scripts/${jpackageExecutableName}_debug.sh\")\n            debug.text = file(\"$projectDir/debug/linux/${jpackageExecutableName}_debug.sh\").text.replace(\n                    'JVM-ARGS',\n                    debugArguments)\n            debug.setExecutable(true, false)\n\n            copy {\n                from \"$distDir/licenses\"\n                into \"$distDir/base/licenses\"\n            }\n        }\n    }\n} else {\n    tasks.register('baseDist', DefaultTask) {\n        doLast {\n            def app = \"${productName}.app\"\n            copy {\n                from \"$distDir/jpackage/${jpackageExecutableName}.app/Contents\"\n                into \"$distDir/$app/Contents/\"\n            }\n            copy {\n                from \"$distDir/licenses\"\n                into \"$distDir/$app/Contents/Resources/licenses\"\n            }\n            copy {\n                from \"$rootDir/lang\"\n                into \"$distDir/$app/Contents/Resources/lang\"\n            }\n\n            def shLauncherFile = file(\"$distDir/$app/Contents/runtime/Contents/Home/bin/${jpackageExecutableName}\")\n            def shLauncherContent = shLauncherFile.text\n            shLauncherContent = shLauncherContent.replace(\" -p \\\"\\$DIR/../app\\\"\", \"\")\n            shLauncherFile.text = shLauncherContent\n            file(\"$distDir/$app/Contents/runtime/Contents/Home/bin/${jpackageExecutableName}.bat\").delete()\n\n            file(\"$distDir/$app/Contents/Resources/scripts\").mkdirs()\n            def debug = file(\"$distDir/$app/Contents/Resources/scripts/${jpackageExecutableName}_debug.sh\")\n            debug.text = file(\"$projectDir/debug/mac/${jpackageExecutableName}_debug.sh\").text.replace(\n                    'JVM-ARGS',\n                    debugArguments)\n            debug.setExecutable(true, false)\n        }\n    }\n}\n\nbaseDist.dependsOn(licenses)\nbaseDist.dependsOn(tasks.jpackage)\ndist.dependsOn(baseDist)\n"
  },
  {
    "path": "dist/build.gradle",
    "content": "\nplugins {\n    id 'org.beryx.jlink' version '3.2.1'\n    id(\"com.netflix.nebula.ospackage\") version \"12.2.0\"\n    id 'org.gradle.crypto.checksum' version '1.4.0'\n    id 'signing'\n}\n\nrepositories {\n    mavenCentral()\n}\n\ntasks.register('dist', DefaultTask) {}\n\n\nrun {\n    enabled = false\n}\n\ndistTar {\n    enabled = false\n}\n\ndistZip {\n    enabled = false\n}\n\nimport org.gradle.crypto.checksum.Checksum\n\nimport java.util.stream.Collectors\n\ntasks.register('createGithubChangelog', DefaultTask) {\n    doLast {\n        def template = file(\"$projectDir/changelog_format/github.md\").text\n        def argument = incrementalChangelogFile.exists() ? incrementalChangelogFile.text : changelogFile.text\n        def text = template.formatted(argument)\n        def dir = layout.buildDirectory.dir(\"dist/release\").get()\n        mkdir(dir)\n        def target = dir.file(\"github-changelog.md\").asFile\n        target.text = text\n    }\n}\n\ntasks.register('createDiscordChangelog', DefaultTask) {\n    doLast {\n        def template = file(\"$projectDir/changelog_format/discord.md\").text\n        def argument = incrementalChangelogFile.exists() ? incrementalChangelogFile.text : changelogFile.text\n        def text = template.formatted(productName, versionString, argument, productName, kebapProductName)\n        def dir = layout.buildDirectory.dir(\"dist/release\").get()\n        mkdir(dir)\n        def target = dir.file(\"discord-changelog.md\").asFile\n        target.text = text\n    }\n}\n\ndef distDir = layout.buildDirectory.get().dir('dist')\ntasks.register('createChecksums', Checksum) {\n    inputFiles.setFrom(distDir.dir('artifacts').getAsFileTree().files)\n    outputDirectory.set(layout.buildDirectory.dir(\"dist/checksums/artifacts\"))\n    checksumAlgorithm.set(Checksum.Algorithm.SHA256)\n\n    doLast {\n        def artifactChecksumsSha256Hex = new HashMap<String, String>()\n        for (final def file in outputDirectory.get().getAsFileTree().files) {\n            def name = file.name.lastIndexOf('.').with { it != -1 ? file.name[0..<it] : file.name }\n            if (name.endsWith('mapping.map') || name.endsWith('.asc')) {\n                continue\n            }\n\n            artifactChecksumsSha256Hex.put(name, file.text.strip())\n        }\n\n        file(layout.buildDirectory.dir(\"dist/checksums/sha256sums.txt\")).text = artifactChecksumsSha256Hex.entrySet().stream()\n                .map(e -> e.getValue() + ' ' + e.getKey())\n                .collect(Collectors.joining('\\n'))\n    }\n}\n\nString getArtifactChecksumSha256Hex(String name) {\n    var file = layout.buildDirectory.file(\"dist/checksums/artifacts/${name}.sha256\")\n    return file.get().getAsFile().exists() ? file.get().getAsFile().text : \"\"\n}\n\nclean {\n    doFirst {\n        // Fix clean failing when file is read-only\n        if (file(\"$distDir\").exists()) {\n            file(\"$distDir\").traverse { f -> if (f.exists() && f.isFile()) f.writable = true }\n        }\n    }\n}\n\napply from: 'base.gradle'\napply from: 'jpackage.gradle'\n\nif (fullVersion) {\n    apply from: 'train.gradle'\n    apply from: 'base_full.gradle'\n    apply from: 'cli.gradle'\n    apply from: 'portable.gradle'\n    apply from: 'proguard.gradle'\n    apply from: 'github.gradle'\n    apply from: 'install.gradle'\n    apply from: 'i18n.gradle'\n\n    if (os.isLinux()) {\n        apply from: 'linux_packages.gradle'\n        apply from: 'appimage/app_image.gradle'\n        apply from: 'nix/nix.gradle'\n        apply from: 'aur/aur.gradle'\n        apply from: 'homebrew/homebrew.gradle'\n        apply from: 'choco/choco.gradle'\n    } else if (os.isWindows()) {\n        apply from: 'msi/msi.gradle'\n        apply from: 'winget/winget.gradle'\n    } else if (os.isMacOsX()) {\n        apply from: 'pkg/pkg.gradle'\n    }\n\n    signing {\n        useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword)\n    }\n\n    tasks.register('signArtifacts', Sign) {\n        def dir = layout.buildDirectory.dir(\"dist/artifacts\").get()\n        dir.asFileTree.files.forEach { sign(it) }\n    }\n\n    tasks.register('signChecksums', Sign) {\n        def checksums = layout.buildDirectory.file(\"dist/checksums/sha256sums.txt\").get().asFile\n        sign(checksums)\n    }\n}\n\ndistTar {\n    enabled = false\n}\n\ndistZip {\n    enabled = false\n}\n\nassembleDist {\n    enabled = false\n    dependsOn.clear()\n}\n\n"
  },
  {
    "path": "dist/changelog/1.0.0.md",
    "content": "## Changes in 1.0.0\n\n- Completely revamp file browser\n- Add more appearance themes to choose from\n- Add arm64 support for homebrew release\n- A lot of bug fixes"
  },
  {
    "path": "dist/changelog/1.0.1.md",
    "content": "## Changes in 1.0.1\n\n- Add ability to open directory as root in file browser\n- Always run shell profile init scripts when opening them in terminals\n- Fix incomplete PATH when opening shell session in iTerm2\n- Fix application not starting up on Windows when username contained special characters\n- Fix shell connection failing when user had no write privileges\n- Fix wrong passwords errors in posix shells\n- Fix homebrew distribution\n- Fix WSL detection failing on non-english systems\n- Many small miscellaneous fixes and improvements"
  },
  {
    "path": "dist/changelog/1.1.0.md",
    "content": "## Changes in 1.1.0\n\n- Add new overview page for file browser sessions\n- Fix file IO being corrupted and freezing on Windows systems with global UTF8 enabled\n- Show error messages in case a file write operation fails in browser\n- Many small miscellaneous fixes and improvements"
  },
  {
    "path": "dist/changelog/1.1.1.md",
    "content": "## Changes in 1.1.1\n\n- Improve startup time\n- Improve error messages when command or shell connection fails\n- Fix directory copy across systems not finishing\n- Fix self-updater not launching on Linux and macOS. Might require a reinstallation to get it working again\n- Fix file browser not working for some Alpine Linux distributions\n- Add support for Tabby terminals\n- Start up local and wsl sessions in user home directory\n- Improve search for connections dialog\n- Improve accessibility support for screen readers\n- Improve error messages\n- Properly clean temp directories\n- Many small miscellaneous fixes and improvements"
  },
  {
    "path": "dist/changelog/1.1.2.md",
    "content": "## Changes in 1.1.2\n\n- Fix browser initialization race condition\n- Fix OOB for some PowerShell sessions\n\n## Changes in 1.1.1\n\n- Improve startup time\n- Improve error messages when command or shell connection fails\n- Fix directory copy across systems not finishing\n- Fix self-updater not launching on Linux and macOS. Might require a reinstallation to get it working again\n- Fix file browser not working for some Alpine Linux distributions\n- Add support for Tabby terminals\n- Start up local and wsl sessions in user home directory\n- Improve search for connections dialog\n- Improve accessibility support for screen readers\n- Improve error messages\n- Properly clean temp directories\n- Many small miscellaneous fixes and improvements"
  },
  {
    "path": "dist/changelog/1.1.3.md",
    "content": "## Changes in 1.1.3\n\n- Fix posix shell detection being wrong and launching wrong shell type\n\n## Changes in 1.1.2\n\n- Fix browser initialization race condition\n- Fix OOB for some PowerShell sessions\n\n## Changes in 1.1.1\n\n- Improve startup time\n- Improve error messages when command or shell connection fails\n- Fix directory copy across systems not finishing\n- Fix self-updater not launching on Linux and macOS. Might require a reinstallation to get it working again\n- Fix file browser not working for some Alpine Linux distributions\n- Add support for Tabby terminals\n- Start up local and wsl sessions in user home directory\n- Improve search for connections dialog\n- Improve accessibility support for screen readers\n- Improve error messages\n- Properly clean temp directories\n- Many small miscellaneous fixes and improvements"
  },
  {
    "path": "dist/changelog/1.2.0.md",
    "content": "## Changes in 1.2.0\n\n- Introduce new landing page in file browser that remembers where you left off\n- Show commands that are executed to open a shell in connection\n  creation wizard so that you are able to manually reproduce any connection issues\n- Merge settings and about pages\n- Add support for handling symbolic links in file browser\n- Add support for many more Linux terminals\n- Improve UI layout on Linux systems\n- Introduce new logo\n- Fix macOS local machine shell connection not finding some executables\n- Many small miscellaneous fixes and improvements\n"
  },
  {
    "path": "dist/changelog/1.3.0.md",
    "content": "## Changes in 1.3.0\n\n- Completely rework connection management\n  (Note that this change might remove some old connections that we're not compatible with the new system.)\n- Add shift-click selection to file browser\n- Many small miscellaneous fixes and improvements\n"
  },
  {
    "path": "dist/changelog/1.3.1.md",
    "content": "## Changes in 1.3.1\n\n- Attempt to fix docker socket permission issues by checking the actual socket permissions rather than just user groups.\n  Accessing docker containers should also now not require elevation when not needed.\n- Fix LXD container list failing due to unsupported compact format option that is not present in older lxc versions\n- Fix storage directory change functionality not working properly and not applying changes\n- Fix temporary scripts directory not being cleaned properly on launch\n- Set TERM variable to dumb for local shells as well to signal profile files to not use any fancy formatting\n- Fix tabby terminal not launching on macOS\n- Fix VSCode not launching on Windows when being installed system-wide\n- Fix some rare startup crashes\n- Many other small miscellaneous fixes and improvements\n"
  },
  {
    "path": "dist/changelog/1.3.2.md",
    "content": "## Changes in 1.3.2\n\n- Rework temporary directory handling.\n  Temporary scripts will now be created in the user home directory ~/.xpipe/temp instead of the global temp directory\n  to fix cases in which permission issues occurred on Linux when trying to clear the shared directory.\n- Fix LXD socket access permission issues by checking the actual socket permissions rather than just user groups.\n- Fix startup errors due to unrecognized shell type on macOS when Fig was installed\n- Fix connection creator dialog not showing an error if it occurred before\n  and also throwing errors when a screen reader was active.\n- Fix filter text field becoming stuck in a loop and freezing up\n- Make docker inspect action more prominent and fix it failing if elevation is needed\n- Use cp -a instead of just cp to copy directories in browser\n- Many other small miscellaneous fixes and improvements\n"
  },
  {
    "path": "dist/changelog/1.4.0.md",
    "content": "## Changes in 1.4.0\n\n- Implement support for SSH tunnels / port forwarding.\n  This includes local, remote, and dynamic tunneling.\n- Rework file browser transfer pane. Files dropped there will no longer be downloaded automatically.\n  Instead, you can also use it just to quickly transfer a set of files across file system tabs.\n  Only when you now click the new download button, the set of files is downloaded\n  to your local machine and can be dragged into your native desktop environment as regular files.\n- Publish xpipe package to the arch user repository\n- Add support for podman containers\n- Add support for BBEdit as an editor\n- Add support for Alacritty on Windows and macOS as well\n- Add support for Kitty on macOS\n- Restyle sidebar to take up less space\n- Improve scaling of connection list display information\n- Improve askpass script retention\n- Properly apply startup mode setting\n- Fix some features not working on busybox systems due to unknown base64 --decode option\n- Fix local elevation not working on macOS with Fig installed\n- Fix commands and psql not launching when username contains spaces\n- Many other small miscellaneous fixes and improvements\n"
  },
  {
    "path": "dist/changelog/1.4.1.md",
    "content": "## Changes in 1.4.1\n\n- Fix application not starting on Windows (Sorry, for that!)\n\n## Changes in 1.4.0\n\n- Implement support for SSH tunnels / port forwarding.\n  This includes local, remote, and dynamic tunneling.\n- Rework file browser transfer pane. Files dropped there will no longer be downloaded automatically.\n  Instead, you can also use it just to quickly transfer a set of files across file system tabs.\n  Only when you now click the new download button, the set of files is downloaded\n  to your local machine and can be dragged into your native desktop environment as regular files.\n- Publish xpipe package to the arch user repository\n- Add support for podman containers\n- Add support for BBEdit as an editor\n- Add support for Alacritty on Windows and macOS as well\n- Add support for Kitty on macOS\n- Restyle sidebar to take up less space\n- Improve scaling of connection list display information\n- Improve askpass script retention\n- Properly apply startup mode setting\n- Fix some features not working on busybox systems due to unknown base64 --decode option\n- Fix local elevation not working on macOS with Fig installed\n- Fix commands and psql not launching when username contains spaces\n- Many other small miscellaneous fixes and improvements\n"
  },
  {
    "path": "dist/changelog/1.4.2.md",
    "content": "## Changes in 1.4.2\n\n- Fix SSH connections where an additional password that was not\n  required was provided failing with the error \"Input was closed before end was read\" on Windows\n- Fix sidebar having an insatiable hunger for more space and crushing\n  everything else when display scale was set to 150%+ on Windows\n\n## Changes in 1.4.1\n\n- Fix application not starting on Windows (Sorry, for that!)\n\n## Changes in 1.4.0\n\n- Implement support for SSH tunnels / port forwarding.\n  This includes local, remote, and dynamic tunneling.\n- Rework file browser transfer pane. Files dropped there will no longer be downloaded automatically.\n  Instead, you can also use it just to quickly transfer a set of files across file system tabs.\n  Only when you now click the new download button, the set of files is downloaded\n  to your local machine and can be dragged into your native desktop environment as regular files.\n- Publish xpipe package to the arch user repository\n- Add support for podman containers\n- Add support for BBEdit as an editor\n- Add support for Alacritty on Windows and macOS as well\n- Add support for Kitty on macOS\n- Restyle sidebar to take up less space\n- Improve scaling of connection list display information\n- Improve askpass script retention\n- Properly apply startup mode setting\n- Fix some features not working on busybox systems due to unknown base64 --decode option\n- Fix local elevation not working on macOS with Fig installed\n- Fix commands and psql not launching when username contains spaces\n- Many other small miscellaneous fixes and improvements\n"
  },
  {
    "path": "dist/changelog/1.5.0.md",
    "content": "## Changes in 1.5.0\n\nThis is the largest update yet and comes with loads of improvements and changes, some of which might require you to update some connection configurations. There might be some rough edges, but these will be quickly ironed out. So please report any issues you can find!\n\n### Passwords & Password managers\n\nThis update comes with a first attempt of supporting the retrieval of passwords from external sources. Due to the variety of available password managers and formats, I went with the most straightforward approach here which is essentially delegating that task to the CLI of your password manager.\n\nEssentially, you're able to specify a command template to retrieve your passwords. For example, by specifying the command template `mypasswordmgr get $KEY`, you can then choose the password when creating connections by just supplying the key argument. XPipe will call the command, read the password, and supply it from there.\n\nThere's also support to specify an arbitrary command or to dynamically prompt the password on each login.\n\n### SSH Configs\n\nIn 1.5, you're also now able to automatically import all hosts stored in your ssh config files.\nIt is also then possible to refresh and update these detected connections at any time in case you make external changes to your config files.\n\n### Fish\n\nThis update brings support for fish as another possible shell type.\n\nNote that there are several limitations with this implementation as fish does not support an interactive mode in headless environments, resulting in XPipe having to use a fallback shell for certain operations.\n\n### CLI\n\nThis update lays the foundation for future advancements in the command-line interface of XPipe. To start off, it comes with a few new commands to read and write files on remote systems directly from your terminal.\n\nThe workflow is designed as follows:\n\n- You can list all available connections and their ids to use with `xpipe list`\n- Using the command `xpipe launch <id>`, you are able to log into a remote shell connection in your existing terminal session \n- Using the command `xpipe drain <id> <remote file path>`, you are able to forward the file contents to the stdout \n- Using the command `xpipe sink <id> <remote file path>`, you are able to forward content from your stdin to the remote file\n\nThe id system is flexible, allowing you to only specify as much of the id as is necessary.\n\nAn easy example would be the following: Assume that you have a Windows server with an id of `ssh-windows` and want to filter a file there, but you are missing `grep`. Then you can execute on your local machine: `xpipe drain ssh-windows \"C:\\myfile.txt\" | grep <filter> | xpipe sink ssh-windows \"C:\\myfile_filtered.txt\"`.\n\nThe XPipe CLI should be put automatically in your path upon installation, you can test that with `xpipe --help`. Otherwise, you will find it in `<xpipe dir>/cli/bin/xpipe`.\n\n### Antivirus adjustments\n\nAs it turns out, several antivirus programs do not like XPipe and what it is doing with shells. As a result, some of them quarantine XPipe and even the system shells itself as they get confused of who is making the calls.\n\nThis update aims to reduce any unexpected issues caused by antivirus programs by automatically detecting whether a problematic antivirus is installed and giving the user the chance to prepare for any upcoming issues.\n\n### Cygwin and MSYS2\n\nXPipe can now automatically detect Cygwin and MSYS2 environments on your machine. This also comes with full support of the feature set for these environments\n\n### Misc\n\n- For every system, XPipe will now also display the appropriate OS/distro logo (if recognized)\n- Rework SSH key-based authentication to properly interact with agents, now also including pageant\n- Add ability to test out terminals and editors directly in the settings menu\n- Implement a new internal API to better assemble complex commands\n- Rework os detection logic for passthrough environments like Cygwin and MSYS2\n- Fix desktop directory not being determined correctly on Windows when it was moved from the default location\n- Fix various checks in file browser not being applied properly and leading to wrong error messages\n- Add alternative ways of resolving path in case realpath was not available\n- Rework threading in navigation bar in browser to improve responsiveness\n- Recheck if prepared update is still the latest one prior to installing it\n- Keep connection configuration when refreshing parent\n- Properly use shell script file extension for external editor when creating shell environments\n- Built-in documentation popups now honour the dark mode setting\n- Properly detect applications such as editors and terminals when they are present in the path on Windows\n- Rework operation mode handling to properly honor the startup mode setting\n- Many other small miscellaneous fixes and improvements\n- Improve app detection on macOS\n"
  },
  {
    "path": "dist/changelog/1.5.1.md",
    "content": "## Changes in 1.5.1\n\n- Add ARM build for Linux to available releases\n- Add ability to sort connections by name and last access date\n- Add ability to automatically accept new ssh host key when required\n- Improve performance when adding are removing connections\n- Look in PATH for terminals on windows\n- Fix CLI error messages not being able to be parsed\n- Many other small miscellaneous fixes and improvements\n"
  },
  {
    "path": "dist/changelog/1.5.2.md",
    "content": "## Changes in 1.5.2\n\n- Make automatic connection search dialog accessible from a separate button\n- Add filter bar to connection chooser\n- Add Kubernetes describe action for pods\n- Fix Kubernetes functionality breaking when a pot contain multiple containers\n- Use Kubernetes context names instead of cluster names to access resources\n- Fix automatic ssh host key acceptance not working\n- Fix paste not working in file browser\n- Rework dynamic script generation to apply some properties after init scripts\n- Many other small miscellaneous fixes and improvements\n\n## Changes in 1.5.1\n\n- Add ARM build for Linux to available releases\n- Add ability to sort connections by name and last access date\n- Add ability to automatically accept new ssh host key when required\n- Improve performance when adding are removing connections\n- Look in PATH for terminals on windows\n- Fix CLI error messages not being able to be parsed\n- Many other small miscellaneous fixes and improvements\n\n## Changes in 1.5.0\n\nhttps://github.com/xpipe-io/xpipe/releases/tag/1.5.0 is the largest update yet and comes with loads of improvements and changes, some of which might require you to update some connection configurations. There might be some rough edges, but these will be quickly ironed out. So please report any issues you can find!\n"
  },
  {
    "path": "dist/changelog/1.5.3.md",
    "content": "## Changes in 1.5.3\n\n- Add connection timeout setting for cases in which some connections are slow to start up\n- Fix connection timeout not being properly applied for ssh config connections\n- Fix sudo elevation password not passed to ssh config connections\n- Fix sudo elevation not being possible for some commands even though it should\n- Fix terminal session sometimes not opening the correct system when using SSH jump hosts\n- Fix debug mode not launching in some cases on Linux due to script permission issues\n- Fix CLI crashing due to missing CPU features on outdated CPUs\n- Fix SSH key file not being properly validated\n- Fix integer field breaking when pasting into it\n- Fix crash with a cryptic error message when temporary directory variable was invalid\n- Fix Notepad++ not being detected when it was a 32-bit installation\n- Fix NullPointer when undoing a rename operation in file browser\n- Fix NullPointer when no editor was set in file browser\n- Fix shell connection state not being properly reset on unexpected termination\n- Fix fish error check sometimes being displayed in cmd\n- Fix file browser tab closing failing if underlying shell connection has died\n- Fix about screen on macOS\n"
  },
  {
    "path": "dist/changelog/1.6.0.md",
    "content": "## Changes in 1.6.0\n\n- Implement new category tree organization functionality for connections\n- Rework connection chooser in popup window and in file browser\n- Rework user interface\n- Add support for sharing your storage via a remote git repository\n- Add support for a transparent window mode\n- Upgrade to GraalVM 21\n- Improve command process synchronization to try to fix rare race conditions and deadlocks\n- Dynamically check whether kubectl requires elevation to fix permission issues, for example when using rancher k3s\n- Add attach and logs context menu actions for docker containers\n- Add support for VSCode Insiders\n- Add support for ElementaryOS terminal\n- Add support ash shells\n- Improve error handler to also show a graphical window before the application window is opened\n- Make shell environment init script apply changes to the shell session by sourcing it\n- Rework powershell execution policy usage to not override system default\n- Improve resilience of storage loading and saving in case of IO errors\n- Improve browser tab naming and sizing\n- Automatically apply local clipboard changes to browser\n- Preserve clipboard contents after exit\n- Add support to open ssh: URLs\n- Add functionality to open ssh connections in Termius\n- Add functionality to open ssh connections in default SFTP client\n- Add functionality to create desktop shortcuts and URLs for certain actions within XPipe\n- Rework installer packages\n- Properly query desktop directory on Windows and Linux in case it was at non-standard locations\n- Check whether target exists when renaming or moving in file browser\n- Fix ssh config entry being added even if it was empty\n- Fix passcode PAM authentication caching responses\n- Fix Powershell remote sessions not working correctly\n- Many other small miscellaneous fixes and improvements\n\n## Experimental releases\n\nThere are already many other feature branches in the pipeline and will be released soon.\nIn fact as of now, you can already try out the next major 1.7 release in the [XPipe PTB](https://github.com/xpipe-io/xpipe-ptb) (Public Test Build).\nThe regular releases and new PTB releases are designed to not interfere with each other and can therefore be installed and used side by side."
  },
  {
    "path": "dist/changelog/1.7.0.md",
    "content": "# Update procedure\n\nNote that the automatic updater is broken in version 1.6.0. It will freeze the application and not perform the update. **So do not try to click the install button in XPipe**!\nYou have to install it manually from https://github.com/xpipe-io/xpipe/releases/tag/1.7.0. You can easily do this as uninstalling the old version does not delete any user data. Installing a newer version of XPipe also automatically uninstalls any old ones, so you don't have to manually uninstall it.\n\n## Changes in 1.7.0\n\n### Scripts\n\nXPipe 1.7 comes with a new scripting system, you now can take your environment everywhere.\nThe idea is to create modular and reusable init scripts in XPipe that will be run on login but are independent of your profile files.\nYou can set certain scripts to be executed for every connection, allowing you to create a consistent environment across all remote systems.\n\nAs of now, there is only one set of scripts for enabling starship in your shell connections as a proof of concept.\nHowever, you can contribute custom scripts [here](https://github.com/xpipe-io/xpipe/tree/master/ext/base/src/main/java/io/xpipe/ext/base/script/PredefinedScriptStore.java) and [here](https://github.com/xpipe-io/xpipe/tree/master/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts).\n\n### Connection states\n\nThe second big change is a rework of the state system.\nThis merges the process of validating/refreshing with the process of establishing a connection, allowing for a much faster creation and launch of new connections.\nIt also enables a custom display and instant updates of the information displayed for a connection.\nYou will definitely notice this change when you connect to a system.\n\n### Performance improvements\n\nThe entire storage and UI handling of connections has been reworked to improve performance.\nEspecially if you're dealing with a large amount of connections, this will be noticeable.\n\n### Colors\n\nYou can now assign colors to connections for organizational purposes to help in situations when many connections are opened in the file browser and terminals at the same time.\nThese colors will be shown to identify tabs everywhere within XPipe and also outside of XPipe, for example in terminals.\n\n### Other changes\n\n- Codesign executables on Windows\n- Fix application not starting up or exiting properly sometimes\n- Add support for bsd-based systems\n- Fix OPNsense shells timing out\n- Make window transparency setting a slider\n- Save configuration data more frequently to avoid any data loss\n- Fix shutdown error caused by clipboard being inaccessible\n- Fix some environment scripts not being sourced correctly\n- Fix autoupdater not working properly\n- Fix application not exiting properly on SIGTERM\n- Many other small miscellaneous fixes and improvements"
  },
  {
    "path": "dist/changelog/1.7.1.md",
    "content": "# Update procedure\n\nNote that the automatic updater is broken in version 1.6.0. It will freeze the application and not perform the update. **So do not try to click the install button in XPipe**!\nYou have to install it manually from https://github.com/xpipe-io/xpipe/releases/tag/1.7.0. You can easily do this as uninstalling the old version does not delete any user data. Installing a newer version of XPipe also automatically uninstalls any old ones, so you don't have to manually uninstall it.\n\n## Changes in 1.7.0\n\n### Scripts\n\nXPipe 1.7 comes with a new scripting system, you now can take your environment everywhere.\nThe idea is to create modular and reusable init scripts in XPipe that will be run on login but are independent of your profile files.\nYou can set certain scripts to be executed for every connection, allowing you to create a consistent environment across all remote systems.\n\nAs of now, there is only one set of scripts for enabling starship in your shell connections as a proof of concept.\nHowever, you can contribute custom scripts [here](https://github.com/xpipe-io/xpipe/tree/master/ext/base/src/main/java/io/xpipe/ext/base/script/PredefinedScriptStore.java) and [here](https://github.com/xpipe-io/xpipe/tree/master/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts).\n\n### Connection states\n\nThe second big change is a rework of the state system.\nThis merges the process of validating/refreshing with the process of establishing a connection, allowing for a much faster creation and launch of new connections.\nIt also enables a custom display and instant updates of the information displayed for a connection.\nYou will definitely notice this change when you connect to a system.\n\n### Performance improvements\n\nThe entire storage and UI handling of connections has been reworked to improve performance.\nEspecially if you're dealing with a large amount of connections, this will be noticeable.\n\n### Colors\n\nYou can now assign colors to connections for organizational purposes to help in situations when many connections are opened in the file browser and terminals at the same time.\nThese colors will be shown to identify tabs everywhere within XPipe and also outside of XPipe, for example in terminals.\n\n### Other changes\n\n- Codesign executables on Windows\n- Fix application not starting up or exiting properly sometimes\n- Add support for bsd-based systems\n- Fix OPNsense shells timing out\n- Make window transparency setting a slider\n- Save configuration data more frequently to avoid any data loss\n- Fix shutdown error caused by clipboard being inaccessible\n- Fix some environment scripts not being sourced correctly\n- Fix autoupdater not working properly\n- Fix application not exiting properly on SIGTERM\n- Many other small miscellaneous fixes and improvements"
  },
  {
    "path": "dist/changelog/1.7.10.md",
    "content": "## Changes in 1.7.10\n\n- Fix application freezing under heavy load, usually with the git storage\n- Fix git storage asking multiple times for login information when needed\n- Fix git storage push failing when login information was required\n- Fix rpm installation upgrade removing desktop file\n- Make exit timeout always run to quit after max 30 seconds\n- Improve readme description for causes of an empty git storage\n- Add chmod file browser action\n- Many other small miscellaneous fixes and improvements\n\n## Changes in 1.7.9\n\n### Git storage rework\n\nThe git storage functionality has been in a bad state, hopefully this update will change that.\nFirst of all, several bugs and inconsistencies have been fixed.\n\nFurthermore, the authentication options have been greatly expanded.\nYou can now supply both HTTP and SSH git URLs. If any input is required like a username/password/passphrase, XPipe will show a prompt.\nIf you chose to use an SSH git URL, you can also set key-based authentication options just as for other ssh connections.\n\nLastly, there is now a general data directory as well in which you can put any additional files like SSH keys that you want to include in the repository. You can then refer to them just as normal within XPipe but their file paths are automatically adapted on any system you clone the repository to. You can open this data directory from the settings menu.\n\nIt is recommended to start with a remote git repository from scratch though to properly fix previous issues.\n\n### Other changes\n\n- Fix new daemon instances getting stuck when trying to communicate with an already running daemon that does not respond\n- Fix daemon not properly exiting when stuck on shutdown\n- Fix elevated commands getting stuck if no sudo password was available\n- Fix some commands getting stuck under rare conditions on Linux and macOS\n- Fix some windows being shown outside of screen bounds when display scaling values were set very high\n- Fix macOS .pkg installers requiring Rosetta to be installed on ARM even though it wasn't needed\n- Fix powerlevel10k breaking terminal integrations on macOS\n- Fix screen clear on terminal launch sometimes breaking terminal formatting\n- Fix process exit codes outside of signed 32-bit int causing errors\n- Fix local shell process not restarting if it somehow died\n- Fix errors not showing when GUI could not be initialized\n- Fix some NullPointerExceptions\n- Fix file browser execute action not launching scripts\n- Fix some license related errors\n- Fix Windows os detection on remote servers if cmd was not available\n- Many other small miscellaneous fixes and improvements\n"
  },
  {
    "path": "dist/changelog/1.7.11.md",
    "content": "### New professional features\n\n- Add support for Yubikey PKCS#11 identities for SSH connections\n- Add support for custom PKCS#11 libraries to use with SSH connections\n- Add support for the gpg-agent for SSH connections, e.g. for smart cards\n\n### Other changes\n\n- Add ESC and CTRL/CMD+W shortcuts to close dialog windows\n- Add support for JetBrains editors\n- Add support for docker versions <20\n- Add history button for file browser\n- Improve information display for docker containers\n- Rework SSH agent integration for all agent types to fix many issues\n- Properly update state of child connections on refresh\n- Fix SSH identity file chooser not opening up the correct system when using jump servers\n- Make git always use the native ssh client on Windows\n- Properly disable remote git storage during a session if a remote operation failed before\n- Show unavailable connection types when searching for connections automatically\n- Add ability to open compatible SSH connections in vscode\n- Fix some connections not being shared in a git repository even though they should\n\n### Preview pro features\n\nFor anyone interested in giving any new professional features a try without having to commit to buying a full license,\nthere is now a special preview mode available: Simply enter the license key `D18D1C9F-D3CB-49CA-A909-FF385DECD948` and get full access to newly released professional features for two weeks after their initial release date. In fact, you can try it out right now to get access to the new SSH authentication features.\n"
  },
  {
    "path": "dist/changelog/1.7.12.md",
    "content": "## New professional features\n\n- Add ability to open files and directories in VSCode SSH remote environment in file browser\n- Added support for fully offline licenses. You can obtain them via email request\n  in case you're running it on a system without internet connectivity or restricted proxy settings\n\n## Changes\n\n- Make current default shell also show up in shell environments to prevent confusion about missing bash environment\n- Improve error messages when an additional password was requested by the connection\n  when none was provided instead of just showing permission denied\n- Make SSH connection starting from a WSL environment use the native Windows key helper for FIDO2 keys\n- Rework insights button for connection creation across the board\n\n## Additions\n\n- Add warning message if git vault URL was an HTTP URL and you are trying to use an SSH identity\n- Add ability to clone existing connections to make the process of adding similar connections easier\n- Add ability to debug local background shell in developer options\n- Add notice when a professional feature is available in preview mode\n- Add some more OS logos\n- Add check to verify whether font loading with fontconfig works on Linux on startup\n- Add more extensive note on first startup for potential issues when Malwarebytes, McAfee, or Bitdefender are installed\n\n## Fixes\n\n- Fix application not starting on Asahi Linux due to executable page size issue\n- Fix file existence check for SSH key files reporting wrong results on Windows in directory links/junctions\n- Fix k8s integration not working when user did not have permission to list nodes\n- Fix rare error when switching to tray operation mode on Linux\n- Fix connection state not being preserved when being added the first time\n- Fix application failing to start up if OS reported invalid screen size bounds\n- Fix VMware VM not being able to be parsed if configuration file did not specify an encoding or name\n- Fix startup failing when installation was located on a ramdisk\n- Fix some miscellaneous cache data being stored in the user home directory\n- Fix error handling when jump host chain formed a loop\n- Fix PowerShell remote sessions being blocked by execution policy\n- Fix race condition when locking user data directory\n- Fix some CLI commands not starting daemon correctly if it is not already running\n- Fix text field when showing askpass window not being focused automatically\n- Fix combobox selections not working well with keyboard-only workflows\n- Fix many possible small NullPointerExceptions\n"
  },
  {
    "path": "dist/changelog/1.7.13.md",
    "content": "## Changes\n\n- You can now add SSH connections from arbitrary OpenSSH config files under `Add remote host` -> `SSH Config`\n- The SSH config importer now supports include statements. Included files are automatically resolved and joined\n- Add experimental ability to automatically fix SSH key file permissions on Windows if OpenSSH complains\n- Rework file browser connection history overview to always update when you close a tab\n- The Linux installers now correctly report their dependencies. This was not really a problem on any\n  normal desktop system, but should make it easier to run on embedded systems or in WSL2g\n- Improve performance mode speedup by removing more styling. You can enable the mode under Settings -> Appearance\n- Change layout of connection names and status to better handle very long connection names across all window sizes\n- Make any connection quickly renameable in the edit window without verifying whether we can actually connect\n- Allow for creation of multiple connections with the same name\n- Add a self test functionality on startup to handle cases where the local shell could not be initialized correctly\n- Implement fallback to bundled fonts on Linux systems that do not have fontconfig\n- There is now a repository for nixos releases at https://github.com/xpipe-io/nixpkg\n- Improve documentation for custom terminal command setting\n\n## Fixes\n\n- Fix some zsh shells not properly setting up the PATH\n- Fix git vault repository throwing initialization errors when shared with multiple Windows user permissions\n- Fix displayed connection summary not updating on edit\n- Fix copying and pasting a file into the same directory returning an error\n- Fix connections being accidentally listed under scripts category\n  if they were added while scripts category was selected\n- Fix default terminal detection sometimes selecting iTerm even though it was not installed\n- Fix shell environments for BSD bourne shell failing with invalid -l switch\n- Fix connections to pfSense systems not working\n"
  },
  {
    "path": "dist/changelog/1.7.14.md",
    "content": "This is just a small hotfix update to fix a few important issues:\n- Fix license validation throwing errors due to mismatched date format\n- Fix .deb installers not being able to resolve some packages on Ubuntu < 22\n- Fix command-line installation script on homepage not refreshing package repositories\n  on Linux if needed. It also now supports dnf, yum, and zypper as well\n"
  },
  {
    "path": "dist/changelog/1.7.15.md",
    "content": "## Changes\n\n- Add support to create customized SSH connections using arbitrary options.\n  This can be done using the SSH config format but without having to create an actual file.\n- Unify all SSH connection types to support the same functionality.\n  I.e. they all now support host key and identity file fixes plus can be used with SSH tunnels.\n- Make it possible to specify any identity to be used for SSH config connections\n- Properly detect when an active connection has unexpectedly reset during a file browser session.\n  It will now be automatically restarted when any action is performed and fails.\n- Rework connection creation menu layout to give a better overview\n- Make the connection timeout value in the settings properly apply to ssh connections as well.\n  This should help with SSH connections that take a long time to connect.\n- Include pre-rasterized images for various sizes to reduce the render load at runtime\n- Implement various performance improvements\n- Rework some UI elements to better work with keyboard navigation and screen readers\n- Add unsupported shell notice when restricted bash is detected\n- The daemon now properly reports any startup failure causes when started from the CLI via `xpipe open`\n- Regularly clean logs directory to free up older log files\n- Improve file browser handling in smaller window sizes\n- Add support for WezTerm and Windows Terminal Preview\n\n## Fixes\n\n- Fix application windows on Linux not initializing with the correct size\n- Fix connections to pfSense systems not working (This time properly)\n- Fix NullPointerException when a Linux system did not provide any release name\n- Fix startup errors when operating system reported invalid window sizes\n- Fix various Exceptions caused by race conditions\n"
  },
  {
    "path": "dist/changelog/1.7.16.md",
    "content": "\n## SSH Timeouts and connection time\n\nOver time, there have always been a few complaints about SSH connection timeout errors and slow SSH connection startup. These especially popped up in the latest release even though no obvious code was changed.\n\nAs it turns out, increasing the value for `ConnectTimeout` in SSH does not actually only change the timeout after which an error is thrown, it is also used by some servers as a guideline for their response time. E.g. if you specify a 10s timeout, some servers will always take 10s to respond. This is of course not mentioned in any of the spec but is more of an implementation choice.\n\nIn the latest release this caused more errors as the timeout was set higher. It should also have affected many SSH connections basically since the release of XPipe. I don't know how many people have been affected by this, it heavily depends on which ssh server and configuration your server runs. It happens for example on my proxmox instances and my AWS EC2 instances. If your connections now start up much faster than before, then you are probably affected by it.\n\nThis release should fix all of these issues simply by not specifying a connect timeout at all. Great work there. If you are using `ConnectTimeout` in your SSH configs, just remove it as it makes everything slower without having the effect of a timeout.\n\nI would like to exchange a few words with whoever thought: *A newly connected SSH client specified a 10s connect timeout? That means we can sit around idle for 9 seconds. That is a great idea.*\n\n## Fixes\n\n- Fix annoying log directory errors that occurred on first startup\n- Fix SSH connections failing on Windows systems where the username contained non-ASCII characters due to an OpenSSH client bug by working around it\n- Fix SSH connection failing when another RemoteCommand was set in a config file\n- Fix child connection validity not updating when parent is changed from invalid to valid\n- Fix some applications launched on Windows, e.g. some terminals and editors, starting in minimized mode\n- Fix SSH config importer not handling file wildcards correctly when they also contained a file extension\n- Fix actions that shut down XPipe, e.g. automatic updates and debug mode, not correctly executing if it exited too fast\n- Fix error about nonexistent logs directory on first startup\n- Fix possible NullPointers when checking whether current SSH session has died\n"
  },
  {
    "path": "dist/changelog/1.7.2.md",
    "content": "# Update procedure\n\nNote that the automatic updater is broken in version 1.6.0. It will freeze the application and not perform the update. **So do not try to click the install button in XPipe**!\nYou have to install it manually from https://github.com/xpipe-io/xpipe/releases/tag/1.7.2. You can easily do this as uninstalling the old version does not delete any user data. Installing a newer version of XPipe also automatically uninstalls any old ones, so you don't have to manually uninstall it.\n\n## Changes in 1.7.2\n\n### Bring your scripts with you\n\nThis update introduces a new toggle available for all scripts that if enabled, will automatically copy these scripts to the target system and put them into the PATH when launching a new terminal session. This allows you to easily call your scripts on any system without any setup.\n\n### Professional edition changes\n\nAfter taking feedback and examples of other applications into consideration, I restructured the professional edition pricing.\nThere is now the option for a one-time payment, which will give you permanent access to all current professional features plus all that are released in the next year.\nThis one-time payment also makes it possible accept a lot more payment methods than before.\n\n### Other changes\n\n- Fix refresh of connections leading to an inconsistent state and some connections not being displayed\n- The CI pipeline is now fully automated including ARM builds for Linux and macOS\n- Improve startup time on Linux and macOS by skipping tray initialization\n- Add support for tray icon on newer Gnome desktop environments\n- Add support qterminal, xterm, and deepin-terminal\n- Fix configs being unnecessarily saved even when not needed\n- Fix application not starting up on newer Gnome desktop environments\n- Fix killing of local unresponsive shell leading to further errors\n\n## Changes in 1.7.0\n\n### Scripts\n\nXPipe 1.7 comes with a new scripting system, you now can take your environment everywhere.\nThe idea is to create modular and reusable init scripts in XPipe that will be run on login but are independent of your profile files.\nYou can set certain scripts to be executed for every connection, allowing you to create a consistent environment across all remote systems.\n\nAs of now, there is only one set of scripts for enabling starship in your shell connections as a proof of concept.\nHowever, you can contribute custom scripts [here](https://github.com/xpipe-io/xpipe/tree/master/ext/base/src/main/java/io/xpipe/ext/base/script/PredefinedScriptStore.java) and [here](https://github.com/xpipe-io/xpipe/tree/master/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts).\n\n### Connection states\n\nThe second big change is a rework of the state system.\nThis merges the process of validating/refreshing with the process of establishing a connection, allowing for a much faster creation and launch of new connections.\nIt also enables a custom display and instant updates of the information displayed for a connection.\nYou will definitely notice this change when you connect to a system.\n\n### Performance improvements\n\nThe entire storage and UI handling of connections has been reworked to improve performance.\nEspecially if you're dealing with a large amount of connections, this will be noticeable.\n\n### Colors\n\nYou can now assign colors to connections for organizational purposes to help in situations when many connections are opened in the file browser and terminals at the same time.\nThese colors will be shown to identify tabs everywhere within XPipe and also outside of XPipe, for example in terminals.\n\n### Other changes\n\n- Codesign executables on Windows\n- Fix application not starting up or exiting properly sometimes\n- Add support for bsd-based systems\n- Fix OPNsense shells timing out\n- Make window transparency setting a slider\n- Save configuration data more frequently to avoid any data loss\n- Fix shutdown error caused by clipboard being inaccessible\n- Fix some environment scripts not being sourced correctly\n- Fix autoupdater not working properly\n- Fix application not exiting properly on SIGTERM\n- Many other small miscellaneous fixes and improvements"
  },
  {
    "path": "dist/changelog/1.7.3.md",
    "content": "## Changes in 1.7.3\n\n- Use newly created macOS app icons that better fit in with the general macOS aesthetic\n- Fix connection freezing when sudo askpass dialog was cancelled by now just executing commands as normal user\n- Fix Tabby installation not being detected on Windows if it was system-wide instead of per-user\n- Fix some settings values being incorrectly labelled as professional-only\n- Fix application restart for license activation not working\n- Fix open browser tab list being broken when reordering tabs by dragging\n- Fix browser welcome screen connection list jumping around\n- Fix connection sorting sometimes not working\n- Fix connections being duplicated in all connections overview\n- Fix move to category functionality being broken\n- Fix bring scripts functionality throwing errors on script setup\n- Fix scripts possibly being called multiple times when set as default and as dependencies\n- Improve scripts help documentation\n- Improve styling in some areas\n\n## Previous changes in 1.7\n\n- [https://github.com/xpipe-io/xpipe/releases/tag/1.7.2](https://github.com/xpipe-io/xpipe/releases/tag/1.7.2)\n- [https://github.com/xpipe-io/xpipe/releases/tag/1.7.1](https://github.com/xpipe-io/xpipe/releases/tag/1.7.1)\n\n## Other changes\n\nThe entire website at [https://xpipe.io](https://xpipe.io) has been redone, so you can check it out if you want and share your feedback on it.\nFurthermore, XPipe is now also available in the [Microsoft Store](https://apps.microsoft.com/detail/xpipe/XP9KK2PJ9JDQ6G)"
  },
  {
    "path": "dist/changelog/1.7.4.md",
    "content": "## Changes in 1.7.4\n\n### VMware support for desktop hypervisors\n\nThis update introduces an experimental implementation to support VMware virtual machines in VMware Player, Workstation, and Fusion installations.\nThe support includes actions like listing, starting, stopping, and pausing VMs plus opening a shell session or file browser session via SSH.\n\nNote that the initial connection to a VM, which runs some setup, can take a long time.\nIt seems like the VMware CLI it is very slow in that regard, maybe I can find some improvements.\n\nIf everything works out well with this first attempt at VM support, it can be expanded to other hypervisors.\n\n### Git storage for everyone\n\nUp until now, the git storage functionality has only been available with a professional license.\nHowever, due to the complex nature of git repositories, this feature had some inevitable rough edges\nand did not live up to the robustness of a professional product.\n\nAs a result, I am moving this feature into the community edition.\n\n### UI rework\n\nSome parts of the UI have been reworked to achieve a more consistent appearance.\nFurthermore, it has also been improved in regard to accessibility and its interaction with screen readers. \n\n### Other changes\n\n- The left sidebars in the connection overview and browser can now be persistently resized\n- Implement various performance improvements\n- When dragging files straight out of the browser, they now can also resolve to text output.\n  You can therefore now drag files into a terminal to quickly paste their file names for example.\n- Rework connection creation to automatically preselect most commonly used type\n- Fix browser exit race conditions\n- Fix application not starting up when settings file was corrupted\n- Fix connection getting stuck when shell did not support stderr. It will now just stop after a few seconds\n- Fix application not starting up on Windows systems older than Windows 10\n- Fix negative process exit codes being interpreted as internal errors and not shown\n\n## Previous changes in 1.7\n\n- [1.7.3](https://github.com/xpipe-io/xpipe/releases/tag/1.7.3)\n- [1.7.2](https://github.com/xpipe-io/xpipe/releases/tag/1.7.2)\n- [1.7.1](https://github.com/xpipe-io/xpipe/releases/tag/1.7.1)\n"
  },
  {
    "path": "dist/changelog/1.7.5.md",
    "content": "## Changes in 1.7.5\n\n- Implement some more performance improvements\n- Fix file browser transfer freezing up when trying to copy/move nested directories\n- Fix file browser transfer failing when trying to copy symbolic links\n- Fix file browser jittering when dragging and dropping files\n- Fix performance regression when transferring large files\n\n## Previous changes in 1.7\n\n- [1.7.4](https://github.com/xpipe-io/xpipe/releases/tag/1.7.4)\n- [1.7.3](https://github.com/xpipe-io/xpipe/releases/tag/1.7.3)\n- [1.7.2](https://github.com/xpipe-io/xpipe/releases/tag/1.7.2)\n- [1.7.1](https://github.com/xpipe-io/xpipe/releases/tag/1.7.1)\n"
  },
  {
    "path": "dist/changelog/1.7.6.md",
    "content": "## Changes in 1.7.6\n\n- Fix file transfer being broken in 1.7.5 on Linux and macOS due native stream implementation differences\n- Fix loading state not graying out background when creating new connections\n- Improved connection creation navigation by automatically shifting focus to the item when a choice is selected in a dropdown\n\n## Previous changes in 1.7\n\n- [1.7.5](https://github.com/xpipe-io/xpipe/releases/tag/1.7.5)\n- [1.7.4](https://github.com/xpipe-io/xpipe/releases/tag/1.7.4)\n- [1.7.3](https://github.com/xpipe-io/xpipe/releases/tag/1.7.3)\n- [1.7.2](https://github.com/xpipe-io/xpipe/releases/tag/1.7.2)\n- [1.7.1](https://github.com/xpipe-io/xpipe/releases/tag/1.7.1)\n"
  },
  {
    "path": "dist/changelog/1.7.7.md",
    "content": "## Changes in 1.7.7\n\n- More performance improvements\n- Fix file browser navbar commands failing. Since no one has reported this yet,\n  I assume that most people don't know that you can run commands and shells if you type them into the file browser navigation bar\n- Fix file browser icons being broken since 1.7.4\n- Fix connection list updates sometimes not being reflected in file browser connection list\n- Fix WSL integration not working when system language was not set to english\n  due to missing command-line options in the non-english WSL CLI\n- Fix application not working on Windows systems where the system code page did support all characters in username\n- Fix exit code not being detected and causing a failure on Windows under certain conditions\n- Fix file browser getting stuck in an invalid state when maximum file display limit was reached\n- Fix file browser transfer into macOS finder not updating state and causing errors\n\n## Previous changes in 1.7\n\n- [1.7.6](https://github.com/xpipe-io/xpipe/releases/tag/1.7.6)\n- [1.7.5](https://github.com/xpipe-io/xpipe/releases/tag/1.7.5)\n- [1.7.4](https://github.com/xpipe-io/xpipe/releases/tag/1.7.4)\n- [1.7.3](https://github.com/xpipe-io/xpipe/releases/tag/1.7.3)\n- [1.7.2](https://github.com/xpipe-io/xpipe/releases/tag/1.7.2)\n- [1.7.1](https://github.com/xpipe-io/xpipe/releases/tag/1.7.1)\n"
  },
  {
    "path": "dist/changelog/1.7.8.md",
    "content": "## Changes in 1.7.8\n\n- Make created scripts fully apply to file browser sessions as well.\n  Any environment changes will apply to the whole file browser, plus all selected\n  terminals scripts will be executed whenever you open a terminal session into a directory.\n- More startup performance improvements\n- Fix dialog window order sometimes being wrong and shown behind main window\n- Fix macOS Terminal.app sometimes not launching connection due to a race condition\n- Many other small miscellaneous fixes and improvements\n\n## Previous changes in 1.7\n\n- [1.7.7](https://github.com/xpipe-io/xpipe/releases/tag/1.7.7)\n- [1.7.6](https://github.com/xpipe-io/xpipe/releases/tag/1.7.6)\n- [1.7.5](https://github.com/xpipe-io/xpipe/releases/tag/1.7.5)\n- [1.7.4](https://github.com/xpipe-io/xpipe/releases/tag/1.7.4)\n- [1.7.3](https://github.com/xpipe-io/xpipe/releases/tag/1.7.3)\n- [1.7.2](https://github.com/xpipe-io/xpipe/releases/tag/1.7.2)\n- [1.7.1](https://github.com/xpipe-io/xpipe/releases/tag/1.7.1)\n"
  },
  {
    "path": "dist/changelog/1.7.9.md",
    "content": "### Git storage rework\n\nThe git storage functionality has been in a bad state, hopefully this update will change that.\nFirst of all, several bugs and inconsistencies have been fixed.\n\nFurthermore, the authentication options have been greatly expanded.\nYou can now supply both HTTP and SSH git URLs. If any input is required like a username/password/passphrase, XPipe will show a prompt.\nIf you chose to use an SSH git URL, you can also set key-based authentication options just as for other ssh connections.\n\nLastly, there is now a general data directory as well in which you can put any additional files like SSH keys that you want to include in the repository. You can then refer to them just as normal within XPipe but their file paths are automatically adapted on any system you clone the repository to. You can open this data directory from the settings menu.\n\nIt is recommended to start with a remote git repository from scratch though to properly fix previous issues.\n\n### Other changes\n\n- Fix new daemon instances getting stuck when trying to communicate with an already running daemon that does not respond\n- Fix daemon not properly exiting when stuck on shutdown\n- Fix elevated commands getting stuck if no sudo password was available\n- Fix some commands getting stuck under rare conditions on Linux and macOS\n- Fix some windows being shown outside of screen bounds when display scaling values were set very high\n- Fix macOS .pkg installers requiring Rosetta to be installed on ARM even though it wasn't needed\n- Fix powerlevel10k breaking terminal integrations on macOS\n- Fix screen clear on terminal launch sometimes breaking terminal formatting\n- Fix process exit codes outside of signed 32-bit int causing errors\n- Fix local shell process not restarting if it somehow died\n- Fix errors not showing when GUI could not be initialized\n- Fix some NullPointerExceptions\n- Fix file browser execute action not launching scripts\n- Fix some license related errors\n- Fix Windows os detection on remote servers if cmd was not available\n- Many other small miscellaneous fixes and improvements\n\nPlease make sure to report any issue you can find. This helps the development a lot.\n"
  },
  {
    "path": "dist/changelog/10.0.1_incremental.md",
    "content": "- Fix arm64 releases not being built for Linux\n- Fix /shell/exec API endpoint not returning proper error when command timed out\n- Fix docker context elevation failing when docker daemon responded slowly\n- Fix files not being able to renamed with only case differences on Windows\n- Fix some keys not working when renaming files\n- Fix NullPointer on Windows when window was initialized too early\n- Fix NullPointer when clearing markdown notes\n"
  },
  {
    "path": "dist/changelog/10.0.2_incremental.md",
    "content": "- Fix mismatched input exception when migrating old existing SSH config connections\n- Fix connection entries sometimes jumping around in the ordering when refreshing them\n- Fix some rare NullPointers\n- Fix file browser resize messing up column widths\n- Fix AppImages not being built for arm64\n- Fix kitty terminal not launching on Linux if it responded slowly\n"
  },
  {
    "path": "dist/changelog/10.0.3_incremental.md",
    "content": "- Fix application windows being blank on some Windows 10 systems\n- Fix services for docker and podman containers not being expanded automatically\n- Fix some NullPointers\n"
  },
  {
    "path": "dist/changelog/10.0.4_incremental.md",
    "content": "- Fix application windows being blank on some Windows 11 systems with certain GPUs\n- Fix some NullPointers for postgres connections\n- Fix some missing translations for postgres connections\n"
  },
  {
    "path": "dist/changelog/10.0.md",
    "content": "## A new HTTP API\n\nThere is now a new HTTP API for the XPipe daemon, which allows you to programmatically manage remote systems. You can find details and an OpenAPI specification at the new API button in the sidebar. The API page contains everything you need to get started, including code samples for various different programming languages.\n\nTo start off, you can query connections based on various filters. With the matched connections, you can start remote shell sessions for each one and run arbitrary commands in them. You get the command exit code and output as a response, allowing you to adapt your control flow based on command outputs. Any kind of passwords and other secrets are automatically provided by XPipe when establishing a shell connection. You can also access the file systems via these shell connections to read and write remote files.\n\nThere already exists a community made [XPipe API library for python](https://github.com/coandco/python_xpipe_client) and a [python CLI client](https://github.com/coandco/xpipe_cli). These tools allow you to interact with the API more ergonomically and can also serve as an inspiration of what you can do with the new API. If you also built a tool to interact with the XPipe API, you can let me know and I can compile a list of community development projects.\n\n## Service integration\n\nMany systems run a variety of different services such as web services and others. There is now support to detect, forward, and open the services. For example, if you are running a web service on a remote container, you can automatically forward the service port via SSH tunnels, allowing you to access these services from your local machine, e.g. in a web browser. These service tunnels can be toggled at any time. The port forwarding supports specifying a custom local target port and also works for connections with multiple intermediate systems through chained tunnels. For containers, services are automatically detected via their exposed mapped ports. For other systems, you can manually add services via their port.\n\nYou can use an unlimited amount of local services and one active tunneled service in the community edition.\n\n## Script rework\n\nThe scripting system has been reworked. There have been several issues with it being clunky and not fun to use. The new system allows you to assign each script one of multiple execution types. Based on these execution types, you can make scripts active or inactive with a toggle. If they are active, the scripts will apply in the selected use cases. There currently are these types:\n- Init scripts: When enabled, they will automatically run on init in all compatible shells. This is useful for setting things like aliases consistently\n- Shell scripts: When enabled, they will be copied over to the target system and put into the PATH. You can then call them in a normal shell session by their name, e.g. `myscript.sh`, also with arguments.\n- File scripts: When enabled, you can call them in the file browser with the selected files as arguments. Useful to perform common actions with files\n\nIf you have existing scripts, they will have to be manually adjusted by setting their execution types.\n\n## Docker improvements\n\nThe docker integration has been updated to support docker contexts. You can use the default context in the community edition, essentially being the same as before as XPipe previously only used the default context. Support for using multiple contexts is included in the professional edition.\n\nThere's now support for Windows docker containers running on HyperV.\n\nNote that old docker container connections will be removed as they are incompatible with the new version.\n\n## Proxmox improvements\n\nYou can now automatically open the Proxmox dashboard website through the new service integration. This will also work with the service tunneling feature for remote servers.\n\nYou can now open VNC sessions to Proxmox VMs.\n\nThe Proxmox professional license requirement has been reworked to support one non-enterprise PVE node in the community edition.\n\n## Better connection organization\n\nThe toggle to show only running connections will now no longer actually remove the connections internally and instead just not display them. This will reduce git vault updates and is faster in general.\n\nYou can now order connections relative to other sibling connections. This ordering will also persist when changing the global order in the top left.\n\nThe UI has also been streamlined to make common actions and toggles more easily accessible.\n\n## Other\n\n- The title bar on Windows will now follow the appearance theme\n- Several more actions have been added for podman containers\n- Support VMs for tunneling\n- Searching for connections has been improved to show children as well\n- There is now an AppImage portable release\n- The welcome screen will now also contain the option to straight up jump to the synchronization settings\n- You can now launch xpipe in another data directory with `xpipe open -d \"<dir>\"`\n- Add option to use double clicks to open connections instead of single clicks\n- Add support for foot terminal\n- Fix rare null pointers and freezes in file browser\n- Fix PowerShell remote session file editing not transferring file correctly\n- Fix elementary terminal not launching correctly\n- Fix windows jumping around when created\n- Fix kubernetes not elevating correctly for non-default contexts\n- Fix ohmyzsh update notification freezing shell\n- Fix file browser icons being broken for links\n- The Linux installers now contain application icons from multiple sizes which should increase the icon display quality\n- The Linux builds now list socat as a dependency such that the kitty terminal integration will work without issues\n"
  },
  {
    "path": "dist/changelog/10.1.1.md",
    "content": "## A new HTTP API\n\nThere is now a new HTTP API for the XPipe daemon, which allows you to programmatically manage remote systems. You can find details and an OpenAPI specification at the new API button in the sidebar. The API page contains everything you need to get started, including code samples for various different programming languages.\n\nTo start off, you can query connections based on various filters. With the matched connections, you can start remote shell sessions for each one and run arbitrary commands in them. You get the command exit code and output as a response, allowing you to adapt your control flow based on command outputs. Any kind of passwords and other secrets are automatically provided by XPipe when establishing a shell connection. You can also access the file systems via these shell connections to read and write remote files.\n\nThere already exists a community made [XPipe API library for python](https://github.com/coandco/python_xpipe_client) and a [python CLI client](https://github.com/coandco/xpipe_cli). These tools allow you to interact with the API more ergonomically and can also serve as an inspiration of what you can do with the new API. If you also built a tool to interact with the XPipe API, you can let me know and I can compile a list of community development projects.\n\n## Service integration\n\nMany systems run a variety of different services such as web services and others. There is now support to detect, forward, and open the services. For example, if you are running a web service on a remote container, you can automatically forward the service port via SSH tunnels, allowing you to access these services from your local machine, e.g. in a web browser. These service tunnels can be toggled at any time. The port forwarding supports specifying a custom local target port and also works for connections with multiple intermediate systems through chained tunnels. For containers, services are automatically detected via their exposed mapped ports. For other systems, you can manually add services via their port.\n\nYou can use an unlimited amount of local services and one active tunneled service in the community edition.\n\n## Script rework\n\nThe scripting system has been reworked. There have been several issues with it being clunky and not fun to use. The new system allows you to assign each script one of multiple execution types. Based on these execution types, you can make scripts active or inactive with a toggle. If they are active, the scripts will apply in the selected use cases. There currently are these types:\n- Init scripts: When enabled, they will automatically run on init in all compatible shells. This is useful for setting things like aliases consistently\n- Shell scripts: When enabled, they will be copied over to the target system and put into the PATH. You can then call them in a normal shell session by their name, e.g. `myscript.sh`, also with arguments.\n- File scripts: When enabled, you can call them in the file browser with the selected files as arguments. Useful to perform common actions with files\n\nIf you have existing scripts, they will have to be manually adjusted by setting their execution types.\n\n## Docker improvements\n\nThe docker integration has been updated to support docker contexts. You can use the default context in the community edition, essentially being the same as before as XPipe previously only used the default context. Support for using multiple contexts is included in the professional edition.\n\nThere's now support for Windows docker containers running on HyperV.\n\nNote that old docker container connections will be removed as they are incompatible with the new version.\n\n## Proxmox improvements\n\nYou can now automatically open the Proxmox dashboard website through the new service integration. This will also work with the service tunneling feature for remote servers.\n\nYou can now open VNC sessions to Proxmox VMs.\n\nThe Proxmox professional license requirement has been reworked to support one non-enterprise PVE node in the community edition.\n\n## Better connection organization\n\nThe toggle to show only running connections will now no longer actually remove the connections internally and instead just not display them. This will reduce git vault updates and is faster in general.\n\nYou can now order connections relative to other sibling connections. This ordering will also persist when changing the global order in the top left.\n\nThe UI has also been streamlined to make common actions and toggles more easily accessible.\n\n## Other\n\n- The title bar on Windows will now follow the appearance theme\n- Several more actions have been added for podman containers\n- Support VMs for tunneling\n- Searching for connections has been improved to show children as well\n- There is now an AppImage portable release\n- The welcome screen will now also contain the option to straight up jump to the synchronization settings\n- You can now launch xpipe in another data directory with `xpipe open -d \"<dir>\"`\n- Add option to use double clicks to open connections instead of single clicks\n- Add support for foot terminal\n- Fix rare null pointers and freezes in file browser\n- Fix PowerShell remote session file editing not transferring file correctly\n- Fix elementary terminal not launching correctly\n- Fix windows jumping around when created\n- Fix kubernetes not elevating correctly for non-default contexts\n- Fix ohmyzsh update notification freezing shell\n- Fix file browser icons being broken for links\n- The Linux installers now contain application icons from multiple sizes which should increase the icon display quality\n- The Linux builds now list socat as a dependency such that the kitty terminal integration will work without issues\n"
  },
  {
    "path": "dist/changelog/10.1.1_incremental.md",
    "content": "- Fix terminal window closing instantly if connection failed, not showing error messages\n- Fix file browser editor sometimes not applying changes\n- Fix updater not doing anything when trying to install an update when downloaded installer had been deleted on a restart\n- Fix xpipe CLI executable missing signature on Windows\n- Fix various smaller bugs"
  },
  {
    "path": "dist/changelog/10.1.md",
    "content": "## A new HTTP API\n\nThere is now a new HTTP API for the XPipe daemon, which allows you to programmatically manage remote systems. You can find details and an OpenAPI specification at the new API button in the sidebar. The API page contains everything you need to get started, including code samples for various different programming languages.\n\nTo start off, you can query connections based on various filters. With the matched connections, you can start remote shell sessions for each one and run arbitrary commands in them. You get the command exit code and output as a response, allowing you to adapt your control flow based on command outputs. Any kind of passwords and other secrets are automatically provided by XPipe when establishing a shell connection. You can also access the file systems via these shell connections to read and write remote files.\n\nThere already exists a community made [XPipe API library for python](https://github.com/coandco/python_xpipe_client) and a [python CLI client](https://github.com/coandco/xpipe_cli). These tools allow you to interact with the API more ergonomically and can also serve as an inspiration of what you can do with the new API. If you also built a tool to interact with the XPipe API, you can let me know and I can compile a list of community development projects.\n\n## Service integration\n\nMany systems run a variety of different services such as web services and others. There is now support to detect, forward, and open the services. For example, if you are running a web service on a remote container, you can automatically forward the service port via SSH tunnels, allowing you to access these services from your local machine, e.g. in a web browser. These service tunnels can be toggled at any time. The port forwarding supports specifying a custom local target port and also works for connections with multiple intermediate systems through chained tunnels. For containers, services are automatically detected via their exposed mapped ports. For other systems, you can manually add services via their port.\n\nYou can use an unlimited amount of local services and one active tunneled service in the community edition.\n\n## Script rework\n\nThe scripting system has been reworked. There have been several issues with it being clunky and not fun to use. The new system allows you to assign each script one of multiple execution types. Based on these execution types, you can make scripts active or inactive with a toggle. If they are active, the scripts will apply in the selected use cases. There currently are these types:\n- Init scripts: When enabled, they will automatically run on init in all compatible shells. This is useful for setting things like aliases consistently\n- Shell scripts: When enabled, they will be copied over to the target system and put into the PATH. You can then call them in a normal shell session by their name, e.g. `myscript.sh`, also with arguments.\n- File scripts: When enabled, you can call them in the file browser with the selected files as arguments. Useful to perform common actions with files\n\nIf you have existing scripts, they will have to be manually adjusted by setting their execution types.\n\n## Docker improvements\n\nThe docker integration has been updated to support docker contexts. You can use the default context in the community edition, essentially being the same as before as XPipe previously only used the default context. Support for using multiple contexts is included in the professional edition.\n\nThere's now support for Windows docker containers running on HyperV.\n\nNote that old docker container connections will be removed as they are incompatible with the new version.\n\n## Proxmox improvements\n\nYou can now automatically open the Proxmox dashboard website through the new service integration. This will also work with the service tunneling feature for remote servers.\n\nYou can now open VNC sessions to Proxmox VMs.\n\nThe Proxmox professional license requirement has been reworked to support one non-enterprise PVE node in the community edition.\n\n## Better connection organization\n\nThe toggle to show only running connections will now no longer actually remove the connections internally and instead just not display them. This will reduce git vault updates and is faster in general.\n\nYou can now order connections relative to other sibling connections. This ordering will also persist when changing the global order in the top left.\n\nThe UI has also been streamlined to make common actions and toggles more easily accessible.\n\n## Other\n\n- The title bar on Windows will now follow the appearance theme\n- Several more actions have been added for podman containers\n- Support VMs for tunneling\n- Searching for connections has been improved to show children as well\n- There is now an AppImage portable release\n- The welcome screen will now also contain the option to straight up jump to the synchronization settings\n- You can now launch xpipe in another data directory with `xpipe open -d \"<dir>\"`\n- Add option to use double clicks to open connections instead of single clicks\n- Add support for foot terminal\n- Fix rare null pointers and freezes in file browser\n- Fix PowerShell remote session file editing not transferring file correctly\n- Fix elementary terminal not launching correctly\n- Fix windows jumping around when created\n- Fix kubernetes not elevating correctly for non-default contexts\n- Fix ohmyzsh update notification freezing shell\n- Fix file browser icons being broken for links\n- The Linux installers now contain application icons from multiple sizes which should increase the icon display quality\n- The Linux builds now list socat as a dependency such that the kitty terminal integration will work without issues\n"
  },
  {
    "path": "dist/changelog/10.1_incremental.md",
    "content": "## Browser improvements\n\nFeedback showed that the file browser transfer pane in the bottom left was confusing and unintuitive to use. Therefore, it has now been changed to be a more straightforward download area. You can drag files into it to automatically download them. From there you can either drag them directly where you want them to be in your local desktop environment or move them into the downloads directory.\n\nThere is now the possibility to jump to a file in a directory by typing the first few characters of its name.\n\nThere were also a couple of bug fixes:\n- Fix file transfers on Windows systems failing for files > 2GB due to overflow\n- Fix remote file editing sometimes creating blank file when using vscode\n- Fix file transfers failing at the end with a timeout when the connection speed was very slow\n\n## API additions\n\nSeveral new endpoints have been added to widen the capabilities for external clients:\n\n- Add /connection/add endpoint to allow creating connections from the API\n- Add /connection/remove endpoint to allow removing existing connections from the API\n- Add /connection/browse endpoint to open connections in the file browser\n- Add /connection/terminal endpoint to open a terminal session four of connection\n- Add /connection/toggle endpoint to enable or disable connections such as tunnels and service forwards\n- Add /connection/refresh endpoint to refresh a connection state and its children\n\n## Other\n\n- Fix xpipe not starting up when changing user on Linux\n- Fix some editors and terminals not launching when using the fallback sh system shell due to missing disown command\n- Fix csh sudo elevation not working\n- Implement various application performance improvements\n- Rework sidebar styling\n- Improve transparency styling on Windows 11\n- Add support for zed editor"
  },
  {
    "path": "dist/changelog/10.2.1.md",
    "content": "## A new HTTP API\n\nThere is now a new HTTP API for the XPipe daemon, which allows you to programmatically manage remote systems. You can find details and an OpenAPI specification at the new API button in the sidebar. The API page contains everything you need to get started, including code samples for various different programming languages.\n\nTo start off, you can query connections based on various filters. With the matched connections, you can start remote shell sessions for each one and run arbitrary commands in them. You get the command exit code and output as a response, allowing you to adapt your control flow based on command outputs. Any kind of passwords and other secrets are automatically provided by XPipe when establishing a shell connection. You can also access the file systems via these shell connections to read and write remote files.\n\nThere already exists a community made [XPipe API library for python](https://github.com/coandco/python_xpipe_client) and a [python CLI client](https://github.com/coandco/xpipe_cli). These tools allow you to interact with the API more ergonomically and can also serve as an inspiration of what you can do with the new API. If you also built a tool to interact with the XPipe API, you can let me know and I can compile a list of community development projects.\n\n## Service integration\n\nMany systems run a variety of different services such as web services and others. There is now support to detect, forward, and open the services. For example, if you are running a web service on a remote container, you can automatically forward the service port via SSH tunnels, allowing you to access these services from your local machine, e.g. in a web browser. These service tunnels can be toggled at any time. The port forwarding supports specifying a custom local target port and also works for connections with multiple intermediate systems through chained tunnels. For containers, services are automatically detected via their exposed mapped ports. For other systems, you can manually add services via their port.\n\nYou can use an unlimited amount of local services and one active tunneled service in the community edition.\n\n## Script rework\n\nThe scripting system has been reworked. There have been several issues with it being clunky and not fun to use. The new system allows you to assign each script one of multiple execution types. Based on these execution types, you can make scripts active or inactive with a toggle. If they are active, the scripts will apply in the selected use cases. There currently are these types:\n- Init scripts: When enabled, they will automatically run on init in all compatible shells. This is useful for setting things like aliases consistently\n- Shell scripts: When enabled, they will be copied over to the target system and put into the PATH. You can then call them in a normal shell session by their name, e.g. `myscript.sh`, also with arguments.\n- File scripts: When enabled, you can call them in the file browser with the selected files as arguments. Useful to perform common actions with files\n\nIf you have existing scripts, they will have to be manually adjusted by setting their execution types.\n\n## Docker improvements\n\nThe docker integration has been updated to support docker contexts. You can use the default context in the community edition, essentially being the same as before as XPipe previously only used the default context. Support for using multiple contexts is included in the professional edition.\n\nThere's now support for Windows docker containers running on HyperV.\n\nNote that old docker container connections will be removed as they are incompatible with the new version.\n\n## Proxmox improvements\n\nYou can now automatically open the Proxmox dashboard website through the new service integration. This will also work with the service tunneling feature for remote servers.\n\nYou can now open VNC sessions to Proxmox VMs.\n\nThe Proxmox professional license requirement has been reworked to support one non-enterprise PVE node in the community edition.\n\n## Better connection organization\n\nThe toggle to show only running connections will now no longer actually remove the connections internally and instead just not display them. This will reduce git vault updates and is faster in general.\n\nYou can now order connections relative to other sibling connections. This ordering will also persist when changing the global order in the top left.\n\nThe UI has also been streamlined to make common actions and toggles more easily accessible.\n\n## Other\n\n- The title bar on Windows will now follow the appearance theme\n- Several more actions have been added for podman containers\n- Support VMs for tunneling\n- Searching for connections has been improved to show children as well\n- There is now an AppImage portable release\n- The welcome screen will now also contain the option to straight up jump to the synchronization settings\n- You can now launch xpipe in another data directory with `xpipe open -d \"<dir>\"`\n- Add option to use double clicks to open connections instead of single clicks\n- Add support for foot terminal\n- Fix rare null pointers and freezes in file browser\n- Fix PowerShell remote session file editing not transferring file correctly\n- Fix elementary terminal not launching correctly\n- Fix windows jumping around when created\n- Fix kubernetes not elevating correctly for non-default contexts\n- Fix ohmyzsh update notification freezing shell\n- Fix file browser icons being broken for links\n- The Linux installers now contain application icons from multiple sizes which should increase the icon display quality\n- The Linux builds now list socat as a dependency such that the kitty terminal integration will work without issues\n"
  },
  {
    "path": "dist/changelog/10.2.1_incremental.md",
    "content": "- Fix startup issue on older x86_64 macOS systems\n"
  },
  {
    "path": "dist/changelog/10.2.2.md",
    "content": "## A new HTTP API\n\nThere is now a new HTTP API for the XPipe daemon, which allows you to programmatically manage remote systems. You can find details and an OpenAPI specification at the new API button in the sidebar. The API page contains everything you need to get started, including code samples for various different programming languages.\n\nTo start off, you can query connections based on various filters. With the matched connections, you can start remote shell sessions for each one and run arbitrary commands in them. You get the command exit code and output as a response, allowing you to adapt your control flow based on command outputs. Any kind of passwords and other secrets are automatically provided by XPipe when establishing a shell connection. You can also access the file systems via these shell connections to read and write remote files.\n\nThere already exists a community made [XPipe API library for python](https://github.com/coandco/python_xpipe_client) and a [python CLI client](https://github.com/coandco/xpipe_cli). These tools allow you to interact with the API more ergonomically and can also serve as an inspiration of what you can do with the new API. If you also built a tool to interact with the XPipe API, you can let me know and I can compile a list of community development projects.\n\n## Service integration\n\nMany systems run a variety of different services such as web services and others. There is now support to detect, forward, and open the services. For example, if you are running a web service on a remote container, you can automatically forward the service port via SSH tunnels, allowing you to access these services from your local machine, e.g. in a web browser. These service tunnels can be toggled at any time. The port forwarding supports specifying a custom local target port and also works for connections with multiple intermediate systems through chained tunnels. For containers, services are automatically detected via their exposed mapped ports. For other systems, you can manually add services via their port.\n\nYou can use an unlimited amount of local services and one active tunneled service in the community edition.\n\n## Script rework\n\nThe scripting system has been reworked. There have been several issues with it being clunky and not fun to use. The new system allows you to assign each script one of multiple execution types. Based on these execution types, you can make scripts active or inactive with a toggle. If they are active, the scripts will apply in the selected use cases. There currently are these types:\n- Init scripts: When enabled, they will automatically run on init in all compatible shells. This is useful for setting things like aliases consistently\n- Shell scripts: When enabled, they will be copied over to the target system and put into the PATH. You can then call them in a normal shell session by their name, e.g. `myscript.sh`, also with arguments.\n- File scripts: When enabled, you can call them in the file browser with the selected files as arguments. Useful to perform common actions with files\n\nIf you have existing scripts, they will have to be manually adjusted by setting their execution types.\n\n## Docker improvements\n\nThe docker integration has been updated to support docker contexts. You can use the default context in the community edition, essentially being the same as before as XPipe previously only used the default context. Support for using multiple contexts is included in the professional edition.\n\nThere's now support for Windows docker containers running on HyperV.\n\nNote that old docker container connections will be removed as they are incompatible with the new version.\n\n## Proxmox improvements\n\nYou can now automatically open the Proxmox dashboard website through the new service integration. This will also work with the service tunneling feature for remote servers.\n\nYou can now open VNC sessions to Proxmox VMs.\n\nThe Proxmox professional license requirement has been reworked to support one non-enterprise PVE node in the community edition.\n\n## Better connection organization\n\nThe toggle to show only running connections will now no longer actually remove the connections internally and instead just not display them. This will reduce git vault updates and is faster in general.\n\nYou can now order connections relative to other sibling connections. This ordering will also persist when changing the global order in the top left.\n\nThe UI has also been streamlined to make common actions and toggles more easily accessible.\n\n## Other\n\n- The title bar on Windows will now follow the appearance theme\n- Several more actions have been added for podman containers\n- Support VMs for tunneling\n- Searching for connections has been improved to show children as well\n- There is now an AppImage portable release\n- The welcome screen will now also contain the option to straight up jump to the synchronization settings\n- You can now launch xpipe in another data directory with `xpipe open -d \"<dir>\"`\n- Add option to use double clicks to open connections instead of single clicks\n- Add support for foot terminal\n- Fix rare null pointers and freezes in file browser\n- Fix PowerShell remote session file editing not transferring file correctly\n- Fix elementary terminal not launching correctly\n- Fix windows jumping around when created\n- Fix kubernetes not elevating correctly for non-default contexts\n- Fix ohmyzsh update notification freezing shell\n- Fix file browser icons being broken for links\n- The Linux installers now contain application icons from multiple sizes which should increase the icon display quality\n- The Linux builds now list socat as a dependency such that the kitty terminal integration will work without issues\n"
  },
  {
    "path": "dist/changelog/10.2.2_incremental.md",
    "content": "- Fix Windows installers producing SmartScreen warning\n- Fix setting to use double clicks when launching connections not working\n- Fix potential stack overflow when opening VNC connections\n- Fix file browser shortcuts conflicting with others and intercepting others\n- Fix some broken keyboard shortcuts\n- Fix certain special character key combinations being wrongfully intercepted by window, leading to a window close when typing @ on some european keyboards\n"
  },
  {
    "path": "dist/changelog/10.2.md",
    "content": "## A new HTTP API\n\nThere is now a new HTTP API for the XPipe daemon, which allows you to programmatically manage remote systems. You can find details and an OpenAPI specification at the new API button in the sidebar. The API page contains everything you need to get started, including code samples for various different programming languages.\n\nTo start off, you can query connections based on various filters. With the matched connections, you can start remote shell sessions for each one and run arbitrary commands in them. You get the command exit code and output as a response, allowing you to adapt your control flow based on command outputs. Any kind of passwords and other secrets are automatically provided by XPipe when establishing a shell connection. You can also access the file systems via these shell connections to read and write remote files.\n\nThere already exists a community made [XPipe API library for python](https://github.com/coandco/python_xpipe_client) and a [python CLI client](https://github.com/coandco/xpipe_cli). These tools allow you to interact with the API more ergonomically and can also serve as an inspiration of what you can do with the new API. If you also built a tool to interact with the XPipe API, you can let me know and I can compile a list of community development projects.\n\n## Service integration\n\nMany systems run a variety of different services such as web services and others. There is now support to detect, forward, and open the services. For example, if you are running a web service on a remote container, you can automatically forward the service port via SSH tunnels, allowing you to access these services from your local machine, e.g. in a web browser. These service tunnels can be toggled at any time. The port forwarding supports specifying a custom local target port and also works for connections with multiple intermediate systems through chained tunnels. For containers, services are automatically detected via their exposed mapped ports. For other systems, you can manually add services via their port.\n\nYou can use an unlimited amount of local services and one active tunneled service in the community edition.\n\n## Script rework\n\nThe scripting system has been reworked. There have been several issues with it being clunky and not fun to use. The new system allows you to assign each script one of multiple execution types. Based on these execution types, you can make scripts active or inactive with a toggle. If they are active, the scripts will apply in the selected use cases. There currently are these types:\n- Init scripts: When enabled, they will automatically run on init in all compatible shells. This is useful for setting things like aliases consistently\n- Shell scripts: When enabled, they will be copied over to the target system and put into the PATH. You can then call them in a normal shell session by their name, e.g. `myscript.sh`, also with arguments.\n- File scripts: When enabled, you can call them in the file browser with the selected files as arguments. Useful to perform common actions with files\n\nIf you have existing scripts, they will have to be manually adjusted by setting their execution types.\n\n## Docker improvements\n\nThe docker integration has been updated to support docker contexts. You can use the default context in the community edition, essentially being the same as before as XPipe previously only used the default context. Support for using multiple contexts is included in the professional edition.\n\nThere's now support for Windows docker containers running on HyperV.\n\nNote that old docker container connections will be removed as they are incompatible with the new version.\n\n## Proxmox improvements\n\nYou can now automatically open the Proxmox dashboard website through the new service integration. This will also work with the service tunneling feature for remote servers.\n\nYou can now open VNC sessions to Proxmox VMs.\n\nThe Proxmox professional license requirement has been reworked to support one non-enterprise PVE node in the community edition.\n\n## Better connection organization\n\nThe toggle to show only running connections will now no longer actually remove the connections internally and instead just not display them. This will reduce git vault updates and is faster in general.\n\nYou can now order connections relative to other sibling connections. This ordering will also persist when changing the global order in the top left.\n\nThe UI has also been streamlined to make common actions and toggles more easily accessible.\n\n## Other\n\n- The title bar on Windows will now follow the appearance theme\n- Several more actions have been added for podman containers\n- Support VMs for tunneling\n- Searching for connections has been improved to show children as well\n- There is now an AppImage portable release\n- The welcome screen will now also contain the option to straight up jump to the synchronization settings\n- You can now launch xpipe in another data directory with `xpipe open -d \"<dir>\"`\n- Add option to use double clicks to open connections instead of single clicks\n- Add support for foot terminal\n- Fix rare null pointers and freezes in file browser\n- Fix PowerShell remote session file editing not transferring file correctly\n- Fix elementary terminal not launching correctly\n- Fix windows jumping around when created\n- Fix kubernetes not elevating correctly for non-default contexts\n- Fix ohmyzsh update notification freezing shell\n- Fix file browser icons being broken for links\n- The Linux installers now contain application icons from multiple sizes which should increase the icon display quality\n- The Linux builds now list socat as a dependency such that the kitty terminal integration will work without issues\n"
  },
  {
    "path": "dist/changelog/10.2_incremental.md",
    "content": "## File browser improvements\n\n- Add right click context menu to browser tabs\n- Add ability to select tabs with function keys, e.g. F1, F2, ...\n- Add ability to cycle between tabs with CTRL+TAB and CTRL+SHIFT+TAB\n- Fix some keyboard shortcuts being broken\n- Fix pressing enter on rename also opening file\n- Fix right click not opening context menu in empty directory\n- Fix shell opener in navigation bar being broken, so you can now run programs and shells again from the navigation bar similar to Windows explorer\n- There is now an always visible loading indicator when a tab is being opened\n- Add timeout to file selection when typing a file name that was not found\n- Improve flow of file selection by when typing its name\n- Remove limitation of only being able to open one system at the time while it is loading\n\n## Other\n\n- Rework UI to be more compact and show more connections\n- Implement native window styling on macOS\n- Add support for VNC RSA-AES authentication schemes, allowing to connect to more types of VNC servers\n- Services can now be opened in a browser using either HTTP or HTTPs\n- You can now create shortcuts to automatically forward and open services in a browser\n- Fix docker containers in some cases not persisting, leaving invalid orphan connections behind on the bottom\n- Fix connection failures to proxmox VMs that have additional custom network interfaces\n- Fix window not saving maximized state on restart\n- Don't modify git URLs anymore to fix sync with certain providers like azure\n- Improve git remote connection error messages\n- Replace system tray mode with background mode on Linux\n- Improve description for service groups\n- Publish API libraries to maven central\n- Show warning when launching PowerShell in constrained language mode\n- Fix rare NullPointers when migrating an old vault\n"
  },
  {
    "path": "dist/changelog/11.0.md",
    "content": "## Scripting improvements\n\nThe scripting system has been reworked in order to make it more intuitive and powerful.\n\nThe script execution types have been renamed, the documentation has been improved, and a new execution type has been added. The new runnable execution type will allow you to call a script from the connection hub directly in a dropdown for each connection when the script is active. This will also replace the current terminal command functionality, which has been removed.\n\nAny file browser scripts are now grouped by the scripts groups they are in, improving the overview when having many file browser scripts. Furthermore, you can now launch these scripts in the file browser either in the background or in a terminal if they are intended to be interactive. When multiple files are selected, a script is now called only once with all the selected files as arguments.\n\n## More terminal support\n\nThere is now support to use the following terminals:\n- Termius\n- MobaXterm\n- Xshell\n- SecureCRT\n\nThese work via a local SSH bridge that is managed by XPipe.\n\n## Teleport support\n\nThere is now support to add teleport connections that are available via tsh. You can do that by searching for available connections on any system which has tsh installed. This is a separate integration from SSH, SSH config entries for teleport proxies do not work and are automatically filtered out. It solely works through the tsh tool.\n\nThis feature is available in the Professional edition and is freely available to anyone for two weeks after this release using the Pro Preview.\n\n## Workspaces\n\nYou can now create multiple user workspaces in the settings menu. This will create desktop shortcuts that you can use to start XPipe with different workspaces active. Having multiple workspaces is useful if you want to separate your personal and work environments for example.\n\nThis feature is available in the Professional edition and is freely available to anyone for two weeks after this release using the Pro Preview.\n\n## TTYs and PTYs\n\nUp until now, if you added a connection that always allocated pty, XPipe would complain about a missing stderr. This was usually the case with badly implemented third-party ssh wrappers and proxies. In XPipe 11, there has been a ground up rework of the shell initialization code which will allow for a better handling of these cases. You can therefore now also launch such connections from the hub in a terminal. More advanced operations, such as the file browser, are not possible for these connections though.\n\n## Serial connection support\n\nThere is now support to add serial connections. This is implemented by delegating the serial connection to another installed tool of your choice and opening that in a terminal session.\n\nNote that this feature is untested due to me not having physical serial devices around. The plan for this feature is to evolve over time with user feedback and issue reports. It is not expected that this will actually work at the initial release. You can help the development of this feature by reporting any issues and testing it with various devices you have.\n\n## Pricing model updates\n\nI received plenty of user feedback and had time to observe the inner workings of potential customers, so I changed the old pricing model to one that should capture the demand better. The old pricing model was created at a time when XPipe had no customers at all and did not reflect the actual user demand. The main changes are the addition of a homelab plan, a monthly subscription, and some changes to the one-year professional edition.\n\nAll changes only apply to new orders. If you have previously purchased any XPipe edition, nothing will change for you. Any prices and conditions will stay the same for you. The community edition is also not changed. If you are interested, you can read about the changes in detail at `https://xpipe.io/blog/pricing-updates`.\n\n## Fixes\n\n- Fix git sync freezing when using ssh key with passphrase\n- Fix git sync restarting daemon after exit when using git ssh key with passphrase\n- Fix git vault readme not being generated on first push when no connections were added\n- Fix terminal exit not working properly in fish\n- Fix renaming a connection clearing all saved state information\n- Fix script enabled status being wrong after editing an enabled script\n- Fix download move operation failing when moving a directory that already existed in the downloads folder\n- Fix some scrollbars unnecessarily showing\n- Fix file browser list jumping around on first show\n- Fix missing libxtst6 dependency on some debian-based systems\n- Fix file browser root session not applying same color of original connection\n- Fix macOS kitty terminal netcat incompatibility with homebrew versions\n\n## Other\n\n- Categories can now be assigned colors\n- There is now support to view and change users/groups in the file browser\n- External git vault data files are now also encrypted by default\n- Rework state information display for proxmox VMs\n- Automatically fill identity file for ssh config wildcard keys as well\n- Improve error messages when system interaction was disabled for a system\n- Don't show git vault compatibility warnings on minor version updates\n- Enable ZGC on Linux and macOS\n- Some small appearance improvements\n- Many other miscellaneous fixes all over the place\n"
  },
  {
    "path": "dist/changelog/11.1.md",
    "content": "## Scripting improvements\n\nThe scripting system has been reworked in order to make it more intuitive and powerful.\n\nThe script execution types have been renamed, the documentation has been improved, and a new execution type has been added. The new runnable execution type will allow you to call a script from the connection hub directly in a dropdown for each connection when the script is active. This will also replace the current terminal command functionality, which has been removed.\n\nAny file browser scripts are now grouped by the scripts groups they are in, improving the overview when having many file browser scripts. Furthermore, you can now launch these scripts in the file browser either in the background or in a terminal if they are intended to be interactive. When multiple files are selected, a script is now called only once with all the selected files as arguments.\n\n## More terminal support\n\nThere is now support to use the following terminals:\n- Termius\n- MobaXterm\n- Xshell\n- SecureCRT\n\nThese work via a local SSH bridge that is managed by XPipe.\n\n## Teleport support\n\nThere is now support to add teleport connections that are available via tsh. You can do that by searching for available connections on any system which has tsh installed. This is a separate integration from SSH, SSH config entries for teleport proxies do not work and are automatically filtered out. It solely works through the tsh tool.\n\nThis feature is available in the Professional edition and is freely available to anyone for two weeks after this release using the Pro Preview.\n\n## Workspaces\n\nYou can now create multiple user workspaces in the settings menu. This will create desktop shortcuts that you can use to start XPipe with different workspaces active. Having multiple workspaces is useful if you want to separate your personal and work environments for example.\n\nThis feature is available in the Professional edition and is freely available to anyone for two weeks after this release using the Pro Preview.\n\n## TTYs and PTYs\n\nUp until now, if you added a connection that always allocated pty, XPipe would complain about a missing stderr. This was usually the case with badly implemented third-party ssh wrappers and proxies. In XPipe 11, there has been a ground up rework of the shell initialization code which will allow for a better handling of these cases. You can therefore now also launch such connections from the hub in a terminal. More advanced operations, such as the file browser, are not possible for these connections though.\n\n## Serial connection support\n\nThere is now support to add serial connections. This is implemented by delegating the serial connection to another installed tool of your choice and opening that in a terminal session.\n\nNote that this feature is untested due to me not having physical serial devices around. The plan for this feature is to evolve over time with user feedback and issue reports. It is not expected that this will actually work at the initial release. You can help the development of this feature by reporting any issues and testing it with various devices you have.\n\n## Pricing model updates\n\nI received plenty of user feedback and had time to observe the inner workings of potential customers, so I changed the old pricing model to one that should capture the demand better. The old pricing model was created at a time when XPipe had no customers at all and did not reflect the actual user demand. The main changes are the addition of a homelab plan, a monthly subscription, and some changes to the one-year professional edition.\n\nAll changes only apply to new orders. If you have previously purchased any XPipe edition, nothing will change for you. Any prices and conditions will stay the same for you. The community edition is also not changed. If you are interested, you can read about the changes in detail at `https://xpipe.io/blog/pricing-updates`.\n\n## Fixes\n\n- Fix git sync freezing when using ssh key with passphrase\n- Fix git sync restarting daemon after exit when using git ssh key with passphrase\n- Fix git vault readme not being generated on first push when no connections were added\n- Fix terminal exit not working properly in fish\n- Fix renaming a connection clearing all saved state information\n- Fix script enabled status being wrong after editing an enabled script\n- Fix download move operation failing when moving a directory that already existed in the downloads folder\n- Fix some scrollbars unnecessarily showing\n- Fix file browser list jumping around on first show\n- Fix missing libxtst6 dependency on some debian-based systems\n- Fix file browser root session not applying same color of original connection\n- Fix macOS kitty terminal netcat incompatibility with homebrew versions\n\n## Other\n\n- Categories can now be assigned colors\n- There is now support to view and change users/groups in the file browser\n- External git vault data files are now also encrypted by default\n- Rework state information display for proxmox VMs\n- Automatically fill identity file for ssh config wildcard keys as well\n- Improve error messages when system interaction was disabled for a system\n- Don't show git vault compatibility warnings on minor version updates\n- Enable ZGC on Linux and macOS\n- Some small appearance improvements\n- Many other miscellaneous fixes all over the place\n"
  },
  {
    "path": "dist/changelog/11.1_incremental.md",
    "content": "- Fix proxmox license check mistaking some clusters to use the enterprise repository and causing issues with the new homelab plan\n- Fix git vault commit not properly adding connections when none were added before\n- Fix NullPointers when adding new users/groups after opening file browser session\n- Add button to test git connection in the settings menu without having to restart\n- Fix scroll value not resetting when changing categories\n- Add warning when disabling vault advanced encryption setting\n- Fix error when creating new shell command\n- Improve git category icons\n- Fix update with sh fallback shell printing errors\n- Fix SSH bridge terminals (Termius, MobaXterm, Xshell, SecureCRT) not working for troubleshooting and updater purposes\n- Don't automatically set SSH bridge terminals to force explicit selection\n- Add Termius support for Linux and macOS\n- Fix some Termius launching issues\n- Fix MobaXterm launching issues with PowerShell\n- Fix macOS kitty launching issues\n"
  },
  {
    "path": "dist/changelog/11.2.md",
    "content": "## Scripting improvements\n\nThe scripting system has been reworked in order to make it more intuitive and powerful.\n\nThe script execution types have been renamed, the documentation has been improved, and a new execution type has been added. The new runnable execution type will allow you to call a script from the connection hub directly in a dropdown for each connection when the script is active. This will also replace the current terminal command functionality, which has been removed.\n\nAny file browser scripts are now grouped by the scripts groups they are in, improving the overview when having many file browser scripts. Furthermore, you can now launch these scripts in the file browser either in the background or in a terminal if they are intended to be interactive. When multiple files are selected, a script is now called only once with all the selected files as arguments.\n\n## More terminal support\n\nThere is now support to use the following terminals:\n- Termius\n- MobaXterm\n- Xshell\n- SecureCRT\n\nThese work via a local SSH bridge that is managed by XPipe.\n\n## Teleport support\n\nThere is now support to add teleport connections that are available via tsh. You can do that by searching for available connections on any system which has tsh installed. This is a separate integration from SSH, SSH config entries for teleport proxies do not work and are automatically filtered out. It solely works through the tsh tool.\n\nThis feature is available in the Professional edition and is freely available to anyone for two weeks after this release using the Pro Preview.\n\n## Workspaces\n\nYou can now create multiple user workspaces in the settings menu. This will create desktop shortcuts that you can use to start XPipe with different workspaces active. Having multiple workspaces is useful if you want to separate your personal and work environments for example.\n\nThis feature is available in the Professional edition and is freely available to anyone for two weeks after this release using the Pro Preview.\n\n## TTYs and PTYs\n\nUp until now, if you added a connection that always allocated pty, XPipe would complain about a missing stderr. This was usually the case with badly implemented third-party ssh wrappers and proxies. In XPipe 11, there has been a ground up rework of the shell initialization code which will allow for a better handling of these cases. You can therefore now also launch such connections from the hub in a terminal. More advanced operations, such as the file browser, are not possible for these connections though.\n\n## Serial connection support\n\nThere is now support to add serial connections. This is implemented by delegating the serial connection to another installed tool of your choice and opening that in a terminal session.\n\nNote that this feature is untested due to me not having physical serial devices around. The plan for this feature is to evolve over time with user feedback and issue reports. It is not expected that this will actually work at the initial release. You can help the development of this feature by reporting any issues and testing it with various devices you have.\n\n## Pricing model updates\n\nI received plenty of user feedback and had time to observe the inner workings of potential customers, so I changed the old pricing model to one that should capture the demand better. The old pricing model was created at a time when XPipe had no customers at all and did not reflect the actual user demand. The main changes are the addition of a homelab plan, a monthly subscription, and some changes to the one-year professional edition.\n\nAll changes only apply to new orders. If you have previously purchased any XPipe edition, nothing will change for you. Any prices and conditions will stay the same for you. The community edition is also not changed. If you are interested, you can read about the changes in detail at `https://xpipe.io/blog/pricing-updates`.\n\n## Fixes\n\n- Fix git sync freezing when using ssh key with passphrase\n- Fix git sync restarting daemon after exit when using git ssh key with passphrase\n- Fix git vault readme not being generated on first push when no connections were added\n- Fix terminal exit not working properly in fish\n- Fix renaming a connection clearing all saved state information\n- Fix script enabled status being wrong after editing an enabled script\n- Fix download move operation failing when moving a directory that already existed in the downloads folder\n- Fix some scrollbars unnecessarily showing\n- Fix file browser list jumping around on first show\n- Fix missing libxtst6 dependency on some debian-based systems\n- Fix file browser root session not applying same color of original connection\n- Fix macOS kitty terminal netcat incompatibility with homebrew versions\n\n## Other\n\n- Categories can now be assigned colors\n- There is now support to view and change users/groups in the file browser\n- External git vault data files are now also encrypted by default\n- Rework state information display for proxmox VMs\n- Automatically fill identity file for ssh config wildcard keys as well\n- Improve error messages when system interaction was disabled for a system\n- Don't show git vault compatibility warnings on minor version updates\n- Enable ZGC on Linux and macOS\n- Some small appearance improvements\n- Many other miscellaneous fixes all over the place\n"
  },
  {
    "path": "dist/changelog/11.2_incremental.md",
    "content": "## Hyper-V support\n\nThis release comes with an integration for Hyper-V. Searching for connections on a system where Hyper-V is installed should automatically add connections to your VMs. Note that Hyper-V requires Administrator privileges to interact with the VMs, so you have to start XPipe as an administrator if accessing a local Hyper-V VM and login as a user with Administrator privileges if you're accessing a remote Hyper-V instance.\n\nXPipe can connect to a VM via PSSession or SSH. PSSession is used by default for Windows guests if no SSH server is available on the guest. In all other cases, it will try to connect via SSH. Since Hyper-V cannot run guest commands on non-Windows systems from the outside, you have to make sure that an SSH server is already running in the VM in that case.\n\nI'm looking into integrating this feature with RDP next.\n\nThe Hyper-V integration is available starting from the homelab plan. It is also available in the new feature preview for two weeks after release.\n\n## PSSession improvements\n\nIn conjunction with Hyper-V, the PSSession support has also been reworked. Several broken things have been fixed, so it functions properly now. There's also now support to use gateways, similar to SSH connections. Any specified ComputerName also does not have to be already added to the trusted hosts anymore, XPipe now asks automatically whether to add an entry to the PowerShell remote trusted hosts.\n\n## SSH identity sources\n\nWith XPipe 11, it was implemented that selecting `None` for an SSH identity would prevent any SSH keys from being offered, including from external sources like agents and password managers. This however broke some connections where a more exotic type of agent was used that was not explicitly supported by XPipe. One example would be password managers that offer SSH key integration, as they come with their own agent. \n\nYou can now select the new identity option `Other external source` to allow these external programs offering their keys to the SSH client automatically again.\n\n## VNC improvements\n\nThe VNC integration has been reworked. It now supports more encrypted authentication methods, allowing it to connect to more servers. Furthermore, it is also now possible to create VNC connections without an SSH tunnel for systems that do not have SSH connectivity. The error handling has also been improved to silently ignore unknown server messages instead of displaying errors all the time. You can also now send CTRL+ALT+DEL via SHIFT+CTRL+ALT+DEL.\n\n## Other\n\n- Automatically select correct connection category if filter string has an unambiguous match\n- Connections being cloned are now added relatively to the original connection instead of at the bottom\n- Newly added connections are now added on the top instead of on the bottom\n- The HTTP API now has to be enabled explicitly in the settings menu to strengthen security\n- There is now a new context menu action to copy the current IP of any VM\n- Fix exception when not allowing XPipe access to certain directories on macOS\n- Fix file browser failing when passwd or groups file was corrupt\n- Fix various errors when trying to shut down application while it is still starting up\n"
  },
  {
    "path": "dist/changelog/11.3.md",
    "content": "## Scripting improvements\n\nThe scripting system has been reworked in order to make it more intuitive and powerful.\n\nThe script execution types have been renamed, the documentation has been improved, and a new execution type has been added. The new runnable execution type will allow you to call a script from the connection hub directly in a dropdown for each connection when the script is active. This will also replace the current terminal command functionality, which has been removed.\n\nAny file browser scripts are now grouped by the scripts groups they are in, improving the overview when having many file browser scripts. Furthermore, you can now launch these scripts in the file browser either in the background or in a terminal if they are intended to be interactive. When multiple files are selected, a script is now called only once with all the selected files as arguments.\n\n## More terminal support\n\nThere is now support to use the following terminals:\n- Termius\n- MobaXterm\n- Xshell\n- SecureCRT\n\nThese work via a local SSH bridge that is managed by XPipe.\n\n## Teleport support\n\nThere is now support to add teleport connections that are available via tsh. You can do that by searching for available connections on any system which has tsh installed. This is a separate integration from SSH, SSH config entries for teleport proxies do not work and are automatically filtered out. It solely works through the tsh tool.\n\nThis feature is available in the Professional edition and is freely available to anyone for two weeks after this release using the Pro Preview.\n\n## Workspaces\n\nYou can now create multiple user workspaces in the settings menu. This will create desktop shortcuts that you can use to start XPipe with different workspaces active. Having multiple workspaces is useful if you want to separate your personal and work environments for example.\n\nThis feature is available in the Professional edition and is freely available to anyone for two weeks after this release using the Pro Preview.\n\n## TTYs and PTYs\n\nUp until now, if you added a connection that always allocated pty, XPipe would complain about a missing stderr. This was usually the case with badly implemented third-party ssh wrappers and proxies. In XPipe 11, there has been a ground up rework of the shell initialization code which will allow for a better handling of these cases. You can therefore now also launch such connections from the hub in a terminal. More advanced operations, such as the file browser, are not possible for these connections though.\n\n## Serial connection support\n\nThere is now support to add serial connections. This is implemented by delegating the serial connection to another installed tool of your choice and opening that in a terminal session.\n\nNote that this feature is untested due to me not having physical serial devices around. The plan for this feature is to evolve over time with user feedback and issue reports. It is not expected that this will actually work at the initial release. You can help the development of this feature by reporting any issues and testing it with various devices you have.\n\n## Pricing model updates\n\nI received plenty of user feedback and had time to observe the inner workings of potential customers, so I changed the old pricing model to one that should capture the demand better. The old pricing model was created at a time when XPipe had no customers at all and did not reflect the actual user demand. The main changes are the addition of a homelab plan, a monthly subscription, and some changes to the one-year professional edition.\n\nAll changes only apply to new orders. If you have previously purchased any XPipe edition, nothing will change for you. Any prices and conditions will stay the same for you. The community edition is also not changed. If you are interested, you can read about the changes in detail at `https://xpipe.io/blog/pricing-updates`.\n\n## Fixes\n\n- Fix git sync freezing when using ssh key with passphrase\n- Fix git sync restarting daemon after exit when using git ssh key with passphrase\n- Fix git vault readme not being generated on first push when no connections were added\n- Fix terminal exit not working properly in fish\n- Fix renaming a connection clearing all saved state information\n- Fix script enabled status being wrong after editing an enabled script\n- Fix download move operation failing when moving a directory that already existed in the downloads folder\n- Fix some scrollbars unnecessarily showing\n- Fix file browser list jumping around on first show\n- Fix missing libxtst6 dependency on some debian-based systems\n- Fix file browser root session not applying same color of original connection\n- Fix macOS kitty terminal netcat incompatibility with homebrew versions\n\n## Other\n\n- Categories can now be assigned colors\n- There is now support to view and change users/groups in the file browser\n- External git vault data files are now also encrypted by default\n- Rework state information display for proxmox VMs\n- Automatically fill identity file for ssh config wildcard keys as well\n- Improve error messages when system interaction was disabled for a system\n- Don't show git vault compatibility warnings on minor version updates\n- Enable ZGC on Linux and macOS\n- Some small appearance improvements\n- Many other miscellaneous fixes all over the place\n"
  },
  {
    "path": "dist/changelog/11.3_incremental.md",
    "content": "## Changes\n\n- You can now set shell environments as a default login environment for a system\n- You can now instantly open an existing script in a text editor by clicking on it\n- There are now context menu actions to open a specific proxmox VM/container in the dashboard\n- You can now toggle to show only running systems for Proxmox and VMware\n- Fix PowerShell encoding issues on some Windows systems\n- Fix kubectl versions not being displayed for newer clients\n- Fix issue with zsh loading, causing potential freezing issues with zsh extensions\n- Fix application not starting up when PATH was corrupted on Windows\n- Fix shell environments running init script twice\n- Fix cmd shell environments not displaying a version\n- Fix window close freezing for a short time\n- Fix proxmox pvesh issue\n\n## News\n\nThe [XPipe python API](https://github.com/xpipe-io/xpipe-python-api) has now been designated the official API library to interact with XPipe. If you ever thought about programmatically interacting with systems through XPipe, feel free to check it out.\n\nThe website now contains a few new documents to maybe help you to convince your boss when you're thinking about deploying XPipe at your workplace. There is the [executive summary](http://localhost:3000/assets/documents/XPipe%20for%20Enterprises.pdf) for a short overview of XPipe and the [security whitepaper](http://localhost:3000/assets/documents/Security%20in%20XPipe.pdf) for CISOs.\n"
  },
  {
    "path": "dist/changelog/12.0.md",
    "content": "## Changes\n\n- Add popup to automatically save file with sudo when permissions are denied in file browser\n- You can now restart any ended terminal session by pressing R in the terminal\n- Add support for the windows credential manager as a password manager\n- Reuse existing shell session when adding new connection and searching for available connections\n- Implement support for setting custom icons, thanks to [https://github.com/selfhst/icons](https://github.com/selfhst/icons)\n- Replace deprecated wmic tool interaction on Windows\n- Add button to log in as a different user for RDP tunnel connections\n- Properly terminate all running connections when shutting down\n- Improve color scheme contrast for light themes\n- Improve connection hub styling\n- Rework Windows OS name detection\n- Improve script summary display\n- Upgrade to GraalVM 22.0.2\n- There is now a docker image with a web-based desktop environment for XPipe at [https://github.com/xpipe-io/xpipe-webtop](https://github.com/xpipe-io/xpipe-webtop)\n\n## Fixes\n\n- Fix csh, opnsense, pfsense shells being broken\n- Fix VM start/stop actions only being visible when user credentials were supplied\n- Fix tunnels failing to start when the remote login shell was fish\n- Fix dashlane password manager configuration being wrong\n- Fix some shell sessions staying open in the background when closing connection creation dialog\n- Fix SSH bridge not launching on Linux with missing sshd\n- Fix browser transfer progress flickering\n- Fix powershell type not being able to be recognized in certain language modes\n- Fix Cygwin/Msys2/GitForWindows not showing up in connection search sometimes\n- Fix some startup checks not working\n- Fix scrollbar not resetting when changing application tabs\n- Fix file modified dates and color names not being translated\n\n## Git vault improvements\n\n- Add more extensive documentation to the remote git repository settings menu\n- Add restart button to the sync settings menu\n- Improve git failure messages\n- Fix git CLI check not working on macOS due to xcode-select\n- Fix git sync failing when multiple gpg programs were present in PATH\n\n## Product hunt\n\nXPipe will be on ProductHunt on October 22. If you interested, you can follow XPipe at [https://www.producthunt.com/products/xpipe](https://www.producthunt.com/products/xpipe) to get notified.\n"
  },
  {
    "path": "dist/changelog/12.1.md",
    "content": "## Changes\n\n- Add popup to automatically save file with sudo when permissions are denied in file browser\n- You can now restart any ended terminal session by pressing R in the terminal\n- Add support for the windows credential manager as a password manager\n- Reuse existing shell session when adding new connection and searching for available connections\n- Implement support for setting custom icons, thanks to [https://github.com/selfhst/icons](https://github.com/selfhst/icons)\n- Replace deprecated wmic tool interaction on Windows\n- Add button to log in as a different user for RDP tunnel connections\n- Properly terminate all running connections when shutting down\n- Improve color scheme contrast for light themes\n- Improve connection hub styling\n- Rework Windows OS name detection\n- Improve script summary display\n- Upgrade to GraalVM 22.0.2\n- There is now a docker image with a web-based desktop environment for XPipe at [https://github.com/xpipe-io/xpipe-webtop](https://github.com/xpipe-io/xpipe-webtop)\n\n## Fixes\n\n- Fix csh, opnsense, pfsense shells being broken\n- Fix VM start/stop actions only being visible when user credentials were supplied\n- Fix tunnels failing to start when the remote login shell was fish\n- Fix dashlane password manager configuration being wrong\n- Fix some shell sessions staying open in the background when closing connection creation dialog\n- Fix SSH bridge not launching on Linux with missing sshd\n- Fix browser transfer progress flickering\n- Fix powershell type not being able to be recognized in certain language modes\n- Fix Cygwin/Msys2/GitForWindows not showing up in connection search sometimes\n- Fix some startup checks not working\n- Fix scrollbar not resetting when changing application tabs\n- Fix file modified dates and color names not being translated\n\n## Git vault improvements\n\n- Add more extensive documentation to the remote git repository settings menu\n- Add restart button to the sync settings menu\n- Improve git failure messages\n- Fix git CLI check not working on macOS due to xcode-select\n- Fix git sync failing when multiple gpg programs were present in PATH\n\n## Product hunt\n\nXPipe will be on ProductHunt on October 22. If you interested, you can follow XPipe at [https://www.producthunt.com/products/xpipe](https://www.producthunt.com/products/xpipe) to get notified.\n"
  },
  {
    "path": "dist/changelog/12.1_incremental.md",
    "content": "- Add new actions in file browser to compress/uncompress zip/tar/tar.gz/7z\n- Implement various performance improvements for lower-end systems\n- Add dropdown to quickly select SSH key files that are already synced via git\n- Add option to change SSH port for VM connections\n- Improve category display on the left for large amounts of connection categories\n- Add ability to fill in missing git author data within xpipe\n- Fix file chooser dialog not closing\n- Fix file browser locking previously visited directories, preventing their deletions\n- Fix some systems being mistakenly identified as Proxmox PVE systems\n- Fix connection counts not being updated when moving connections across categories\n- Fix a few NullPointers\n"
  },
  {
    "path": "dist/changelog/12.2.md",
    "content": "## Changes\n\n- Add popup to automatically save file with sudo when permissions are denied in file browser\n- You can now restart any ended terminal session by pressing R in the terminal\n- Add support for the windows credential manager as a password manager\n- Reuse existing shell session when adding new connection and searching for available connections\n- Implement support for setting custom icons, thanks to [https://github.com/selfhst/icons](https://github.com/selfhst/icons)\n- Replace deprecated wmic tool interaction on Windows\n- Add button to log in as a different user for RDP tunnel connections\n- Properly terminate all running connections when shutting down\n- Improve color scheme contrast for light themes\n- Improve connection hub styling\n- Rework Windows OS name detection\n- Improve script summary display\n- Upgrade to GraalVM 22.0.2\n- There is now a docker image with a web-based desktop environment for XPipe at [https://github.com/xpipe-io/xpipe-webtop](https://github.com/xpipe-io/xpipe-webtop)\n\n## Fixes\n\n- Fix csh, opnsense, pfsense shells being broken\n- Fix VM start/stop actions only being visible when user credentials were supplied\n- Fix tunnels failing to start when the remote login shell was fish\n- Fix dashlane password manager configuration being wrong\n- Fix some shell sessions staying open in the background when closing connection creation dialog\n- Fix SSH bridge not launching on Linux with missing sshd\n- Fix browser transfer progress flickering\n- Fix powershell type not being able to be recognized in certain language modes\n- Fix Cygwin/Msys2/GitForWindows not showing up in connection search sometimes\n- Fix some startup checks not working\n- Fix scrollbar not resetting when changing application tabs\n- Fix file modified dates and color names not being translated\n\n## Git vault improvements\n\n- Add more extensive documentation to the remote git repository settings menu\n- Add restart button to the sync settings menu\n- Improve git failure messages\n- Fix git CLI check not working on macOS due to xcode-select\n- Fix git sync failing when multiple gpg programs were present in PATH\n\n## Product hunt\n\nXPipe will be on ProductHunt on October 22. If you interested, you can follow XPipe at [https://www.producthunt.com/products/xpipe](https://www.producthunt.com/products/xpipe) to get notified.\n"
  },
  {
    "path": "dist/changelog/12.2_incremental.md",
    "content": "- Add new rename file conflict action in file browser\n- Add functionality to duplicate files when copy-pasting files onto themselves\n- Add keeper commander password manager template\n- Always show script action in file browser to make it more obvious to set up\n- Improve some error vague messages\n- This release fixes an issue on macOS where XPipe defaulted to sh instead of zsh.\n  There seems to be some bugs in macOS 15 where the spawning of external programs fails sometimes,\n  leading XPipe to believe that zsh does not work correctly.\n  XPipe will now no longer fall back to sh and instead fail to start,\n  with the hope that on the next start the process spawning issue won't occur again.\n- Fix macOS window getting smaller on each successive launch\n- Fix tooltip dropshadows not working in webtop\n- Fix XPipe not being in taskbar by default in webtop\n- Fix file browser transfer progress being wrong for files < 1kb\n- Show proper error when a source file gets deleted while a transfer is in progress\n- Fix empty storage directory setting leading to startup crash\n- Fix update script reporting syntax error on zsh\n- Fix NullPointers when a script had no commands on its own\n"
  },
  {
    "path": "dist/changelog/12.3.1.md",
    "content": "## Changes\n\n- Add popup to automatically save file with sudo when permissions are denied in file browser\n- You can now restart any ended terminal session by pressing R in the terminal\n- Add support for the windows credential manager as a password manager\n- Reuse existing shell session when adding new connection and searching for available connections\n- Implement support for setting custom icons, thanks to [https://github.com/selfhst/icons](https://github.com/selfhst/icons)\n- Replace deprecated wmic tool interaction on Windows\n- Add button to log in as a different user for RDP tunnel connections\n- Properly terminate all running connections when shutting down\n- Improve color scheme contrast for light themes\n- Improve connection hub styling\n- Rework Windows OS name detection\n- Improve script summary display\n- Upgrade to GraalVM 22.0.2\n- There is now a docker image with a web-based desktop environment for XPipe at [https://github.com/xpipe-io/xpipe-webtop](https://github.com/xpipe-io/xpipe-webtop)\n\n## Fixes\n\n- Fix csh, opnsense, pfsense shells being broken\n- Fix VM start/stop actions only being visible when user credentials were supplied\n- Fix tunnels failing to start when the remote login shell was fish\n- Fix dashlane password manager configuration being wrong\n- Fix some shell sessions staying open in the background when closing connection creation dialog\n- Fix SSH bridge not launching on Linux with missing sshd\n- Fix browser transfer progress flickering\n- Fix powershell type not being able to be recognized in certain language modes\n- Fix Cygwin/Msys2/GitForWindows not showing up in connection search sometimes\n- Fix some startup checks not working\n- Fix scrollbar not resetting when changing application tabs\n- Fix file modified dates and color names not being translated\n\n## Git vault improvements\n\n- Add more extensive documentation to the remote git repository settings menu\n- Add restart button to the sync settings menu\n- Improve git failure messages\n- Fix git CLI check not working on macOS due to xcode-select\n- Fix git sync failing when multiple gpg programs were present in PATH\n\n## Product hunt\n\nXPipe will be on ProductHunt on October 22. If you interested, you can follow XPipe at [https://www.producthunt.com/products/xpipe](https://www.producthunt.com/products/xpipe) to get notified.\n"
  },
  {
    "path": "dist/changelog/12.3.1_incremental.md",
    "content": "This is a hotfix release for the [12.3 release](https://github.com/xpipe-io/xpipe/releases/tag/12.3) to fix the new feature preview not working correctly with the homelab plan."
  },
  {
    "path": "dist/changelog/12.3.2.md",
    "content": "## Changes\n\n- Add popup to automatically save file with sudo when permissions are denied in file browser\n- You can now restart any ended terminal session by pressing R in the terminal\n- Add support for the windows credential manager as a password manager\n- Reuse existing shell session when adding new connection and searching for available connections\n- Implement support for setting custom icons, thanks to [https://github.com/selfhst/icons](https://github.com/selfhst/icons)\n- Replace deprecated wmic tool interaction on Windows\n- Add button to log in as a different user for RDP tunnel connections\n- Properly terminate all running connections when shutting down\n- Improve color scheme contrast for light themes\n- Improve connection hub styling\n- Rework Windows OS name detection\n- Improve script summary display\n- Upgrade to GraalVM 22.0.2\n- There is now a docker image with a web-based desktop environment for XPipe at [https://github.com/xpipe-io/xpipe-webtop](https://github.com/xpipe-io/xpipe-webtop)\n\n## Fixes\n\n- Fix csh, opnsense, pfsense shells being broken\n- Fix VM start/stop actions only being visible when user credentials were supplied\n- Fix tunnels failing to start when the remote login shell was fish\n- Fix dashlane password manager configuration being wrong\n- Fix some shell sessions staying open in the background when closing connection creation dialog\n- Fix SSH bridge not launching on Linux with missing sshd\n- Fix browser transfer progress flickering\n- Fix powershell type not being able to be recognized in certain language modes\n- Fix Cygwin/Msys2/GitForWindows not showing up in connection search sometimes\n- Fix some startup checks not working\n- Fix scrollbar not resetting when changing application tabs\n- Fix file modified dates and color names not being translated\n\n## Git vault improvements\n\n- Add more extensive documentation to the remote git repository settings menu\n- Add restart button to the sync settings menu\n- Improve git failure messages\n- Fix git CLI check not working on macOS due to xcode-select\n- Fix git sync failing when multiple gpg programs were present in PATH\n\n## Product hunt\n\nXPipe will be on ProductHunt on October 22. If you interested, you can follow XPipe at [https://www.producthunt.com/products/xpipe](https://www.producthunt.com/products/xpipe) to get notified.\n"
  },
  {
    "path": "dist/changelog/12.3.2_incremental.md",
    "content": "This is a hotfix release for the [12.3 release](https://github.com/xpipe-io/xpipe/releases/tag/12.3) to fix the license check being bugged when launching a terminal. Sorry for the inconvenience!"
  },
  {
    "path": "dist/changelog/12.3.3.md",
    "content": "## Changes\n\n- Add popup to automatically save file with sudo when permissions are denied in file browser\n- You can now restart any ended terminal session by pressing R in the terminal\n- Add support for the windows credential manager as a password manager\n- Reuse existing shell session when adding new connection and searching for available connections\n- Implement support for setting custom icons, thanks to [https://github.com/selfhst/icons](https://github.com/selfhst/icons)\n- Replace deprecated wmic tool interaction on Windows\n- Add button to log in as a different user for RDP tunnel connections\n- Properly terminate all running connections when shutting down\n- Improve color scheme contrast for light themes\n- Improve connection hub styling\n- Rework Windows OS name detection\n- Improve script summary display\n- Upgrade to GraalVM 22.0.2\n- There is now a docker image with a web-based desktop environment for XPipe at [https://github.com/xpipe-io/xpipe-webtop](https://github.com/xpipe-io/xpipe-webtop)\n\n## Fixes\n\n- Fix csh, opnsense, pfsense shells being broken\n- Fix VM start/stop actions only being visible when user credentials were supplied\n- Fix tunnels failing to start when the remote login shell was fish\n- Fix dashlane password manager configuration being wrong\n- Fix some shell sessions staying open in the background when closing connection creation dialog\n- Fix SSH bridge not launching on Linux with missing sshd\n- Fix browser transfer progress flickering\n- Fix powershell type not being able to be recognized in certain language modes\n- Fix Cygwin/Msys2/GitForWindows not showing up in connection search sometimes\n- Fix some startup checks not working\n- Fix scrollbar not resetting when changing application tabs\n- Fix file modified dates and color names not being translated\n\n## Git vault improvements\n\n- Add more extensive documentation to the remote git repository settings menu\n- Add restart button to the sync settings menu\n- Improve git failure messages\n- Fix git CLI check not working on macOS due to xcode-select\n- Fix git sync failing when multiple gpg programs were present in PATH\n\n## Product hunt\n\nXPipe will be on ProductHunt on October 22. If you interested, you can follow XPipe at [https://www.producthunt.com/products/xpipe](https://www.producthunt.com/products/xpipe) to get notified.\n"
  },
  {
    "path": "dist/changelog/12.3.3_incremental.md",
    "content": "- Work around a Windows Terminal issue where terminal launches would fail if the Windows Terminal default profile was configured to run as administrator\n- Fix terminal launches failing if local fallback shell was enabled and set to powershell\n"
  },
  {
    "path": "dist/changelog/12.3.4.md",
    "content": "## Changes\n\n- Add popup to automatically save file with sudo when permissions are denied in file browser\n- You can now restart any ended terminal session by pressing R in the terminal\n- Add support for the windows credential manager as a password manager\n- Reuse existing shell session when adding new connection and searching for available connections\n- Implement support for setting custom icons, thanks to [https://github.com/selfhst/icons](https://github.com/selfhst/icons)\n- Replace deprecated wmic tool interaction on Windows\n- Add button to log in as a different user for RDP tunnel connections\n- Properly terminate all running connections when shutting down\n- Improve color scheme contrast for light themes\n- Improve connection hub styling\n- Rework Windows OS name detection\n- Improve script summary display\n- Upgrade to GraalVM 22.0.2\n- There is now a docker image with a web-based desktop environment for XPipe at [https://github.com/xpipe-io/xpipe-webtop](https://github.com/xpipe-io/xpipe-webtop)\n\n## Fixes\n\n- Fix csh, opnsense, pfsense shells being broken\n- Fix VM start/stop actions only being visible when user credentials were supplied\n- Fix tunnels failing to start when the remote login shell was fish\n- Fix dashlane password manager configuration being wrong\n- Fix some shell sessions staying open in the background when closing connection creation dialog\n- Fix SSH bridge not launching on Linux with missing sshd\n- Fix browser transfer progress flickering\n- Fix powershell type not being able to be recognized in certain language modes\n- Fix Cygwin/Msys2/GitForWindows not showing up in connection search sometimes\n- Fix some startup checks not working\n- Fix scrollbar not resetting when changing application tabs\n- Fix file modified dates and color names not being translated\n\n## Git vault improvements\n\n- Add more extensive documentation to the remote git repository settings menu\n- Add restart button to the sync settings menu\n- Improve git failure messages\n- Fix git CLI check not working on macOS due to xcode-select\n- Fix git sync failing when multiple gpg programs were present in PATH\n\n## Product hunt\n\nXPipe will be on ProductHunt on October 22. If you interested, you can follow XPipe at [https://www.producthunt.com/products/xpipe](https://www.producthunt.com/products/xpipe) to get notified.\n"
  },
  {
    "path": "dist/changelog/12.3.4_incremental.md",
    "content": "- Update expired code signing certificates\n"
  },
  {
    "path": "dist/changelog/12.3.5_incremental.md",
    "content": "- Fix terminal session logging setting not being able to deactivated after the preview was done (Sorry for the inconvenience)\n- Fix shell environment scan restarting parent shell, resulting in many connections\n- Fix local zsh not starting on macOS if an extension had a pending update\n\nNext up will be XPipe 13 soon, you can already find early test builds at [https://github.com/xpipe-io/xpipe-ptb](https://github.com/xpipe-io/xpipe-ptb)"
  },
  {
    "path": "dist/changelog/12.3.md",
    "content": "## Changes\n\n- Add popup to automatically save file with sudo when permissions are denied in file browser\n- You can now restart any ended terminal session by pressing R in the terminal\n- Add support for the windows credential manager as a password manager\n- Reuse existing shell session when adding new connection and searching for available connections\n- Implement support for setting custom icons, thanks to [https://github.com/selfhst/icons](https://github.com/selfhst/icons)\n- Replace deprecated wmic tool interaction on Windows\n- Add button to log in as a different user for RDP tunnel connections\n- Properly terminate all running connections when shutting down\n- Improve color scheme contrast for light themes\n- Improve connection hub styling\n- Rework Windows OS name detection\n- Improve script summary display\n- Upgrade to GraalVM 22.0.2\n- There is now a docker image with a web-based desktop environment for XPipe at [https://github.com/xpipe-io/xpipe-webtop](https://github.com/xpipe-io/xpipe-webtop)\n\n## Fixes\n\n- Fix csh, opnsense, pfsense shells being broken\n- Fix VM start/stop actions only being visible when user credentials were supplied\n- Fix tunnels failing to start when the remote login shell was fish\n- Fix dashlane password manager configuration being wrong\n- Fix some shell sessions staying open in the background when closing connection creation dialog\n- Fix SSH bridge not launching on Linux with missing sshd\n- Fix browser transfer progress flickering\n- Fix powershell type not being able to be recognized in certain language modes\n- Fix Cygwin/Msys2/GitForWindows not showing up in connection search sometimes\n- Fix some startup checks not working\n- Fix scrollbar not resetting when changing application tabs\n- Fix file modified dates and color names not being translated\n\n## Git vault improvements\n\n- Add more extensive documentation to the remote git repository settings menu\n- Add restart button to the sync settings menu\n- Improve git failure messages\n- Fix git CLI check not working on macOS due to xcode-select\n- Fix git sync failing when multiple gpg programs were present in PATH\n\n## Product hunt\n\nXPipe will be on ProductHunt on October 22. If you interested, you can follow XPipe at [https://www.producthunt.com/products/xpipe](https://www.producthunt.com/products/xpipe) to get notified.\n"
  },
  {
    "path": "dist/changelog/12.3_incremental.md",
    "content": "## Logging support\n\nThere is now the option to enable terminal session logging where all inputs and outputs of your terminal sessions are written into session log files. This is implemented via either PowerShell transcripts or the util-linux script command. Any sensitive information you type into the terminal like password prompts are not recorded. You can enable this feature in the settings menu under the new logging category.\n\nThis feature is available in the professional plan. It is also available in the community edition for two weeks after this release as a preview so that anyone interested can try it out.\n\n## Other\n\n- Fix shell initialization loop error when printed lines were too long\n- Fix exception in file browser when home path was missing\n- Fix multiple exceptions when platform integration could not be started\n- Fix terminal restart starting daemon if it is not running\n- Fix NullPointer when enabling a service tunnel on a VM\n- Fix a few StackOverflow issues when creating a script dependency loop\n- Improve documentation for new VM SSH identity option to clarify where the SSH keys files are used from\n- Improve error message when SSH key file could not be found\n"
  },
  {
    "path": "dist/changelog/13.0.1.md",
    "content": "## VMs\n\n- There is now support for KVM/QEMU virtual machines that can be accessed via the libvirt CLI tools `virsh`. This includes support for other driver URLs as well aside from KVM and QEMU. This integration is available starting from the homelab plan and can be used for free for two weeks after this release using the new release preview\n- You can now override a VM IP if you're using an advanced networking setup where the default IP detection is not suitable\n- Fix remote VM SSH connections not being able to use the keys and identities from the local system\n- There is now a new restart button for containers and VMs\n\n## File browser\n\n- There is now a new option in the context menu of a tab to pin it, allowing for having a split view with two different file systems\n- There is now the option to dock terminals in the file browser (this is only available on Windows for now). You can disable this in Settings -> File browser -> Terminal docking if you don't like it\n- The previous system history tab is now always shown\n- You can now change the default download location for the move to downloads button\n\n## Shell sessions\n\nMany improvements have been implemented for reusability of shell sessions running in the background. Whenever you access a system or a parent system, XPipe will connect to it just as before but keep this session open in the background for some time, under the assumption that you will typically perform multiple actions shortly afterward. This will improve the speed of many actions and also results in less authentication prompts when you are using something like 2FA.\n\n## Terminals\n\n- Closing a terminal tab/window while the session is loading will now cancel the loading process in XPipe as well\n- A newly opened terminal will now regain focus after any password prompt was entered in xpipe\n\n## Security updates\n\nThere's now a new mechanism in place for checking for security updates separately from the normal update check. This is important going forward, to be able to act quickly when any security patch is published, so that all users have the possibility to get notified even if they don't follow announcements on the GitHub repo or on Discord. You can also disable this functionality in the settings if you want.\n\n## Other\n\n- The application style has been reworked\n- The settings menu now shows a restart button when a setting has been changed that requires a restart to apply\n- There is now an intro to scripts to provide some more information before using scripts\n- Add ability to enable agent forwarding when using the SSH-Agent for identities\n- The .rpm releases are now signed\n\n## Fixes\n\n- Fix Proxmox detection not working when not logging in as root\n- Fix tunnels not closing properly when having to be closed forcefully\n- Fix vmware integration failing when files other than .vmx were in the VM directories\n- Fix Tabby not launching properly on Windows\n- Fix SSH and docker issues with home assistant systems\n- Fix git readme not showing connections in nested children categories\n- Fix Windows Terminal Preview and Canary not being recognized\n- Fix style issues with the mocha theme\n- Fix color contrast for some themes\n- Fix system dark mode changes not being applied if they were changed while XPipe was not running\n"
  },
  {
    "path": "dist/changelog/13.0.1_incremental.md",
    "content": "- Fix startup error when old legacy SSH config connections were present\n"
  },
  {
    "path": "dist/changelog/13.0.md",
    "content": "## VMs\n\n- There is now support for KVM/QEMU virtual machines that can be accessed via the libvirt CLI tools `virsh`. This includes support for other driver URLs as well aside from KVM and QEMU. This integration is available starting from the homelab plan and can be used for free for two weeks after this release using the new release preview\n- You can now override a VM IP if you're using an advanced networking setup where the default IP detection is not suitable\n- Fix remote VM SSH connections not being able to use the keys and identities from the local system\n- There is now a new restart button for containers and VMs\n\n## File browser\n\n- There is now a new option in the context menu of a tab to pin it, allowing for having a split view with two different file systems\n- There is now the option to dock terminals in the file browser (this is only available on Windows for now). You can disable this in Settings -> File browser -> Terminal docking if you don't like it\n- The previous system history tab is now always shown\n- You can now change the default download location for the move to downloads button\n\n## Shell sessions\n\nMany improvements have been implemented for reusability of shell sessions running in the background. Whenever you access a system or a parent system, XPipe will connect to it just as before but keep this session open in the background for some time, under the assumption that you will typically perform multiple actions shortly afterward. This will improve the speed of many actions and also results in less authentication prompts when you are using something like 2FA.\n\n## Terminals\n\n- Closing a terminal tab/window while the session is loading will now cancel the loading process in XPipe as well\n- A newly opened terminal will now regain focus after any password prompt was entered in xpipe\n\n## Security updates\n\nThere's now a new mechanism in place for checking for security updates separately from the normal update check. This is important going forward, to be able to act quickly when any security patch is published, so that all users have the possibility to get notified even if they don't follow announcements on the GitHub repo or on Discord. You can also disable this functionality in the settings if you want.\n\n## Other\n\n- The application style has been reworked\n- The settings menu now shows a restart button when a setting has been changed that requires a restart to apply\n- There is now an intro to scripts to provide some more information before using scripts\n- Add ability to enable agent forwarding when using the SSH-Agent for identities\n- The .rpm releases are now signed\n\n## Fixes\n\n- Fix Proxmox detection not working when not logging in as root\n- Fix tunnels not closing properly when having to be closed forcefully\n- Fix vmware integration failing when files other than .vmx were in the VM directories\n- Fix Tabby not launching properly on Windows\n- Fix SSH and docker issues with home assistant systems\n- Fix git readme not showing connections in nested children categories\n- Fix Windows Terminal Preview and Canary not being recognized\n- Fix style issues with the mocha theme\n- Fix color contrast for some themes\n- Fix system dark mode changes not being applied if they were changed while XPipe was not running\n"
  },
  {
    "path": "dist/changelog/13.1.1.md",
    "content": "## VMs\n\n- There is now support for KVM/QEMU virtual machines that can be accessed via the libvirt CLI tools `virsh`. This includes support for other driver URLs as well aside from KVM and QEMU. This integration is available starting from the homelab plan and can be used for free for two weeks after this release using the new release preview\n- You can now override a VM IP if you're using an advanced networking setup where the default IP detection is not suitable\n- Fix remote VM SSH connections not being able to use the keys and identities from the local system\n- There is now a new restart button for containers and VMs\n\n## File browser\n\n- There is now a new option in the context menu of a tab to pin it, allowing for having a split view with two different file systems\n- There is now the option to dock terminals in the file browser (this is only available on Windows for now). You can disable this in Settings -> File browser -> Terminal docking if you don't like it\n- The previous system history tab is now always shown\n- You can now change the default download location for the move to downloads button\n\n## Shell sessions\n\nMany improvements have been implemented for reusability of shell sessions running in the background. Whenever you access a system or a parent system, XPipe will connect to it just as before but keep this session open in the background for some time, under the assumption that you will typically perform multiple actions shortly afterward. This will improve the speed of many actions and also results in less authentication prompts when you are using something like 2FA.\n\n## Terminals\n\n- Closing a terminal tab/window while the session is loading will now cancel the loading process in XPipe as well\n- A newly opened terminal will now regain focus after any password prompt was entered in xpipe\n\n## Security updates\n\nThere's now a new mechanism in place for checking for security updates separately from the normal update check. This is important going forward, to be able to act quickly when any security patch is published, so that all users have the possibility to get notified even if they don't follow announcements on the GitHub repo or on Discord. You can also disable this functionality in the settings if you want.\n\n## Other\n\n- The application style has been reworked\n- The settings menu now shows a restart button when a setting has been changed that requires a restart to apply\n- There is now an intro to scripts to provide some more information before using scripts\n- Add ability to enable agent forwarding when using the SSH-Agent for identities\n- The .rpm releases are now signed\n\n## Fixes\n\n- Fix Proxmox detection not working when not logging in as root\n- Fix tunnels not closing properly when having to be closed forcefully\n- Fix vmware integration failing when files other than .vmx were in the VM directories\n- Fix Tabby not launching properly on Windows\n- Fix SSH and docker issues with home assistant systems\n- Fix git readme not showing connections in nested children categories\n- Fix Windows Terminal Preview and Canary not being recognized\n- Fix style issues with the mocha theme\n- Fix color contrast for some themes\n- Fix system dark mode changes not being applied if they were changed while XPipe was not running\n"
  },
  {
    "path": "dist/changelog/13.1.1_incremental.md",
    "content": "- Rework Windows msi installer to support both per-user and system-wide installations. The installer will also now respect the properties `ALLUSERS`. This makes it possible to install XPipe with tools such as intune\n- Add pin tab button to the file browser menu bar to make tab splitting more prominent\n- Add file browser setting to automatically start up with local machine tab pinned\n- Add download context menu action in file browser as an alternative to dragging files to the download box\n- Add warning message when the incompatible coreutils homebrew package is in the PATH on macOS\n- Fix git sync failing when it required a password prompt\n- Fix proxmox detection not working when not using the PVE distro and not logging in as root\n- Fix startup check for user directory permissions not working\n- Fix various small bugs\n"
  },
  {
    "path": "dist/changelog/13.1.md",
    "content": "## VMs\n\n- There is now support for KVM/QEMU virtual machines that can be accessed via the libvirt CLI tools `virsh`. This includes support for other driver URLs as well aside from KVM and QEMU. This integration is available starting from the homelab plan and can be used for free for two weeks after this release using the new release preview\n- You can now override a VM IP if you're using an advanced networking setup where the default IP detection is not suitable\n- Fix remote VM SSH connections not being able to use the keys and identities from the local system\n- There is now a new restart button for containers and VMs\n\n## File browser\n\n- There is now a new option in the context menu of a tab to pin it, allowing for having a split view with two different file systems\n- There is now the option to dock terminals in the file browser (this is only available on Windows for now). You can disable this in Settings -> File browser -> Terminal docking if you don't like it\n- The previous system history tab is now always shown\n- You can now change the default download location for the move to downloads button\n\n## Shell sessions\n\nMany improvements have been implemented for reusability of shell sessions running in the background. Whenever you access a system or a parent system, XPipe will connect to it just as before but keep this session open in the background for some time, under the assumption that you will typically perform multiple actions shortly afterward. This will improve the speed of many actions and also results in less authentication prompts when you are using something like 2FA.\n\n## Terminals\n\n- Closing a terminal tab/window while the session is loading will now cancel the loading process in XPipe as well\n- A newly opened terminal will now regain focus after any password prompt was entered in xpipe\n\n## Security updates\n\nThere's now a new mechanism in place for checking for security updates separately from the normal update check. This is important going forward, to be able to act quickly when any security patch is published, so that all users have the possibility to get notified even if they don't follow announcements on the GitHub repo or on Discord. You can also disable this functionality in the settings if you want.\n\n## Other\n\n- The application style has been reworked\n- The settings menu now shows a restart button when a setting has been changed that requires a restart to apply\n- There is now an intro to scripts to provide some more information before using scripts\n- Add ability to enable agent forwarding when using the SSH-Agent for identities\n- The .rpm releases are now signed\n\n## Fixes\n\n- Fix Proxmox detection not working when not logging in as root\n- Fix tunnels not closing properly when having to be closed forcefully\n- Fix vmware integration failing when files other than .vmx were in the VM directories\n- Fix Tabby not launching properly on Windows\n- Fix SSH and docker issues with home assistant systems\n- Fix git readme not showing connections in nested children categories\n- Fix Windows Terminal Preview and Canary not being recognized\n- Fix style issues with the mocha theme\n- Fix color contrast for some themes\n- Fix system dark mode changes not being applied if they were changed while XPipe was not running\n"
  },
  {
    "path": "dist/changelog/13.1_incremental.md",
    "content": "- Rework Windows msi installer to support both per-user and system-wide installations. The installer will also now respect the properties `ALLUSERS`. This makes it possible to install XPipe with tools such as intune\n- Add pin tab button to the file browser menu bar to make tab splitting more prominent\n- Add file browser setting to automatically start up with local machine tab pinned\n- Add download context menu action in file browser as an alternative to dragging files to the download box\n- Add warning message when the incompatible coreutils homebrew package is in the PATH on macOS\n- Fix git sync failing when it required a password prompt\n- Fix proxmox detection not working when not using the PVE distro and not logging in as root\n- Fix startup check for user directory permissions not working\n- Fix various small bugs\n"
  },
  {
    "path": "dist/changelog/13.2.md",
    "content": "## VMs\n\n- There is now support for KVM/QEMU virtual machines that can be accessed via the libvirt CLI tools `virsh`. This includes support for other driver URLs as well aside from KVM and QEMU. This integration is available starting from the homelab plan and can be used for free for two weeks after this release using the new release preview\n- You can now override a VM IP if you're using an advanced networking setup where the default IP detection is not suitable\n- Fix remote VM SSH connections not being able to use the keys and identities from the local system\n- There is now a new restart button for containers and VMs\n\n## File browser\n\n- There is now a new option in the context menu of a tab to pin it, allowing for having a split view with two different file systems\n- There is now the option to dock terminals in the file browser (this is only available on Windows for now). You can disable this in Settings -> File browser -> Terminal docking if you don't like it\n- The previous system history tab is now always shown\n- You can now change the default download location for the move to downloads button\n\n## Shell sessions\n\nMany improvements have been implemented for reusability of shell sessions running in the background. Whenever you access a system or a parent system, XPipe will connect to it just as before but keep this session open in the background for some time, under the assumption that you will typically perform multiple actions shortly afterward. This will improve the speed of many actions and also results in less authentication prompts when you are using something like 2FA.\n\n## Terminals\n\n- Closing a terminal tab/window while the session is loading will now cancel the loading process in XPipe as well\n- A newly opened terminal will now regain focus after any password prompt was entered in xpipe\n\n## Security updates\n\nThere's now a new mechanism in place for checking for security updates separately from the normal update check. This is important going forward, to be able to act quickly when any security patch is published, so that all users have the possibility to get notified even if they don't follow announcements on the GitHub repo or on Discord. You can also disable this functionality in the settings if you want.\n\n## Other\n\n- The application style has been reworked\n- The settings menu now shows a restart button when a setting has been changed that requires a restart to apply\n- There is now an intro to scripts to provide some more information before using scripts\n- Add ability to enable agent forwarding when using the SSH-Agent for identities\n- The .rpm releases are now signed\n\n## Fixes\n\n- Fix Proxmox detection not working when not logging in as root\n- Fix tunnels not closing properly when having to be closed forcefully\n- Fix vmware integration failing when files other than .vmx were in the VM directories\n- Fix Tabby not launching properly on Windows\n- Fix SSH and docker issues with home assistant systems\n- Fix git readme not showing connections in nested children categories\n- Fix Windows Terminal Preview and Canary not being recognized\n- Fix style issues with the mocha theme\n- Fix color contrast for some themes\n- Fix system dark mode changes not being applied if they were changed while XPipe was not running\n"
  },
  {
    "path": "dist/changelog/13.2_incremental.md",
    "content": "- Improve license requirement handling for systems. You can now add all systems without a license and also search for available subconnections. Only establishing the actual connection in a terminal or in the file browser will show any license requirement notice. This allows you to check whether all systems and installed tools are correctly recognized before considering purchasing a license.\n- Add support for forward and backwards mouse buttons for the file browser navigation\n- Fix NullPointer when trying to connect to VM connections created in older XPipe versions\n- Fix Local Machine connection entry being able to get deleted from the context menu, causing various Exceptions\n- Fix kubectl being wrongly detected in WSL\n"
  },
  {
    "path": "dist/changelog/13.3.1.md",
    "content": "## VMs\n\n- There is now support for KVM/QEMU virtual machines that can be accessed via the libvirt CLI tools `virsh`. This includes support for other driver URLs as well aside from KVM and QEMU. This integration is available starting from the homelab plan and can be used for free for two weeks after this release using the new release preview\n- You can now override a VM IP if you're using an advanced networking setup where the default IP detection is not suitable\n- Fix remote VM SSH connections not being able to use the keys and identities from the local system\n- There is now a new restart button for containers and VMs\n\n## File browser\n\n- There is now a new option in the context menu of a tab to pin it, allowing for having a split view with two different file systems\n- There is now the option to dock terminals in the file browser (this is only available on Windows for now). You can disable this in Settings -> File browser -> Terminal docking if you don't like it\n- The previous system history tab is now always shown\n- You can now change the default download location for the move to downloads button\n\n## Shell sessions\n\nMany improvements have been implemented for reusability of shell sessions running in the background. Whenever you access a system or a parent system, XPipe will connect to it just as before but keep this session open in the background for some time, under the assumption that you will typically perform multiple actions shortly afterward. This will improve the speed of many actions and also results in less authentication prompts when you are using something like 2FA.\n\n## Terminals\n\n- Closing a terminal tab/window while the session is loading will now cancel the loading process in XPipe as well\n- A newly opened terminal will now regain focus after any password prompt was entered in xpipe\n\n## Security updates\n\nThere's now a new mechanism in place for checking for security updates separately from the normal update check. This is important going forward, to be able to act quickly when any security patch is published, so that all users have the possibility to get notified even if they don't follow announcements on the GitHub repo or on Discord. You can also disable this functionality in the settings if you want.\n\n## Other\n\n- The application style has been reworked\n- The settings menu now shows a restart button when a setting has been changed that requires a restart to apply\n- There is now an intro to scripts to provide some more information before using scripts\n- Add ability to enable agent forwarding when using the SSH-Agent for identities\n- The .rpm releases are now signed\n\n## Fixes\n\n- Fix Proxmox detection not working when not logging in as root\n- Fix tunnels not closing properly when having to be closed forcefully\n- Fix vmware integration failing when files other than .vmx were in the VM directories\n- Fix Tabby not launching properly on Windows\n- Fix SSH and docker issues with home assistant systems\n- Fix git readme not showing connections in nested children categories\n- Fix Windows Terminal Preview and Canary not being recognized\n- Fix style issues with the mocha theme\n- Fix color contrast for some themes\n- Fix system dark mode changes not being applied if they were changed while XPipe was not running\n"
  },
  {
    "path": "dist/changelog/13.3.1_incremental.md",
    "content": "- Fix typo in proxmox enterprise id causing missing feature errors\n- Fix file browser pin tab operation switching back to history tab if it was the last open tab\n- Fix some broken german translations\n"
  },
  {
    "path": "dist/changelog/13.3.2.md",
    "content": "## VMs\n\n- There is now support for KVM/QEMU virtual machines that can be accessed via the libvirt CLI tools `virsh`. This includes support for other driver URLs as well aside from KVM and QEMU. This integration is available starting from the homelab plan and can be used for free for two weeks after this release using the new release preview\n- You can now override a VM IP if you're using an advanced networking setup where the default IP detection is not suitable\n- Fix remote VM SSH connections not being able to use the keys and identities from the local system\n- There is now a new restart button for containers and VMs\n\n## File browser\n\n- There is now a new option in the context menu of a tab to pin it, allowing for having a split view with two different file systems\n- There is now the option to dock terminals in the file browser (this is only available on Windows for now). You can disable this in Settings -> File browser -> Terminal docking if you don't like it\n- The previous system history tab is now always shown\n- You can now change the default download location for the move to downloads button\n\n## Shell sessions\n\nMany improvements have been implemented for reusability of shell sessions running in the background. Whenever you access a system or a parent system, XPipe will connect to it just as before but keep this session open in the background for some time, under the assumption that you will typically perform multiple actions shortly afterward. This will improve the speed of many actions and also results in less authentication prompts when you are using something like 2FA.\n\n## Terminals\n\n- Closing a terminal tab/window while the session is loading will now cancel the loading process in XPipe as well\n- A newly opened terminal will now regain focus after any password prompt was entered in xpipe\n\n## Security updates\n\nThere's now a new mechanism in place for checking for security updates separately from the normal update check. This is important going forward, to be able to act quickly when any security patch is published, so that all users have the possibility to get notified even if they don't follow announcements on the GitHub repo or on Discord. You can also disable this functionality in the settings if you want.\n\n## Other\n\n- The application style has been reworked\n- The settings menu now shows a restart button when a setting has been changed that requires a restart to apply\n- There is now an intro to scripts to provide some more information before using scripts\n- Add ability to enable agent forwarding when using the SSH-Agent for identities\n- The .rpm releases are now signed\n\n## Fixes\n\n- Fix Proxmox detection not working when not logging in as root\n- Fix tunnels not closing properly when having to be closed forcefully\n- Fix vmware integration failing when files other than .vmx were in the VM directories\n- Fix Tabby not launching properly on Windows\n- Fix SSH and docker issues with home assistant systems\n- Fix git readme not showing connections in nested children categories\n- Fix Windows Terminal Preview and Canary not being recognized\n- Fix style issues with the mocha theme\n- Fix color contrast for some themes\n- Fix system dark mode changes not being applied if they were changed while XPipe was not running\n"
  },
  {
    "path": "dist/changelog/13.3.2_incremental.md",
    "content": "- Fix Windows Terminal docking not working probably if new instance behavior had been set to attach in the Windows Terminal settings\n- Fix terminal dock not moving terminals anymore after tab switch\n- Fix terminal still trying to dock when launched from a pinned tab\n"
  },
  {
    "path": "dist/changelog/13.3.md",
    "content": "## VMs\n\n- There is now support for KVM/QEMU virtual machines that can be accessed via the libvirt CLI tools `virsh`. This includes support for other driver URLs as well aside from KVM and QEMU. This integration is available starting from the homelab plan and can be used for free for two weeks after this release using the new release preview\n- You can now override a VM IP if you're using an advanced networking setup where the default IP detection is not suitable\n- Fix remote VM SSH connections not being able to use the keys and identities from the local system\n- There is now a new restart button for containers and VMs\n\n## File browser\n\n- There is now a new option in the context menu of a tab to pin it, allowing for having a split view with two different file systems\n- There is now the option to dock terminals in the file browser (this is only available on Windows for now). You can disable this in Settings -> File browser -> Terminal docking if you don't like it\n- The previous system history tab is now always shown\n- You can now change the default download location for the move to downloads button\n\n## Shell sessions\n\nMany improvements have been implemented for reusability of shell sessions running in the background. Whenever you access a system or a parent system, XPipe will connect to it just as before but keep this session open in the background for some time, under the assumption that you will typically perform multiple actions shortly afterward. This will improve the speed of many actions and also results in less authentication prompts when you are using something like 2FA.\n\n## Terminals\n\n- Closing a terminal tab/window while the session is loading will now cancel the loading process in XPipe as well\n- A newly opened terminal will now regain focus after any password prompt was entered in xpipe\n\n## Security updates\n\nThere's now a new mechanism in place for checking for security updates separately from the normal update check. This is important going forward, to be able to act quickly when any security patch is published, so that all users have the possibility to get notified even if they don't follow announcements on the GitHub repo or on Discord. You can also disable this functionality in the settings if you want.\n\n## Other\n\n- The application style has been reworked\n- The settings menu now shows a restart button when a setting has been changed that requires a restart to apply\n- There is now an intro to scripts to provide some more information before using scripts\n- Add ability to enable agent forwarding when using the SSH-Agent for identities\n- The .rpm releases are now signed\n\n## Fixes\n\n- Fix Proxmox detection not working when not logging in as root\n- Fix tunnels not closing properly when having to be closed forcefully\n- Fix vmware integration failing when files other than .vmx were in the VM directories\n- Fix Tabby not launching properly on Windows\n- Fix SSH and docker issues with home assistant systems\n- Fix git readme not showing connections in nested children categories\n- Fix Windows Terminal Preview and Canary not being recognized\n- Fix style issues with the mocha theme\n- Fix color contrast for some themes\n- Fix system dark mode changes not being applied if they were changed while XPipe was not running\n"
  },
  {
    "path": "dist/changelog/13.3_incremental.md",
    "content": "- Improve license handling to also allow for searching subconnections of VMs and k8s prior to checking any license requirement. This allows you to check whether all systems and installed tools are correctly recognized and work before considering purchasing a license\n- Fix split file browser tabs sometimes not having the correct size\n- Fix race condition in local split file browser tab startup leading to it sometimes not showing up or causing errors\n- Fix scripts context menu not showing up for VMs\n- Fix PSSession connection search showing up even if it's not supported\n- Fix some SSH authentication options showing as Pro instead of Homelab\n"
  },
  {
    "path": "dist/changelog/13.4.1.md",
    "content": "## VMs\n\n- There is now support for KVM/QEMU virtual machines that can be accessed via the libvirt CLI tools `virsh`. This includes support for other driver URLs as well aside from KVM and QEMU. This integration is available starting from the homelab plan and can be used for free for two weeks after this release using the new release preview\n- You can now override a VM IP if you're using an advanced networking setup where the default IP detection is not suitable\n- Fix remote VM SSH connections not being able to use the keys and identities from the local system\n- There is now a new restart button for containers and VMs\n\n## File browser\n\n- There is now a new option in the context menu of a tab to pin it, allowing for having a split view with two different file systems\n- There is now the option to dock terminals in the file browser (this is only available on Windows for now). You can disable this in Settings -> File browser -> Terminal docking if you don't like it\n- The previous system history tab is now always shown\n- You can now change the default download location for the move to downloads button\n\n## Shell sessions\n\nMany improvements have been implemented for reusability of shell sessions running in the background. Whenever you access a system or a parent system, XPipe will connect to it just as before but keep this session open in the background for some time, under the assumption that you will typically perform multiple actions shortly afterward. This will improve the speed of many actions and also results in less authentication prompts when you are using something like 2FA.\n\n## Terminals\n\n- Closing a terminal tab/window while the session is loading will now cancel the loading process in XPipe as well\n- A newly opened terminal will now regain focus after any password prompt was entered in xpipe\n\n## Security updates\n\nThere's now a new mechanism in place for checking for security updates separately from the normal update check. This is important going forward, to be able to act quickly when any security patch is published, so that all users have the possibility to get notified even if they don't follow announcements on the GitHub repo or on Discord. You can also disable this functionality in the settings if you want.\n\n## Other\n\n- The application style has been reworked\n- The settings menu now shows a restart button when a setting has been changed that requires a restart to apply\n- There is now an intro to scripts to provide some more information before using scripts\n- Add ability to enable agent forwarding when using the SSH-Agent for identities\n- The .rpm releases are now signed\n\n## Fixes\n\n- Fix Proxmox detection not working when not logging in as root\n- Fix tunnels not closing properly when having to be closed forcefully\n- Fix vmware integration failing when files other than .vmx were in the VM directories\n- Fix Tabby not launching properly on Windows\n- Fix SSH and docker issues with home assistant systems\n- Fix git readme not showing connections in nested children categories\n- Fix Windows Terminal Preview and Canary not being recognized\n- Fix style issues with the mocha theme\n- Fix color contrast for some themes\n- Fix system dark mode changes not being applied if they were changed while XPipe was not running\n"
  },
  {
    "path": "dist/changelog/13.4.1_incremental.md",
    "content": "- Add ability to set up VNC connection for unconfigured Proxmox VMs as well so that you are not required to set credentials for every VM\n- Fix Proxmox VNC setup action always starting VM even when it was not running before\n- Fix Proxmox VNC setup action sometimes requiring two tries to add VNC entry\n"
  },
  {
    "path": "dist/changelog/13.4.2.md",
    "content": "## VMs\n\n- There is now support for KVM/QEMU virtual machines that can be accessed via the libvirt CLI tools `virsh`. This includes support for other driver URLs as well aside from KVM and QEMU. This integration is available starting from the homelab plan and can be used for free for two weeks after this release using the new release preview\n- You can now override a VM IP if you're using an advanced networking setup where the default IP detection is not suitable\n- Fix remote VM SSH connections not being able to use the keys and identities from the local system\n- There is now a new restart button for containers and VMs\n\n## File browser\n\n- There is now a new option in the context menu of a tab to pin it, allowing for having a split view with two different file systems\n- There is now the option to dock terminals in the file browser (this is only available on Windows for now). You can disable this in Settings -> File browser -> Terminal docking if you don't like it\n- The previous system history tab is now always shown\n- You can now change the default download location for the move to downloads button\n\n## Shell sessions\n\nMany improvements have been implemented for reusability of shell sessions running in the background. Whenever you access a system or a parent system, XPipe will connect to it just as before but keep this session open in the background for some time, under the assumption that you will typically perform multiple actions shortly afterward. This will improve the speed of many actions and also results in less authentication prompts when you are using something like 2FA.\n\n## Terminals\n\n- Closing a terminal tab/window while the session is loading will now cancel the loading process in XPipe as well\n- A newly opened terminal will now regain focus after any password prompt was entered in xpipe\n\n## Security updates\n\nThere's now a new mechanism in place for checking for security updates separately from the normal update check. This is important going forward, to be able to act quickly when any security patch is published, so that all users have the possibility to get notified even if they don't follow announcements on the GitHub repo or on Discord. You can also disable this functionality in the settings if you want.\n\n## Other\n\n- The application style has been reworked\n- The settings menu now shows a restart button when a setting has been changed that requires a restart to apply\n- There is now an intro to scripts to provide some more information before using scripts\n- Add ability to enable agent forwarding when using the SSH-Agent for identities\n- The .rpm releases are now signed\n\n## Fixes\n\n- Fix Proxmox detection not working when not logging in as root\n- Fix tunnels not closing properly when having to be closed forcefully\n- Fix vmware integration failing when files other than .vmx were in the VM directories\n- Fix Tabby not launching properly on Windows\n- Fix SSH and docker issues with home assistant systems\n- Fix git readme not showing connections in nested children categories\n- Fix Windows Terminal Preview and Canary not being recognized\n- Fix style issues with the mocha theme\n- Fix color contrast for some themes\n- Fix system dark mode changes not being applied if they were changed while XPipe was not running\n"
  },
  {
    "path": "dist/changelog/13.4.2_incremental.md",
    "content": "- Rework Proxmox VNC setup action name and explanations to be more clear\n- Fix Proxmox VNC setup action working with outdated data when the VM config was changed externally\n- Fix Terminal recognition failing on some macOS systems\n- Fix RDP client recognition failing on macOS\n- Fix corrupted caches not being cleaned properly"
  },
  {
    "path": "dist/changelog/13.4.3.md",
    "content": "## VMs\n\n- There is now support for KVM/QEMU virtual machines that can be accessed via the libvirt CLI tools `virsh`. This includes support for other driver URLs as well aside from KVM and QEMU. This integration is available starting from the homelab plan and can be used for free for two weeks after this release using the new release preview\n- You can now override a VM IP if you're using an advanced networking setup where the default IP detection is not suitable\n- Fix remote VM SSH connections not being able to use the keys and identities from the local system\n- There is now a new restart button for containers and VMs\n\n## File browser\n\n- There is now a new option in the context menu of a tab to pin it, allowing for having a split view with two different file systems\n- There is now the option to dock terminals in the file browser (this is only available on Windows for now). You can disable this in Settings -> File browser -> Terminal docking if you don't like it\n- The previous system history tab is now always shown\n- You can now change the default download location for the move to downloads button\n\n## Shell sessions\n\nMany improvements have been implemented for reusability of shell sessions running in the background. Whenever you access a system or a parent system, XPipe will connect to it just as before but keep this session open in the background for some time, under the assumption that you will typically perform multiple actions shortly afterward. This will improve the speed of many actions and also results in less authentication prompts when you are using something like 2FA.\n\n## Terminals\n\n- Closing a terminal tab/window while the session is loading will now cancel the loading process in XPipe as well\n- A newly opened terminal will now regain focus after any password prompt was entered in xpipe\n\n## Security updates\n\nThere's now a new mechanism in place for checking for security updates separately from the normal update check. This is important going forward, to be able to act quickly when any security patch is published, so that all users have the possibility to get notified even if they don't follow announcements on the GitHub repo or on Discord. You can also disable this functionality in the settings if you want.\n\n## Other\n\n- The application style has been reworked\n- The settings menu now shows a restart button when a setting has been changed that requires a restart to apply\n- There is now an intro to scripts to provide some more information before using scripts\n- Add ability to enable agent forwarding when using the SSH-Agent for identities\n- The .rpm releases are now signed\n\n## Fixes\n\n- Fix Proxmox detection not working when not logging in as root\n- Fix tunnels not closing properly when having to be closed forcefully\n- Fix vmware integration failing when files other than .vmx were in the VM directories\n- Fix Tabby not launching properly on Windows\n- Fix SSH and docker issues with home assistant systems\n- Fix git readme not showing connections in nested children categories\n- Fix Windows Terminal Preview and Canary not being recognized\n- Fix style issues with the mocha theme\n- Fix color contrast for some themes\n- Fix system dark mode changes not being applied if they were changed while XPipe was not running\n"
  },
  {
    "path": "dist/changelog/13.4.3_incremental.md",
    "content": "- Add support for creating tunneled services and tunneled VNC connections for VM guest systems\n"
  },
  {
    "path": "dist/changelog/13.4.4.md",
    "content": "## VMs\n\n- There is now support for KVM/QEMU virtual machines that can be accessed via the libvirt CLI tools `virsh`. This includes support for other driver URLs as well aside from KVM and QEMU. This integration is available starting from the homelab plan and can be used for free for two weeks after this release using the new release preview\n- You can now override a VM IP if you're using an advanced networking setup where the default IP detection is not suitable\n- Fix remote VM SSH connections not being able to use the keys and identities from the local system\n- There is now a new restart button for containers and VMs\n\n## File browser\n\n- There is now a new option in the context menu of a tab to pin it, allowing for having a split view with two different file systems\n- There is now the option to dock terminals in the file browser (this is only available on Windows for now). You can disable this in Settings -> File browser -> Terminal docking if you don't like it\n- The previous system history tab is now always shown\n- You can now change the default download location for the move to downloads button\n\n## Shell sessions\n\nMany improvements have been implemented for reusability of shell sessions running in the background. Whenever you access a system or a parent system, XPipe will connect to it just as before but keep this session open in the background for some time, under the assumption that you will typically perform multiple actions shortly afterward. This will improve the speed of many actions and also results in less authentication prompts when you are using something like 2FA.\n\n## Terminals\n\n- Closing a terminal tab/window while the session is loading will now cancel the loading process in XPipe as well\n- A newly opened terminal will now regain focus after any password prompt was entered in xpipe\n\n## Security updates\n\nThere's now a new mechanism in place for checking for security updates separately from the normal update check. This is important going forward, to be able to act quickly when any security patch is published, so that all users have the possibility to get notified even if they don't follow announcements on the GitHub repo or on Discord. You can also disable this functionality in the settings if you want.\n\n## Other\n\n- The application style has been reworked\n- The settings menu now shows a restart button when a setting has been changed that requires a restart to apply\n- There is now an intro to scripts to provide some more information before using scripts\n- Add ability to enable agent forwarding when using the SSH-Agent for identities\n- The .rpm releases are now signed\n\n## Fixes\n\n- Fix Proxmox detection not working when not logging in as root\n- Fix tunnels not closing properly when having to be closed forcefully\n- Fix vmware integration failing when files other than .vmx were in the VM directories\n- Fix Tabby not launching properly on Windows\n- Fix SSH and docker issues with home assistant systems\n- Fix git readme not showing connections in nested children categories\n- Fix Windows Terminal Preview and Canary not being recognized\n- Fix style issues with the mocha theme\n- Fix color contrast for some themes\n- Fix system dark mode changes not being applied if they were changed while XPipe was not running\n"
  },
  {
    "path": "dist/changelog/13.4.4_incremental.md",
    "content": "- Fix kubectl container shells failing to launch when a non-default context or namespace was used\n- Fix VNC mouse scroll being broken\n- Fix custom SSH connections not working when a Host header was explicitly specified\n- Fix kitty terminal failing to launch on macOS when another instance was already open before\n- Fix Proxmox PBS systems being detected as PVE systems\n- Fix invalid %ComSpec% variable causing startup errors\n- Fix setting for double clicks for launching connections also applying to the context menu buttons\n- Improve GitHub sync password prompt to clarify that a personal access token should be entered instead of the password\n- Disable category expand buttons when there are no child categories to make it less confusing\n- Improve mouse handling on macOS. If the window did not have focus before, mouse events will be ignored until the window has focus. This is now in line with other native macOS apps\n- Update french translations (Thanks @Quezaquo)\n"
  },
  {
    "path": "dist/changelog/13.4.md",
    "content": "## VMs\n\n- There is now support for KVM/QEMU virtual machines that can be accessed via the libvirt CLI tools `virsh`. This includes support for other driver URLs as well aside from KVM and QEMU. This integration is available starting from the homelab plan and can be used for free for two weeks after this release using the new release preview\n- You can now override a VM IP if you're using an advanced networking setup where the default IP detection is not suitable\n- Fix remote VM SSH connections not being able to use the keys and identities from the local system\n- There is now a new restart button for containers and VMs\n\n## File browser\n\n- There is now a new option in the context menu of a tab to pin it, allowing for having a split view with two different file systems\n- There is now the option to dock terminals in the file browser (this is only available on Windows for now). You can disable this in Settings -> File browser -> Terminal docking if you don't like it\n- The previous system history tab is now always shown\n- You can now change the default download location for the move to downloads button\n\n## Shell sessions\n\nMany improvements have been implemented for reusability of shell sessions running in the background. Whenever you access a system or a parent system, XPipe will connect to it just as before but keep this session open in the background for some time, under the assumption that you will typically perform multiple actions shortly afterward. This will improve the speed of many actions and also results in less authentication prompts when you are using something like 2FA.\n\n## Terminals\n\n- Closing a terminal tab/window while the session is loading will now cancel the loading process in XPipe as well\n- A newly opened terminal will now regain focus after any password prompt was entered in xpipe\n\n## Security updates\n\nThere's now a new mechanism in place for checking for security updates separately from the normal update check. This is important going forward, to be able to act quickly when any security patch is published, so that all users have the possibility to get notified even if they don't follow announcements on the GitHub repo or on Discord. You can also disable this functionality in the settings if you want.\n\n## Other\n\n- The application style has been reworked\n- The settings menu now shows a restart button when a setting has been changed that requires a restart to apply\n- There is now an intro to scripts to provide some more information before using scripts\n- Add ability to enable agent forwarding when using the SSH-Agent for identities\n- The .rpm releases are now signed\n\n## Fixes\n\n- Fix Proxmox detection not working when not logging in as root\n- Fix tunnels not closing properly when having to be closed forcefully\n- Fix vmware integration failing when files other than .vmx were in the VM directories\n- Fix Tabby not launching properly on Windows\n- Fix SSH and docker issues with home assistant systems\n- Fix git readme not showing connections in nested children categories\n- Fix Windows Terminal Preview and Canary not being recognized\n- Fix style issues with the mocha theme\n- Fix color contrast for some themes\n- Fix system dark mode changes not being applied if they were changed while XPipe was not running\n"
  },
  {
    "path": "dist/changelog/13.4_incremental.md",
    "content": "- There's now a new setting that you can enable to open files with your text editor when double-clicking them\n- You can now input custom values for the chmod, chown, and chgrp actions in the context menu when clicking on `...`\n- Fix RDP launcher on macOS getting confused by the rename from Microsoft Remote Desktop to Windows App\n- Fix terminal autodetection mistakenly choosing Warp on macOS if another app that contained the word warp was installed\n\n"
  },
  {
    "path": "dist/changelog/14.0.md",
    "content": "XPipe 14 is the biggest rework so far and provides an improved user experience, better team features, performance and memory improvements, and fixes to many existing bugs and limitations. It will take some days until the initial rough edges are ironed out, but it will get there eventually. So please make sure to report any issues you find, even the small ones.\n\n## Team vaults + Reusable identities\n\nYou can now create reusable identities for connections instead of having to enter authentication information for each connection separately. This will also make it easier to handle any authentication changes later on, as only one config has to be changed.\n\nFurthermore, there is a new encryption mechanism for git vaults, allowing multiple users to have their own private connections and identities in a shared vault by encrypting them with the personal key of the user.\n\nYou can combine the reusable identities with the new per-user encryption. Essentially, if you mark a certain identity as being for your user only, it will be encrypted with your personal key and won't be accessible to other team users that have access to the vault without knowing your secret. Any connection that uses this per-user identity, will also be encrypted with your personal secret key, also making them only accessible to you. That way you can control who has access to which connections and login information in your team. You can of course also set identities to be global, so that all team users can utilize them.\n\nIf you have previously used a custom vault passphrase to lock your vault, this will be migrated to a user account with that passphrase as its secret. If you have not used that before, you can create your own user in the settings menu. Having multiple vault users requires the Professional plan but team vaults are available for free for two weeks after release.\n\n## Incus support\n\n- There is now support for incus\n- The newly added features for incus have also been ported to the LXD integration\n\n## Services\n\n- There is now the option to specify a URL path for services that will be appended when opened in the browser\n- You can now specify the service type instead of always having to choose between http and https when opening it\n- Services for containers can now be refreshed from a dedicated button instead of a fixed services entry, saving some vertical display space\n- Services now show better when they are active or inactive\n- The custom service creation has been moved to the top level to make it easier to locate\n\n## File transfers\n\n- You can now abort an active file transfer. You can find the button for that on the bottom right of the browser status bar\n- File transfers where the target write fails due to permissions issues or missing disk space are now better cancelled\n\n## Git vault\n\n- XPipe will now commit a dummy private key to your repository to make your git provider potentially detect any leaks of your repository contents\n- Any keys committed to the repository will now be forced to LF to prevent issues with keys generated on Windows\n- XPipe will now explicitly configure the setting `pull.rebase` for the local git repository as having that set to `rebase` globally would break the git sync\n\n## Other\n\n- Improve RAM usage\n- Implement performance improvements for local shells\n- The Windows Terminal integration will now create and use its own profile to prevent certain settings from breaking the terminal integration\n- Future updates on Windows will be faster\n- There is now the option to censor all displayed contents, allowing for a more simple screensharing workflow for XPipe\n- Implement startup speed improvements\n- The file browser selected file arguments for scripts are now passed in order of selection time, allowing for more advanced scripting\n- Improve error messages when VMs could not be reached due to a custom firewall setup\n- The XPipe [Python API](https://github.com/xpipe-io/xpipe-python-api) is now featured more prominently\n- Launched terminals are now automatically focused after launch\n- Add support for Ghostty on Linux\n- The webtop docker image is now also available for arm64 platforms\n- The webtop is now also available for Kasm Workspaces at https://github.com/xpipe-io/kasm-registry\n- The Yubikey PIV and PKCS#11 SSH auth option have been made more resilient for any PATH issues\n- Add translations for Swedish, Polish, Indonesian\n- Add more docs to the password manager settings\n- Improve error message for libvirt when user was missing group permissions\n\n## Fixes\n\n- Fix password manager requests not being cached and requiring an unlock every time\n- Fix connection icon being removed when the connection is edited\n- Fix Windows updates breaking pinned shortcuts and some registry keys (This will only work in future updates from now on)\n- Fix Yubikey PIV and other PKCS#11 SSH libraries not asking for pin on macOS\n- Fix launched terminal not getting focus again after a password prompt\n- Fix some container shells not working do to some issues with /tmp\n- Fix fish shells launching as sh in the file browser terminal\n- Fix zsh terminal not launching in the current working directory in file browser\n- Fix unrecognized \\r showing up in various error messages\n- Fix sudo elevation not working properly in VMs\n- Fix permission denied errors for script files in some containers\n- Fix API not respecting category field when adding connections\n- Fix some files names that required escapes not being displayed in file browser\n- Fix special Windows files like OneDrive links not being shown in file browser\n- Fix built-in services like the Proxmox dashboard also counting for the service license limit\n- Fix titlebar on Windows 11 being overlapped in fullscreen mode\n"
  },
  {
    "path": "dist/changelog/14.1.1.md",
    "content": "XPipe 14 is the biggest rework so far and provides an improved user experience, better team features, performance and memory improvements, and fixes to many existing bugs and limitations. It will take some days until the initial rough edges are ironed out, but it will get there eventually. So please make sure to report any issues you find, even the small ones.\n\n## Team vaults + Reusable identities\n\nYou can now create reusable identities for connections instead of having to enter authentication information for each connection separately. This will also make it easier to handle any authentication changes later on, as only one config has to be changed.\n\nFurthermore, there is a new encryption mechanism for git vaults, allowing multiple users to have their own private connections and identities in a shared vault by encrypting them with the personal key of the user.\n\nYou can combine the reusable identities with the new per-user encryption. Essentially, if you mark a certain identity as being for your user only, it will be encrypted with your personal key and won't be accessible to other team users that have access to the vault without knowing your secret. Any connection that uses this per-user identity, will also be encrypted with your personal secret key, also making them only accessible to you. That way you can control who has access to which connections and login information in your team. You can of course also set identities to be global, so that all team users can utilize them.\n\nIf you have previously used a custom vault passphrase to lock your vault, this will be migrated to a user account with that passphrase as its secret. If you have not used that before, you can create your own user in the settings menu. Having multiple vault users requires the Professional plan but team vaults are available for free for two weeks after release.\n\n## Incus support\n\n- There is now support for incus\n- The newly added features for incus have also been ported to the LXD integration\n\n## Services\n\n- There is now the option to specify a URL path for services that will be appended when opened in the browser\n- You can now specify the service type instead of always having to choose between http and https when opening it\n- Services for containers can now be refreshed from a dedicated button instead of a fixed services entry, saving some vertical display space\n- Services now show better when they are active or inactive\n- The custom service creation has been moved to the top level to make it easier to locate\n\n## File transfers\n\n- You can now abort an active file transfer. You can find the button for that on the bottom right of the browser status bar\n- File transfers where the target write fails due to permissions issues or missing disk space are now better cancelled\n\n## Git vault\n\n- XPipe will now commit a dummy private key to your repository to make your git provider potentially detect any leaks of your repository contents\n- Any keys committed to the repository will now be forced to LF to prevent issues with keys generated on Windows\n- XPipe will now explicitly configure the setting `pull.rebase` for the local git repository as having that set to `rebase` globally would break the git sync\n\n## Other\n\n- Improve RAM usage\n- Implement performance improvements for local shells\n- The Windows Terminal integration will now create and use its own profile to prevent certain settings from breaking the terminal integration\n- Future updates on Windows will be faster\n- There is now the option to censor all displayed contents, allowing for a more simple screensharing workflow for XPipe\n- Implement startup speed improvements\n- The file browser selected file arguments for scripts are now passed in order of selection time, allowing for more advanced scripting\n- Improve error messages when VMs could not be reached due to a custom firewall setup\n- The XPipe [Python API](https://github.com/xpipe-io/xpipe-python-api) is now featured more prominently\n- Launched terminals are now automatically focused after launch\n- Add support for Ghostty on Linux\n- The webtop docker image is now also available for arm64 platforms\n- The webtop is now also available for Kasm Workspaces at https://github.com/xpipe-io/kasm-registry\n- The Yubikey PIV and PKCS#11 SSH auth option have been made more resilient for any PATH issues\n- Add translations for Swedish, Polish, Indonesian\n- Add more docs to the password manager settings\n- Improve error message for libvirt when user was missing group permissions\n\n## Fixes\n\n- Fix password manager requests not being cached and requiring an unlock every time\n- Fix connection icon being removed when the connection is edited\n- Fix Windows updates breaking pinned shortcuts and some registry keys (This will only work in future updates from now on)\n- Fix Yubikey PIV and other PKCS#11 SSH libraries not asking for pin on macOS\n- Fix launched terminal not getting focus again after a password prompt\n- Fix some container shells not working do to some issues with /tmp\n- Fix fish shells launching as sh in the file browser terminal\n- Fix zsh terminal not launching in the current working directory in file browser\n- Fix unrecognized \\r showing up in various error messages\n- Fix sudo elevation not working properly in VMs\n- Fix permission denied errors for script files in some containers\n- Fix API not respecting category field when adding connections\n- Fix some files names that required escapes not being displayed in file browser\n- Fix special Windows files like OneDrive links not being shown in file browser\n- Fix built-in services like the Proxmox dashboard also counting for the service license limit\n- Fix titlebar on Windows 11 being overlapped in fullscreen mode\n"
  },
  {
    "path": "dist/changelog/14.1.1_incremental.md",
    "content": "- Fix synced identities committing ssh key when switching access scope even when it was not synced yet\n"
  },
  {
    "path": "dist/changelog/14.1.md",
    "content": "XPipe 14 is the biggest rework so far and provides an improved user experience, better team features, performance and memory improvements, and fixes to many existing bugs and limitations. It will take some days until the initial rough edges are ironed out, but it will get there eventually. So please make sure to report any issues you find, even the small ones.\n\n## Team vaults + Reusable identities\n\nYou can now create reusable identities for connections instead of having to enter authentication information for each connection separately. This will also make it easier to handle any authentication changes later on, as only one config has to be changed.\n\nFurthermore, there is a new encryption mechanism for git vaults, allowing multiple users to have their own private connections and identities in a shared vault by encrypting them with the personal key of the user.\n\nYou can combine the reusable identities with the new per-user encryption. Essentially, if you mark a certain identity as being for your user only, it will be encrypted with your personal key and won't be accessible to other team users that have access to the vault without knowing your secret. Any connection that uses this per-user identity, will also be encrypted with your personal secret key, also making them only accessible to you. That way you can control who has access to which connections and login information in your team. You can of course also set identities to be global, so that all team users can utilize them.\n\nIf you have previously used a custom vault passphrase to lock your vault, this will be migrated to a user account with that passphrase as its secret. If you have not used that before, you can create your own user in the settings menu. Having multiple vault users requires the Professional plan but team vaults are available for free for two weeks after release.\n\n## Incus support\n\n- There is now support for incus\n- The newly added features for incus have also been ported to the LXD integration\n\n## Services\n\n- There is now the option to specify a URL path for services that will be appended when opened in the browser\n- You can now specify the service type instead of always having to choose between http and https when opening it\n- Services for containers can now be refreshed from a dedicated button instead of a fixed services entry, saving some vertical display space\n- Services now show better when they are active or inactive\n- The custom service creation has been moved to the top level to make it easier to locate\n\n## File transfers\n\n- You can now abort an active file transfer. You can find the button for that on the bottom right of the browser status bar\n- File transfers where the target write fails due to permissions issues or missing disk space are now better cancelled\n\n## Git vault\n\n- XPipe will now commit a dummy private key to your repository to make your git provider potentially detect any leaks of your repository contents\n- Any keys committed to the repository will now be forced to LF to prevent issues with keys generated on Windows\n- XPipe will now explicitly configure the setting `pull.rebase` for the local git repository as having that set to `rebase` globally would break the git sync\n\n## Other\n\n- Improve RAM usage\n- Implement performance improvements for local shells\n- The Windows Terminal integration will now create and use its own profile to prevent certain settings from breaking the terminal integration\n- Future updates on Windows will be faster\n- There is now the option to censor all displayed contents, allowing for a more simple screensharing workflow for XPipe\n- Implement startup speed improvements\n- The file browser selected file arguments for scripts are now passed in order of selection time, allowing for more advanced scripting\n- Improve error messages when VMs could not be reached due to a custom firewall setup\n- The XPipe [Python API](https://github.com/xpipe-io/xpipe-python-api) is now featured more prominently\n- Launched terminals are now automatically focused after launch\n- Add support for Ghostty on Linux\n- The webtop docker image is now also available for arm64 platforms\n- The webtop is now also available for Kasm Workspaces at https://github.com/xpipe-io/kasm-registry\n- The Yubikey PIV and PKCS#11 SSH auth option have been made more resilient for any PATH issues\n- Add translations for Swedish, Polish, Indonesian\n- Add more docs to the password manager settings\n- Improve error message for libvirt when user was missing group permissions\n\n## Fixes\n\n- Fix password manager requests not being cached and requiring an unlock every time\n- Fix connection icon being removed when the connection is edited\n- Fix Windows updates breaking pinned shortcuts and some registry keys (This will only work in future updates from now on)\n- Fix Yubikey PIV and other PKCS#11 SSH libraries not asking for pin on macOS\n- Fix launched terminal not getting focus again after a password prompt\n- Fix some container shells not working do to some issues with /tmp\n- Fix fish shells launching as sh in the file browser terminal\n- Fix zsh terminal not launching in the current working directory in file browser\n- Fix unrecognized \\r showing up in various error messages\n- Fix sudo elevation not working properly in VMs\n- Fix permission denied errors for script files in some containers\n- Fix API not respecting category field when adding connections\n- Fix some files names that required escapes not being displayed in file browser\n- Fix special Windows files like OneDrive links not being shown in file browser\n- Fix built-in services like the Proxmox dashboard also counting for the service license limit\n- Fix titlebar on Windows 11 being overlapped in fullscreen mode\n"
  },
  {
    "path": "dist/changelog/14.1_incremental.md",
    "content": "- Make identities have a password and ssh key of none selected by default to avoid the need of setting that manually\n- Improve gui handling when switching between predefined and in place identities\n- Fix some Windows Terminal integrations failing due to json comments in the config file\n- Fix NullPointer on Windows systems with special characters in username\n- Fix window not starting up properly if an error occurred early\n- Fix application in docker webtop not starting automatically\n- Fix some config save operations not being executed, leading to lost data\n- Fix category organization not handling cases well where connections were moved across different categories\n- Fix moving connections also moving children even if they were already moved to another category\n- Fix some concurrent modification exceptions\n- Improve some pfsense initialization errors\n- Fix api returning error when trying to create a category and it already exists\n"
  },
  {
    "path": "dist/changelog/14.2.md",
    "content": "XPipe 14 is the biggest rework so far and provides an improved user experience, better team features, performance and memory improvements, and fixes to many existing bugs and limitations. It will take some days until the initial rough edges are ironed out, but it will get there eventually. So please make sure to report any issues you find, even the small ones.\n\n## Team vaults + Reusable identities\n\nYou can now create reusable identities for connections instead of having to enter authentication information for each connection separately. This will also make it easier to handle any authentication changes later on, as only one config has to be changed.\n\nFurthermore, there is a new encryption mechanism for git vaults, allowing multiple users to have their own private connections and identities in a shared vault by encrypting them with the personal key of the user.\n\nYou can combine the reusable identities with the new per-user encryption. Essentially, if you mark a certain identity as being for your user only, it will be encrypted with your personal key and won't be accessible to other team users that have access to the vault without knowing your secret. Any connection that uses this per-user identity, will also be encrypted with your personal secret key, also making them only accessible to you. That way you can control who has access to which connections and login information in your team. You can of course also set identities to be global, so that all team users can utilize them.\n\nIf you have previously used a custom vault passphrase to lock your vault, this will be migrated to a user account with that passphrase as its secret. If you have not used that before, you can create your own user in the settings menu. Having multiple vault users requires the Professional plan but team vaults are available for free for two weeks after release.\n\n## Incus support\n\n- There is now support for incus\n- The newly added features for incus have also been ported to the LXD integration\n\n## Services\n\n- There is now the option to specify a URL path for services that will be appended when opened in the browser\n- You can now specify the service type instead of always having to choose between http and https when opening it\n- Services for containers can now be refreshed from a dedicated button instead of a fixed services entry, saving some vertical display space\n- Services now show better when they are active or inactive\n- The custom service creation has been moved to the top level to make it easier to locate\n\n## File transfers\n\n- You can now abort an active file transfer. You can find the button for that on the bottom right of the browser status bar\n- File transfers where the target write fails due to permissions issues or missing disk space are now better cancelled\n\n## Git vault\n\n- XPipe will now commit a dummy private key to your repository to make your git provider potentially detect any leaks of your repository contents\n- Any keys committed to the repository will now be forced to LF to prevent issues with keys generated on Windows\n- XPipe will now explicitly configure the setting `pull.rebase` for the local git repository as having that set to `rebase` globally would break the git sync\n\n## Other\n\n- Improve RAM usage\n- Implement performance improvements for local shells\n- The Windows Terminal integration will now create and use its own profile to prevent certain settings from breaking the terminal integration\n- Future updates on Windows will be faster\n- There is now the option to censor all displayed contents, allowing for a more simple screensharing workflow for XPipe\n- Implement startup speed improvements\n- The file browser selected file arguments for scripts are now passed in order of selection time, allowing for more advanced scripting\n- Improve error messages when VMs could not be reached due to a custom firewall setup\n- The XPipe [Python API](https://github.com/xpipe-io/xpipe-python-api) is now featured more prominently\n- Launched terminals are now automatically focused after launch\n- Add support for Ghostty on Linux\n- The webtop docker image is now also available for arm64 platforms\n- The webtop is now also available for Kasm Workspaces at https://github.com/xpipe-io/kasm-registry\n- The Yubikey PIV and PKCS#11 SSH auth option have been made more resilient for any PATH issues\n- Add translations for Swedish, Polish, Indonesian\n- Add more docs to the password manager settings\n- Improve error message for libvirt when user was missing group permissions\n\n## Fixes\n\n- Fix password manager requests not being cached and requiring an unlock every time\n- Fix connection icon being removed when the connection is edited\n- Fix Windows updates breaking pinned shortcuts and some registry keys (This will only work in future updates from now on)\n- Fix Yubikey PIV and other PKCS#11 SSH libraries not asking for pin on macOS\n- Fix launched terminal not getting focus again after a password prompt\n- Fix some container shells not working do to some issues with /tmp\n- Fix fish shells launching as sh in the file browser terminal\n- Fix zsh terminal not launching in the current working directory in file browser\n- Fix unrecognized \\r showing up in various error messages\n- Fix sudo elevation not working properly in VMs\n- Fix permission denied errors for script files in some containers\n- Fix API not respecting category field when adding connections\n- Fix some files names that required escapes not being displayed in file browser\n- Fix special Windows files like OneDrive links not being shown in file browser\n- Fix built-in services like the Proxmox dashboard also counting for the service license limit\n- Fix titlebar on Windows 11 being overlapped in fullscreen mode\n"
  },
  {
    "path": "dist/changelog/14.2_incremental.md",
    "content": "- There is now a new custom open action for services instead of just the http browser open actions.\n  This allows you to run a custom command after a service tunnel is started and launch custom commands with the locally tunneled address as inputs.\n- Add support for Wave terminal on all platforms\n- Fix ssh RemoteCommand option ignore to support OpenSSH versions prior to 7.6\n- Fix some system state display not being updated sometimes\n- Fix license expiry message showing up on every launch\n- Fix performance issue with connection state updates\n- Fix errors when closing window while loading\n- Fix rare concurrent modification exception when updating connection list\n- Fix icons only having the default color\n"
  },
  {
    "path": "dist/changelog/15.0.1.md",
    "content": "XPipe 15 comes with many new features, performance improvements, and many bug fixes. Note that the installation data layout has been changed and executables have been moved around. This will break some shortcuts and the old restart functionality after an update. So if you're updating from within XPipe, it won't automatically restart for this update.\n\n## Tailscale SSH support\n\nYou can now connect to devices in your tailnet via Tailscale SSH and your locally installed tailscale command-line client. This integration supports multiple accounts as well to switch between different tailnets.\n\n## Custom icons\n\nYou can now add custom icons to use for your connections. This implementation replaces the old model of shipping the icons from https://github.com/selfhst/icons along XPipe. Instead, you can now dynamically add sources of icons. This can either be a local directory or a remote git repository that can be cloned and pulled by xpipe. XPipe will pick up any .svg files in there, rasterize them to cached .pngs, and display them in XPipe. As default icon sources, it will still come with https://github.com/selfhst/icons, but now it can fetch these icons at runtime. If you are using the git vault, you can also add icons to a synced directory in your git vault to have access to them on all systems.\n\nYour existing custom icons set for connections are not lost, it just requires you to first update the icons and then restart XPipe.\n\n## Package manager repositories\n\nThere is now an apt repository available at https://apt.xpipe.io and an rpm repository available at https://rpm.xpipe.io. You can add them as sources to apt or your rpm-based package manager. This allows you to also install and upgrade xpipe via your native package manager instead of using the built-in self-updater. \n\n## New docs\n\nThere is a new documentation site https://docs.xpipe.io. The goal is to expand this over time to provide proper documentation for many features. If you're looking for documentation for a certain feature, let me know.\n\n## Other\n\n- Rework application styling\n- Improve performance when having many connections and categories\n- Add new action to run scripts in the file browser and show their output without having to open a terminal\n- The Homelab/Pro preview for new features is now handled automatically, you don't have to enable it anymore\n- You can now import saved PuTTY sessions on a system when searching for available connections. This also works for KiTTY\n- The custom service command opener will now use \\$PORT instead of \\$ADDRESS to allow for the use of commands that have a separate port argument\n- Add support for Gnome Console and Ptyxis Terminal\n\n## Fixes\n\n- Fix user interface not being responsive for a few seconds after launch\n- Fix VSCode open actions not showing if code executable was not in PATH\n- Fix startup failure on Windows systems when vcredist140.dll was missing\n- Fix various issues with shells to Android systems\n- Fix issues on Linux systems where language en_US.UTF-8 was not available\n- Fix docked file browser terminal staying ahead of other windows even if XPipe loses focus\n- Fix permission denied errors on terminal launch when file system had noexec flag set\n- Fix git synced vault keys not working on other systems\n- Fix double sudo prompt when elevating to root in file browser\n- Fix file browser shift selection not marking selected files\n- Fix file browser yellow keyboard focus indicator showing after typing path\n- Fix ssh service tunnel sometimes failing with a timeout on close\n- Fix modal dialogs flickering a bit\n- Fix some icons resetting on updates\n- Fix desktop shortcuts not launching actions properly\n- Fix teleport integration failing for newer teleport versions\n- Fix MobaXterm integration not working correctly\n- Fix git sync SSH key password always prompting, even if it is specified in place\n- Fix creation dialog for scripts and identities still referring to the name as connection name\n- Fix password prompts for tunneled VM SSH connections showing wrong hostname as localhost\n- Fix Windows Terminal start up failing if it was the first time that it was launched on the system\n- Fix some translations not updating when changing display language\n- Fix custom service open command not working properly with PowerShell\n- Fix shortcut actions not running when daemon had to be started first\n- Fix browser directory list entering endless loop if directory contained symlink to itself\n- Fix certain actions like RDP failing when XPipe was launched from pwsh\n- Fix terminal selection defaulting to Wave if no other terminal is found\n- Fix vault version incompatibility notice only offering to disable the git sync\n- Fix several cases where adding/deleting vault users would corrupt vault data\n- Fix vault user encryption settings not updating properly\n- Fix shell init scripts being run multiple times when background shell session was active\n- Fix restart button not working for custom workspace locations\n- Fix git readme list updating each time when using a different vault user\n- Fix installation type detection being wrong when using installer and portable installation side by side\n"
  },
  {
    "path": "dist/changelog/15.0.1_incremental.md",
    "content": "- Fix custom icons for a connection sometimes not showing due to a loading race condition\n- Fix NullPointer when launching a terminal for a new connection while any scripts were enabled\n- Fix NullPointer for old VM SSH connections when no X11 forward was set\n- Fix terminal loading animation not displaying correctly with PowerShell as local shell\n- Fix some broken links\n"
  },
  {
    "path": "dist/changelog/15.0.2.md",
    "content": "XPipe 15 comes with many new features, performance improvements, and many bug fixes. Note that the installation data layout has been changed and executables have been moved around. This will break some shortcuts and the old restart functionality after an update. So if you're updating from within XPipe, it won't automatically restart for this update.\n\n## Tailscale SSH support\n\nYou can now connect to devices in your tailnet via Tailscale SSH and your locally installed tailscale command-line client. This integration supports multiple accounts as well to switch between different tailnets.\n\n## Custom icons\n\nYou can now add custom icons to use for your connections. This implementation replaces the old model of shipping the icons from https://github.com/selfhst/icons along XPipe. Instead, you can now dynamically add sources of icons. This can either be a local directory or a remote git repository that can be cloned and pulled by xpipe. XPipe will pick up any .svg files in there, rasterize them to cached .pngs, and display them in XPipe. As default icon sources, it will still come with https://github.com/selfhst/icons, but now it can fetch these icons at runtime. If you are using the git vault, you can also add icons to a synced directory in your git vault to have access to them on all systems.\n\nYour existing custom icons set for connections are not lost, it just requires you to first update the icons and then restart XPipe.\n\n## Package manager repositories\n\nThere is now an apt repository available at https://apt.xpipe.io and an rpm repository available at https://rpm.xpipe.io. You can add them as sources to apt or your rpm-based package manager. This allows you to also install and upgrade xpipe via your native package manager instead of using the built-in self-updater. \n\n## New docs\n\nThere is a new documentation site https://docs.xpipe.io. The goal is to expand this over time to provide proper documentation for many features. If you're looking for documentation for a certain feature, let me know.\n\n## Other\n\n- Rework application styling\n- Improve performance when having many connections and categories\n- Add new action to run scripts in the file browser and show their output without having to open a terminal\n- The Homelab/Pro preview for new features is now handled automatically, you don't have to enable it anymore\n- You can now import saved PuTTY sessions on a system when searching for available connections. This also works for KiTTY\n- The custom service command opener will now use \\$PORT instead of \\$ADDRESS to allow for the use of commands that have a separate port argument\n- Add support for Gnome Console and Ptyxis Terminal\n\n## Fixes\n\n- Fix user interface not being responsive for a few seconds after launch\n- Fix VSCode open actions not showing if code executable was not in PATH\n- Fix startup failure on Windows systems when vcredist140.dll was missing\n- Fix various issues with shells to Android systems\n- Fix issues on Linux systems where language en_US.UTF-8 was not available\n- Fix docked file browser terminal staying ahead of other windows even if XPipe loses focus\n- Fix permission denied errors on terminal launch when file system had noexec flag set\n- Fix git synced vault keys not working on other systems\n- Fix double sudo prompt when elevating to root in file browser\n- Fix file browser shift selection not marking selected files\n- Fix file browser yellow keyboard focus indicator showing after typing path\n- Fix ssh service tunnel sometimes failing with a timeout on close\n- Fix modal dialogs flickering a bit\n- Fix some icons resetting on updates\n- Fix desktop shortcuts not launching actions properly\n- Fix teleport integration failing for newer teleport versions\n- Fix MobaXterm integration not working correctly\n- Fix git sync SSH key password always prompting, even if it is specified in place\n- Fix creation dialog for scripts and identities still referring to the name as connection name\n- Fix password prompts for tunneled VM SSH connections showing wrong hostname as localhost\n- Fix Windows Terminal start up failing if it was the first time that it was launched on the system\n- Fix some translations not updating when changing display language\n- Fix custom service open command not working properly with PowerShell\n- Fix shortcut actions not running when daemon had to be started first\n- Fix browser directory list entering endless loop if directory contained symlink to itself\n- Fix certain actions like RDP failing when XPipe was launched from pwsh\n- Fix terminal selection defaulting to Wave if no other terminal is found\n- Fix vault version incompatibility notice only offering to disable the git sync\n- Fix several cases where adding/deleting vault users would corrupt vault data\n- Fix vault user encryption settings not updating properly\n- Fix shell init scripts being run multiple times when background shell session was active\n- Fix restart button not working for custom workspace locations\n- Fix git readme list updating each time when using a different vault user\n- Fix installation type detection being wrong when using installer and portable installation side by side\n"
  },
  {
    "path": "dist/changelog/15.0.2_incremental.md",
    "content": "- Fix NullPointers in shell environments when no explicit shell type was set\n- Fix shell environment script creation failing when no explicit shell type was set\n- Fix some outdated translations\n"
  },
  {
    "path": "dist/changelog/15.0.md",
    "content": "XPipe 15 comes with many new features, performance improvements, and many bug fixes. Note that the installation data layout has been changed and executables have been moved around. This will break some shortcuts and the old restart functionality after an update. So if you're updating from within XPipe, it won't automatically restart for this update.\n\n## Tailscale SSH support\n\nYou can now connect to devices in your tailnet via Tailscale SSH and your locally installed tailscale command-line client. This integration supports multiple accounts as well to switch between different tailnets.\n\n## Custom icons\n\nYou can now add custom icons to use for your connections. This implementation replaces the old model of shipping the icons from https://github.com/selfhst/icons along XPipe. Instead, you can now dynamically add sources of icons. This can either be a local directory or a remote git repository that can be cloned and pulled by xpipe. XPipe will pick up any .svg files in there, rasterize them to cached .pngs, and display them in XPipe. As default icon sources, it will still come with https://github.com/selfhst/icons, but now it can fetch these icons at runtime. If you are using the git vault, you can also add icons to a synced directory in your git vault to have access to them on all systems.\n\nYour existing custom icons set for connections are not lost, it just requires you to first update the icons and then restart XPipe.\n\n## Package manager repositories\n\nThere is now an apt repository available at https://apt.xpipe.io and an rpm repository available at https://rpm.xpipe.io. You can add them as sources to apt or your rpm-based package manager. This allows you to also install and upgrade xpipe via your native package manager instead of using the built-in self-updater. \n\n## New docs\n\nThere is a new documentation site https://docs.xpipe.io. The goal is to expand this over time to provide proper documentation for many features. If you're looking for documentation for a certain feature, let me know.\n\n## Other\n\n- Rework application styling\n- Improve performance when having many connections and categories\n- Add new action to run scripts in the file browser and show their output without having to open a terminal\n- The Homelab/Pro preview for new features is now handled automatically, you don't have to enable it anymore\n- You can now import saved PuTTY sessions on a system when searching for available connections. This also works for KiTTY\n- The custom service command opener will now use \\$PORT instead of \\$ADDRESS to allow for the use of commands that have a separate port argument\n- Add support for Gnome Console and Ptyxis Terminal\n\n## Fixes\n\n- Fix user interface not being responsive for a few seconds after launch\n- Fix VSCode open actions not showing if code executable was not in PATH\n- Fix startup failure on Windows systems when vcredist140.dll was missing\n- Fix various issues with shells to Android systems\n- Fix issues on Linux systems where language en_US.UTF-8 was not available\n- Fix docked file browser terminal staying ahead of other windows even if XPipe loses focus\n- Fix permission denied errors on terminal launch when file system had noexec flag set\n- Fix git synced vault keys not working on other systems\n- Fix double sudo prompt when elevating to root in file browser\n- Fix file browser shift selection not marking selected files\n- Fix file browser yellow keyboard focus indicator showing after typing path\n- Fix ssh service tunnel sometimes failing with a timeout on close\n- Fix modal dialogs flickering a bit\n- Fix some icons resetting on updates\n- Fix desktop shortcuts not launching actions properly\n- Fix teleport integration failing for newer teleport versions\n- Fix MobaXterm integration not working correctly\n- Fix git sync SSH key password always prompting, even if it is specified in place\n- Fix creation dialog for scripts and identities still referring to the name as connection name\n- Fix password prompts for tunneled VM SSH connections showing wrong hostname as localhost\n- Fix Windows Terminal start up failing if it was the first time that it was launched on the system\n- Fix some translations not updating when changing display language\n- Fix custom service open command not working properly with PowerShell\n- Fix shortcut actions not running when daemon had to be started first\n- Fix browser directory list entering endless loop if directory contained symlink to itself\n- Fix certain actions like RDP failing when XPipe was launched from pwsh\n- Fix terminal selection defaulting to Wave if no other terminal is found\n- Fix vault version incompatibility notice only offering to disable the git sync\n- Fix several cases where adding/deleting vault users would corrupt vault data\n- Fix vault user encryption settings not updating properly\n- Fix shell init scripts being run multiple times when background shell session was active\n- Fix restart button not working for custom workspace locations\n- Fix git readme list updating each time when using a different vault user\n- Fix installation type detection being wrong when using installer and portable installation side by side\n"
  },
  {
    "path": "dist/changelog/15.1.md",
    "content": "XPipe 15 comes with many new features, performance improvements, and many bug fixes. Note that the installation data layout has been changed and executables have been moved around. This will break some shortcuts and the old restart functionality after an update. So if you're updating from within XPipe, it won't automatically restart for this update.\n\n## Tailscale SSH support\n\nYou can now connect to devices in your tailnet via Tailscale SSH and your locally installed tailscale command-line client. This integration supports multiple accounts as well to switch between different tailnets.\n\n## Custom icons\n\nYou can now add custom icons to use for your connections. This implementation replaces the old model of shipping the icons from https://github.com/selfhst/icons along XPipe. Instead, you can now dynamically add sources of icons. This can either be a local directory or a remote git repository that can be cloned and pulled by xpipe. XPipe will pick up any .svg files in there, rasterize them to cached .pngs, and display them in XPipe. As default icon sources, it will still come with https://github.com/selfhst/icons, but now it can fetch these icons at runtime. If you are using the git vault, you can also add icons to a synced directory in your git vault to have access to them on all systems.\n\nYour existing custom icons set for connections are not lost, it just requires you to first update the icons and then restart XPipe.\n\n## Package manager repositories\n\nThere is now an apt repository available at https://apt.xpipe.io and an rpm repository available at https://rpm.xpipe.io. You can add them as sources to apt or your rpm-based package manager. This allows you to also install and upgrade xpipe via your native package manager instead of using the built-in self-updater. \n\n## New docs\n\nThere is a new documentation site https://docs.xpipe.io. The goal is to expand this over time to provide proper documentation for many features. If you're looking for documentation for a certain feature, let me know.\n\n## Other\n\n- Rework application styling\n- Improve performance when having many connections and categories\n- Add new action to run scripts in the file browser and show their output without having to open a terminal\n- The Homelab/Pro preview for new features is now handled automatically, you don't have to enable it anymore\n- You can now import saved PuTTY sessions on a system when searching for available connections. This also works for KiTTY\n- The custom service command opener will now use \\$PORT instead of \\$ADDRESS to allow for the use of commands that have a separate port argument\n- Add support for Gnome Console and Ptyxis Terminal\n\n## Fixes\n\n- Fix user interface not being responsive for a few seconds after launch\n- Fix VSCode open actions not showing if code executable was not in PATH\n- Fix startup failure on Windows systems when vcredist140.dll was missing\n- Fix various issues with shells to Android systems\n- Fix issues on Linux systems where language en_US.UTF-8 was not available\n- Fix docked file browser terminal staying ahead of other windows even if XPipe loses focus\n- Fix permission denied errors on terminal launch when file system had noexec flag set\n- Fix git synced vault keys not working on other systems\n- Fix double sudo prompt when elevating to root in file browser\n- Fix file browser shift selection not marking selected files\n- Fix file browser yellow keyboard focus indicator showing after typing path\n- Fix ssh service tunnel sometimes failing with a timeout on close\n- Fix modal dialogs flickering a bit\n- Fix some icons resetting on updates\n- Fix desktop shortcuts not launching actions properly\n- Fix teleport integration failing for newer teleport versions\n- Fix MobaXterm integration not working correctly\n- Fix git sync SSH key password always prompting, even if it is specified in place\n- Fix creation dialog for scripts and identities still referring to the name as connection name\n- Fix password prompts for tunneled VM SSH connections showing wrong hostname as localhost\n- Fix Windows Terminal start up failing if it was the first time that it was launched on the system\n- Fix some translations not updating when changing display language\n- Fix custom service open command not working properly with PowerShell\n- Fix shortcut actions not running when daemon had to be started first\n- Fix browser directory list entering endless loop if directory contained symlink to itself\n- Fix certain actions like RDP failing when XPipe was launched from pwsh\n- Fix terminal selection defaulting to Wave if no other terminal is found\n- Fix vault version incompatibility notice only offering to disable the git sync\n- Fix several cases where adding/deleting vault users would corrupt vault data\n- Fix vault user encryption settings not updating properly\n- Fix shell init scripts being run multiple times when background shell session was active\n- Fix restart button not working for custom workspace locations\n- Fix git readme list updating each time when using a different vault user\n- Fix installation type detection being wrong when using installer and portable installation side by side\n"
  },
  {
    "path": "dist/changelog/15.1_incremental.md",
    "content": "- Improve webtop handling of terminals, editors, and rdp clients. This includes many new additions and fixes for preinstalled tools and the desktop environment. There is also now more webtop documentation at [https://docs.xpipe.io/guide/webtop](https://docs.xpipe.io/guide/webtop)\n- Fix tailscale port being overwritten by local SSH config files\n- Fix tailscale macOS app store version not getting recognized\n- Fix gpg signing not working with git vault on Windows when the git bash gpg was used\n- Fix file browser list not updating properly when setting login details for a connection\n- Fix git sync for data files failing for large files\n- Fix agent forwarding being selectable for git sync SSH identity\n- Fix NullPointer when a referenced predefined identity is not found\n- Fix category arrows being offset on Linux\n- Add support to automatically fill passwords in remmina for tunneled RDP connections\n- Add support for xfreerdp on Linux\n- Add support for uxterm, the unicode version of xterm\n"
  },
  {
    "path": "dist/changelog/15.2.md",
    "content": "XPipe 15 comes with many new features, performance improvements, and many bug fixes. Note that the installation data layout has been changed and executables have been moved around. This will break some shortcuts and the old restart functionality after an update. So if you're updating from within XPipe, it won't automatically restart for this update.\n\n## Tailscale SSH support\n\nYou can now connect to devices in your tailnet via Tailscale SSH and your locally installed tailscale command-line client. This integration supports multiple accounts as well to switch between different tailnets.\n\n## Custom icons\n\nYou can now add custom icons to use for your connections. This implementation replaces the old model of shipping the icons from https://github.com/selfhst/icons along XPipe. Instead, you can now dynamically add sources of icons. This can either be a local directory or a remote git repository that can be cloned and pulled by xpipe. XPipe will pick up any .svg files in there, rasterize them to cached .pngs, and display them in XPipe. As default icon sources, it will still come with https://github.com/selfhst/icons, but now it can fetch these icons at runtime. If you are using the git vault, you can also add icons to a synced directory in your git vault to have access to them on all systems.\n\nYour existing custom icons set for connections are not lost, it just requires you to first update the icons and then restart XPipe.\n\n## Package manager repositories\n\nThere is now an apt repository available at https://apt.xpipe.io and an rpm repository available at https://rpm.xpipe.io. You can add them as sources to apt or your rpm-based package manager. This allows you to also install and upgrade xpipe via your native package manager instead of using the built-in self-updater. \n\n## New docs\n\nThere is a new documentation site https://docs.xpipe.io. The goal is to expand this over time to provide proper documentation for many features. If you're looking for documentation for a certain feature, let me know.\n\n## Other\n\n- Rework application styling\n- Improve performance when having many connections and categories\n- Add new action to run scripts in the file browser and show their output without having to open a terminal\n- The Homelab/Pro preview for new features is now handled automatically, you don't have to enable it anymore\n- You can now import saved PuTTY sessions on a system when searching for available connections. This also works for KiTTY\n- The custom service command opener will now use \\$PORT instead of \\$ADDRESS to allow for the use of commands that have a separate port argument\n- Add support for Gnome Console and Ptyxis Terminal\n\n## Fixes\n\n- Fix user interface not being responsive for a few seconds after launch\n- Fix VSCode open actions not showing if code executable was not in PATH\n- Fix startup failure on Windows systems when vcredist140.dll was missing\n- Fix various issues with shells to Android systems\n- Fix issues on Linux systems where language en_US.UTF-8 was not available\n- Fix docked file browser terminal staying ahead of other windows even if XPipe loses focus\n- Fix permission denied errors on terminal launch when file system had noexec flag set\n- Fix git synced vault keys not working on other systems\n- Fix double sudo prompt when elevating to root in file browser\n- Fix file browser shift selection not marking selected files\n- Fix file browser yellow keyboard focus indicator showing after typing path\n- Fix ssh service tunnel sometimes failing with a timeout on close\n- Fix modal dialogs flickering a bit\n- Fix some icons resetting on updates\n- Fix desktop shortcuts not launching actions properly\n- Fix teleport integration failing for newer teleport versions\n- Fix MobaXterm integration not working correctly\n- Fix git sync SSH key password always prompting, even if it is specified in place\n- Fix creation dialog for scripts and identities still referring to the name as connection name\n- Fix password prompts for tunneled VM SSH connections showing wrong hostname as localhost\n- Fix Windows Terminal start up failing if it was the first time that it was launched on the system\n- Fix some translations not updating when changing display language\n- Fix custom service open command not working properly with PowerShell\n- Fix shortcut actions not running when daemon had to be started first\n- Fix browser directory list entering endless loop if directory contained symlink to itself\n- Fix certain actions like RDP failing when XPipe was launched from pwsh\n- Fix terminal selection defaulting to Wave if no other terminal is found\n- Fix vault version incompatibility notice only offering to disable the git sync\n- Fix several cases where adding/deleting vault users would corrupt vault data\n- Fix vault user encryption settings not updating properly\n- Fix shell init scripts being run multiple times when background shell session was active\n- Fix restart button not working for custom workspace locations\n- Fix git readme list updating each time when using a different vault user\n- Fix installation type detection being wrong when using installer and portable installation side by side\n"
  },
  {
    "path": "dist/changelog/15.2_incremental.md",
    "content": "- Add ability to launch connections from the command-line with the [`xpipe launch`](https://docs.xpipe.io/guide/connection-launch#from-the-terminal) command. With the `xpipe` CLI executable in the PATH, you can run `xpipe launch <connection name>` to open a shell session in an existing terminal session\n- Add support for cursor, windsurf, and trae editor\n- Add support for cosmic-term of the new cosmic desktop environment\n- Update homepage and documentation page. There's now much more content at [https://docs.xpipe.io](https://docs.xpipe.io)\n- Fix terminal restart failing with a wrong secret message\n- Fix ssh failing on some Windows systems when userprofile contained special characters\n- Fix script context menu in hub description being misleading when compatibility could not be determined\n- Fix git sync opening a lot of empty windows on Windows in some cases\n- Fix git sync test failing with no clear message when git wasn't installed\n- Fix podman container state being wrong on refresh\n- Fix some connection states not showing when first added\n- Fix vmware connections requiring setting VM encryption password that was hidden\n- Fix update button having a misleading description\n- Fix recognition of Hetzner Storage Box systems\n- Fix tailscale translations\n"
  },
  {
    "path": "dist/changelog/15.3.md",
    "content": "XPipe 15 comes with many new features, performance improvements, and many bug fixes. Note that the installation data layout has been changed and executables have been moved around. This will break some shortcuts and the old restart functionality after an update. So if you're updating from within XPipe, it won't automatically restart for this update.\n\n## Tailscale SSH support\n\nYou can now connect to devices in your tailnet via Tailscale SSH and your locally installed tailscale command-line client. This integration supports multiple accounts as well to switch between different tailnets.\n\n## Custom icons\n\nYou can now add custom icons to use for your connections. This implementation replaces the old model of shipping the icons from https://github.com/selfhst/icons along XPipe. Instead, you can now dynamically add sources of icons. This can either be a local directory or a remote git repository that can be cloned and pulled by xpipe. XPipe will pick up any .svg files in there, rasterize them to cached .pngs, and display them in XPipe. As default icon sources, it will still come with https://github.com/selfhst/icons, but now it can fetch these icons at runtime. If you are using the git vault, you can also add icons to a synced directory in your git vault to have access to them on all systems.\n\nYour existing custom icons set for connections are not lost, it just requires you to first update the icons and then restart XPipe.\n\n## Package manager repositories\n\nThere is now an apt repository available at https://apt.xpipe.io and an rpm repository available at https://rpm.xpipe.io. You can add them as sources to apt or your rpm-based package manager. This allows you to also install and upgrade xpipe via your native package manager instead of using the built-in self-updater. \n\n## New docs\n\nThere is a new documentation site https://docs.xpipe.io. The goal is to expand this over time to provide proper documentation for many features. If you're looking for documentation for a certain feature, let me know.\n\n## Other\n\n- Rework application styling\n- Improve performance when having many connections and categories\n- Add new action to run scripts in the file browser and show their output without having to open a terminal\n- The Homelab/Pro preview for new features is now handled automatically, you don't have to enable it anymore\n- You can now import saved PuTTY sessions on a system when searching for available connections. This also works for KiTTY\n- The custom service command opener will now use \\$PORT instead of \\$ADDRESS to allow for the use of commands that have a separate port argument\n- Add support for Gnome Console and Ptyxis Terminal\n\n## Fixes\n\n- Fix user interface not being responsive for a few seconds after launch\n- Fix VSCode open actions not showing if code executable was not in PATH\n- Fix startup failure on Windows systems when vcredist140.dll was missing\n- Fix various issues with shells to Android systems\n- Fix issues on Linux systems where language en_US.UTF-8 was not available\n- Fix docked file browser terminal staying ahead of other windows even if XPipe loses focus\n- Fix permission denied errors on terminal launch when file system had noexec flag set\n- Fix git synced vault keys not working on other systems\n- Fix double sudo prompt when elevating to root in file browser\n- Fix file browser shift selection not marking selected files\n- Fix file browser yellow keyboard focus indicator showing after typing path\n- Fix ssh service tunnel sometimes failing with a timeout on close\n- Fix modal dialogs flickering a bit\n- Fix some icons resetting on updates\n- Fix desktop shortcuts not launching actions properly\n- Fix teleport integration failing for newer teleport versions\n- Fix MobaXterm integration not working correctly\n- Fix git sync SSH key password always prompting, even if it is specified in place\n- Fix creation dialog for scripts and identities still referring to the name as connection name\n- Fix password prompts for tunneled VM SSH connections showing wrong hostname as localhost\n- Fix Windows Terminal start up failing if it was the first time that it was launched on the system\n- Fix some translations not updating when changing display language\n- Fix custom service open command not working properly with PowerShell\n- Fix shortcut actions not running when daemon had to be started first\n- Fix browser directory list entering endless loop if directory contained symlink to itself\n- Fix certain actions like RDP failing when XPipe was launched from pwsh\n- Fix terminal selection defaulting to Wave if no other terminal is found\n- Fix vault version incompatibility notice only offering to disable the git sync\n- Fix several cases where adding/deleting vault users would corrupt vault data\n- Fix vault user encryption settings not updating properly\n- Fix shell init scripts being run multiple times when background shell session was active\n- Fix restart button not working for custom workspace locations\n- Fix git readme list updating each time when using a different vault user\n- Fix installation type detection being wrong when using installer and portable installation side by side\n"
  },
  {
    "path": "dist/changelog/15.3_incremental.md",
    "content": "- Fix SSH connections failing when file system the key was on did not support some operations\n- Fix tailscale connections not reflecting custom hostname change\n- Fix file transfer stop button not working if transfer was stuck already\n- Fix NullPointer for some old Proxmox VMs\n- Fix newly created identities not filling out default values when created\n- Fix custom icons having a bad contrast in dark mode by generating a light variant if needed\n- Fix some custom icons showing up even if they are blank\n- Fix dark mode detection on some Linux systems\n- Fix default display scaling factor calculation on KDE\n- Fix file creation dialog intersecting with docked terminal windows\n- Fix file creation option not always showing when right-clicking\n"
  },
  {
    "path": "dist/changelog/15.4.md",
    "content": "XPipe 15 comes with many new features, performance improvements, and many bug fixes. Note that the installation data layout has been changed and executables have been moved around. This will break some shortcuts and the old restart functionality after an update. So if you're updating from within XPipe, it won't automatically restart for this update.\n\n## Tailscale SSH support\n\nYou can now connect to devices in your tailnet via Tailscale SSH and your locally installed tailscale command-line client. This integration supports multiple accounts as well to switch between different tailnets.\n\n## Custom icons\n\nYou can now add custom icons to use for your connections. This implementation replaces the old model of shipping the icons from https://github.com/selfhst/icons along XPipe. Instead, you can now dynamically add sources of icons. This can either be a local directory or a remote git repository that can be cloned and pulled by xpipe. XPipe will pick up any .svg files in there, rasterize them to cached .pngs, and display them in XPipe. As default icon sources, it will still come with https://github.com/selfhst/icons, but now it can fetch these icons at runtime. If you are using the git vault, you can also add icons to a synced directory in your git vault to have access to them on all systems.\n\nYour existing custom icons set for connections are not lost, it just requires you to first update the icons and then restart XPipe.\n\n## Package manager repositories\n\nThere is now an apt repository available at https://apt.xpipe.io and an rpm repository available at https://rpm.xpipe.io. You can add them as sources to apt or your rpm-based package manager. This allows you to also install and upgrade xpipe via your native package manager instead of using the built-in self-updater. \n\n## New docs\n\nThere is a new documentation site https://docs.xpipe.io. The goal is to expand this over time to provide proper documentation for many features. If you're looking for documentation for a certain feature, let me know.\n\n## Other\n\n- Rework application styling\n- Improve performance when having many connections and categories\n- Add new action to run scripts in the file browser and show their output without having to open a terminal\n- The Homelab/Pro preview for new features is now handled automatically, you don't have to enable it anymore\n- You can now import saved PuTTY sessions on a system when searching for available connections. This also works for KiTTY\n- The custom service command opener will now use \\$PORT instead of \\$ADDRESS to allow for the use of commands that have a separate port argument\n- Add support for Gnome Console and Ptyxis Terminal\n\n## Fixes\n\n- Fix user interface not being responsive for a few seconds after launch\n- Fix VSCode open actions not showing if code executable was not in PATH\n- Fix startup failure on Windows systems when vcredist140.dll was missing\n- Fix various issues with shells to Android systems\n- Fix issues on Linux systems where language en_US.UTF-8 was not available\n- Fix docked file browser terminal staying ahead of other windows even if XPipe loses focus\n- Fix permission denied errors on terminal launch when file system had noexec flag set\n- Fix git synced vault keys not working on other systems\n- Fix double sudo prompt when elevating to root in file browser\n- Fix file browser shift selection not marking selected files\n- Fix file browser yellow keyboard focus indicator showing after typing path\n- Fix ssh service tunnel sometimes failing with a timeout on close\n- Fix modal dialogs flickering a bit\n- Fix some icons resetting on updates\n- Fix desktop shortcuts not launching actions properly\n- Fix teleport integration failing for newer teleport versions\n- Fix MobaXterm integration not working correctly\n- Fix git sync SSH key password always prompting, even if it is specified in place\n- Fix creation dialog for scripts and identities still referring to the name as connection name\n- Fix password prompts for tunneled VM SSH connections showing wrong hostname as localhost\n- Fix Windows Terminal start up failing if it was the first time that it was launched on the system\n- Fix some translations not updating when changing display language\n- Fix custom service open command not working properly with PowerShell\n- Fix shortcut actions not running when daemon had to be started first\n- Fix browser directory list entering endless loop if directory contained symlink to itself\n- Fix certain actions like RDP failing when XPipe was launched from pwsh\n- Fix terminal selection defaulting to Wave if no other terminal is found\n- Fix vault version incompatibility notice only offering to disable the git sync\n- Fix several cases where adding/deleting vault users would corrupt vault data\n- Fix vault user encryption settings not updating properly\n- Fix shell init scripts being run multiple times when background shell session was active\n- Fix restart button not working for custom workspace locations\n- Fix git readme list updating each time when using a different vault user\n- Fix installation type detection being wrong when using installer and portable installation side by side\n"
  },
  {
    "path": "dist/changelog/15.4_incremental.md",
    "content": "## Performance\n\nThis release mainly focuses on performance improvements across the board.\nIt especially tackles bad application performance when a lot of connections are present. \n\n## Fixes\n\n- Fix potential issues with terminals spamming restart requests, slowing down xpipe to a halt\n- Fix XPipe freezing when many too tunnels were set to start automatically on XPipe launch\n- Fix custom SSH connections failing when connection name contained some special characters\n- Fix some connections not being able to be added again after being deleted when searching for connections\n- Fix some .svg icons missing from the icon list\n- Fix some small styling issues\n"
  },
  {
    "path": "dist/changelog/15.5.md",
    "content": "XPipe 15 comes with many new features, performance improvements, and many bug fixes. Note that the installation data layout has been changed and executables have been moved around. This will break some shortcuts and the old restart functionality after an update. So if you're updating from within XPipe, it won't automatically restart for this update.\n\n## Tailscale SSH support\n\nYou can now connect to devices in your tailnet via Tailscale SSH and your locally installed tailscale command-line client. This integration supports multiple accounts as well to switch between different tailnets.\n\n## Custom icons\n\nYou can now add custom icons to use for your connections. This implementation replaces the old model of shipping the icons from https://github.com/selfhst/icons along XPipe. Instead, you can now dynamically add sources of icons. This can either be a local directory or a remote git repository that can be cloned and pulled by xpipe. XPipe will pick up any .svg files in there, rasterize them to cached .pngs, and display them in XPipe. As default icon sources, it will still come with https://github.com/selfhst/icons, but now it can fetch these icons at runtime. If you are using the git vault, you can also add icons to a synced directory in your git vault to have access to them on all systems.\n\nYour existing custom icons set for connections are not lost, it just requires you to first update the icons and then restart XPipe.\n\n## Package manager repositories\n\nThere is now an apt repository available at https://apt.xpipe.io and an rpm repository available at https://rpm.xpipe.io. You can add them as sources to apt or your rpm-based package manager. This allows you to also install and upgrade xpipe via your native package manager instead of using the built-in self-updater. \n\n## New docs\n\nThere is a new documentation site https://docs.xpipe.io. The goal is to expand this over time to provide proper documentation for many features. If you're looking for documentation for a certain feature, let me know.\n\n## Other\n\n- Rework application styling\n- Improve performance when having many connections and categories\n- Add new action to run scripts in the file browser and show their output without having to open a terminal\n- The Homelab/Pro preview for new features is now handled automatically, you don't have to enable it anymore\n- You can now import saved PuTTY sessions on a system when searching for available connections. This also works for KiTTY\n- The custom service command opener will now use \\$PORT instead of \\$ADDRESS to allow for the use of commands that have a separate port argument\n- Add support for Gnome Console and Ptyxis Terminal\n\n## Fixes\n\n- Fix user interface not being responsive for a few seconds after launch\n- Fix VSCode open actions not showing if code executable was not in PATH\n- Fix startup failure on Windows systems when vcredist140.dll was missing\n- Fix various issues with shells to Android systems\n- Fix issues on Linux systems where language en_US.UTF-8 was not available\n- Fix docked file browser terminal staying ahead of other windows even if XPipe loses focus\n- Fix permission denied errors on terminal launch when file system had noexec flag set\n- Fix git synced vault keys not working on other systems\n- Fix double sudo prompt when elevating to root in file browser\n- Fix file browser shift selection not marking selected files\n- Fix file browser yellow keyboard focus indicator showing after typing path\n- Fix ssh service tunnel sometimes failing with a timeout on close\n- Fix modal dialogs flickering a bit\n- Fix some icons resetting on updates\n- Fix desktop shortcuts not launching actions properly\n- Fix teleport integration failing for newer teleport versions\n- Fix MobaXterm integration not working correctly\n- Fix git sync SSH key password always prompting, even if it is specified in place\n- Fix creation dialog for scripts and identities still referring to the name as connection name\n- Fix password prompts for tunneled VM SSH connections showing wrong hostname as localhost\n- Fix Windows Terminal start up failing if it was the first time that it was launched on the system\n- Fix some translations not updating when changing display language\n- Fix custom service open command not working properly with PowerShell\n- Fix shortcut actions not running when daemon had to be started first\n- Fix browser directory list entering endless loop if directory contained symlink to itself\n- Fix certain actions like RDP failing when XPipe was launched from pwsh\n- Fix terminal selection defaulting to Wave if no other terminal is found\n- Fix vault version incompatibility notice only offering to disable the git sync\n- Fix several cases where adding/deleting vault users would corrupt vault data\n- Fix vault user encryption settings not updating properly\n- Fix shell init scripts being run multiple times when background shell session was active\n- Fix restart button not working for custom workspace locations\n- Fix git readme list updating each time when using a different vault user\n- Fix installation type detection being wrong when using installer and portable installation side by side\n"
  },
  {
    "path": "dist/changelog/15.5_incremental.md",
    "content": "- Fix connection hierarchy backgrounds not updating probably when expanding them\n- Fix some issues on macOS when zsh failed to start\n- Fix XPipe not falling back to sh in some cases on macOS when zsh failed to start\n- Fix vscode remote open functionality using wrong app path on macOS\n- Fix zsh module zsh/stat breaking some file browser functionality when enabled\n- Fix tailscale refresh operations failing with an out-of-bounds error in some cases\n- Fix some OS logos not showing correctly\n- Fix NullPointer when launching FreeRDP\n- Fix outdated manpages docs link\n"
  },
  {
    "path": "dist/changelog/15.6.md",
    "content": "XPipe 15 comes with many new features, performance improvements, and many bug fixes. Note that the installation data layout has been changed and executables have been moved around. This will break some shortcuts and the old restart functionality after an update. So if you're updating from within XPipe, it won't automatically restart for this update.\n\n## Tailscale SSH support\n\nYou can now connect to devices in your tailnet via Tailscale SSH and your locally installed tailscale command-line client. This integration supports multiple accounts as well to switch between different tailnets.\n\n## Custom icons\n\nYou can now add custom icons to use for your connections. This implementation replaces the old model of shipping the icons from https://github.com/selfhst/icons along XPipe. Instead, you can now dynamically add sources of icons. This can either be a local directory or a remote git repository that can be cloned and pulled by xpipe. XPipe will pick up any .svg files in there, rasterize them to cached .pngs, and display them in XPipe. As default icon sources, it will still come with https://github.com/selfhst/icons, but now it can fetch these icons at runtime. If you are using the git vault, you can also add icons to a synced directory in your git vault to have access to them on all systems.\n\nYour existing custom icons set for connections are not lost, it just requires you to first update the icons and then restart XPipe.\n\n## Package manager repositories\n\nThere is now an apt repository available at https://apt.xpipe.io and an rpm repository available at https://rpm.xpipe.io. You can add them as sources to apt or your rpm-based package manager. This allows you to also install and upgrade xpipe via your native package manager instead of using the built-in self-updater. \n\n## New docs\n\nThere is a new documentation site https://docs.xpipe.io. The goal is to expand this over time to provide proper documentation for many features. If you're looking for documentation for a certain feature, let me know.\n\n## Other\n\n- Rework application styling\n- Improve performance when having many connections and categories\n- Add new action to run scripts in the file browser and show their output without having to open a terminal\n- The Homelab/Pro preview for new features is now handled automatically, you don't have to enable it anymore\n- You can now import saved PuTTY sessions on a system when searching for available connections. This also works for KiTTY\n- The custom service command opener will now use \\$PORT instead of \\$ADDRESS to allow for the use of commands that have a separate port argument\n- Add support for Gnome Console and Ptyxis Terminal\n\n## Fixes\n\n- Fix user interface not being responsive for a few seconds after launch\n- Fix VSCode open actions not showing if code executable was not in PATH\n- Fix startup failure on Windows systems when vcredist140.dll was missing\n- Fix various issues with shells to Android systems\n- Fix issues on Linux systems where language en_US.UTF-8 was not available\n- Fix docked file browser terminal staying ahead of other windows even if XPipe loses focus\n- Fix permission denied errors on terminal launch when file system had noexec flag set\n- Fix git synced vault keys not working on other systems\n- Fix double sudo prompt when elevating to root in file browser\n- Fix file browser shift selection not marking selected files\n- Fix file browser yellow keyboard focus indicator showing after typing path\n- Fix ssh service tunnel sometimes failing with a timeout on close\n- Fix modal dialogs flickering a bit\n- Fix some icons resetting on updates\n- Fix desktop shortcuts not launching actions properly\n- Fix teleport integration failing for newer teleport versions\n- Fix MobaXterm integration not working correctly\n- Fix git sync SSH key password always prompting, even if it is specified in place\n- Fix creation dialog for scripts and identities still referring to the name as connection name\n- Fix password prompts for tunneled VM SSH connections showing wrong hostname as localhost\n- Fix Windows Terminal start up failing if it was the first time that it was launched on the system\n- Fix some translations not updating when changing display language\n- Fix custom service open command not working properly with PowerShell\n- Fix shortcut actions not running when daemon had to be started first\n- Fix browser directory list entering endless loop if directory contained symlink to itself\n- Fix certain actions like RDP failing when XPipe was launched from pwsh\n- Fix terminal selection defaulting to Wave if no other terminal is found\n- Fix vault version incompatibility notice only offering to disable the git sync\n- Fix several cases where adding/deleting vault users would corrupt vault data\n- Fix vault user encryption settings not updating properly\n- Fix shell init scripts being run multiple times when background shell session was active\n- Fix restart button not working for custom workspace locations\n- Fix git readme list updating each time when using a different vault user\n- Fix installation type detection being wrong when using installer and portable installation side by side\n"
  },
  {
    "path": "dist/changelog/15.6_incremental.md",
    "content": "- Fix some cases where the application used excessive memory\n- Fix fish shell session scripts not getting added to PATH\n- Fix window becoming unusable on Linux when changing appearance settings while window was maximized\n- Fix various rare OutOfBounds exceptions that would break the GUI layout\n- Fix StackOverflow for certain sudo elevation requests for shell environments\n- Fix terminal launches failing for alpine LXD/incus containers\n- Fix NullPointer when pressing undo in a filter text field\n- Fix NumberFormatException when launching some xshell connections\n"
  },
  {
    "path": "dist/changelog/15.7.1.md",
    "content": "XPipe 15 comes with many new features, performance improvements, and many bug fixes. Note that the installation data layout has been changed and executables have been moved around. This will break some shortcuts and the old restart functionality after an update. So if you're updating from within XPipe, it won't automatically restart for this update.\n\n## Tailscale SSH support\n\nYou can now connect to devices in your tailnet via Tailscale SSH and your locally installed tailscale command-line client. This integration supports multiple accounts as well to switch between different tailnets.\n\n## Custom icons\n\nYou can now add custom icons to use for your connections. This implementation replaces the old model of shipping the icons from https://github.com/selfhst/icons along XPipe. Instead, you can now dynamically add sources of icons. This can either be a local directory or a remote git repository that can be cloned and pulled by xpipe. XPipe will pick up any .svg files in there, rasterize them to cached .pngs, and display them in XPipe. As default icon sources, it will still come with https://github.com/selfhst/icons, but now it can fetch these icons at runtime. If you are using the git vault, you can also add icons to a synced directory in your git vault to have access to them on all systems.\n\nYour existing custom icons set for connections are not lost, it just requires you to first update the icons and then restart XPipe.\n\n## Package manager repositories\n\nThere is now an apt repository available at https://apt.xpipe.io and an rpm repository available at https://rpm.xpipe.io. You can add them as sources to apt or your rpm-based package manager. This allows you to also install and upgrade xpipe via your native package manager instead of using the built-in self-updater. \n\n## New docs\n\nThere is a new documentation site https://docs.xpipe.io. The goal is to expand this over time to provide proper documentation for many features. If you're looking for documentation for a certain feature, let me know.\n\n## Other\n\n- Rework application styling\n- Improve performance when having many connections and categories\n- Add new action to run scripts in the file browser and show their output without having to open a terminal\n- The Homelab/Pro preview for new features is now handled automatically, you don't have to enable it anymore\n- You can now import saved PuTTY sessions on a system when searching for available connections. This also works for KiTTY\n- The custom service command opener will now use \\$PORT instead of \\$ADDRESS to allow for the use of commands that have a separate port argument\n- Add support for Gnome Console and Ptyxis Terminal\n\n## Fixes\n\n- Fix user interface not being responsive for a few seconds after launch\n- Fix VSCode open actions not showing if code executable was not in PATH\n- Fix startup failure on Windows systems when vcredist140.dll was missing\n- Fix various issues with shells to Android systems\n- Fix issues on Linux systems where language en_US.UTF-8 was not available\n- Fix docked file browser terminal staying ahead of other windows even if XPipe loses focus\n- Fix permission denied errors on terminal launch when file system had noexec flag set\n- Fix git synced vault keys not working on other systems\n- Fix double sudo prompt when elevating to root in file browser\n- Fix file browser shift selection not marking selected files\n- Fix file browser yellow keyboard focus indicator showing after typing path\n- Fix ssh service tunnel sometimes failing with a timeout on close\n- Fix modal dialogs flickering a bit\n- Fix some icons resetting on updates\n- Fix desktop shortcuts not launching actions properly\n- Fix teleport integration failing for newer teleport versions\n- Fix MobaXterm integration not working correctly\n- Fix git sync SSH key password always prompting, even if it is specified in place\n- Fix creation dialog for scripts and identities still referring to the name as connection name\n- Fix password prompts for tunneled VM SSH connections showing wrong hostname as localhost\n- Fix Windows Terminal start up failing if it was the first time that it was launched on the system\n- Fix some translations not updating when changing display language\n- Fix custom service open command not working properly with PowerShell\n- Fix shortcut actions not running when daemon had to be started first\n- Fix browser directory list entering endless loop if directory contained symlink to itself\n- Fix certain actions like RDP failing when XPipe was launched from pwsh\n- Fix terminal selection defaulting to Wave if no other terminal is found\n- Fix vault version incompatibility notice only offering to disable the git sync\n- Fix several cases where adding/deleting vault users would corrupt vault data\n- Fix vault user encryption settings not updating properly\n- Fix shell init scripts being run multiple times when background shell session was active\n- Fix restart button not working for custom workspace locations\n- Fix git readme list updating each time when using a different vault user\n- Fix installation type detection being wrong when using installer and portable installation side by side\n"
  },
  {
    "path": "dist/changelog/15.7.1_incremental.md",
    "content": "## Performance\n\nA severe performance regression was accidentally introduced in the recent 15.4 release. This release fixes this issue, so you will get much better performance in this version. It is recommended that you upgrade to 15.7.\n\nWhile investigating, there were also a few other performance issues discovered that will be addressed in one of the next releases.\n\n## Changes\n\n- Add support for Warp on Windows and Linux\n- Fix right part of file browser becoming blocked after a tab is split\n- Fix tailscale refresh operations failing with an out-of-bounds error in some cases\n- Fix vmware .vmx failing to load if they had an unknown encoding\n- Fix some translations\n"
  },
  {
    "path": "dist/changelog/15.7.md",
    "content": "XPipe 15 comes with many new features, performance improvements, and many bug fixes. Note that the installation data layout has been changed and executables have been moved around. This will break some shortcuts and the old restart functionality after an update. So if you're updating from within XPipe, it won't automatically restart for this update.\n\n## Tailscale SSH support\n\nYou can now connect to devices in your tailnet via Tailscale SSH and your locally installed tailscale command-line client. This integration supports multiple accounts as well to switch between different tailnets.\n\n## Custom icons\n\nYou can now add custom icons to use for your connections. This implementation replaces the old model of shipping the icons from https://github.com/selfhst/icons along XPipe. Instead, you can now dynamically add sources of icons. This can either be a local directory or a remote git repository that can be cloned and pulled by xpipe. XPipe will pick up any .svg files in there, rasterize them to cached .pngs, and display them in XPipe. As default icon sources, it will still come with https://github.com/selfhst/icons, but now it can fetch these icons at runtime. If you are using the git vault, you can also add icons to a synced directory in your git vault to have access to them on all systems.\n\nYour existing custom icons set for connections are not lost, it just requires you to first update the icons and then restart XPipe.\n\n## Package manager repositories\n\nThere is now an apt repository available at https://apt.xpipe.io and an rpm repository available at https://rpm.xpipe.io. You can add them as sources to apt or your rpm-based package manager. This allows you to also install and upgrade xpipe via your native package manager instead of using the built-in self-updater. \n\n## New docs\n\nThere is a new documentation site https://docs.xpipe.io. The goal is to expand this over time to provide proper documentation for many features. If you're looking for documentation for a certain feature, let me know.\n\n## Other\n\n- Rework application styling\n- Improve performance when having many connections and categories\n- Add new action to run scripts in the file browser and show their output without having to open a terminal\n- The Homelab/Pro preview for new features is now handled automatically, you don't have to enable it anymore\n- You can now import saved PuTTY sessions on a system when searching for available connections. This also works for KiTTY\n- The custom service command opener will now use \\$PORT instead of \\$ADDRESS to allow for the use of commands that have a separate port argument\n- Add support for Gnome Console and Ptyxis Terminal\n\n## Fixes\n\n- Fix user interface not being responsive for a few seconds after launch\n- Fix VSCode open actions not showing if code executable was not in PATH\n- Fix startup failure on Windows systems when vcredist140.dll was missing\n- Fix various issues with shells to Android systems\n- Fix issues on Linux systems where language en_US.UTF-8 was not available\n- Fix docked file browser terminal staying ahead of other windows even if XPipe loses focus\n- Fix permission denied errors on terminal launch when file system had noexec flag set\n- Fix git synced vault keys not working on other systems\n- Fix double sudo prompt when elevating to root in file browser\n- Fix file browser shift selection not marking selected files\n- Fix file browser yellow keyboard focus indicator showing after typing path\n- Fix ssh service tunnel sometimes failing with a timeout on close\n- Fix modal dialogs flickering a bit\n- Fix some icons resetting on updates\n- Fix desktop shortcuts not launching actions properly\n- Fix teleport integration failing for newer teleport versions\n- Fix MobaXterm integration not working correctly\n- Fix git sync SSH key password always prompting, even if it is specified in place\n- Fix creation dialog for scripts and identities still referring to the name as connection name\n- Fix password prompts for tunneled VM SSH connections showing wrong hostname as localhost\n- Fix Windows Terminal start up failing if it was the first time that it was launched on the system\n- Fix some translations not updating when changing display language\n- Fix custom service open command not working properly with PowerShell\n- Fix shortcut actions not running when daemon had to be started first\n- Fix browser directory list entering endless loop if directory contained symlink to itself\n- Fix certain actions like RDP failing when XPipe was launched from pwsh\n- Fix terminal selection defaulting to Wave if no other terminal is found\n- Fix vault version incompatibility notice only offering to disable the git sync\n- Fix several cases where adding/deleting vault users would corrupt vault data\n- Fix vault user encryption settings not updating properly\n- Fix shell init scripts being run multiple times when background shell session was active\n- Fix restart button not working for custom workspace locations\n- Fix git readme list updating each time when using a different vault user\n- Fix installation type detection being wrong when using installer and portable installation side by side\n"
  },
  {
    "path": "dist/changelog/15.7_incremental.md",
    "content": "## Performance\n\nA severe performance regression was accidentally introduced in the recent 15.4 release. This release fixes this issue, so you will get much better performance in this version. It is recommended that you upgrade to 15.7.\n\nWhile investigating, there were also a few other performance issues discovered that will be addressed in one of the next releases.\n\n## Changes\n\n- Add support for Warp on Windows and Linux\n- Fix right part of file browser becoming blocked after a tab is split\n- Fix tailscale refresh operations failing with an out-of-bounds error in some cases\n- Fix vmware .vmx failing to load if they had an unknown encoding\n- Fix some translations\n"
  },
  {
    "path": "dist/changelog/15.8.md",
    "content": "XPipe 15 comes with many new features, performance improvements, and many bug fixes. Note that the installation data layout has been changed and executables have been moved around. This will break some shortcuts and the old restart functionality after an update. So if you're updating from within XPipe, it won't automatically restart for this update.\n\n## Tailscale SSH support\n\nYou can now connect to devices in your tailnet via Tailscale SSH and your locally installed tailscale command-line client. This integration supports multiple accounts as well to switch between different tailnets.\n\n## Custom icons\n\nYou can now add custom icons to use for your connections. This implementation replaces the old model of shipping the icons from https://github.com/selfhst/icons along XPipe. Instead, you can now dynamically add sources of icons. This can either be a local directory or a remote git repository that can be cloned and pulled by xpipe. XPipe will pick up any .svg files in there, rasterize them to cached .pngs, and display them in XPipe. As default icon sources, it will still come with https://github.com/selfhst/icons, but now it can fetch these icons at runtime. If you are using the git vault, you can also add icons to a synced directory in your git vault to have access to them on all systems.\n\nYour existing custom icons set for connections are not lost, it just requires you to first update the icons and then restart XPipe.\n\n## Package manager repositories\n\nThere is now an apt repository available at https://apt.xpipe.io and an rpm repository available at https://rpm.xpipe.io. You can add them as sources to apt or your rpm-based package manager. This allows you to also install and upgrade xpipe via your native package manager instead of using the built-in self-updater. \n\n## New docs\n\nThere is a new documentation site https://docs.xpipe.io. The goal is to expand this over time to provide proper documentation for many features. If you're looking for documentation for a certain feature, let me know.\n\n## Other\n\n- Rework application styling\n- Improve performance when having many connections and categories\n- Add new action to run scripts in the file browser and show their output without having to open a terminal\n- The Homelab/Pro preview for new features is now handled automatically, you don't have to enable it anymore\n- You can now import saved PuTTY sessions on a system when searching for available connections. This also works for KiTTY\n- The custom service command opener will now use \\$PORT instead of \\$ADDRESS to allow for the use of commands that have a separate port argument\n- Add support for Gnome Console and Ptyxis Terminal\n\n## Fixes\n\n- Fix user interface not being responsive for a few seconds after launch\n- Fix VSCode open actions not showing if code executable was not in PATH\n- Fix startup failure on Windows systems when vcredist140.dll was missing\n- Fix various issues with shells to Android systems\n- Fix issues on Linux systems where language en_US.UTF-8 was not available\n- Fix docked file browser terminal staying ahead of other windows even if XPipe loses focus\n- Fix permission denied errors on terminal launch when file system had noexec flag set\n- Fix git synced vault keys not working on other systems\n- Fix double sudo prompt when elevating to root in file browser\n- Fix file browser shift selection not marking selected files\n- Fix file browser yellow keyboard focus indicator showing after typing path\n- Fix ssh service tunnel sometimes failing with a timeout on close\n- Fix modal dialogs flickering a bit\n- Fix some icons resetting on updates\n- Fix desktop shortcuts not launching actions properly\n- Fix teleport integration failing for newer teleport versions\n- Fix MobaXterm integration not working correctly\n- Fix git sync SSH key password always prompting, even if it is specified in place\n- Fix creation dialog for scripts and identities still referring to the name as connection name\n- Fix password prompts for tunneled VM SSH connections showing wrong hostname as localhost\n- Fix Windows Terminal start up failing if it was the first time that it was launched on the system\n- Fix some translations not updating when changing display language\n- Fix custom service open command not working properly with PowerShell\n- Fix shortcut actions not running when daemon had to be started first\n- Fix browser directory list entering endless loop if directory contained symlink to itself\n- Fix certain actions like RDP failing when XPipe was launched from pwsh\n- Fix terminal selection defaulting to Wave if no other terminal is found\n- Fix vault version incompatibility notice only offering to disable the git sync\n- Fix several cases where adding/deleting vault users would corrupt vault data\n- Fix vault user encryption settings not updating properly\n- Fix shell init scripts being run multiple times when background shell session was active\n- Fix restart button not working for custom workspace locations\n- Fix git readme list updating each time when using a different vault user\n- Fix installation type detection being wrong when using installer and portable installation side by side\n"
  },
  {
    "path": "dist/changelog/15.8_incremental.md",
    "content": "- Add ability to launch custom terminal-based editors in the custom editor settings. This makes it much easier to use editors like nano or vim\n- Fix PowerShell-based shell sessions freezing after some time due to a wrong $ErrorActionPreference. This issue especially broke local machine shell sessions on Windows system with PowerShell being the default shell\n- Fix VNC connections with 24-bit color depths getting rendered with switched colors\n- Fix SSH key permissions being changed to 400 even if the key had compatible permissions\n- Fix SSH askpass failing for portable installations in a directory with non-ASCII characters due to an OpenSSH bug\n- Fix random ConcurrentModificationExceptions breaking the gui layout\n- Fix file browser download box buttons being usable and potentially misleading while a download was in progress\n- Fix startup error when process information access was blocked on Windows\n- Fix SSH config connections not allowing to specify an inline identity username\n- Fix SSH config connections not applying username specified for the identity\n- Fix SSH config parse error when all connections were set to be a tsh ProxyCommand\n- Fix LXD unsupported flag errors frequently showing up when searching for connections\n- Fix various rendering issues with svg icons\n- Fix errors when pressing undo or pasting into a port integer text field\n- Fix terminal logging staying enabled after showing notice that it is not available\n- Fix connection icon selection not working well with keyboard\n- Fix some directories failing to show in file browser on Windows systems\n- Fix directories failing to open in native file manager in the webtop container\n- Fix application window always taking focus after starting up\n- Fix many chinese translations\n"
  },
  {
    "path": "dist/changelog/16.0.md",
    "content": "## Terminals\n\nThe terminal integration comes with many new features, so make sure to check out the settings menu:\n- There is now built-in support for the terminal multiplexers tmux, zellij, and screen. This is especially useful for terminals without tabbing support.\n- There is also now built-in support for custom prompts with starship, oh-my-posh, and oh-my-zsh.\n- On Windows, you now have the ability to use a WSL distribution as the terminal environment, allowing you to use the new terminal multiplexer integration seamlessly on Windows systems as well.\n\n## Password managers\n\nThe password manager integrations have been upgraded:\n- There is now support for KeePassXC (Thanks to @illnesse for the large contribution of this feature)\n- All password manager integrations have been reworked to work out of the box without configuration\n- There is now support to use password manager SSH agents more easily\n- The documentation site now contains setup instructions for all supported password managers\n- You can now unlock the xpipe vault with your password manager\n- Password manager requests are cached, fixing potential password manager overload issues when a lot of passwords are queried\n- The password $KEY identifier has been adapted to reflect the individual name/schema of the password key reference\n\n## Docker\n\nThis release introduces support for docker compose. Containers in compose projects are grouped together and can be managed all at the same time via compose project entries.\n\nThe container state information shown is also improved, always showing the container state in combination with the system information.\n\n## Batch mode\n\nThere is now a batch mode available which allows you to select multiple systems via checkboxes and perform actions for the entire batch. This can include starting/stopping, automatically adding available subconnections, or running scripts on all selected systems.\n\nYou can toggle the batch mode in the top left corner.\n\n## SSH\n\nVarious improvements were made to the SSH implementation:\n- The SSH gateway implementation has been reworked so that you can now use local SSH keys and other identities for connections with gateways\n- The VSCode SSH remote integration has been reworked to allow more connections it to be opened in vscode. It now supports essentially all simple SSH connections, custom SSH connections, SSH config connections, and VM SSH connections. This support includes gateways\n- There is now built-in support to refresh an SSO openpubkey with the opkssh tool when needed\n- When the SSH password is set to none, XPipe will no longer prompt for it anyway if the preferred auth failed\n- There is now the option to enable verbose ssh output to diagnose connection issues better\n- For VMs, you can now choose to not use the hypervisor host as SSH gateway and instead directly connect to the VM IP\n\n## Category configuration\n\nThere is now a new category configuration dialog available. You can find it by clicking on any category -> Configure. The goal of this is to unify the various available configuration options for categories, such as git sync status, color, and more.\n\nThis was added to give finer-grained control over how and when to enable scripts on systems within the category, especially now that there is now support for terminal prompts that can be automatically installed on a system. If you have production systems in a category, you can now exclude those easily from any kind of custom scripting functionality and other modifications to not touch them at all. \n\nThe available configuration options will also be expanded in future updates.\n\n## Other\n\n- Generated connection names, e.g. VM names, will now automatically update on refresh when they were changed\n- Various speed improvements for shell operations\n- Various startup speed improvements\n- You can now launch custom scripts within XPipe with a command output dialog window without having to open a terminal\n- Various installation types like the linux apt/rpm repository and homebrew installations now support automatic updates as well\n- Improve sudo file write dialog to support more permission cases\n- Handle cases where file transfer was interrupted better by not overriding the open local file on re-edit with a blank file\n- Many dialog windows have now been merged with the main window\n- The settings menu has been updated to support continuous scrolling\n- Key files synced via git are now synced as pairs if a public key is available\n- The script context menus will now use the respective icons of the script entries\n- The k8s integration will now automatically add all namespaces for the current context when searching for connections\n- The application window will now hide any unnecessary sidebars when being resized to a small width. This makes it much easier to use XPipe in a tiling window arrangement\n- The webtop has been updated to have terminal multiplexers, proper konsole tab support, disabled kwallet, and more\n- Various error messages and connection creation dialogs now contain a help link to the documentation sections\n- The Windows application will now block the shutdown until save/sync has finished, preventing vault corruption caused by a sudden system shutdown\n- RDP tunnel connections can now be configured with custom authentication and additional RDP options\n- There is now a new introduction quick setup dialog to set the most important options on startup\n- The scripts context menu now shows the respective scripts icons instead of generic ones\n- The predefined sample script selection has been updated\n- Add setting to disable HTTPs TLS verification for license activation API calls for cases where TLS traffic is decrypted in your organization\n- Clarify more git password prompts for when a token was required\n- Upgrade to GraalVM OpenJDK 24\n- Add Korean translations\n- Various community-made translation fixes\n\n## Fixes\n\n- Fix various embedded and busybox systems failing to open in file browser when essential commands like stat were missing\n- Fix Windows msi updates failing when initial installation was installed per-user with Administrator privileges (will only work for new installs)\n- Fix Windows msi updates launching XPipe as Administrator when restarting if it was a system-wide program files installation\n- Fix some dialog content shifting around on initial show\n- Fix custom service commands not launching properly with PowerShell as the local shell\n- Fix update check being influenced by the local GitHub rate limiting\n- Fix some file browser terminal dock window ordering issues\n- Fix Windows terminal launch failing if default profile was set to launch as admin\n- Fix tailscale login check not opening website on Linux\n- Fix terminal connections failing to launch for some systems with a read-only file system\n- Fix Windows Terminal launch failing if it was not added to the PATH\n- Fix some launched applications exiting on Windows if XPipe is closed\n- Fix powershell profile modules potentially breaking powershell shell environments\n- Fix terminal launch failing on Windows when connection name contained some special characters\n- Fix selfhst icons git clone not reliably working for chinese users\n- Fix application restart after update not applying current workspace directory\n"
  },
  {
    "path": "dist/changelog/16.1.md",
    "content": "## Docker\n\nThis release introduces support for docker compose. Containers in compose projects are grouped together and can be managed all at the same time via compose project entries.\n\nThe container state information shown is also improved, always showing the container state in combination with the system information.\n\n## Batch mode\n\nThere is now a batch mode available which allows you to select multiple systems via checkboxes and perform actions for the entire batch. This can include starting/stopping, automatically adding available subconnections, or running scripts on all selected systems.\n\nYou can toggle the batch mode in the top left corner.\n\n## Terminals\n\nThe terminal integration comes with many new features, so make sure to check out the settings menu:\n- There is now built-in support for the terminal multiplexers tmux, zellij, and screen. This is especially useful for terminals without tabbing support.\n- There is also now built-in support for custom prompts with starship, oh-my-posh, and oh-my-zsh.\n- On Windows, you now have the ability to use a WSL distribution as the terminal environment, allowing you to use the new terminal multiplexer integration seamlessly on Windows systems as well.\n\n## Password managers\n\nThe password manager integrations have been upgraded:\n- There is now support for KeePassXC (Thanks to @illnesse for the large contribution of this feature)\n- All password manager integrations have been reworked to work out of the box without configuration\n- There is now support to use password manager SSH agents more easily\n- The documentation site now contains setup instructions for all supported password managers\n- You can now unlock the xpipe vault with your password manager\n- Password manager requests are cached, fixing potential password manager overload issues when a lot of passwords are queried\n- The password $KEY identifier has been adapted to reflect the individual name/schema of the password key reference\n\n## SSH\n\nVarious improvements were made to the SSH implementation:\n- The SSH gateway implementation has been reworked so that you can now use local SSH keys and other identities for connections with gateways\n- The VSCode SSH remote integration has been reworked to allow more connections it to be opened in vscode. It now supports essentially all simple SSH connections, custom SSH connections, SSH config connections, and VM SSH connections. This support includes gateways\n- There is now built-in support to refresh an SSO openpubkey with the opkssh tool when needed\n- When the SSH password is set to none, XPipe will no longer prompt for it anyway if the preferred auth failed\n- There is now the option to enable verbose ssh output to diagnose connection issues better\n- For VMs, you can now choose to not use the hypervisor host as SSH gateway and instead directly connect to the VM IP\n\n## Category configuration\n\nThere is now a new category configuration dialog available. You can find it by clicking on any category -> Configure. The goal of this is to unify the various available configuration options for categories, such as git sync status, color, and more.\n\nThis was added to give finer-grained control over how and when to enable scripts on systems within the category, especially now that there is now support for terminal prompts that can be automatically installed on a system. If you have production systems in a category, you can now exclude those easily from any kind of custom scripting functionality and other modifications to not touch them at all. \n\nThe available configuration options will also be expanded in future updates.\n\n## Other\n\n- Generated connection names, e.g. VM names, will now automatically update on refresh when they were changed\n- Various speed improvements for shell operations\n- Various startup speed improvements\n- You can now launch custom scripts within XPipe with a command output dialog window without having to open a terminal\n- Various installation types like the linux apt/rpm repository and homebrew installations now support automatic updates as well\n- Improve sudo file write dialog to support more permission cases\n- Handle cases where file transfer was interrupted better by not overriding the open local file on re-edit with a blank file\n- Many dialog windows have now been merged with the main window\n- The settings menu has been updated to support continuous scrolling\n- Key files synced via git are now synced as pairs if a public key is available\n- The script context menus will now use the respective icons of the script entries\n- The k8s integration will now automatically add all namespaces for the current context when searching for connections\n- The application window will now hide any unnecessary sidebars when being resized to a small width. This makes it much easier to use XPipe in a tiling window arrangement\n- The webtop has been updated to have terminal multiplexers, proper konsole tab support, disabled kwallet, and more\n- Various error messages and connection creation dialogs now contain a help link to the documentation sections\n- The Windows application will now block the shutdown until save/sync has finished, preventing vault corruption caused by a sudden system shutdown\n- RDP tunnel connections can now be configured with custom authentication and additional RDP options\n- There is now a new introduction quick setup dialog to set the most important options on startup\n- The scripts context menu now shows the respective scripts icons instead of generic ones\n- The predefined sample script selection has been updated\n- Add setting to disable HTTPs TLS verification for license activation API calls for cases where TLS traffic is decrypted in your organization\n- Clarify more git password prompts for when a token was required\n- Upgrade to GraalVM OpenJDK 24\n- Add Korean translations\n\n## Fixes\n\n- Fix various embedded and busybox systems failing to open in file browser when essential commands like stat were missing\n- Fix Windows msi updates failing when initial installation was installed per-user with Administrator privileges (will only work for new installs)\n- Fix Windows msi updates launching XPipe as Administrator when restarting if it was a system-wide program files installation\n- Fix some dialog content shifting around on initial show\n- Fix custom service commands not launching properly with PowerShell as the local shell\n- Fix update check being influenced by the local GitHub rate limiting\n- Fix some file browser terminal dock window ordering issues\n- Fix Windows terminal launch failing if default profile was set to launch as admin\n- Fix tailscale login check not opening website on Linux\n- Fix terminal connections failing to launch for some systems with a read-only file system\n- Fix Windows Terminal launch failing if it was not added to the PATH\n- Fix some launched applications exiting on Windows if XPipe is closed\n- Fix powershell profile modules potentially breaking powershell shell environments\n- Fix terminal launch failing on Windows when connection name contained some special characters\n- Fix selfhst icons git clone not reliably working for chinese users\n- Fix application restart after update not applying current workspace directory\n"
  },
  {
    "path": "dist/changelog/16.1_incremental.md",
    "content": "- Fix StackOverflow on Windows systems with special characters in username and no available 8.3 file names\n- Fix docker refresh exception when a compose project did not have any associated files\n- Fix vscode actions showing even if vscode was not installed\n- Fix tailscale connection list being removed when tailscale daemon was stuck starting up\n- Fix tailscale error messages not being shown properly when daemon could not connect to tailscale servers\n- Fix file browser open with action not working for some files\n- Fix NullPointer when parsing invalid file browser history\n- Fix NullPointer in script context menu when having enabled a generic script\n- Fix various other NullPointers\n"
  },
  {
    "path": "dist/changelog/16.2.1.md",
    "content": "## Docker\n\nThis release introduces support for docker compose. Containers in compose projects are grouped together and can be managed all at the same time via compose project entries.\n\nThe container state information shown is also improved, always showing the container state in combination with the system information.\n\n## Batch mode\n\nThere is now a batch mode available which allows you to select multiple systems via checkboxes and perform actions for the entire batch. This can include starting/stopping, automatically adding available subconnections, or running scripts on all selected systems.\n\nYou can toggle the batch mode in the top left corner.\n\n## Terminals\n\nThe terminal integration comes with many new features, so make sure to check out the settings menu:\n- There is now built-in support for the terminal multiplexers tmux, zellij, and screen. This is especially useful for terminals without tabbing support.\n- There is also now built-in support for custom prompts with starship, oh-my-posh, and oh-my-zsh.\n- On Windows, you now have the ability to use a WSL distribution as the terminal environment, allowing you to use the new terminal multiplexer integration seamlessly on Windows systems as well.\n\n## Password managers\n\nThe password manager integrations have been upgraded:\n- There is now support for KeePassXC (Thanks to @illnesse for the large contribution of this feature)\n- All password manager integrations have been reworked to work out of the box without configuration\n- There is now support to use password manager SSH agents more easily\n- The documentation site now contains setup instructions for all supported password managers\n- You can now unlock the xpipe vault with your password manager\n- Password manager requests are cached, fixing potential password manager overload issues when a lot of passwords are queried\n- The password $KEY identifier has been adapted to reflect the individual name/schema of the password key reference\n\n## SSH\n\nVarious improvements were made to the SSH implementation:\n- The SSH gateway implementation has been reworked so that you can now use local SSH keys and other identities for connections with gateways\n- The VSCode SSH remote integration has been reworked to allow more connections it to be opened in vscode. It now supports essentially all simple SSH connections, custom SSH connections, SSH config connections, and VM SSH connections. This support includes gateways\n- There is now built-in support to refresh an SSO openpubkey with the opkssh tool when needed\n- When the SSH password is set to none, XPipe will no longer prompt for it anyway if the preferred auth failed\n- There is now the option to enable verbose ssh output to diagnose connection issues better\n- For VMs, you can now choose to not use the hypervisor host as SSH gateway and instead directly connect to the VM IP\n\n## Category configuration\n\nThere is now a new category configuration dialog available. You can find it by clicking on any category -> Configure. The goal of this is to unify the various available configuration options for categories, such as git sync status, color, and more.\n\nThis was added to give finer-grained control over how and when to enable scripts on systems within the category, especially now that there is now support for terminal prompts that can be automatically installed on a system. If you have production systems in a category, you can now exclude those easily from any kind of custom scripting functionality and other modifications to not touch them at all. \n\nThe available configuration options will also be expanded in future updates.\n\n## Other\n\n- Generated connection names, e.g. VM names, will now automatically update on refresh when they were changed\n- Various speed improvements for shell operations\n- Various startup speed improvements\n- You can now launch custom scripts within XPipe with a command output dialog window without having to open a terminal\n- Various installation types like the linux apt/rpm repository and homebrew installations now support automatic updates as well\n- Improve sudo file write dialog to support more permission cases\n- Handle cases where file transfer was interrupted better by not overriding the open local file on re-edit with a blank file\n- Many dialog windows have now been merged with the main window\n- The settings menu has been updated to support continuous scrolling\n- Key files synced via git are now synced as pairs if a public key is available\n- The script context menus will now use the respective icons of the script entries\n- The k8s integration will now automatically add all namespaces for the current context when searching for connections\n- The application window will now hide any unnecessary sidebars when being resized to a small width. This makes it much easier to use XPipe in a tiling window arrangement\n- The webtop has been updated to have terminal multiplexers, proper konsole tab support, disabled kwallet, and more\n- Various error messages and connection creation dialogs now contain a help link to the documentation sections\n- The Windows application will now block the shutdown until save/sync has finished, preventing vault corruption caused by a sudden system shutdown\n- RDP tunnel connections can now be configured with custom authentication and additional RDP options\n- There is now a new introduction quick setup dialog to set the most important options on startup\n- The scripts context menu now shows the respective scripts icons instead of generic ones\n- The predefined sample script selection has been updated\n- Add setting to disable HTTPs TLS verification for license activation API calls for cases where TLS traffic is decrypted in your organization\n- Clarify more git password prompts for when a token was required\n- Upgrade to GraalVM OpenJDK 24\n- Add Korean translations\n\n## Fixes\n\n- Fix various embedded and busybox systems failing to open in file browser when essential commands like stat were missing\n- Fix Windows msi updates failing when initial installation was installed per-user with Administrator privileges (will only work for new installs)\n- Fix Windows msi updates launching XPipe as Administrator when restarting if it was a system-wide program files installation\n- Fix some dialog content shifting around on initial show\n- Fix custom service commands not launching properly with PowerShell as the local shell\n- Fix update check being influenced by the local GitHub rate limiting\n- Fix some file browser terminal dock window ordering issues\n- Fix Windows terminal launch failing if default profile was set to launch as admin\n- Fix tailscale login check not opening website on Linux\n- Fix terminal connections failing to launch for some systems with a read-only file system\n- Fix Windows Terminal launch failing if it was not added to the PATH\n- Fix some launched applications exiting on Windows if XPipe is closed\n- Fix powershell profile modules potentially breaking powershell shell environments\n- Fix terminal launch failing on Windows when connection name contained some special characters\n- Fix selfhst icons git clone not reliably working for chinese users\n- Fix application restart after update not applying current workspace directory\n"
  },
  {
    "path": "dist/changelog/16.2.1_incremental.md",
    "content": "- Fix SSH config parsing failing for some host definitions in 16.2\n- Fix docker refresh failing for old docker versions due to the new compose integration\n- Fix k8s errors when auto-detecting subconnections when no kubectl context was set\n"
  },
  {
    "path": "dist/changelog/16.2.md",
    "content": "## Docker\n\nThis release introduces support for docker compose. Containers in compose projects are grouped together and can be managed all at the same time via compose project entries.\n\nThe container state information shown is also improved, always showing the container state in combination with the system information.\n\n## Batch mode\n\nThere is now a batch mode available which allows you to select multiple systems via checkboxes and perform actions for the entire batch. This can include starting/stopping, automatically adding available subconnections, or running scripts on all selected systems.\n\nYou can toggle the batch mode in the top left corner.\n\n## Terminals\n\nThe terminal integration comes with many new features, so make sure to check out the settings menu:\n- There is now built-in support for the terminal multiplexers tmux, zellij, and screen. This is especially useful for terminals without tabbing support.\n- There is also now built-in support for custom prompts with starship, oh-my-posh, and oh-my-zsh.\n- On Windows, you now have the ability to use a WSL distribution as the terminal environment, allowing you to use the new terminal multiplexer integration seamlessly on Windows systems as well.\n\n## Password managers\n\nThe password manager integrations have been upgraded:\n- There is now support for KeePassXC (Thanks to @illnesse for the large contribution of this feature)\n- All password manager integrations have been reworked to work out of the box without configuration\n- There is now support to use password manager SSH agents more easily\n- The documentation site now contains setup instructions for all supported password managers\n- You can now unlock the xpipe vault with your password manager\n- Password manager requests are cached, fixing potential password manager overload issues when a lot of passwords are queried\n- The password $KEY identifier has been adapted to reflect the individual name/schema of the password key reference\n\n## SSH\n\nVarious improvements were made to the SSH implementation:\n- The SSH gateway implementation has been reworked so that you can now use local SSH keys and other identities for connections with gateways\n- The VSCode SSH remote integration has been reworked to allow more connections it to be opened in vscode. It now supports essentially all simple SSH connections, custom SSH connections, SSH config connections, and VM SSH connections. This support includes gateways\n- There is now built-in support to refresh an SSO openpubkey with the opkssh tool when needed\n- When the SSH password is set to none, XPipe will no longer prompt for it anyway if the preferred auth failed\n- There is now the option to enable verbose ssh output to diagnose connection issues better\n- For VMs, you can now choose to not use the hypervisor host as SSH gateway and instead directly connect to the VM IP\n\n## Category configuration\n\nThere is now a new category configuration dialog available. You can find it by clicking on any category -> Configure. The goal of this is to unify the various available configuration options for categories, such as git sync status, color, and more.\n\nThis was added to give finer-grained control over how and when to enable scripts on systems within the category, especially now that there is now support for terminal prompts that can be automatically installed on a system. If you have production systems in a category, you can now exclude those easily from any kind of custom scripting functionality and other modifications to not touch them at all. \n\nThe available configuration options will also be expanded in future updates.\n\n## Other\n\n- Generated connection names, e.g. VM names, will now automatically update on refresh when they were changed\n- Various speed improvements for shell operations\n- Various startup speed improvements\n- You can now launch custom scripts within XPipe with a command output dialog window without having to open a terminal\n- Various installation types like the linux apt/rpm repository and homebrew installations now support automatic updates as well\n- Improve sudo file write dialog to support more permission cases\n- Handle cases where file transfer was interrupted better by not overriding the open local file on re-edit with a blank file\n- Many dialog windows have now been merged with the main window\n- The settings menu has been updated to support continuous scrolling\n- Key files synced via git are now synced as pairs if a public key is available\n- The script context menus will now use the respective icons of the script entries\n- The k8s integration will now automatically add all namespaces for the current context when searching for connections\n- The application window will now hide any unnecessary sidebars when being resized to a small width. This makes it much easier to use XPipe in a tiling window arrangement\n- The webtop has been updated to have terminal multiplexers, proper konsole tab support, disabled kwallet, and more\n- Various error messages and connection creation dialogs now contain a help link to the documentation sections\n- The Windows application will now block the shutdown until save/sync has finished, preventing vault corruption caused by a sudden system shutdown\n- RDP tunnel connections can now be configured with custom authentication and additional RDP options\n- There is now a new introduction quick setup dialog to set the most important options on startup\n- The scripts context menu now shows the respective scripts icons instead of generic ones\n- The predefined sample script selection has been updated\n- Add setting to disable HTTPs TLS verification for license activation API calls for cases where TLS traffic is decrypted in your organization\n- Clarify more git password prompts for when a token was required\n- Upgrade to GraalVM OpenJDK 24\n- Add Korean translations\n\n## Fixes\n\n- Fix various embedded and busybox systems failing to open in file browser when essential commands like stat were missing\n- Fix Windows msi updates failing when initial installation was installed per-user with Administrator privileges (will only work for new installs)\n- Fix Windows msi updates launching XPipe as Administrator when restarting if it was a system-wide program files installation\n- Fix some dialog content shifting around on initial show\n- Fix custom service commands not launching properly with PowerShell as the local shell\n- Fix update check being influenced by the local GitHub rate limiting\n- Fix some file browser terminal dock window ordering issues\n- Fix Windows terminal launch failing if default profile was set to launch as admin\n- Fix tailscale login check not opening website on Linux\n- Fix terminal connections failing to launch for some systems with a read-only file system\n- Fix Windows Terminal launch failing if it was not added to the PATH\n- Fix some launched applications exiting on Windows if XPipe is closed\n- Fix powershell profile modules potentially breaking powershell shell environments\n- Fix terminal launch failing on Windows when connection name contained some special characters\n- Fix selfhst icons git clone not reliably working for chinese users\n- Fix application restart after update not applying current workspace directory\n"
  },
  {
    "path": "dist/changelog/16.2_incremental.md",
    "content": "## Compose\n\nSeveral docker management platforms like portainer restrict external control of their compose projects. Instead of throwing confusing error messages, XPipe now detects if a compose project is managed by an external tool like portainer and displays this state for the compose project and prevents these external modifications. This also fixes cases where some compose projects were not listed by XPipe.\n\n## Fixes\n\n- Fix directory rename in file browser not working when pressing enter\n- Fix launched electron applications (e.g. vscode) not using the wayland platform if possible, resulting in worse performance\n- Fix elevation check for Administrator not working in PowerShell environments\n- Fix HyperV VMs not having the port field filled out by default\n- Fix file download causing issues with unsupported characters from another file system in file name\n- Fix NullPointer when an VM connection did not have a port set\n- Fix RDP client files using wrong temp dir\n- Fix rare StackOverflow when normalizing files\n- Fix service types not updating in display when changed\n- Fix WSL terminal environment on Windows not working without multiplexer\n- Fix terminal recommended status not being updated when multiplexer settings change\n- Fix exception when entering space in file choice text field\n- Fix various other rare exceptions\n"
  },
  {
    "path": "dist/changelog/16.3.md",
    "content": "## Docker\n\nThis release introduces support for docker compose. Containers in compose projects are grouped together and can be managed all at the same time via compose project entries.\n\nThe container state information shown is also improved, always showing the container state in combination with the system information.\n\n## Batch mode\n\nThere is now a batch mode available which allows you to select multiple systems via checkboxes and perform actions for the entire batch. This can include starting/stopping, automatically adding available subconnections, or running scripts on all selected systems.\n\nYou can toggle the batch mode in the top left corner.\n\n## Terminals\n\nThe terminal integration comes with many new features, so make sure to check out the settings menu:\n- There is now built-in support for the terminal multiplexers tmux, zellij, and screen. This is especially useful for terminals without tabbing support.\n- There is also now built-in support for custom prompts with starship, oh-my-posh, and oh-my-zsh.\n- On Windows, you now have the ability to use a WSL distribution as the terminal environment, allowing you to use the new terminal multiplexer integration seamlessly on Windows systems as well.\n\n## Password managers\n\nThe password manager integrations have been upgraded:\n- There is now support for KeePassXC (Thanks to @illnesse for the large contribution of this feature)\n- All password manager integrations have been reworked to work out of the box without configuration\n- There is now support to use password manager SSH agents more easily\n- The documentation site now contains setup instructions for all supported password managers\n- You can now unlock the xpipe vault with your password manager\n- Password manager requests are cached, fixing potential password manager overload issues when a lot of passwords are queried\n- The password $KEY identifier has been adapted to reflect the individual name/schema of the password key reference\n\n## SSH\n\nVarious improvements were made to the SSH implementation:\n- The SSH gateway implementation has been reworked so that you can now use local SSH keys and other identities for connections with gateways\n- The VSCode SSH remote integration has been reworked to allow more connections it to be opened in vscode. It now supports essentially all simple SSH connections, custom SSH connections, SSH config connections, and VM SSH connections. This support includes gateways\n- There is now built-in support to refresh an SSO openpubkey with the opkssh tool when needed\n- When the SSH password is set to none, XPipe will no longer prompt for it anyway if the preferred auth failed\n- There is now the option to enable verbose ssh output to diagnose connection issues better\n- For VMs, you can now choose to not use the hypervisor host as SSH gateway and instead directly connect to the VM IP\n\n## Category configuration\n\nThere is now a new category configuration dialog available. You can find it by clicking on any category -> Configure. The goal of this is to unify the various available configuration options for categories, such as git sync status, color, and more.\n\nThis was added to give finer-grained control over how and when to enable scripts on systems within the category, especially now that there is now support for terminal prompts that can be automatically installed on a system. If you have production systems in a category, you can now exclude those easily from any kind of custom scripting functionality and other modifications to not touch them at all. \n\nThe available configuration options will also be expanded in future updates.\n\n## Other\n\n- Generated connection names, e.g. VM names, will now automatically update on refresh when they were changed\n- Various speed improvements for shell operations\n- Various startup speed improvements\n- You can now launch custom scripts within XPipe with a command output dialog window without having to open a terminal\n- Various installation types like the linux apt/rpm repository and homebrew installations now support automatic updates as well\n- Improve sudo file write dialog to support more permission cases\n- Handle cases where file transfer was interrupted better by not overriding the open local file on re-edit with a blank file\n- Many dialog windows have now been merged with the main window\n- The settings menu has been updated to support continuous scrolling\n- Key files synced via git are now synced as pairs if a public key is available\n- The script context menus will now use the respective icons of the script entries\n- The k8s integration will now automatically add all namespaces for the current context when searching for connections\n- The application window will now hide any unnecessary sidebars when being resized to a small width. This makes it much easier to use XPipe in a tiling window arrangement\n- The webtop has been updated to have terminal multiplexers, proper konsole tab support, disabled kwallet, and more\n- Various error messages and connection creation dialogs now contain a help link to the documentation sections\n- The Windows application will now block the shutdown until save/sync has finished, preventing vault corruption caused by a sudden system shutdown\n- RDP tunnel connections can now be configured with custom authentication and additional RDP options\n- There is now a new introduction quick setup dialog to set the most important options on startup\n- The scripts context menu now shows the respective scripts icons instead of generic ones\n- The predefined sample script selection has been updated\n- Add setting to disable HTTPs TLS verification for license activation API calls for cases where TLS traffic is decrypted in your organization\n- Clarify more git password prompts for when a token was required\n- Upgrade to GraalVM OpenJDK 24\n- Add Korean translations\n\n## Fixes\n\n- Fix various embedded and busybox systems failing to open in file browser when essential commands like stat were missing\n- Fix Windows msi updates failing when initial installation was installed per-user with Administrator privileges (will only work for new installs)\n- Fix Windows msi updates launching XPipe as Administrator when restarting if it was a system-wide program files installation\n- Fix some dialog content shifting around on initial show\n- Fix custom service commands not launching properly with PowerShell as the local shell\n- Fix update check being influenced by the local GitHub rate limiting\n- Fix some file browser terminal dock window ordering issues\n- Fix Windows terminal launch failing if default profile was set to launch as admin\n- Fix tailscale login check not opening website on Linux\n- Fix terminal connections failing to launch for some systems with a read-only file system\n- Fix Windows Terminal launch failing if it was not added to the PATH\n- Fix some launched applications exiting on Windows if XPipe is closed\n- Fix powershell profile modules potentially breaking powershell shell environments\n- Fix terminal launch failing on Windows when connection name contained some special characters\n- Fix selfhst icons git clone not reliably working for chinese users\n- Fix application restart after update not applying current workspace directory\n"
  },
  {
    "path": "dist/changelog/16.3_incremental.md",
    "content": "## SSH\n\n- The shell profile loading on Linux has been reworked with the goal of better handling the SSH agent detection\n- The SSH identity none selection will now always disable using default keys like id_rsa\n- There is now a new dialog to add a host key type to a connection if none of available ones are supported by the client\n- The support for FIDO2 SSH keys on Linux and macOS has been improved:\n  - PINs are now cached by default so they only have to be entered once. Caching can be disabled in the security settings\n  - User presence confirmation requests are now shown with an icon notification in XPipe\n  - Fix FIDO2 keys not working with xpipe if they were already added to the SSH agent\n- You can now create all types of tunnels to VMs as well\n- The tunnel addresses now support IPv6 addresses\n- Add some more automatic recognition for some Mikrotik devices\n\n## Other\n\n- There's a new option to automatically close terminals on successful exit without having to press any key\n- The custom icons now dynamically check whether the colored icon variant has enough contrast in dark mode.\n  This will make icons more colorful instead of mostly using black-and-white icons in dark mode.\n  You have to refresh the icons in the icon settings for this to apply.\n- There is now a confirmation prompt when erasing file contents in the file browser\n- Add support for Enpass password manager\n- The arch AUR package now supports automatic updates via makepkg from within XPipe \n- There is now an automatic check and a new button for merge conflicts when\n  another user/instance pushed to a remote git sync repo\n- Add support for Void editor\n- The docker parent entry no longer has a refresh button to prevent confusion with multiple refresh buttons\n- The actual file names of shell session script entries are now shown on hover\n- Add ability to close connection creation dialog quickly with ESC if no changes were made\n- Improve category styling\n\n## Fixes\n\n- Fix terminal logging being broken on Linux and macOS\n- Fix some file existence checks failing on Windows 10 since the latest Windows updates\n- Fix zsh errors due to unquoted glob pattern\n- Fix slow macOS startup due to mdfind being slow\n- Fix zellij multiplexer not properly opening first tab sometimes due to timing issues\n- Fix tmux window immediately closing if connection failed\n- Fix choco updater showing available updates before they are available in choco\n- Fix choco updater not launching as admin\n- Fix updater terminal window not closing automatically on success\n- Fix script group naming in creation dialog\n- Fix various NullPointers\n"
  },
  {
    "path": "dist/changelog/16.4.1.md",
    "content": "## Docker\n\nThis release introduces support for docker compose. Containers in compose projects are grouped together and can be managed all at the same time via compose project entries.\n\nThe container state information shown is also improved, always showing the container state in combination with the system information.\n\n## Batch mode\n\nThere is now a batch mode available which allows you to select multiple systems via checkboxes and perform actions for the entire batch. This can include starting/stopping, automatically adding available subconnections, or running scripts on all selected systems.\n\nYou can toggle the batch mode in the top left corner.\n\n## Terminals\n\nThe terminal integration comes with many new features, so make sure to check out the settings menu:\n- There is now built-in support for the terminal multiplexers tmux, zellij, and screen. This is especially useful for terminals without tabbing support.\n- There is also now built-in support for custom prompts with starship, oh-my-posh, and oh-my-zsh.\n- On Windows, you now have the ability to use a WSL distribution as the terminal environment, allowing you to use the new terminal multiplexer integration seamlessly on Windows systems as well.\n\n## Password managers\n\nThe password manager integrations have been upgraded:\n- There is now support for KeePassXC (Thanks to @illnesse for the large contribution of this feature)\n- All password manager integrations have been reworked to work out of the box without configuration\n- There is now support to use password manager SSH agents more easily\n- The documentation site now contains setup instructions for all supported password managers\n- You can now unlock the xpipe vault with your password manager\n- Password manager requests are cached, fixing potential password manager overload issues when a lot of passwords are queried\n- The password $KEY identifier has been adapted to reflect the individual name/schema of the password key reference\n\n## SSH\n\nVarious improvements were made to the SSH implementation:\n- The SSH gateway implementation has been reworked so that you can now use local SSH keys and other identities for connections with gateways\n- The VSCode SSH remote integration has been reworked to allow more connections it to be opened in vscode. It now supports essentially all simple SSH connections, custom SSH connections, SSH config connections, and VM SSH connections. This support includes gateways\n- There is now built-in support to refresh an SSO openpubkey with the opkssh tool when needed\n- When the SSH password is set to none, XPipe will no longer prompt for it anyway if the preferred auth failed\n- There is now the option to enable verbose ssh output to diagnose connection issues better\n- For VMs, you can now choose to not use the hypervisor host as SSH gateway and instead directly connect to the VM IP\n\n## Category configuration\n\nThere is now a new category configuration dialog available. You can find it by clicking on any category -> Configure. The goal of this is to unify the various available configuration options for categories, such as git sync status, color, and more.\n\nThis was added to give finer-grained control over how and when to enable scripts on systems within the category, especially now that there is now support for terminal prompts that can be automatically installed on a system. If you have production systems in a category, you can now exclude those easily from any kind of custom scripting functionality and other modifications to not touch them at all. \n\nThe available configuration options will also be expanded in future updates.\n\n## Other\n\n- Generated connection names, e.g. VM names, will now automatically update on refresh when they were changed\n- Various speed improvements for shell operations\n- Various startup speed improvements\n- You can now launch custom scripts within XPipe with a command output dialog window without having to open a terminal\n- Various installation types like the linux apt/rpm repository and homebrew installations now support automatic updates as well\n- Improve sudo file write dialog to support more permission cases\n- Handle cases where file transfer was interrupted better by not overriding the open local file on re-edit with a blank file\n- Many dialog windows have now been merged with the main window\n- The settings menu has been updated to support continuous scrolling\n- Key files synced via git are now synced as pairs if a public key is available\n- The script context menus will now use the respective icons of the script entries\n- The k8s integration will now automatically add all namespaces for the current context when searching for connections\n- The application window will now hide any unnecessary sidebars when being resized to a small width. This makes it much easier to use XPipe in a tiling window arrangement\n- The webtop has been updated to have terminal multiplexers, proper konsole tab support, disabled kwallet, and more\n- Various error messages and connection creation dialogs now contain a help link to the documentation sections\n- The Windows application will now block the shutdown until save/sync has finished, preventing vault corruption caused by a sudden system shutdown\n- RDP tunnel connections can now be configured with custom authentication and additional RDP options\n- There is now a new introduction quick setup dialog to set the most important options on startup\n- The scripts context menu now shows the respective scripts icons instead of generic ones\n- The predefined sample script selection has been updated\n- Add setting to disable HTTPs TLS verification for license activation API calls for cases where TLS traffic is decrypted in your organization\n- Clarify more git password prompts for when a token was required\n- Upgrade to GraalVM OpenJDK 24\n- Add Korean translations\n\n## Fixes\n\n- Fix various embedded and busybox systems failing to open in file browser when essential commands like stat were missing\n- Fix Windows msi updates failing when initial installation was installed per-user with Administrator privileges (will only work for new installs)\n- Fix Windows msi updates launching XPipe as Administrator when restarting if it was a system-wide program files installation\n- Fix some dialog content shifting around on initial show\n- Fix custom service commands not launching properly with PowerShell as the local shell\n- Fix update check being influenced by the local GitHub rate limiting\n- Fix some file browser terminal dock window ordering issues\n- Fix Windows terminal launch failing if default profile was set to launch as admin\n- Fix tailscale login check not opening website on Linux\n- Fix terminal connections failing to launch for some systems with a read-only file system\n- Fix Windows Terminal launch failing if it was not added to the PATH\n- Fix some launched applications exiting on Windows if XPipe is closed\n- Fix powershell profile modules potentially breaking powershell shell environments\n- Fix terminal launch failing on Windows when connection name contained some special characters\n- Fix selfhst icons git clone not reliably working for chinese users\n- Fix application restart after update not applying current workspace directory\n"
  },
  {
    "path": "dist/changelog/16.4.1_incremental.md",
    "content": "- Fix key file selection throwing NullPointer for VMs in 16.4\n- Fix choco updater showing updates prior to availability\n"
  },
  {
    "path": "dist/changelog/16.4.md",
    "content": "## Docker\n\nThis release introduces support for docker compose. Containers in compose projects are grouped together and can be managed all at the same time via compose project entries.\n\nThe container state information shown is also improved, always showing the container state in combination with the system information.\n\n## Batch mode\n\nThere is now a batch mode available which allows you to select multiple systems via checkboxes and perform actions for the entire batch. This can include starting/stopping, automatically adding available subconnections, or running scripts on all selected systems.\n\nYou can toggle the batch mode in the top left corner.\n\n## Terminals\n\nThe terminal integration comes with many new features, so make sure to check out the settings menu:\n- There is now built-in support for the terminal multiplexers tmux, zellij, and screen. This is especially useful for terminals without tabbing support.\n- There is also now built-in support for custom prompts with starship, oh-my-posh, and oh-my-zsh.\n- On Windows, you now have the ability to use a WSL distribution as the terminal environment, allowing you to use the new terminal multiplexer integration seamlessly on Windows systems as well.\n\n## Password managers\n\nThe password manager integrations have been upgraded:\n- There is now support for KeePassXC (Thanks to @illnesse for the large contribution of this feature)\n- All password manager integrations have been reworked to work out of the box without configuration\n- There is now support to use password manager SSH agents more easily\n- The documentation site now contains setup instructions for all supported password managers\n- You can now unlock the xpipe vault with your password manager\n- Password manager requests are cached, fixing potential password manager overload issues when a lot of passwords are queried\n- The password $KEY identifier has been adapted to reflect the individual name/schema of the password key reference\n\n## SSH\n\nVarious improvements were made to the SSH implementation:\n- The SSH gateway implementation has been reworked so that you can now use local SSH keys and other identities for connections with gateways\n- The VSCode SSH remote integration has been reworked to allow more connections it to be opened in vscode. It now supports essentially all simple SSH connections, custom SSH connections, SSH config connections, and VM SSH connections. This support includes gateways\n- There is now built-in support to refresh an SSO openpubkey with the opkssh tool when needed\n- When the SSH password is set to none, XPipe will no longer prompt for it anyway if the preferred auth failed\n- There is now the option to enable verbose ssh output to diagnose connection issues better\n- For VMs, you can now choose to not use the hypervisor host as SSH gateway and instead directly connect to the VM IP\n\n## Category configuration\n\nThere is now a new category configuration dialog available. You can find it by clicking on any category -> Configure. The goal of this is to unify the various available configuration options for categories, such as git sync status, color, and more.\n\nThis was added to give finer-grained control over how and when to enable scripts on systems within the category, especially now that there is now support for terminal prompts that can be automatically installed on a system. If you have production systems in a category, you can now exclude those easily from any kind of custom scripting functionality and other modifications to not touch them at all. \n\nThe available configuration options will also be expanded in future updates.\n\n## Other\n\n- Generated connection names, e.g. VM names, will now automatically update on refresh when they were changed\n- Various speed improvements for shell operations\n- Various startup speed improvements\n- You can now launch custom scripts within XPipe with a command output dialog window without having to open a terminal\n- Various installation types like the linux apt/rpm repository and homebrew installations now support automatic updates as well\n- Improve sudo file write dialog to support more permission cases\n- Handle cases where file transfer was interrupted better by not overriding the open local file on re-edit with a blank file\n- Many dialog windows have now been merged with the main window\n- The settings menu has been updated to support continuous scrolling\n- Key files synced via git are now synced as pairs if a public key is available\n- The script context menus will now use the respective icons of the script entries\n- The k8s integration will now automatically add all namespaces for the current context when searching for connections\n- The application window will now hide any unnecessary sidebars when being resized to a small width. This makes it much easier to use XPipe in a tiling window arrangement\n- The webtop has been updated to have terminal multiplexers, proper konsole tab support, disabled kwallet, and more\n- Various error messages and connection creation dialogs now contain a help link to the documentation sections\n- The Windows application will now block the shutdown until save/sync has finished, preventing vault corruption caused by a sudden system shutdown\n- RDP tunnel connections can now be configured with custom authentication and additional RDP options\n- There is now a new introduction quick setup dialog to set the most important options on startup\n- The scripts context menu now shows the respective scripts icons instead of generic ones\n- The predefined sample script selection has been updated\n- Add setting to disable HTTPs TLS verification for license activation API calls for cases where TLS traffic is decrypted in your organization\n- Clarify more git password prompts for when a token was required\n- Upgrade to GraalVM OpenJDK 24\n- Add Korean translations\n\n## Fixes\n\n- Fix various embedded and busybox systems failing to open in file browser when essential commands like stat were missing\n- Fix Windows msi updates failing when initial installation was installed per-user with Administrator privileges (will only work for new installs)\n- Fix Windows msi updates launching XPipe as Administrator when restarting if it was a system-wide program files installation\n- Fix some dialog content shifting around on initial show\n- Fix custom service commands not launching properly with PowerShell as the local shell\n- Fix update check being influenced by the local GitHub rate limiting\n- Fix some file browser terminal dock window ordering issues\n- Fix Windows terminal launch failing if default profile was set to launch as admin\n- Fix tailscale login check not opening website on Linux\n- Fix terminal connections failing to launch for some systems with a read-only file system\n- Fix Windows Terminal launch failing if it was not added to the PATH\n- Fix some launched applications exiting on Windows if XPipe is closed\n- Fix powershell profile modules potentially breaking powershell shell environments\n- Fix terminal launch failing on Windows when connection name contained some special characters\n- Fix selfhst icons git clone not reliably working for chinese users\n- Fix application restart after update not applying current workspace directory\n"
  },
  {
    "path": "dist/changelog/16.4_incremental.md",
    "content": "- The actions for chmod, chown, and chgrp in the file browser now also support recursive changes for directories\n- Fix Open with ... functionality not working in file browser when using shell environments\n- Fix some cases where the docker integration still broke due to compose not being supported in old versions\n- Fix RDP launches failing in some cases due to wrong argument escaping\n- Fix icon source pull failure blocking refresh for other sources.\n  This will make custom icons work properly while offline\n- Fix potential NullPointer when using custom icon that failed to render\n- Fix slashes in script names not being escaped when files are created\n- Fix potential NullPointers for identity serialization failures\n- Fix k8s namespace names not removing quotes on Windows\n- Fix API query failing for local machine subconnections. \n  This will now require the local machine/ prefix to be included when querying connections\n- Fix dead intro link\n- Use default shell environment when opening file chooser for a system if set\n- Improve SSH agent error messages on Windows when the agent service was not available\n- Improve vault key error description\n- Improve styling at a few places\n- The documentation sources are now available at https://github.com/xpipe-io/docs if you're looking to contribute\n"
  },
  {
    "path": "dist/changelog/16.5.md",
    "content": "## Docker\n\nThis release introduces support for docker compose. Containers in compose projects are grouped together and can be managed all at the same time via compose project entries.\n\nThe container state information shown is also improved, always showing the container state in combination with the system information.\n\n## Batch mode\n\nThere is now a batch mode available which allows you to select multiple systems via checkboxes and perform actions for the entire batch. This can include starting/stopping, automatically adding available subconnections, or running scripts on all selected systems.\n\nYou can toggle the batch mode in the top left corner.\n\n## Terminals\n\nThe terminal integration comes with many new features, so make sure to check out the settings menu:\n- There is now built-in support for the terminal multiplexers tmux, zellij, and screen. This is especially useful for terminals without tabbing support.\n- There is also now built-in support for custom prompts with starship, oh-my-posh, and oh-my-zsh.\n- On Windows, you now have the ability to use a WSL distribution as the terminal environment, allowing you to use the new terminal multiplexer integration seamlessly on Windows systems as well.\n\n## Password managers\n\nThe password manager integrations have been upgraded:\n- There is now support for KeePassXC (Thanks to @illnesse for the large contribution of this feature)\n- All password manager integrations have been reworked to work out of the box without configuration\n- There is now support to use password manager SSH agents more easily\n- The documentation site now contains setup instructions for all supported password managers\n- You can now unlock the xpipe vault with your password manager\n- Password manager requests are cached, fixing potential password manager overload issues when a lot of passwords are queried\n- The password $KEY identifier has been adapted to reflect the individual name/schema of the password key reference\n\n## SSH\n\nVarious improvements were made to the SSH implementation:\n- The SSH gateway implementation has been reworked so that you can now use local SSH keys and other identities for connections with gateways\n- The VSCode SSH remote integration has been reworked to allow more connections it to be opened in vscode. It now supports essentially all simple SSH connections, custom SSH connections, SSH config connections, and VM SSH connections. This support includes gateways\n- There is now built-in support to refresh an SSO openpubkey with the opkssh tool when needed\n- When the SSH password is set to none, XPipe will no longer prompt for it anyway if the preferred auth failed\n- There is now the option to enable verbose ssh output to diagnose connection issues better\n- For VMs, you can now choose to not use the hypervisor host as SSH gateway and instead directly connect to the VM IP\n\n## Category configuration\n\nThere is now a new category configuration dialog available. You can find it by clicking on any category -> Configure. The goal of this is to unify the various available configuration options for categories, such as git sync status, color, and more.\n\nThis was added to give finer-grained control over how and when to enable scripts on systems within the category, especially now that there is now support for terminal prompts that can be automatically installed on a system. If you have production systems in a category, you can now exclude those easily from any kind of custom scripting functionality and other modifications to not touch them at all. \n\nThe available configuration options will also be expanded in future updates.\n\n## Other\n\n- Generated connection names, e.g. VM names, will now automatically update on refresh when they were changed\n- Various speed improvements for shell operations\n- Various startup speed improvements\n- You can now launch custom scripts within XPipe with a command output dialog window without having to open a terminal\n- Various installation types like the linux apt/rpm repository and homebrew installations now support automatic updates as well\n- Improve sudo file write dialog to support more permission cases\n- Handle cases where file transfer was interrupted better by not overriding the open local file on re-edit with a blank file\n- Many dialog windows have now been merged with the main window\n- The settings menu has been updated to support continuous scrolling\n- Key files synced via git are now synced as pairs if a public key is available\n- The script context menus will now use the respective icons of the script entries\n- The k8s integration will now automatically add all namespaces for the current context when searching for connections\n- The application window will now hide any unnecessary sidebars when being resized to a small width. This makes it much easier to use XPipe in a tiling window arrangement\n- The webtop has been updated to have terminal multiplexers, proper konsole tab support, disabled kwallet, and more\n- Various error messages and connection creation dialogs now contain a help link to the documentation sections\n- The Windows application will now block the shutdown until save/sync has finished, preventing vault corruption caused by a sudden system shutdown\n- RDP tunnel connections can now be configured with custom authentication and additional RDP options\n- There is now a new introduction quick setup dialog to set the most important options on startup\n- The scripts context menu now shows the respective scripts icons instead of generic ones\n- The predefined sample script selection has been updated\n- Add setting to disable HTTPs TLS verification for license activation API calls for cases where TLS traffic is decrypted in your organization\n- Clarify more git password prompts for when a token was required\n- Upgrade to GraalVM OpenJDK 24\n- Add Korean translations\n\n## Fixes\n\n- Fix various embedded and busybox systems failing to open in file browser when essential commands like stat were missing\n- Fix Windows msi updates failing when initial installation was installed per-user with Administrator privileges (will only work for new installs)\n- Fix Windows msi updates launching XPipe as Administrator when restarting if it was a system-wide program files installation\n- Fix some dialog content shifting around on initial show\n- Fix custom service commands not launching properly with PowerShell as the local shell\n- Fix update check being influenced by the local GitHub rate limiting\n- Fix some file browser terminal dock window ordering issues\n- Fix Windows terminal launch failing if default profile was set to launch as admin\n- Fix tailscale login check not opening website on Linux\n- Fix terminal connections failing to launch for some systems with a read-only file system\n- Fix Windows Terminal launch failing if it was not added to the PATH\n- Fix some launched applications exiting on Windows if XPipe is closed\n- Fix powershell profile modules potentially breaking powershell shell environments\n- Fix terminal launch failing on Windows when connection name contained some special characters\n- Fix selfhst icons git clone not reliably working for chinese users\n- Fix application restart after update not applying current workspace directory\n"
  },
  {
    "path": "dist/changelog/16.5_incremental.md",
    "content": "- Add support for psono password manager\n- Fix crash when changing clipboard while a VNC session was active\n- Fix clipboard changes not always being applied in VNC sessions\n- Fix headless msi installer force restarting system if XPipe process was running while installing\n- Fix msi close running application dialog not properly closing XPipe\n- Fix errors on Windows when users directory was moved to a different drive and username had non-ascii characters\n- Fix pwsh shell environments listing invalid file system root paths in file browser\n- Fix errors when deleting file in file browser while a filter was active\n- Fix auto-generated xpipe SSH config entries showing in SSH config connection list\n- Fix lastpass login not working\n- Fix misleading vault user deletion warning\n- Add more documentation links within the app\n- Update translations to fix mistranslations for vault-related settings\n"
  },
  {
    "path": "dist/changelog/16.6.md",
    "content": "## Docker\n\nThis release introduces support for docker compose. Containers in compose projects are grouped together and can be managed all at the same time via compose project entries.\n\nThe container state information shown is also improved, always showing the container state in combination with the system information.\n\n## Batch mode\n\nThere is now a batch mode available which allows you to select multiple systems via checkboxes and perform actions for the entire batch. This can include starting/stopping, automatically adding available subconnections, or running scripts on all selected systems.\n\nYou can toggle the batch mode in the top left corner.\n\n## Terminals\n\nThe terminal integration comes with many new features, so make sure to check out the settings menu:\n- There is now built-in support for the terminal multiplexers tmux, zellij, and screen. This is especially useful for terminals without tabbing support.\n- There is also now built-in support for custom prompts with starship, oh-my-posh, and oh-my-zsh.\n- On Windows, you now have the ability to use a WSL distribution as the terminal environment, allowing you to use the new terminal multiplexer integration seamlessly on Windows systems as well.\n\n## Password managers\n\nThe password manager integrations have been upgraded:\n- There is now support for KeePassXC (Thanks to @illnesse for the large contribution of this feature)\n- All password manager integrations have been reworked to work out of the box without configuration\n- There is now support to use password manager SSH agents more easily\n- The documentation site now contains setup instructions for all supported password managers\n- You can now unlock the xpipe vault with your password manager\n- Password manager requests are cached, fixing potential password manager overload issues when a lot of passwords are queried\n- The password $KEY identifier has been adapted to reflect the individual name/schema of the password key reference\n\n## SSH\n\nVarious improvements were made to the SSH implementation:\n- The SSH gateway implementation has been reworked so that you can now use local SSH keys and other identities for connections with gateways\n- The VSCode SSH remote integration has been reworked to allow more connections it to be opened in vscode. It now supports essentially all simple SSH connections, custom SSH connections, SSH config connections, and VM SSH connections. This support includes gateways\n- There is now built-in support to refresh an SSO openpubkey with the opkssh tool when needed\n- When the SSH password is set to none, XPipe will no longer prompt for it anyway if the preferred auth failed\n- There is now the option to enable verbose ssh output to diagnose connection issues better\n- For VMs, you can now choose to not use the hypervisor host as SSH gateway and instead directly connect to the VM IP\n\n## Category configuration\n\nThere is now a new category configuration dialog available. You can find it by clicking on any category -> Configure. The goal of this is to unify the various available configuration options for categories, such as git sync status, color, and more.\n\nThis was added to give finer-grained control over how and when to enable scripts on systems within the category, especially now that there is now support for terminal prompts that can be automatically installed on a system. If you have production systems in a category, you can now exclude those easily from any kind of custom scripting functionality and other modifications to not touch them at all. \n\nThe available configuration options will also be expanded in future updates.\n\n## Other\n\n- Generated connection names, e.g. VM names, will now automatically update on refresh when they were changed\n- Various speed improvements for shell operations\n- Various startup speed improvements\n- You can now launch custom scripts within XPipe with a command output dialog window without having to open a terminal\n- Various installation types like the linux apt/rpm repository and homebrew installations now support automatic updates as well\n- Improve sudo file write dialog to support more permission cases\n- Handle cases where file transfer was interrupted better by not overriding the open local file on re-edit with a blank file\n- Many dialog windows have now been merged with the main window\n- The settings menu has been updated to support continuous scrolling\n- Key files synced via git are now synced as pairs if a public key is available\n- The script context menus will now use the respective icons of the script entries\n- The k8s integration will now automatically add all namespaces for the current context when searching for connections\n- The application window will now hide any unnecessary sidebars when being resized to a small width. This makes it much easier to use XPipe in a tiling window arrangement\n- The webtop has been updated to have terminal multiplexers, proper konsole tab support, disabled kwallet, and more\n- Various error messages and connection creation dialogs now contain a help link to the documentation sections\n- The Windows application will now block the shutdown until save/sync has finished, preventing vault corruption caused by a sudden system shutdown\n- RDP tunnel connections can now be configured with custom authentication and additional RDP options\n- There is now a new introduction quick setup dialog to set the most important options on startup\n- The scripts context menu now shows the respective scripts icons instead of generic ones\n- The predefined sample script selection has been updated\n- Add setting to disable HTTPs TLS verification for license activation API calls for cases where TLS traffic is decrypted in your organization\n- Clarify more git password prompts for when a token was required\n- Upgrade to GraalVM OpenJDK 24\n- Add Korean translations\n\n## Fixes\n\n- Fix various embedded and busybox systems failing to open in file browser when essential commands like stat were missing\n- Fix Windows msi updates failing when initial installation was installed per-user with Administrator privileges (will only work for new installs)\n- Fix Windows msi updates launching XPipe as Administrator when restarting if it was a system-wide program files installation\n- Fix some dialog content shifting around on initial show\n- Fix custom service commands not launching properly with PowerShell as the local shell\n- Fix update check being influenced by the local GitHub rate limiting\n- Fix some file browser terminal dock window ordering issues\n- Fix Windows terminal launch failing if default profile was set to launch as admin\n- Fix tailscale login check not opening website on Linux\n- Fix terminal connections failing to launch for some systems with a read-only file system\n- Fix Windows Terminal launch failing if it was not added to the PATH\n- Fix some launched applications exiting on Windows if XPipe is closed\n- Fix powershell profile modules potentially breaking powershell shell environments\n- Fix terminal launch failing on Windows when connection name contained some special characters\n- Fix selfhst icons git clone not reliably working for chinese users\n- Fix application restart after update not applying current workspace directory\n"
  },
  {
    "path": "dist/changelog/16.6_incremental.md",
    "content": "- Add new setting to control whether the XPipe window should move into focus on any notifications or error messages\n- The state shown for tunnels now includes the port mapping, addresses, and translated status\n- Fix connection status, e.g. for tunnels, not updating after editing them\n- Fix batch mode buttons not working when the window was too small\n- Fix k8s connections being added with empty context and namespaces if none were present\n- Fix startup error on Windows with some chinese 8.3 file names\n- Fix some NullPointers\n"
  },
  {
    "path": "dist/changelog/16.7.md",
    "content": "## Docker\n\nThis release introduces support for docker compose. Containers in compose projects are grouped together and can be managed all at the same time via compose project entries.\n\nThe container state information shown is also improved, always showing the container state in combination with the system information.\n\n## Batch mode\n\nThere is now a batch mode available which allows you to select multiple systems via checkboxes and perform actions for the entire batch. This can include starting/stopping, automatically adding available subconnections, or running scripts on all selected systems.\n\nYou can toggle the batch mode in the top left corner.\n\n## Terminals\n\nThe terminal integration comes with many new features, so make sure to check out the settings menu:\n- There is now built-in support for the terminal multiplexers tmux, zellij, and screen. This is especially useful for terminals without tabbing support.\n- There is also now built-in support for custom prompts with starship, oh-my-posh, and oh-my-zsh.\n- On Windows, you now have the ability to use a WSL distribution as the terminal environment, allowing you to use the new terminal multiplexer integration seamlessly on Windows systems as well.\n\n## Password managers\n\nThe password manager integrations have been upgraded:\n- There is now support for KeePassXC (Thanks to @illnesse for the large contribution of this feature)\n- All password manager integrations have been reworked to work out of the box without configuration\n- There is now support to use password manager SSH agents more easily\n- The documentation site now contains setup instructions for all supported password managers\n- You can now unlock the xpipe vault with your password manager\n- Password manager requests are cached, fixing potential password manager overload issues when a lot of passwords are queried\n- The password $KEY identifier has been adapted to reflect the individual name/schema of the password key reference\n\n## SSH\n\nVarious improvements were made to the SSH implementation:\n- The SSH gateway implementation has been reworked so that you can now use local SSH keys and other identities for connections with gateways\n- The VSCode SSH remote integration has been reworked to allow more connections it to be opened in vscode. It now supports essentially all simple SSH connections, custom SSH connections, SSH config connections, and VM SSH connections. This support includes gateways\n- There is now built-in support to refresh an SSO openpubkey with the opkssh tool when needed\n- When the SSH password is set to none, XPipe will no longer prompt for it anyway if the preferred auth failed\n- There is now the option to enable verbose ssh output to diagnose connection issues better\n- For VMs, you can now choose to not use the hypervisor host as SSH gateway and instead directly connect to the VM IP\n\n## Category configuration\n\nThere is now a new category configuration dialog available. You can find it by clicking on any category -> Configure. The goal of this is to unify the various available configuration options for categories, such as git sync status, color, and more.\n\nThis was added to give finer-grained control over how and when to enable scripts on systems within the category, especially now that there is now support for terminal prompts that can be automatically installed on a system. If you have production systems in a category, you can now exclude those easily from any kind of custom scripting functionality and other modifications to not touch them at all. \n\nThe available configuration options will also be expanded in future updates.\n\n## Other\n\n- Generated connection names, e.g. VM names, will now automatically update on refresh when they were changed\n- Various speed improvements for shell operations\n- Various startup speed improvements\n- You can now launch custom scripts within XPipe with a command output dialog window without having to open a terminal\n- Various installation types like the linux apt/rpm repository and homebrew installations now support automatic updates as well\n- Improve sudo file write dialog to support more permission cases\n- Handle cases where file transfer was interrupted better by not overriding the open local file on re-edit with a blank file\n- Many dialog windows have now been merged with the main window\n- The settings menu has been updated to support continuous scrolling\n- Key files synced via git are now synced as pairs if a public key is available\n- The script context menus will now use the respective icons of the script entries\n- The k8s integration will now automatically add all namespaces for the current context when searching for connections\n- The application window will now hide any unnecessary sidebars when being resized to a small width. This makes it much easier to use XPipe in a tiling window arrangement\n- The webtop has been updated to have terminal multiplexers, proper konsole tab support, disabled kwallet, and more\n- Various error messages and connection creation dialogs now contain a help link to the documentation sections\n- The Windows application will now block the shutdown until save/sync has finished, preventing vault corruption caused by a sudden system shutdown\n- RDP tunnel connections can now be configured with custom authentication and additional RDP options\n- There is now a new introduction quick setup dialog to set the most important options on startup\n- The scripts context menu now shows the respective scripts icons instead of generic ones\n- The predefined sample script selection has been updated\n- Add setting to disable HTTPs TLS verification for license activation API calls for cases where TLS traffic is decrypted in your organization\n- Clarify more git password prompts for when a token was required\n- Upgrade to GraalVM OpenJDK 24\n- Add Korean translations\n\n## Fixes\n\n- Fix various embedded and busybox systems failing to open in file browser when essential commands like stat were missing\n- Fix Windows msi updates failing when initial installation was installed per-user with Administrator privileges (will only work for new installs)\n- Fix Windows msi updates launching XPipe as Administrator when restarting if it was a system-wide program files installation\n- Fix some dialog content shifting around on initial show\n- Fix custom service commands not launching properly with PowerShell as the local shell\n- Fix update check being influenced by the local GitHub rate limiting\n- Fix some file browser terminal dock window ordering issues\n- Fix Windows terminal launch failing if default profile was set to launch as admin\n- Fix tailscale login check not opening website on Linux\n- Fix terminal connections failing to launch for some systems with a read-only file system\n- Fix Windows Terminal launch failing if it was not added to the PATH\n- Fix some launched applications exiting on Windows if XPipe is closed\n- Fix powershell profile modules potentially breaking powershell shell environments\n- Fix terminal launch failing on Windows when connection name contained some special characters\n- Fix selfhst icons git clone not reliably working for chinese users\n- Fix application restart after update not applying current workspace directory\n"
  },
  {
    "path": "dist/changelog/16.7_incremental.md",
    "content": "- Compatibility fixes in preparation for schema changes in XPipe 17. This update makes sure that vaults used in future v17 installations can still be opened with v16 installations without crashing. If you are interested in trying out early versions of XPipe 17, check out https://github.com/xpipe-io/xpipe-ptb\n- Fix file browser size sorting using alphabetical order instead of actual size (Thanks to @RustyRaptor for the PR)\n- Fix connection search freezing on Ubuntu systems with LXD snap stub installed\n- Fix file browser listing not working for older PowerShell systems\n"
  },
  {
    "path": "dist/changelog/17.0.md",
    "content": "XPipe 17 is a large-scale rework of many existing parts of XPipe. It focuses on fixing many longstanding issues and limitations, so that future updates can easily bring new integrations without having to deal with that baggage.\n\nThe documentation for all the new features is already available at http://docs.xpipe.io/\n\n## Actions\n\nThere is now a new action system, which maps most UI actions to fixed schemas. This means that you can now automate almost any action that you can perform from the UI via desktop shortcuts, URLs, HTTP API calls, and more. You can configure action shortcuts with the new action configuration dialog and control how to call the action from it.\n\nFurthermore, it is also now possible to control how all of these actions are run. For production systems, for example, you can configure that all actions that perform some kind of modification, like deleting a file or running a script, have to be confirmed first. This gives you an added layer of protection to double-check any operation before actually executing it.\n\n![Actions screenshot](https://xpipe.io/assets/images/BlogPage/actions.png)\n\n[Action Documentation](https://docs.xpipe.io/guide/actions)\n\n## SSH\n\nThere is now proper support for jump servers. While gateways worked similar to jump servers, they were a different concept and did not work for cases where the ProxyJump functionality was required. You can now configure an SSH connection in its advanced settings to be treated as a jump server. This will result in XPipe using the ProxyJump syntax when this connection is used as a gateway for other SSH connections. This works for all kinds of connections, including config connections.\n\nFurthermore, the SSH implementation for devices that don't provide a full shell, e.g. embedded devices and other limited systems, has been completely reworked. This fixes many issues where connections to such systems were not possible or failed. You can now designate an SSH connection as a limited system in its advanced settings, without the previous homelab plan requirement. This will then allow you to directly launch the connection, without any issues.\n\n![SSH configuration screenshot](https://xpipe.io/assets/images/BlogPage/ssh-jump.png)\n\nServices also now support opening them for limited/embedded systems with system interaction being disabled. Instead of locally tunneling the remote service to localhost, XPipe will try to open the host URL directly. This allows you to open the web interface of embedded devices for example, which previously was not possible.\n\nOther changes:\n- The option to open a connection in VSCode remote has been expanded to also support Cursor and Windsurf\n- Active tunnels are now periodically refreshed to check if the underlying tunnel was closed\n- Fix SSH agent variables being wrong on macOS when using homebrew ssh\n- Fix automatic x11 display server forward to WSL not working on Windows\n- Fix various gpg agent configuration issues\n\n[SSH Documentation](https://docs.xpipe.io/guide/ssh)\n\n## VNC\n\nUp until now, the internal VNC implementation of XPipe did a somewhat acceptable job for most connections. However, it is not able to match dedicated VNC clients when it comes to more advanced features and authentication methods. There's simply not the development capacity to maintain all of these additional VNC features. For this reason, there is now support to also use an external VNC client with XPipe, just as with any other tool integrations.\n\n![VNC settings screenshot](https://xpipe.io/assets/images/BlogPage/vnc-list.png)\n\n[VNC Documentation](https://docs.xpipe.io/guide/vnc)\n\n## macOS 26 Tahoe\n\nXPipe adopts many of the new features of macOS 26 right away. if you are using the macOS beta, you have access to these right away.\n\nThe application window now uses the new Liquid Glass theming. The application icon has also been reworked with Liquid Glass in mind.\n\nThere's also support for the new apple containers framework, which has just been released. Searching for available connections on the local machine will make apple containers show up if you have installed the package.\n\n![macOS Tahoe screenshot](https://xpipe.io/assets/images/BlogPage/tahoe.png)\n\n## Windows ARM\n\nThere are now native Windows ARM builds. These releases are also available in choco and winget.\n\nNote that you will have to uninstall any old x64 XPipe installation for the upgrade.\n\n## File browser\n\nThe file browser has been improved in many areas:\n\n- Operations which modify a single file, e.g. a file edit, will now automatically refresh the file list to show updated changes\n- Add new file browser action to compute directory sizes\n- The transfer speed in the file browser on Windows for multiple files has been improved\n- Renaming a file now moves the caret to the end of the base file name\n- The file browser now works much better in small windows sizes\n- Fix terminal sometimes not being launched with the correct working directory of the current path\n- Fix file deletion not showing errors when the operation failed in cmd\n- Fix file renaming not working if previous rename operation was cancelled\n- Fix file transfer kill button sometimes not working when transfer was frozen\n- Fix file browser listing not working for older PowerShell systems\n\n[File Browser Documentation](https://docs.xpipe.io/guide/file-browser)\n\n## Connection hub\n\nProper functionality to organize a collection of hundreds of connections was always somewhat limited until now. There is now an additional index-based organization mechanism where you can assign and move indices of connections to have them listed at a certain place in relation to other connections. This can also be combined with the existing sorting methods like time and alphabetical sorting.\n\n![Order settings screenshot](https://xpipe.io/assets/images/BlogPage/order-menu.png)\n\nTo move a set of children connections to the top-level, you can also use the new so-called breakout categories to create subviews of nested connections.\n\n![Breakout category screenshot](https://xpipe.io/assets/images/BlogPage/category-breakout.png)\n\nFurthermore, there are also more improvements in the connection hub:\n- Renaming connection entries can now be done quickly from the context menu without having to open the configuration dialog\n- You can now set connection configurations to be frozen, meaning that the connection entry can't be modified or deleted. This is helpful for templating and team vault setups\n- When editing an incomplete connection configuration, the focus will automatically jump to the first incomplete/invalid value. This makes keyboard usage easier\n\n[Connection Hub Documentation](https://docs.xpipe.io/guide/hub)\n\n## Git vault\n\nThe git vault can now automatically resolve merge conflicts without restarts when multiple systems share the git repository. Any externally pushed commits are integrated into the connection hub instantly.\n\nFurthermore, you can now specify a git username and password in the settings menu if your local system does not have a git client with configured credentials.\n\n![Git auth screenshot](https://xpipe.io/assets/images/BlogPage/sync-auth.png)\n\n[Sync Documentation](https://docs.xpipe.io/guide/sync)\n\n## Serial\n\nThe serial connection support is no longer be considered experimental. It has been reworked, and the following issues have been fixed:\n- Fix serial configuration parameter names being cut off in the dialog\n- Connections to serial devices requiring root access are now automatically elevated\n\n![Git auth screenshot](https://xpipe.io/assets/images/BlogPage/serial.png)\n\n[Serial Documentation](https://docs.xpipe.io/guide/serial)\n\n## HTTP API\n\nThe HTTP API has been improved in various areas. The action system is integrated in it, meaning that you can call any action from the API. Furthermore, there are now more API endpoints for categories so that you have proper control over them as well. There is also now a way to encrypt and decrypt secrets via the API, allowing you to create connections with integrated secrets with it as well.\n\n[API Documentation](https://docs.xpipe.io/api)\n\n## Other\n\n- Various performance improvements\n- Password managers now support retrieving both username and password of an entry. For that, you can now create password manager identities that automatically provide the username and password\n- You can now specify an alternative user for shell environments to switch to via sudo\n- Add support for nushell\n- Add support for xonsh\n- Add support for LXTerminal, the default on Raspberry Pi OS\n- Add support to open a WinSCP session for an SSH connection\n- Teleport connections now support reusable identities\n- Changes made to /etc/sudoers files are now automatically checked with visudo and reverted if necessary\n- Terminal logging on Windows can now use a WSL terminal environment to improve logging quality\n- Terminal logs now strip any terminal escape codes\n- Add a new loading icon\n- Add note on how to fix potential blurriness on Gnome Wayland systems with high display scaling\n- You can now disable icon sources without having to remove them\n- Terminal connections now enable truecolor mode if possible\n- Add notification when a password is copied to the clipboard\n- Add support for Ghostty on macOS\n- There is now a subreddit at https://www.reddit.com/r/xpipe/\n- Update kasm workspaces image to support 1.17\n- Filter mullvad exit nodes from tailscale list\n\n## Fixes\n\n- Fix local to synced identity conversion not updating existing connections\n- Fix local to synced identity conversion not automatically adding key file to git\n- Fix local identities category being able to be synced\n- Fix pwsh connections on Linux/macOS freezing on sudo elevation\n- Fix sudo elevation not working on WSL if WSLInterop for executables was disabled\n- Fix clipboard data of other formats, e.g. files, being cleared after expiration of a copied password\n- Fix text color on hover having low contrast in some themes\n- Fix freeze after waking up the local system from a long hibernation\n- Fix application not starting up if some antivirus interfered with local socket connections\n- Fix kubectl apply changes functionality for pods not working\n- Fix custom workspaces not working on Linux\n- Fix KeePassXC snap installations not being detected\n"
  },
  {
    "path": "dist/changelog/17.1.md",
    "content": "XPipe 17 is a large-scale rework of many existing parts of XPipe. It focuses on fixing many longstanding issues and limitations, so that future updates can easily bring new integrations without having to deal with that baggage.\n\nThe documentation for all the new features is already available at http://docs.xpipe.io/\n\n## Actions\n\nThere is now a new action system, which maps most UI actions to fixed schemas. This means that you can now automate almost any action that you can perform from the UI via desktop shortcuts, URLs, HTTP API calls, and more. You can configure action shortcuts with the new action configuration dialog and control how to call the action from it.\n\nFurthermore, it is also now possible to control how all of these actions are run. For production systems, for example, you can configure that all actions that perform some kind of modification, like deleting a file or running a script, have to be confirmed first. This gives you an added layer of protection to double-check any operation before actually executing it.\n\n![Actions screenshot](https://xpipe.io/assets/images/BlogPage/actions.png)\n\n[Action Documentation](https://docs.xpipe.io/guide/actions)\n\n## SSH\n\nThere is now proper support for jump servers. While gateways worked similar to jump servers, they were a different concept and did not work for cases where the ProxyJump functionality was required. You can now configure an SSH connection in its advanced settings to be treated as a jump server. This will result in XPipe using the ProxyJump syntax when this connection is used as a gateway for other SSH connections. This works for all kinds of connections, including config connections.\n\nFurthermore, the SSH implementation for devices that don't provide a full shell, e.g. embedded devices and other limited systems, has been completely reworked. This fixes many issues where connections to such systems were not possible or failed. You can now designate an SSH connection as a limited system in its advanced settings, without the previous homelab plan requirement. This will then allow you to directly launch the connection, without any issues.\n\n![SSH configuration screenshot](https://xpipe.io/assets/images/BlogPage/ssh-jump.png)\n\nServices also now support opening them for limited/embedded systems with system interaction being disabled. Instead of locally tunneling the remote service to localhost, XPipe will try to open the host URL directly. This allows you to open the web interface of embedded devices for example, which previously was not possible.\n\nOther changes:\n- The option to open a connection in VSCode remote has been expanded to also support Cursor and Windsurf\n- Active tunnels are now periodically refreshed to check if the underlying tunnel was closed\n- Fix SSH agent variables being wrong on macOS when using homebrew ssh\n- Fix automatic x11 display server forward to WSL not working on Windows\n- Fix various gpg agent configuration issues\n\n[SSH Documentation](https://docs.xpipe.io/guide/ssh)\n\n## VNC\n\nUp until now, the internal VNC implementation of XPipe did a somewhat acceptable job for most connections. However, it is not able to match dedicated VNC clients when it comes to more advanced features and authentication methods. There's simply not the development capacity to maintain all of these additional VNC features. For this reason, there is now support to also use an external VNC client with XPipe, just as with any other tool integrations.\n\n![VNC settings screenshot](https://xpipe.io/assets/images/BlogPage/vnc-list.png)\n\n[VNC Documentation](https://docs.xpipe.io/guide/vnc)\n\n## macOS 26 Tahoe\n\nXPipe adopts many of the new features of macOS 26 right away. if you are using the macOS beta, you have access to these right away.\n\nThe application window now uses the new Liquid Glass theming. The application icon has also been reworked with Liquid Glass in mind.\n\nThere's also support for the new apple containers framework, which has just been released. Searching for available connections on the local machine will make apple containers show up if you have installed the package.\n\n![macOS Tahoe screenshot](https://xpipe.io/assets/images/BlogPage/tahoe.png)\n\n## Windows ARM\n\nThere are now native Windows ARM builds. These releases are also available in choco and winget.\n\nNote that you will have to uninstall any old x64 XPipe installation for the upgrade.\n\n## File browser\n\nThe file browser has been improved in many areas:\n\n- Operations which modify a single file, e.g. a file edit, will now automatically refresh the file list to show updated changes\n- Add new file browser action to compute directory sizes\n- The transfer speed in the file browser on Windows for multiple files has been improved\n- Renaming a file now moves the caret to the end of the base file name\n- The file browser now works much better in small windows sizes\n- Fix terminal sometimes not being launched with the correct working directory of the current path\n- Fix file deletion not showing errors when the operation failed in cmd\n- Fix file renaming not working if previous rename operation was cancelled\n- Fix file transfer kill button sometimes not working when transfer was frozen\n- Fix file browser listing not working for older PowerShell systems\n\n[File Browser Documentation](https://docs.xpipe.io/guide/file-browser)\n\n## Connection hub\n\nProper functionality to organize a collection of hundreds of connections was always somewhat limited until now. There is now an additional index-based organization mechanism where you can assign and move indices of connections to have them listed at a certain place in relation to other connections. This can also be combined with the existing sorting methods like time and alphabetical sorting.\n\n![Order settings screenshot](https://xpipe.io/assets/images/BlogPage/order-menu.png)\n\nTo move a set of children connections to the top-level, you can also use the new so-called breakout categories to create subviews of nested connections.\n\n![Breakout category screenshot](https://xpipe.io/assets/images/BlogPage/category-breakout.png)\n\nFurthermore, there are also more improvements in the connection hub:\n- Renaming connection entries can now be done quickly from the context menu without having to open the configuration dialog\n- You can now set connection configurations to be frozen, meaning that the connection entry can't be modified or deleted. This is helpful for templating and team vault setups\n- When editing an incomplete connection configuration, the focus will automatically jump to the first incomplete/invalid value. This makes keyboard usage easier\n\n[Connection Hub Documentation](https://docs.xpipe.io/guide/hub)\n\n## Git vault\n\nThe git vault can now automatically resolve merge conflicts without restarts when multiple systems share the git repository. Any externally pushed commits are integrated into the connection hub instantly.\n\nFurthermore, you can now specify a git username and password in the settings menu if your local system does not have a git client with configured credentials.\n\n![Git auth screenshot](https://xpipe.io/assets/images/BlogPage/sync-auth.png)\n\n[Sync Documentation](https://docs.xpipe.io/guide/sync)\n\n## Serial\n\nThe serial connection support is no longer be considered experimental. It has been reworked, and the following issues have been fixed:\n- Fix serial configuration parameter names being cut off in the dialog\n- Connections to serial devices requiring root access are now automatically elevated\n\n![Git auth screenshot](https://xpipe.io/assets/images/BlogPage/serial.png)\n\n[Serial Documentation](https://docs.xpipe.io/guide/serial)\n\n## HTTP API\n\nThe HTTP API has been improved in various areas. The action system is integrated in it, meaning that you can call any action from the API. Furthermore, there are now more API endpoints for categories so that you have proper control over them as well. There is also now a way to encrypt and decrypt secrets via the API, allowing you to create connections with integrated secrets with it as well.\n\n[API Documentation](https://docs.xpipe.io/api)\n\n## Other\n\n- Various performance improvements\n- Password managers now support retrieving both username and password of an entry. For that, you can now create password manager identities that automatically provide the username and password\n- You can now specify an alternative user for shell environments to switch to via sudo\n- Add support for nushell\n- Add support for xonsh\n- Add support for LXTerminal, the default on Raspberry Pi OS\n- Add support to open a WinSCP session for an SSH connection\n- Teleport connections now support reusable identities\n- Changes made to /etc/sudoers files are now automatically checked with visudo and reverted if necessary\n- Terminal logging on Windows can now use a WSL terminal environment to improve logging quality\n- Terminal logs now strip any terminal escape codes\n- Add a new loading icon\n- Add note on how to fix potential blurriness on Gnome Wayland systems with high display scaling\n- You can now disable icon sources without having to remove them\n- Terminal connections now enable truecolor mode if possible\n- Add notification when a password is copied to the clipboard\n- Add support for Ghostty on macOS\n- There is now a subreddit at https://www.reddit.com/r/xpipe/\n- Update kasm workspaces image to support 1.17\n- Filter mullvad exit nodes from tailscale list\n\n## Fixes\n\n- Fix local to synced identity conversion not updating existing connections\n- Fix local to synced identity conversion not automatically adding key file to git\n- Fix local identities category being able to be synced\n- Fix pwsh connections on Linux/macOS freezing on sudo elevation\n- Fix sudo elevation not working on WSL if WSLInterop for executables was disabled\n- Fix clipboard data of other formats, e.g. files, being cleared after expiration of a copied password\n- Fix text color on hover having low contrast in some themes\n- Fix freeze after waking up the local system from a long hibernation\n- Fix application not starting up if some antivirus interfered with local socket connections\n- Fix kubectl apply changes functionality for pods not working\n- Fix custom workspaces not working on Linux\n- Fix KeePassXC snap installations not being detected\n"
  },
  {
    "path": "dist/changelog/17.1_incremental.md",
    "content": "- Fix exception when running script for a single system\n- Fix NullPointer when launching VNC connections when tunneling is not supported\n- Fix tunnels not restarting when connection exits unexpectedly\n- Fix exception when testing custom password manager command\n- Fix winget and choco build pipeline not working\n"
  },
  {
    "path": "dist/changelog/17.2.md",
    "content": "XPipe 17 is a large-scale rework of many existing parts of XPipe. It focuses on fixing many longstanding issues and limitations, so that future updates can easily bring new integrations without having to deal with that baggage.\n\nThe documentation for all the new features is already available at http://docs.xpipe.io/\n\n## Actions\n\nThere is now a new action system, which maps most UI actions to fixed schemas. This means that you can now automate almost any action that you can perform from the UI via desktop shortcuts, URLs, HTTP API calls, and more. You can configure action shortcuts with the new action configuration dialog and control how to call the action from it.\n\nFurthermore, it is also now possible to control how all of these actions are run. For production systems, for example, you can configure that all actions that perform some kind of modification, like deleting a file or running a script, have to be confirmed first. This gives you an added layer of protection to double-check any operation before actually executing it.\n\n![Actions screenshot](https://xpipe.io/assets/images/BlogPage/actions.png)\n\n[Action Documentation](https://docs.xpipe.io/guide/actions)\n\n## SSH\n\nThere is now proper support for jump servers. While gateways worked similar to jump servers, they were a different concept and did not work for cases where the ProxyJump functionality was required. You can now configure an SSH connection in its advanced settings to be treated as a jump server. This will result in XPipe using the ProxyJump syntax when this connection is used as a gateway for other SSH connections. This works for all kinds of connections, including config connections.\n\nFurthermore, the SSH implementation for devices that don't provide a full shell, e.g. embedded devices and other limited systems, has been completely reworked. This fixes many issues where connections to such systems were not possible or failed. You can now designate an SSH connection as a limited system in its advanced settings, without the previous homelab plan requirement. This will then allow you to directly launch the connection, without any issues.\n\n![SSH configuration screenshot](https://xpipe.io/assets/images/BlogPage/ssh-jump.png)\n\nServices also now support opening them for limited/embedded systems with system interaction being disabled. Instead of locally tunneling the remote service to localhost, XPipe will try to open the host URL directly. This allows you to open the web interface of embedded devices for example, which previously was not possible.\n\nOther changes:\n- The option to open a connection in VSCode remote has been expanded to also support Cursor and Windsurf\n- Active tunnels are now periodically refreshed to check if the underlying tunnel was closed\n- Fix SSH agent variables being wrong on macOS when using homebrew ssh\n- Fix automatic x11 display server forward to WSL not working on Windows\n- Fix various gpg agent configuration issues\n\n[SSH Documentation](https://docs.xpipe.io/guide/ssh)\n\n## VNC\n\nUp until now, the internal VNC implementation of XPipe did a somewhat acceptable job for most connections. However, it is not able to match dedicated VNC clients when it comes to more advanced features and authentication methods. There's simply not the development capacity to maintain all of these additional VNC features. For this reason, there is now support to also use an external VNC client with XPipe, just as with any other tool integrations.\n\n![VNC settings screenshot](https://xpipe.io/assets/images/BlogPage/vnc-list.png)\n\n[VNC Documentation](https://docs.xpipe.io/guide/vnc)\n\n## macOS 26 Tahoe\n\nXPipe adopts many of the new features of macOS 26 right away. if you are using the macOS beta, you have access to these right away.\n\nThe application window now uses the new Liquid Glass theming. The application icon has also been reworked with Liquid Glass in mind.\n\nThere's also support for the new apple containers framework, which has just been released. Searching for available connections on the local machine will make apple containers show up if you have installed the package.\n\n![macOS Tahoe screenshot](https://xpipe.io/assets/images/BlogPage/tahoe.png)\n\n## Windows ARM\n\nThere are now native Windows ARM builds. These releases are also available in choco and winget.\n\nNote that you will have to uninstall any old x64 XPipe installation for the upgrade.\n\n## File browser\n\nThe file browser has been improved in many areas:\n\n- Operations which modify a single file, e.g. a file edit, will now automatically refresh the file list to show updated changes\n- Add new file browser action to compute directory sizes\n- The transfer speed in the file browser on Windows for multiple files has been improved\n- Renaming a file now moves the caret to the end of the base file name\n- The file browser now works much better in small windows sizes\n- Fix terminal sometimes not being launched with the correct working directory of the current path\n- Fix file deletion not showing errors when the operation failed in cmd\n- Fix file renaming not working if previous rename operation was cancelled\n- Fix file transfer kill button sometimes not working when transfer was frozen\n- Fix file browser listing not working for older PowerShell systems\n\n[File Browser Documentation](https://docs.xpipe.io/guide/file-browser)\n\n## Connection hub\n\nProper functionality to organize a collection of hundreds of connections was always somewhat limited until now. There is now an additional index-based organization mechanism where you can assign and move indices of connections to have them listed at a certain place in relation to other connections. This can also be combined with the existing sorting methods like time and alphabetical sorting.\n\n![Order settings screenshot](https://xpipe.io/assets/images/BlogPage/order-menu.png)\n\nTo move a set of children connections to the top-level, you can also use the new so-called breakout categories to create subviews of nested connections.\n\n![Breakout category screenshot](https://xpipe.io/assets/images/BlogPage/category-breakout.png)\n\nFurthermore, there are also more improvements in the connection hub:\n- Renaming connection entries can now be done quickly from the context menu without having to open the configuration dialog\n- You can now set connection configurations to be frozen, meaning that the connection entry can't be modified or deleted. This is helpful for templating and team vault setups\n- When editing an incomplete connection configuration, the focus will automatically jump to the first incomplete/invalid value. This makes keyboard usage easier\n\n[Connection Hub Documentation](https://docs.xpipe.io/guide/hub)\n\n## Git vault\n\nThe git vault can now automatically resolve merge conflicts without restarts when multiple systems share the git repository. Any externally pushed commits are integrated into the connection hub instantly.\n\nFurthermore, you can now specify a git username and password in the settings menu if your local system does not have a git client with configured credentials.\n\n![Git auth screenshot](https://xpipe.io/assets/images/BlogPage/sync-auth.png)\n\n[Sync Documentation](https://docs.xpipe.io/guide/sync)\n\n## Serial\n\nThe serial connection support is no longer be considered experimental. It has been reworked, and the following issues have been fixed:\n- Fix serial configuration parameter names being cut off in the dialog\n- Connections to serial devices requiring root access are now automatically elevated\n\n![Git auth screenshot](https://xpipe.io/assets/images/BlogPage/serial.png)\n\n[Serial Documentation](https://docs.xpipe.io/guide/serial)\n\n## HTTP API\n\nThe HTTP API has been improved in various areas. The action system is integrated in it, meaning that you can call any action from the API. Furthermore, there are now more API endpoints for categories so that you have proper control over them as well. There is also now a way to encrypt and decrypt secrets via the API, allowing you to create connections with integrated secrets with it as well.\n\n[API Documentation](https://docs.xpipe.io/api)\n\n## Other\n\n- Various performance improvements\n- Password managers now support retrieving both username and password of an entry. For that, you can now create password manager identities that automatically provide the username and password\n- You can now specify an alternative user for shell environments to switch to via sudo\n- Add support for nushell\n- Add support for xonsh\n- Add support for LXTerminal, the default on Raspberry Pi OS\n- Add support to open a WinSCP session for an SSH connection\n- Teleport connections now support reusable identities\n- Changes made to /etc/sudoers files are now automatically checked with visudo and reverted if necessary\n- Terminal logging on Windows can now use a WSL terminal environment to improve logging quality\n- Terminal logs now strip any terminal escape codes\n- Add a new loading icon\n- Add note on how to fix potential blurriness on Gnome Wayland systems with high display scaling\n- You can now disable icon sources without having to remove them\n- Terminal connections now enable truecolor mode if possible\n- Add notification when a password is copied to the clipboard\n- Add support for Ghostty on macOS\n- There is now a subreddit at https://www.reddit.com/r/xpipe/\n- Update kasm workspaces image to support 1.17\n- Filter mullvad exit nodes from tailscale list\n\n## Fixes\n\n- Fix local to synced identity conversion not updating existing connections\n- Fix local to synced identity conversion not automatically adding key file to git\n- Fix local identities category being able to be synced\n- Fix pwsh connections on Linux/macOS freezing on sudo elevation\n- Fix sudo elevation not working on WSL if WSLInterop for executables was disabled\n- Fix clipboard data of other formats, e.g. files, being cleared after expiration of a copied password\n- Fix text color on hover having low contrast in some themes\n- Fix freeze after waking up the local system from a long hibernation\n- Fix application not starting up if some antivirus interfered with local socket connections\n- Fix kubectl apply changes functionality for pods not working\n- Fix custom workspaces not working on Linux\n- Fix KeePassXC snap installations not being detected\n"
  },
  {
    "path": "dist/changelog/17.2_incremental.md",
    "content": "- Add support for the Kiro editor\n- Fix file browser copy and move operations on same system manually\n  transferring files instead of just using tools like cp and mv\n- Fix errors when reading local files with invalid encoded characters\n- Fix error for script refresh context menu entry\n- Fix errors when SSH config includes contain unsupported characters\n- Fix error when %TEMP% variable was not set on Windows\n- Fix error when git ssh key file was left empty\n- Fix git sync being disabled if interval merge check failed once\n- Fix winget build pipeline still being broken\n"
  },
  {
    "path": "dist/changelog/17.3.md",
    "content": "3XPipe 17 is a large-scale rework of many existing parts of XPipe. It focuses on fixing many longstanding issues and limitations, so that future updates can easily bring new integrations without having to deal with that baggage.\n\nThe documentation for all the new features is already available at http://docs.xpipe.io/\n\n## Actions\n\nThere is now a new action system, which maps most UI actions to fixed schemas. This means that you can now automate almost any action that you can perform from the UI via desktop shortcuts, URLs, HTTP API calls, and more. You can configure action shortcuts with the new action configuration dialog and control how to call the action from it.\n\nFurthermore, it is also now possible to control how all of these actions are run. For production systems, for example, you can configure that all actions that perform some kind of modification, like deleting a file or running a script, have to be confirmed first. This gives you an added layer of protection to double-check any operation before actually executing it.\n\n![Actions screenshot](https://xpipe.io/assets/images/BlogPage/actions.png)\n\n[Action Documentation](https://docs.xpipe.io/guide/actions)\n\n## SSH\n\nThere is now proper support for jump servers. While gateways worked similar to jump servers, they were a different concept and did not work for cases where the ProxyJump functionality was required. You can now configure an SSH connection in its advanced settings to be treated as a jump server. This will result in XPipe using the ProxyJump syntax when this connection is used as a gateway for other SSH connections. This works for all kinds of connections, including config connections.\n\nFurthermore, the SSH implementation for devices that don't provide a full shell, e.g. embedded devices and other limited systems, has been completely reworked. This fixes many issues where connections to such systems were not possible or failed. You can now designate an SSH connection as a limited system in its advanced settings, without the previous homelab plan requirement. This will then allow you to directly launch the connection, without any issues.\n\n![SSH configuration screenshot](https://xpipe.io/assets/images/BlogPage/ssh-jump.png)\n\nServices also now support opening them for limited/embedded systems with system interaction being disabled. Instead of locally tunneling the remote service to localhost, XPipe will try to open the host URL directly. This allows you to open the web interface of embedded devices for example, which previously was not possible.\n\nOther changes:\n- The option to open a connection in VSCode remote has been expanded to also support Cursor and Windsurf\n- Active tunnels are now periodically refreshed to check if the underlying tunnel was closed\n- Fix SSH agent variables being wrong on macOS when using homebrew ssh\n- Fix automatic x11 display server forward to WSL not working on Windows\n- Fix various gpg agent configuration issues\n\n[SSH Documentation](https://docs.xpipe.io/guide/ssh)\n\n## VNC\n\nUp until now, the internal VNC implementation of XPipe did a somewhat acceptable job for most connections. However, it is not able to match dedicated VNC clients when it comes to more advanced features and authentication methods. There's simply not the development capacity to maintain all of these additional VNC features. For this reason, there is now support to also use an external VNC client with XPipe, just as with any other tool integrations.\n\n![VNC settings screenshot](https://xpipe.io/assets/images/BlogPage/vnc-list.png)\n\n[VNC Documentation](https://docs.xpipe.io/guide/vnc)\n\n## macOS 26 Tahoe\n\nXPipe adopts many of the new features of macOS 26 right away. if you are using the macOS beta, you have access to these right away.\n\nThe application window now uses the new Liquid Glass theming. The application icon has also been reworked with Liquid Glass in mind.\n\nThere's also support for the new apple containers framework, which has just been released. Searching for available connections on the local machine will make apple containers show up if you have installed the package.\n\n![macOS Tahoe screenshot](https://xpipe.io/assets/images/BlogPage/tahoe.png)\n\n## Windows ARM\n\nThere are now native Windows ARM builds. These releases are also available in choco and winget.\n\nNote that you will have to uninstall any old x64 XPipe installation for the upgrade.\n\n## File browser\n\nThe file browser has been improved in many areas:\n\n- Operations which modify a single file, e.g. a file edit, will now automatically refresh the file list to show updated changes\n- Add new file browser action to compute directory sizes\n- The transfer speed in the file browser on Windows for multiple files has been improved\n- Renaming a file now moves the caret to the end of the base file name\n- The file browser now works much better in small windows sizes\n- Fix terminal sometimes not being launched with the correct working directory of the current path\n- Fix file deletion not showing errors when the operation failed in cmd\n- Fix file renaming not working if previous rename operation was cancelled\n- Fix file transfer kill button sometimes not working when transfer was frozen\n- Fix file browser listing not working for older PowerShell systems\n\n[File Browser Documentation](https://docs.xpipe.io/guide/file-browser)\n\n## Connection hub\n\nProper functionality to organize a collection of hundreds of connections was always somewhat limited until now. There is now an additional index-based organization mechanism where you can assign and move indices of connections to have them listed at a certain place in relation to other connections. This can also be combined with the existing sorting methods like time and alphabetical sorting.\n\n![Order settings screenshot](https://xpipe.io/assets/images/BlogPage/order-menu.png)\n\nTo move a set of children connections to the top-level, you can also use the new so-called breakout categories to create subviews of nested connections.\n\n![Breakout category screenshot](https://xpipe.io/assets/images/BlogPage/category-breakout.png)\n\nFurthermore, there are also more improvements in the connection hub:\n- Renaming connection entries can now be done quickly from the context menu without having to open the configuration dialog\n- You can now set connection configurations to be frozen, meaning that the connection entry can't be modified or deleted. This is helpful for templating and team vault setups\n- When editing an incomplete connection configuration, the focus will automatically jump to the first incomplete/invalid value. This makes keyboard usage easier\n\n[Connection Hub Documentation](https://docs.xpipe.io/guide/hub)\n\n## Git vault\n\nThe git vault can now automatically resolve merge conflicts without restarts when multiple systems share the git repository. Any externally pushed commits are integrated into the connection hub instantly.\n\nFurthermore, you can now specify a git username and password in the settings menu if your local system does not have a git client with configured credentials.\n\n![Git auth screenshot](https://xpipe.io/assets/images/BlogPage/sync-auth.png)\n\n[Sync Documentation](https://docs.xpipe.io/guide/sync)\n\n## Serial\n\nThe serial connection support is no longer be considered experimental. It has been reworked, and the following issues have been fixed:\n- Fix serial configuration parameter names being cut off in the dialog\n- Connections to serial devices requiring root access are now automatically elevated\n\n![Git auth screenshot](https://xpipe.io/assets/images/BlogPage/serial.png)\n\n[Serial Documentation](https://docs.xpipe.io/guide/serial)\n\n## HTTP API\n\nThe HTTP API has been improved in various areas. The action system is integrated in it, meaning that you can call any action from the API. Furthermore, there are now more API endpoints for categories so that you have proper control over them as well. There is also now a way to encrypt and decrypt secrets via the API, allowing you to create connections with integrated secrets with it as well.\n\n[API Documentation](https://docs.xpipe.io/api)\n\n## Other\n\n- Various performance improvements\n- Password managers now support retrieving both username and password of an entry. For that, you can now create password manager identities that automatically provide the username and password\n- You can now specify an alternative user for shell environments to switch to via sudo\n- Add support for nushell\n- Add support for xonsh\n- Add support for LXTerminal, the default on Raspberry Pi OS\n- Add support to open a WinSCP session for an SSH connection\n- Teleport connections now support reusable identities\n- Changes made to /etc/sudoers files are now automatically checked with visudo and reverted if necessary\n- Terminal logging on Windows can now use a WSL terminal environment to improve logging quality\n- Terminal logs now strip any terminal escape codes\n- Add a new loading icon\n- Add note on how to fix potential blurriness on Gnome Wayland systems with high display scaling\n- You can now disable icon sources without having to remove them\n- Terminal connections now enable truecolor mode if possible\n- Add notification when a password is copied to the clipboard\n- Add support for Ghostty on macOS\n- There is now a subreddit at https://www.reddit.com/r/xpipe/\n- Update kasm workspaces image to support 1.17\n- Filter mullvad exit nodes from tailscale list\n\n## Fixes\n\n- Fix local to synced identity conversion not updating existing connections\n- Fix local to synced identity conversion not automatically adding key file to git\n- Fix local identities category being able to be synced\n- Fix pwsh connections on Linux/macOS freezing on sudo elevation\n- Fix sudo elevation not working on WSL if WSLInterop for executables was disabled\n- Fix clipboard data of other formats, e.g. files, being cleared after expiration of a copied password\n- Fix text color on hover having low contrast in some themes\n- Fix freeze after waking up the local system from a long hibernation\n- Fix application not starting up if some antivirus interfered with local socket connections\n- Fix kubectl apply changes functionality for pods not working\n- Fix custom workspaces not working on Linux\n- Fix KeePassXC snap installations not being detected\n"
  },
  {
    "path": "dist/changelog/17.3_incremental.md",
    "content": "- Improve terminal logging quality. Terminal escapes are now better cleaned from the logs. To still have access to the raw original logs, there are now two logs written for each connection, one raw and one cleaned.\n- Rework SSH agent socket handling on Linux and macOS. You can now specify the agent socket location in the settings menu to avoid having to deal with various rc files\n- Add support for freerdp on macOS as another alternative RDP client\n- RDP connections do not use the smart resize option anymore to allow the client to change the resolution\n- Improve scaling for remmina and freerdp sessions\n- PowerShell startup failures are now handled better and do not cause multiple errors in various actions\n- Improve git sync handling and documentation for local sync repos with file:// urls\n- Fix clipboard copy sometimes causing the application to freeze\n- Fix macOS app icon brightness\n- Fix actions like a WinSCP open not working\n- Fix custom KeepassXC symlink in PATH not working\n- Fix some children of git vault personal connection frequently moving around in the readme\n- Fix double click setting making some buttons require double-click as well\n- Fix VNC password prompt not resetting cache on wrong password\n"
  },
  {
    "path": "dist/changelog/17.4.md",
    "content": "3XPipe 17 is a large-scale rework of many existing parts of XPipe. It focuses on fixing many longstanding issues and limitations, so that future updates can easily bring new integrations without having to deal with that baggage.\n\nThe documentation for all the new features is already available at http://docs.xpipe.io/\n\n## Actions\n\nThere is now a new action system, which maps most UI actions to fixed schemas. This means that you can now automate almost any action that you can perform from the UI via desktop shortcuts, URLs, HTTP API calls, and more. You can configure action shortcuts with the new action configuration dialog and control how to call the action from it.\n\nFurthermore, it is also now possible to control how all of these actions are run. For production systems, for example, you can configure that all actions that perform some kind of modification, like deleting a file or running a script, have to be confirmed first. This gives you an added layer of protection to double-check any operation before actually executing it.\n\n![Actions screenshot](https://xpipe.io/assets/images/BlogPage/actions.png)\n\n[Action Documentation](https://docs.xpipe.io/guide/actions)\n\n## SSH\n\nThere is now proper support for jump servers. While gateways worked similar to jump servers, they were a different concept and did not work for cases where the ProxyJump functionality was required. You can now configure an SSH connection in its advanced settings to be treated as a jump server. This will result in XPipe using the ProxyJump syntax when this connection is used as a gateway for other SSH connections. This works for all kinds of connections, including config connections.\n\nFurthermore, the SSH implementation for devices that don't provide a full shell, e.g. embedded devices and other limited systems, has been completely reworked. This fixes many issues where connections to such systems were not possible or failed. You can now designate an SSH connection as a limited system in its advanced settings, without the previous homelab plan requirement. This will then allow you to directly launch the connection, without any issues.\n\n![SSH configuration screenshot](https://xpipe.io/assets/images/BlogPage/ssh-jump.png)\n\nServices also now support opening them for limited/embedded systems with system interaction being disabled. Instead of locally tunneling the remote service to localhost, XPipe will try to open the host URL directly. This allows you to open the web interface of embedded devices for example, which previously was not possible.\n\nOther changes:\n- The option to open a connection in VSCode remote has been expanded to also support Cursor and Windsurf\n- Active tunnels are now periodically refreshed to check if the underlying tunnel was closed\n- Fix SSH agent variables being wrong on macOS when using homebrew ssh\n- Fix automatic x11 display server forward to WSL not working on Windows\n- Fix various gpg agent configuration issues\n\n[SSH Documentation](https://docs.xpipe.io/guide/ssh)\n\n## VNC\n\nUp until now, the internal VNC implementation of XPipe did a somewhat acceptable job for most connections. However, it is not able to match dedicated VNC clients when it comes to more advanced features and authentication methods. There's simply not the development capacity to maintain all of these additional VNC features. For this reason, there is now support to also use an external VNC client with XPipe, just as with any other tool integrations.\n\n![VNC settings screenshot](https://xpipe.io/assets/images/BlogPage/vnc-list.png)\n\n[VNC Documentation](https://docs.xpipe.io/guide/vnc)\n\n## macOS 26 Tahoe\n\nXPipe adopts many of the new features of macOS 26 right away. if you are using the macOS beta, you have access to these right away.\n\nThe application window now uses the new Liquid Glass theming. The application icon has also been reworked with Liquid Glass in mind.\n\nThere's also support for the new apple containers framework, which has just been released. Searching for available connections on the local machine will make apple containers show up if you have installed the package.\n\n![macOS Tahoe screenshot](https://xpipe.io/assets/images/BlogPage/tahoe.png)\n\n## Windows ARM\n\nThere are now native Windows ARM builds. These releases are also available in choco and winget.\n\nNote that you will have to uninstall any old x64 XPipe installation for the upgrade.\n\n## File browser\n\nThe file browser has been improved in many areas:\n\n- Operations which modify a single file, e.g. a file edit, will now automatically refresh the file list to show updated changes\n- Add new file browser action to compute directory sizes\n- The transfer speed in the file browser on Windows for multiple files has been improved\n- Renaming a file now moves the caret to the end of the base file name\n- The file browser now works much better in small windows sizes\n- Fix terminal sometimes not being launched with the correct working directory of the current path\n- Fix file deletion not showing errors when the operation failed in cmd\n- Fix file renaming not working if previous rename operation was cancelled\n- Fix file transfer kill button sometimes not working when transfer was frozen\n- Fix file browser listing not working for older PowerShell systems\n\n[File Browser Documentation](https://docs.xpipe.io/guide/file-browser)\n\n## Connection hub\n\nProper functionality to organize a collection of hundreds of connections was always somewhat limited until now. There is now an additional index-based organization mechanism where you can assign and move indices of connections to have them listed at a certain place in relation to other connections. This can also be combined with the existing sorting methods like time and alphabetical sorting.\n\n![Order settings screenshot](https://xpipe.io/assets/images/BlogPage/order-menu.png)\n\nTo move a set of children connections to the top-level, you can also use the new so-called breakout categories to create subviews of nested connections.\n\n![Breakout category screenshot](https://xpipe.io/assets/images/BlogPage/category-breakout.png)\n\nFurthermore, there are also more improvements in the connection hub:\n- Renaming connection entries can now be done quickly from the context menu without having to open the configuration dialog\n- You can now set connection configurations to be frozen, meaning that the connection entry can't be modified or deleted. This is helpful for templating and team vault setups\n- When editing an incomplete connection configuration, the focus will automatically jump to the first incomplete/invalid value. This makes keyboard usage easier\n\n[Connection Hub Documentation](https://docs.xpipe.io/guide/hub)\n\n## Git vault\n\nThe git vault can now automatically resolve merge conflicts without restarts when multiple systems share the git repository. Any externally pushed commits are integrated into the connection hub instantly.\n\nFurthermore, you can now specify a git username and password in the settings menu if your local system does not have a git client with configured credentials.\n\n![Git auth screenshot](https://xpipe.io/assets/images/BlogPage/sync-auth.png)\n\n[Sync Documentation](https://docs.xpipe.io/guide/sync)\n\n## Serial\n\nThe serial connection support is no longer be considered experimental. It has been reworked, and the following issues have been fixed:\n- Fix serial configuration parameter names being cut off in the dialog\n- Connections to serial devices requiring root access are now automatically elevated\n\n![Git auth screenshot](https://xpipe.io/assets/images/BlogPage/serial.png)\n\n[Serial Documentation](https://docs.xpipe.io/guide/serial)\n\n## HTTP API\n\nThe HTTP API has been improved in various areas. The action system is integrated in it, meaning that you can call any action from the API. Furthermore, there are now more API endpoints for categories so that you have proper control over them as well. There is also now a way to encrypt and decrypt secrets via the API, allowing you to create connections with integrated secrets with it as well.\n\n[API Documentation](https://docs.xpipe.io/api)\n\n## Other\n\n- Various performance improvements\n- Password managers now support retrieving both username and password of an entry. For that, you can now create password manager identities that automatically provide the username and password\n- You can now specify an alternative user for shell environments to switch to via sudo\n- Add support for nushell\n- Add support for xonsh\n- Add support for LXTerminal, the default on Raspberry Pi OS\n- Add support to open a WinSCP session for an SSH connection\n- Teleport connections now support reusable identities\n- Changes made to /etc/sudoers files are now automatically checked with visudo and reverted if necessary\n- Terminal logging on Windows can now use a WSL terminal environment to improve logging quality\n- Terminal logs now strip any terminal escape codes\n- Add a new loading icon\n- Add note on how to fix potential blurriness on Gnome Wayland systems with high display scaling\n- You can now disable icon sources without having to remove them\n- Terminal connections now enable truecolor mode if possible\n- Add notification when a password is copied to the clipboard\n- Add support for Ghostty on macOS\n- There is now a subreddit at https://www.reddit.com/r/xpipe/\n- Update kasm workspaces image to support 1.17\n- Filter mullvad exit nodes from tailscale list\n\n## Fixes\n\n- Fix local to synced identity conversion not updating existing connections\n- Fix local to synced identity conversion not automatically adding key file to git\n- Fix local identities category being able to be synced\n- Fix pwsh connections on Linux/macOS freezing on sudo elevation\n- Fix sudo elevation not working on WSL if WSLInterop for executables was disabled\n- Fix clipboard data of other formats, e.g. files, being cleared after expiration of a copied password\n- Fix text color on hover having low contrast in some themes\n- Fix freeze after waking up the local system from a long hibernation\n- Fix application not starting up if some antivirus interfered with local socket connections\n- Fix kubectl apply changes functionality for pods not working\n- Fix custom workspaces not working on Linux\n- Fix KeePassXC snap installations not being detected\n"
  },
  {
    "path": "dist/changelog/17.4_incremental.md",
    "content": "- Add support for SSH agent authentication methods to specify a fixed public key so that only a certain private key will be offered by the agent. This prevents cases in which the authentication fails because the agent tried too many keys\n- Add possible workaround for script execution issues on systems where /tmp is mounted with noexec\n- Improve docs and notes for local directory git repository sync as an alternative to remote repositories for syncing\n- Improve gnu coreutils homebrew package handling on macOS\n- Make bitwarden password manager use existing BW_SESSION environment variable if possible\n- Various macOS styling improvements\n- Fix desktop shortcuts for various actions not being properly recognized by the desktop environment on some Linux systems\n- Fix 1password integration op:// urls being broken\n- Fix exception when VNC connection required username but password manager entry did not provide one\n- Fix long error messages being cut off\n"
  },
  {
    "path": "dist/changelog/17.5.md",
    "content": "3XPipe 17 is a large-scale rework of many existing parts of XPipe. It focuses on fixing many longstanding issues and limitations, so that future updates can easily bring new integrations without having to deal with that baggage.\n\nThe documentation for all the new features is already available at http://docs.xpipe.io/\n\n## Actions\n\nThere is now a new action system, which maps most UI actions to fixed schemas. This means that you can now automate almost any action that you can perform from the UI via desktop shortcuts, URLs, HTTP API calls, and more. You can configure action shortcuts with the new action configuration dialog and control how to call the action from it.\n\nFurthermore, it is also now possible to control how all of these actions are run. For production systems, for example, you can configure that all actions that perform some kind of modification, like deleting a file or running a script, have to be confirmed first. This gives you an added layer of protection to double-check any operation before actually executing it.\n\n![Actions screenshot](https://xpipe.io/assets/images/BlogPage/actions.png)\n\n\n[Action Documentation](https://docs.xpipe.io/guide/actions)\n## SSH\n\nThere is now proper support for jump servers. While gateways worked similar to jump servers, they were a different concept and did not work for cases where the ProxyJump functionality was required. You can now configure an SSH connection in its advanced settings to be treated as a jump server. This will result in XPipe using the ProxyJump syntax when this connection is used as a gateway for other SSH connections. This works for all kinds of connections, including config connections.\n\nFurthermore, the SSH implementation for devices that don't provide a full shell, e.g. embedded devices and other limited systems, has been completely reworked. This fixes many issues where connections to such systems were not possible or failed. You can now designate an SSH connection as a limited system in its advanced settings, without the previous homelab plan requirement. This will then allow you to directly launch the connection, without any issues.\n\n![SSH configuration screenshot](https://xpipe.io/assets/images/BlogPage/ssh-jump.png)\n\nServices also now support opening them for limited/embedded systems with system interaction being disabled. Instead of locally tunneling the remote service to localhost, XPipe will try to open the host URL directly. This allows you to open the web interface of embedded devices for example, which previously was not possible.\n\nOther changes:\n- The option to open a connection in VSCode remote has been expanded to also support Cursor and Windsurf\n- Active tunnels are now periodically refreshed to check if the underlying tunnel was closed\n- Fix SSH agent variables being wrong on macOS when using homebrew ssh\n- Fix automatic x11 display server forward to WSL not working on Windows\n- Fix various gpg agent configuration issues\n\n[SSH Documentation](https://docs.xpipe.io/guide/ssh)\n\n## VNC\n\nUp until now, the internal VNC implementation of XPipe did a somewhat acceptable job for most connections. However, it is not able to match dedicated VNC clients when it comes to more advanced features and authentication methods. There's simply not the development capacity to maintain all of these additional VNC features. For this reason, there is now support to also use an external VNC client with XPipe, just as with any other tool integrations.\n\n![VNC settings screenshot](https://xpipe.io/assets/images/BlogPage/vnc-list.png)\n\n[VNC Documentation](https://docs.xpipe.io/guide/vnc)\n\n## macOS 26 Tahoe\n\nXPipe adopts many of the new features of macOS 26 right away. if you are using the macOS beta, you have access to these right away.\n\nThe application window now uses the new Liquid Glass theming. The application icon has also been reworked with Liquid Glass in mind.\n\nThere's also support for the new apple containers framework, which has just been released. Searching for available connections on the local machine will make apple containers show up if you have installed the package.\n\n![macOS Tahoe screenshot](https://xpipe.io/assets/images/BlogPage/tahoe.png)\n\n## Windows ARM\n\nThere are now native Windows ARM builds. These releases are also available in choco and winget.\n\nNote that you will have to uninstall any old x64 XPipe installation for the upgrade.\n\n## File browser\n\nThe file browser has been improved in many areas:\n\n- Operations which modify a single file, e.g. a file edit, will now automatically refresh the file list to show updated changes\n- Add new file browser action to compute directory sizes\n- The transfer speed in the file browser on Windows for multiple files has been improved\n- Renaming a file now moves the caret to the end of the base file name\n- The file browser now works much better in small windows sizes\n- Fix terminal sometimes not being launched with the correct working directory of the current path\n- Fix file deletion not showing errors when the operation failed in cmd\n- Fix file renaming not working if previous rename operation was cancelled\n- Fix file transfer kill button sometimes not working when transfer was frozen\n- Fix file browser listing not working for older PowerShell systems\n\n[File Browser Documentation](https://docs.xpipe.io/guide/file-browser)\n\n## Connection hub\n\nProper functionality to organize a collection of hundreds of connections was always somewhat limited until now. There is now an additional index-based organization mechanism where you can assign and move indices of connections to have them listed at a certain place in relation to other connections. This can also be combined with the existing sorting methods like time and alphabetical sorting.\n\n![Order settings screenshot](https://xpipe.io/assets/images/BlogPage/order-menu.png)\n\nTo move a set of children connections to the top-level, you can also use the new so-called breakout categories to create subviews of nested connections.\n\n![Breakout category screenshot](https://xpipe.io/assets/images/BlogPage/category-breakout.png)\n\nFurthermore, there are also more improvements in the connection hub:\n- Renaming connection entries can now be done quickly from the context menu without having to open the configuration dialog\n- You can now set connection configurations to be frozen, meaning that the connection entry can't be modified or deleted. This is helpful for templating and team vault setups\n- When editing an incomplete connection configuration, the focus will automatically jump to the first incomplete/invalid value. This makes keyboard usage easier\n\n[Connection Hub Documentation](https://docs.xpipe.io/guide/hub)\n\n## Git vault\n\nThe git vault can now automatically resolve merge conflicts without restarts when multiple systems share the git repository. Any externally pushed commits are integrated into the connection hub instantly.\n\nFurthermore, you can now specify a git username and password in the settings menu if your local system does not have a git client with configured credentials.\n\n![Git auth screenshot](https://xpipe.io/assets/images/BlogPage/sync-auth.png)\n\n[Sync Documentation](https://docs.xpipe.io/guide/sync)\n\n## Serial\n\nThe serial connection support is no longer be considered experimental. It has been reworked, and the following issues have been fixed:\n- Fix serial configuration parameter names being cut off in the dialog\n- Connections to serial devices requiring root access are now automatically elevated\n\n![Git auth screenshot](https://xpipe.io/assets/images/BlogPage/serial.png)\n\n[Serial Documentation](https://docs.xpipe.io/guide/serial)\n\n## HTTP API\n\nThe HTTP API has been improved in various areas. The action system is integrated in it, meaning that you can call any action from the API. Furthermore, there are now more API endpoints for categories so that you have proper control over them as well. There is also now a way to encrypt and decrypt secrets via the API, allowing you to create connections with integrated secrets with it as well.\n\n[API Documentation](https://docs.xpipe.io/api)\n\n## Other\n\n- Various performance improvements\n- Password managers now support retrieving both username and password of an entry. For that, you can now create password manager identities that automatically provide the username and password\n- You can now specify an alternative user for shell environments to switch to via sudo\n- Add support for nushell\n- Add support for xonsh\n- Add support for LXTerminal, the default on Raspberry Pi OS\n- Add support to open a WinSCP session for an SSH connection\n- Teleport connections now support reusable identities\n- Changes made to /etc/sudoers files are now automatically checked with visudo and reverted if necessary\n- Terminal logging on Windows can now use a WSL terminal environment to improve logging quality\n- Terminal logs now strip any terminal escape codes\n- Add a new loading icon\n- Add note on how to fix potential blurriness on Gnome Wayland systems with high display scaling\n- You can now disable icon sources without having to remove them\n- Terminal connections now enable truecolor mode if possible\n- Add notification when a password is copied to the clipboard\n- Add support for Ghostty on macOS\n- There is now a subreddit at https://www.reddit.com/r/xpipe/\n- Update kasm workspaces image to support 1.17\n- Filter mullvad exit nodes from tailscale list\n\n## Fixes\n\n- Fix local to synced identity conversion not updating existing connections\n- Fix local to synced identity conversion not automatically adding key file to git\n- Fix local identities category being able to be synced\n- Fix pwsh connections on Linux/macOS freezing on sudo elevation\n- Fix sudo elevation not working on WSL if WSLInterop for executables was disabled\n- Fix clipboard data of other formats, e.g. files, being cleared after expiration of a copied password\n- Fix text color on hover having low contrast in some themes\n- Fix freeze after waking up the local system from a long hibernation\n- Fix application not starting up if some antivirus interfered with local socket connections\n- Fix kubectl apply changes functionality for pods not working\n- Fix custom workspaces not working on Linux\n- Fix KeePassXC snap installations not being detected\n"
  },
  {
    "path": "dist/changelog/17.5_incremental.md",
    "content": "- Fix 1password integration op:// urls still being broken for vaults and entries with special characters\n- Fix SSH configs being broken when username contained dots\n"
  },
  {
    "path": "dist/changelog/18.0.1.md",
    "content": "XPipe 18 ventures into many new areas. It comes with the first cloud provider integration for Hetzner cloud to kickstart the process of integrating common cloud providers directly into XPipe. Furthermore, it comes with a new MCP server functionality to seamlessly integrate XPipe into your AI agent workflows. On top of everything, there are also many new convenience features like the network scan, multiple hostnames, SSH improvements, many bug fixes, and much more.\n\n## MCP server\n\nThere is now an MCP server available for XPipe which allows you to perform many actions in an agentic workflow via your favourite MCP client, for example Cursor. The MCP server feature is disabled by default has to be enabled in the MCP settings menu.\n\n[Documentation page](https://docs.xpipe.io/guide/mcp)\n\nHere is how it looks in Cursor:\n\n![MCP config](https://xpipe.io/assets/images/BlogPage/cursor-mcp.png)\n\nHere is how a chat that uses the XPipe MCP server looks like:\n\n![MCP screenshot](https://xpipe.io/assets/images/BlogPage/cursor-chat.png)\n\n## Hetzner cloud\n\nThis release introduces support for Hetzner cloud servers via the hcloud CLI tool. You can list all your service automatically and then access them normally as SSH connections. This is the first of hopefully many integrations for cloud providers and will serve as a good proof of concept. This integration is available in the Professional plan.\n\n![hcloud](https://xpipe.io/assets/images/BlogPage/hcloud.png)\n\n## Network scan\n\nThere is now the option to automatically search the local network for any listening SSH servers and add them automatically as new connections. This also works for remote systems and their networks.\n\nYou can find the network scan in the newly organized menu at New -> Network scan.\n\n![scan](https://xpipe.io/assets/images/BlogPage/network-scan.png)\n\n## Host addresses\n\nYou can now configure multiple addresses for a host. This allows you to quickly switch between different addresses if needed. \n\n![addresses](https://xpipe.io/assets/images/BlogPage/addresses.png)\n\n## SSH\n\n- Rework SSH timeout options to hopefully better handle unexpected connection disruptions\n- There is now a new settings option and improved handling of SSH MOTDs to control whether they should be shown or not\n- Fix SSH ProxyCommand not being executed for custom connections\n- Fix SSH agent public key setting not working on Linux and macOS due to permissions issue\n- The custom SSH agent setting now works differently. To use the agent socket specified in the settings menu,\n  you will now have to select the custom SSH agent for a connection. The default OpenSSH agent will no longer use this option\n- Fix custom SSH agent socket location setting not applying correctly\n- Fix SSH gateways not working on systems where username contained a dot\n- Fix some terminal connections asking for passwords even if it was entered before if the connection was edited\n- Add support to launch VsCode Insiders and Trae as well in the VsCode SSH launch menu\n\n## File browser\n\n- The initial transfer size calculation for large directories is now dynamically updated and shown\n- Fix script arguments not being passed with file selection\n- Fix file listing not working on Solaris systems\n- Fix refresh of single file selections failing on Windows systems\n- Fix conflict dialog being cut off\n- Fix renames not working in cmd\n- Fix not supporting dollar signs in directory names\n- Files with trailing or leading spaces are now included in the file list\n- Fix execute action on Windows not being limited to certain file types\n- Fix execute action not showing confirmation dialog if enabled\n\n## Other\n\n- The fonts on macOS have been updated with the goal of better readability.\n  If you don't like the new font style, you can still select the old one in the appearance settings menu\n- Tailscale connections are now a top-level entry and can be synced via git\n- Add ability to specify gateways for direct RDP and VNC connections\n- Add two new connection colors with cyan and purple\n- Add new option to prefer available monochrome icons instead of colored variants\n- Add vietnamese translations\n- Improve local shell fallback handling and docs\n- Add more documentation links to the settings menu\n- Proxmox entries are now ordered by their vmid\n- Kitty and WezTerm now also support the tabs or windows settings option for terminals\n- Any small changes such as a change in color other connection are now synced instantly\n- XPipe will now clean any temp files more often\n\n## Fixes\n\n- Fix application freeze when having password manager identities while password manager was not running\n- Fix apt and rpm package manager installs failing due to two download redirects causing a wrong checksum\n- Fix automatic tunnel restart sometimes resulting in an invalid tunnel state\n- Fix BSD and Solaris systems not being recognized correctly\n- Fix performance issue when opening connection chooser dropdown\n- Fix small parts of the UI moving a bit on hover\n- Fix Bitwarden integration in some cases requiring the master password every time\n- Fix restart button not working on Linux\n- Fix license check becoming invalid if xpipe was left running for more than a week\n- Fix git vaultversion conflict not being solved automatically\n- Fix container service entries not showing the port when added initially\n\nThere are also many other small changes that are not listed explicitly here.\n"
  },
  {
    "path": "dist/changelog/18.0.1_incremental.md",
    "content": "XPipe 18 ventures into many new areas. It comes with the first cloud provider integration for Hetzner cloud to kickstart the process of integrating common cloud providers directly into XPipe. Furthermore, it comes with a new MCP server functionality to seamlessly integrate XPipe into your AI agent workflows. On top of everything, there are also many new convenience features like the network scan, multiple hostnames, SSH improvements, many bug fixes, and much more.\n\n## MCP server\n\nThere is now an MCP server available for XPipe which allows you to perform many actions in an agentic workflow via your favourite MCP client, for example Cursor. The MCP server feature is disabled by default has to be enabled in the MCP settings menu.\n\n[Documentation page](https://docs.xpipe.io/guide/mcp)\n\nHere is how it looks in Cursor:\n\n![MCP config](https://xpipe.io/assets/images/BlogPage/cursor-mcp.png)\n\nHere is how a chat that uses the XPipe MCP server looks like:\n\n![MCP screenshot](https://xpipe.io/assets/images/BlogPage/cursor-chat.png)\n\n## Hetzner cloud\n\nThis release introduces support for Hetzner cloud servers via the hcloud CLI tool. You can list all your service automatically and then access them normally as SSH connections. This is the first of hopefully many integrations for cloud providers and will serve as a good proof of concept. This integration is available in the Professional plan.\n\n![hcloud](https://xpipe.io/assets/images/BlogPage/hcloud.png)\n\n## Network scan\n\nThere is now the option to automatically search the local network for any listening SSH servers and add them automatically as new connections. This also works for remote systems and their networks.\n\nYou can find the network scan in the newly organized menu at New -> Network scan.\n\n![scan](https://xpipe.io/assets/images/BlogPage/network-scan.png)\n\n## Host addresses\n\nYou can now configure multiple addresses for a host. This allows you to quickly switch between different addresses if needed. \n\n![addresses](https://xpipe.io/assets/images/BlogPage/addresses.png)\n\n## SSH\n\n- Rework SSH timeout options to hopefully better handle unexpected connection disruptions\n- There is now a new settings option and improved handling of SSH MOTDs to control whether they should be shown or not\n- Fix SSH ProxyCommand not being executed for custom connections\n- Fix SSH agent public key setting not working on Linux and macOS due to permissions issue\n- The custom SSH agent setting now works differently. To use the agent socket specified in the settings menu,\n  you will now have to select the custom SSH agent for a connection. The default OpenSSH agent will no longer use this option\n- Fix custom SSH agent socket location setting not applying correctly\n- Fix SSH gateways not working on systems where username contained a dot\n- Fix some terminal connections asking for passwords even if it was entered before if the connection was edited\n- Add support to launch VsCode Insiders and Trae as well in the VsCode SSH launch menu\n\n## File browser\n\n- The initial transfer size calculation for large directories is now dynamically updated and shown\n- Fix script arguments not being passed with file selection\n- Fix file listing not working on Solaris systems\n- Fix refresh of single file selections failing on Windows systems\n- Fix conflict dialog being cut off\n- Fix renames not working in cmd\n- Fix not supporting dollar signs in directory names\n- Files with trailing or leading spaces are now included in the file list\n- Fix execute action on Windows not being limited to certain file types\n- Fix execute action not showing confirmation dialog if enabled\n\n## Other\n\n- The fonts on macOS have been updated with the goal of better readability.\n  If you don't like the new font style, you can still select the old one in the appearance settings menu\n- Tailscale connections are now a top-level entry and can be synced via git\n- Add ability to specify gateways for direct RDP and VNC connections\n- Add two new connection colors with cyan and purple\n- Add new option to prefer available monochrome icons instead of colored variants\n- Add vietnamese translations\n- Improve local shell fallback handling and docs\n- Add more documentation links to the settings menu\n- Proxmox entries are now ordered by their vmid\n- Kitty and WezTerm now also support the tabs or windows settings option for terminals\n- Any small changes such as a change in color other connection are now synced instantly\n- XPipe will now clean any temp files more often\n\n## Fixes\n\n- Fix application freeze when having password manager identities while password manager was not running\n- Fix apt and rpm package manager installs failing due to two download redirects causing a wrong checksum\n- Fix automatic tunnel restart sometimes resulting in an invalid tunnel state\n- Fix BSD and Solaris systems not being recognized correctly\n- Fix performance issue when opening connection chooser dropdown\n- Fix small parts of the UI moving a bit on hover\n- Fix Bitwarden integration in some cases requiring the master password every time\n- Fix restart button not working on Linux\n- Fix license check becoming invalid if xpipe was left running for more than a week\n- Fix git vaultversion conflict not being solved automatically\n- Fix container service entries not showing the port when added initially\n\nThere are also many other small changes that are not listed explicitly here.\n"
  },
  {
    "path": "dist/changelog/18.0.md",
    "content": "XPipe 18 ventures into many new areas. It comes with the first cloud provider integration for Hetzner cloud to kickstart the process of integrating common cloud providers directly into XPipe. Furthermore, it comes with a new MCP server functionality to seamlessly integrate XPipe into your AI agent workflows. On top of everything, there are also many new convenience features like the network scan, multiple hostnames, SSH improvements, many bug fixes, and much more.\n\n## MCP server\n\nThere is now an MCP server available for XPipe which allows you to perform many actions in an agentic workflow via your favourite MCP client, for example Cursor. The MCP server feature is disabled by default has to be enabled in the MCP settings menu.\n\n[Documentation page](https://docs.xpipe.io/guide/mcp)\n\nHere is how it looks in Cursor:\n\n![MCP config](https://xpipe.io/assets/images/BlogPage/cursor-mcp.png)\n\nHere is how a chat that uses the XPipe MCP server looks like:\n\n![MCP screenshot](https://xpipe.io/assets/images/BlogPage/cursor-chat.png)\n\n## Hetzner cloud\n\nThis release introduces support for Hetzner cloud servers via the hcloud CLI tool. You can list all your service automatically and then access them normally as SSH connections. This is the first of hopefully many integrations for cloud providers and will serve as a good proof of concept. This integration is available in the Professional plan.\n\n![hcloud](https://xpipe.io/assets/images/BlogPage/hcloud.png)\n\n## Network scan\n\nThere is now the option to automatically search the local network for any listening SSH servers and add them automatically as new connections. This also works for remote systems and their networks.\n\nYou can find the network scan in the newly organized menu at New -> Network scan.\n\n![scan](https://xpipe.io/assets/images/BlogPage/network-scan.png)\n\n## Host addresses\n\nYou can now configure multiple addresses for a host. This allows you to quickly switch between different addresses if needed. \n\n![addresses](https://xpipe.io/assets/images/BlogPage/addresses.png)\n\n## SSH\n\n- Rework SSH timeout options to hopefully better handle unexpected connection disruptions\n- There is now a new settings option and improved handling of SSH MOTDs to control whether they should be shown or not\n- Fix SSH ProxyCommand not being executed for custom connections\n- Fix SSH agent public key setting not working on Linux and macOS due to permissions issue\n- The custom SSH agent setting now works differently. To use the agent socket specified in the settings menu,\n  you will now have to select the custom SSH agent for a connection. The default OpenSSH agent will no longer use this option\n- Fix custom SSH agent socket location setting not applying correctly\n- Fix SSH gateways not working on systems where username contained a dot\n- Fix some terminal connections asking for passwords even if it was entered before if the connection was edited\n- Add support to launch VsCode Insiders and Trae as well in the VsCode SSH launch menu\n\n## File browser\n\n- The initial transfer size calculation for large directories is now dynamically updated and shown\n- Fix script arguments not being passed with file selection\n- Fix file listing not working on Solaris systems\n- Fix refresh of single file selections failing on Windows systems\n- Fix conflict dialog being cut off\n- Fix renames not working in cmd\n- Fix not supporting dollar signs in directory names\n- Files with trailing or leading spaces are now included in the file list\n- Fix execute action on Windows not being limited to certain file types\n- Fix execute action not showing confirmation dialog if enabled\n\n## Other\n\n- The fonts on macOS have been updated with the goal of better readability.\n  If you don't like the new font style, you can still select the old one in the appearance settings menu\n- Tailscale connections are now a top-level entry and can be synced via git\n- Add ability to specify gateways for direct RDP and VNC connections\n- Add two new connection colors with cyan and purple\n- Add new option to prefer available monochrome icons instead of colored variants\n- Add vietnamese translations\n- Improve local shell fallback handling and docs\n- Add more documentation links to the settings menu\n- Proxmox entries are now ordered by their vmid\n- Kitty and WezTerm now also support the tabs or windows settings option for terminals\n- Any small changes such as a change in color other connection are now synced instantly\n- XPipe will now clean any temp files more often\n\n## Fixes\n\n- Fix application freeze when having password manager identities while password manager was not running\n- Fix apt and rpm package manager installs failing due to two download redirects causing a wrong checksum\n- Fix automatic tunnel restart sometimes resulting in an invalid tunnel state\n- Fix BSD and Solaris systems not being recognized correctly\n- Fix performance issue when opening connection chooser dropdown\n- Fix small parts of the UI moving a bit on hover\n- Fix Bitwarden integration in some cases requiring the master password every time\n- Fix restart button not working on Linux\n- Fix license check becoming invalid if xpipe was left running for more than a week\n- Fix git vaultversion conflict not being solved automatically\n- Fix container service entries not showing the port when added initially\n\nThere are also many other small changes that are not listed explicitly here.\n"
  },
  {
    "path": "dist/changelog/18.1.1.md",
    "content": "XPipe 18 ventures into many new areas. It comes with the first cloud provider integration for Hetzner cloud to kickstart the process of integrating common cloud providers directly into XPipe. Furthermore, it comes with a new MCP server functionality to seamlessly integrate XPipe into your AI agent workflows. On top of everything, there are also many new convenience features like the network scan, multiple hostnames, SSH improvements, many bug fixes, and much more.\n\n## MCP server\n\nThere is now an MCP server available for XPipe which allows you to perform many actions in an agentic workflow via your favourite MCP client, for example Cursor. The MCP server feature is disabled by default has to be enabled in the MCP settings menu.\n\n[Documentation page](https://docs.xpipe.io/guide/mcp)\n\nHere is how it looks in Cursor:\n\n![MCP config](https://xpipe.io/assets/images/BlogPage/cursor-mcp.png)\n\nHere is how a chat that uses the XPipe MCP server looks like:\n\n![MCP screenshot](https://xpipe.io/assets/images/BlogPage/cursor-chat.png)\n\n## Hetzner cloud\n\nThis release introduces support for Hetzner cloud servers via the hcloud CLI tool. You can list all your service automatically and then access them normally as SSH connections. This is the first of hopefully many integrations for cloud providers and will serve as a good proof of concept. This integration is available in the Professional plan.\n\n![hcloud](https://xpipe.io/assets/images/BlogPage/hcloud.png)\n\n## Network scan\n\nThere is now the option to automatically search the local network for any listening SSH servers and add them automatically as new connections. This also works for remote systems and their networks.\n\nYou can find the network scan in the newly organized menu at New -> Network scan.\n\n![scan](https://xpipe.io/assets/images/BlogPage/network-scan.png)\n\n## Host addresses\n\nYou can now configure multiple addresses for a host. This allows you to quickly switch between different addresses if needed. \n\n![addresses](https://xpipe.io/assets/images/BlogPage/addresses.png)\n\n## SSH\n\n- Rework SSH timeout options to hopefully better handle unexpected connection disruptions\n- There is now a new settings option and improved handling of SSH MOTDs to control whether they should be shown or not\n- Fix SSH ProxyCommand not being executed for custom connections\n- Fix SSH agent public key setting not working on Linux and macOS due to permissions issue\n- The custom SSH agent setting now works differently. To use the agent socket specified in the settings menu,\n  you will now have to select the custom SSH agent for a connection. The default OpenSSH agent will no longer use this option\n- Fix custom SSH agent socket location setting not applying correctly\n- Fix SSH gateways not working on systems where username contained a dot\n- Fix some terminal connections asking for passwords even if it was entered before if the connection was edited\n- Add support to launch VsCode Insiders and Trae as well in the VsCode SSH launch menu\n\n## File browser\n\n- The initial transfer size calculation for large directories is now dynamically updated and shown\n- Fix script arguments not being passed with file selection\n- Fix file listing not working on Solaris systems\n- Fix refresh of single file selections failing on Windows systems\n- Fix conflict dialog being cut off\n- Fix renames not working in cmd\n- Fix not supporting dollar signs in directory names\n- Files with trailing or leading spaces are now included in the file list\n- Fix execute action on Windows not being limited to certain file types\n- Fix execute action not showing confirmation dialog if enabled\n\n## Other\n\n- The fonts on macOS have been updated with the goal of better readability.\n  If you don't like the new font style, you can still select the old one in the appearance settings menu\n- Tailscale connections are now a top-level entry and can be synced via git\n- Add ability to specify gateways for direct RDP and VNC connections\n- Add two new connection colors with cyan and purple\n- Add new option to prefer available monochrome icons instead of colored variants\n- Add vietnamese translations\n- Improve local shell fallback handling and docs\n- Add more documentation links to the settings menu\n- Proxmox entries are now ordered by their vmid\n- Kitty and WezTerm now also support the tabs or windows settings option for terminals\n- Any small changes such as a change in color other connection are now synced instantly\n- XPipe will now clean any temp files more often\n\n## Fixes\n\n- Fix application freeze when having password manager identities while password manager was not running\n- Fix apt and rpm package manager installs failing due to two download redirects causing a wrong checksum\n- Fix automatic tunnel restart sometimes resulting in an invalid tunnel state\n- Fix BSD and Solaris systems not being recognized correctly\n- Fix performance issue when opening connection chooser dropdown\n- Fix small parts of the UI moving a bit on hover\n- Fix Bitwarden integration in some cases requiring the master password every time\n- Fix restart button not working on Linux\n- Fix license check becoming invalid if xpipe was left running for more than a week\n- Fix git vaultversion conflict not being solved automatically\n- Fix container service entries not showing the port when added initially\n\nThere are also many other small changes that are not listed explicitly here.\n"
  },
  {
    "path": "dist/changelog/18.1.1_incremental.md",
    "content": "- Fix sudo elevation in terminal failing with wrong syntax in some cases\n- Add Windows Terminal profile override for starting directory setting\n"
  },
  {
    "path": "dist/changelog/18.1.md",
    "content": "XPipe 18 ventures into many new areas. It comes with the first cloud provider integration for Hetzner cloud to kickstart the process of integrating common cloud providers directly into XPipe. Furthermore, it comes with a new MCP server functionality to seamlessly integrate XPipe into your AI agent workflows. On top of everything, there are also many new convenience features like the network scan, multiple hostnames, SSH improvements, many bug fixes, and much more.\n\n## MCP server\n\nThere is now an MCP server available for XPipe which allows you to perform many actions in an agentic workflow via your favourite MCP client, for example Cursor. The MCP server feature is disabled by default has to be enabled in the MCP settings menu.\n\n[Documentation page](https://docs.xpipe.io/guide/mcp)\n\nHere is how it looks in Cursor:\n\n![MCP config](https://xpipe.io/assets/images/BlogPage/cursor-mcp.png)\n\nHere is how a chat that uses the XPipe MCP server looks like:\n\n![MCP screenshot](https://xpipe.io/assets/images/BlogPage/cursor-chat.png)\n\n## Hetzner cloud\n\nThis release introduces support for Hetzner cloud servers via the hcloud CLI tool. You can list all your service automatically and then access them normally as SSH connections. This is the first of hopefully many integrations for cloud providers and will serve as a good proof of concept. This integration is available in the Professional plan.\n\n![hcloud](https://xpipe.io/assets/images/BlogPage/hcloud.png)\n\n## Network scan\n\nThere is now the option to automatically search the local network for any listening SSH servers and add them automatically as new connections. This also works for remote systems and their networks.\n\nYou can find the network scan in the newly organized menu at New -> Network scan.\n\n![scan](https://xpipe.io/assets/images/BlogPage/network-scan.png)\n\n## Host addresses\n\nYou can now configure multiple addresses for a host. This allows you to quickly switch between different addresses if needed. \n\n![addresses](https://xpipe.io/assets/images/BlogPage/addresses.png)\n\n## SSH\n\n- Rework SSH timeout options to hopefully better handle unexpected connection disruptions\n- There is now a new settings option and improved handling of SSH MOTDs to control whether they should be shown or not\n- Fix SSH ProxyCommand not being executed for custom connections\n- Fix SSH agent public key setting not working on Linux and macOS due to permissions issue\n- The custom SSH agent setting now works differently. To use the agent socket specified in the settings menu,\n  you will now have to select the custom SSH agent for a connection. The default OpenSSH agent will no longer use this option\n- Fix custom SSH agent socket location setting not applying correctly\n- Fix SSH gateways not working on systems where username contained a dot\n- Fix some terminal connections asking for passwords even if it was entered before if the connection was edited\n- Add support to launch VsCode Insiders and Trae as well in the VsCode SSH launch menu\n\n## File browser\n\n- The initial transfer size calculation for large directories is now dynamically updated and shown\n- Fix script arguments not being passed with file selection\n- Fix file listing not working on Solaris systems\n- Fix refresh of single file selections failing on Windows systems\n- Fix conflict dialog being cut off\n- Fix renames not working in cmd\n- Fix not supporting dollar signs in directory names\n- Files with trailing or leading spaces are now included in the file list\n- Fix execute action on Windows not being limited to certain file types\n- Fix execute action not showing confirmation dialog if enabled\n\n## Other\n\n- The fonts on macOS have been updated with the goal of better readability.\n  If you don't like the new font style, you can still select the old one in the appearance settings menu\n- Tailscale connections are now a top-level entry and can be synced via git\n- Add ability to specify gateways for direct RDP and VNC connections\n- Add two new connection colors with cyan and purple\n- Add new option to prefer available monochrome icons instead of colored variants\n- Add vietnamese translations\n- Improve local shell fallback handling and docs\n- Add more documentation links to the settings menu\n- Proxmox entries are now ordered by their vmid\n- Kitty and WezTerm now also support the tabs or windows settings option for terminals\n- Any small changes such as a change in color other connection are now synced instantly\n- XPipe will now clean any temp files more often\n\n## Fixes\n\n- Fix application freeze when having password manager identities while password manager was not running\n- Fix apt and rpm package manager installs failing due to two download redirects causing a wrong checksum\n- Fix automatic tunnel restart sometimes resulting in an invalid tunnel state\n- Fix BSD and Solaris systems not being recognized correctly\n- Fix performance issue when opening connection chooser dropdown\n- Fix small parts of the UI moving a bit on hover\n- Fix Bitwarden integration in some cases requiring the master password every time\n- Fix restart button not working on Linux\n- Fix license check becoming invalid if xpipe was left running for more than a week\n- Fix git vaultversion conflict not being solved automatically\n- Fix container service entries not showing the port when added initially\n\nThere are also many other small changes that are not listed explicitly here.\n"
  },
  {
    "path": "dist/changelog/18.1_incremental.md",
    "content": "- Fix terminal instantly closing on Windows in some cases on first start after update\n- Fix Windows terminal launches not working if a difference starting directory was configured in the wt settings\n- Fix SSH MOTD behaviour setting being too strict \n- Fix possible NullPointer and race conditions in network scan\n- Fix AUR package checksums issue\n- Include color preview in connection color chooser menu\n"
  },
  {
    "path": "dist/changelog/18.2.md",
    "content": "XPipe 18 ventures into many new areas. It comes with the first cloud provider integration for Hetzner cloud to kickstart the process of integrating common cloud providers directly into XPipe. Furthermore, it comes with a new MCP server functionality to seamlessly integrate XPipe into your AI agent workflows. On top of everything, there are also many new convenience features like the network scan, multiple hostnames, SSH improvements, many bug fixes, and much more.\n\n## MCP server\n\nThere is now an MCP server available for XPipe which allows you to perform many actions in an agentic workflow via your favourite MCP client, for example Cursor. The MCP server feature is disabled by default has to be enabled in the MCP settings menu.\n\n[Documentation page](https://docs.xpipe.io/guide/mcp)\n\nHere is how it looks in Cursor:\n\n![MCP config](https://xpipe.io/assets/images/BlogPage/cursor-mcp.png)\n\nHere is how a chat that uses the XPipe MCP server looks like:\n\n![MCP screenshot](https://xpipe.io/assets/images/BlogPage/cursor-chat.png)\n\n## Hetzner cloud\n\nThis release introduces support for Hetzner cloud servers via the hcloud CLI tool. You can list all your service automatically and then access them normally as SSH connections. This is the first of hopefully many integrations for cloud providers and will serve as a good proof of concept. This integration is available in the Professional plan.\n\n![hcloud](https://xpipe.io/assets/images/BlogPage/hcloud.png)\n\n## Network scan\n\nThere is now the option to automatically search the local network for any listening SSH servers and add them automatically as new connections. This also works for remote systems and their networks.\n\nYou can find the network scan in the newly organized menu at New -> Network scan.\n\n![scan](https://xpipe.io/assets/images/BlogPage/network-scan.png)\n\n## Host addresses\n\nYou can now configure multiple addresses for a host. This allows you to quickly switch between different addresses if needed. \n\n![addresses](https://xpipe.io/assets/images/BlogPage/addresses.png)\n\n## SSH\n\n- Rework SSH timeout options to hopefully better handle unexpected connection disruptions\n- There is now a new settings option and improved handling of SSH MOTDs to control whether they should be shown or not\n- Fix SSH ProxyCommand not being executed for custom connections\n- Fix SSH agent public key setting not working on Linux and macOS due to permissions issue\n- The custom SSH agent setting now works differently. To use the agent socket specified in the settings menu,\n  you will now have to select the custom SSH agent for a connection. The default OpenSSH agent will no longer use this option\n- Fix custom SSH agent socket location setting not applying correctly\n- Fix SSH gateways not working on systems where username contained a dot\n- Fix some terminal connections asking for passwords even if it was entered before if the connection was edited\n- Add support to launch VsCode Insiders and Trae as well in the VsCode SSH launch menu\n\n## File browser\n\n- The initial transfer size calculation for large directories is now dynamically updated and shown\n- Fix script arguments not being passed with file selection\n- Fix file listing not working on Solaris systems\n- Fix refresh of single file selections failing on Windows systems\n- Fix conflict dialog being cut off\n- Fix renames not working in cmd\n- Fix not supporting dollar signs in directory names\n- Files with trailing or leading spaces are now included in the file list\n- Fix execute action on Windows not being limited to certain file types\n- Fix execute action not showing confirmation dialog if enabled\n\n## Other\n\n- The fonts on macOS have been updated with the goal of better readability.\n  If you don't like the new font style, you can still select the old one in the appearance settings menu\n- Tailscale connections are now a top-level entry and can be synced via git\n- Add ability to specify gateways for direct RDP and VNC connections\n- Add two new connection colors with cyan and purple\n- Add new option to prefer available monochrome icons instead of colored variants\n- Add vietnamese translations\n- Improve local shell fallback handling and docs\n- Add more documentation links to the settings menu\n- Proxmox entries are now ordered by their vmid\n- Kitty and WezTerm now also support the tabs or windows settings option for terminals\n- Any small changes such as a change in color other connection are now synced instantly\n- XPipe will now clean any temp files more often\n\n## Fixes\n\n- Fix application freeze when having password manager identities while password manager was not running\n- Fix apt and rpm package manager installs failing due to two download redirects causing a wrong checksum\n- Fix automatic tunnel restart sometimes resulting in an invalid tunnel state\n- Fix BSD and Solaris systems not being recognized correctly\n- Fix performance issue when opening connection chooser dropdown\n- Fix small parts of the UI moving a bit on hover\n- Fix Bitwarden integration in some cases requiring the master password every time\n- Fix restart button not working on Linux\n- Fix license check becoming invalid if xpipe was left running for more than a week\n- Fix git vaultversion conflict not being solved automatically\n- Fix container service entries not showing the port when added initially\n\nThere are also many other small changes that are not listed explicitly here.\n"
  },
  {
    "path": "dist/changelog/18.2_incremental.md",
    "content": "- Rework webtop configuration and documentation to support tailscale properly\n- Make connection clone button preserve all connection entry properties for the copy\n- Fix error on rename of dotfiles in file browser\n- Fix errors in cmd.exe when SSH key file path did contain non-ASCII characters\n- Fix remote cmd.exe startup errors on Linux and macOS\n- Fix file chooser dialog showing all systems instead of only applicable ones\n- Add terminal option to play bell sound on terminal session init\n- Add more documentation links for shell init timeouts\n"
  },
  {
    "path": "dist/changelog/18.3.md",
    "content": "XPipe 18 ventures into many new areas. It comes with the first cloud provider integration for Hetzner cloud to kickstart the process of integrating common cloud providers directly into XPipe. Furthermore, it comes with a new MCP server functionality to seamlessly integrate XPipe into your AI agent workflows. On top of everything, there are also many new convenience features like the network scan, multiple hostnames, SSH improvements, many bug fixes, and much more.\n\n## MCP server\n\nThere is now an MCP server available for XPipe which allows you to perform many actions in an agentic workflow via your favourite MCP client, for example Cursor. The MCP server feature is disabled by default has to be enabled in the MCP settings menu.\n\n[Documentation page](https://docs.xpipe.io/guide/mcp)\n\nHere is how it looks in Cursor:\n\n![MCP config](https://xpipe.io/assets/images/BlogPage/cursor-mcp.png)\n\nHere is how a chat that uses the XPipe MCP server looks like:\n\n![MCP screenshot](https://xpipe.io/assets/images/BlogPage/cursor-chat.png)\n\n## Hetzner cloud\n\nThis release introduces support for Hetzner cloud servers via the hcloud CLI tool. You can list all your service automatically and then access them normally as SSH connections. This is the first of hopefully many integrations for cloud providers and will serve as a good proof of concept. This integration is available in the Professional plan.\n\n![hcloud](https://xpipe.io/assets/images/BlogPage/hcloud.png)\n\n## Network scan\n\nThere is now the option to automatically search the local network for any listening SSH servers and add them automatically as new connections. This also works for remote systems and their networks.\n\nYou can find the network scan in the newly organized menu at New -> Network scan.\n\n![scan](https://xpipe.io/assets/images/BlogPage/network-scan.png)\n\n## Host addresses\n\nYou can now configure multiple addresses for a host. This allows you to quickly switch between different addresses if needed. \n\n![addresses](https://xpipe.io/assets/images/BlogPage/addresses.png)\n\n## SSH\n\n- Rework SSH timeout options to hopefully better handle unexpected connection disruptions\n- There is now a new settings option and improved handling of SSH MOTDs to control whether they should be shown or not\n- Fix SSH ProxyCommand not being executed for custom connections\n- Fix SSH agent public key setting not working on Linux and macOS due to permissions issue\n- The custom SSH agent setting now works differently. To use the agent socket specified in the settings menu,\n  you will now have to select the custom SSH agent for a connection. The default OpenSSH agent will no longer use this option\n- Fix custom SSH agent socket location setting not applying correctly\n- Fix SSH gateways not working on systems where username contained a dot\n- Fix some terminal connections asking for passwords even if it was entered before if the connection was edited\n- Add support to launch VsCode Insiders and Trae as well in the VsCode SSH launch menu\n\n## File browser\n\n- The initial transfer size calculation for large directories is now dynamically updated and shown\n- Fix script arguments not being passed with file selection\n- Fix file listing not working on Solaris systems\n- Fix refresh of single file selections failing on Windows systems\n- Fix conflict dialog being cut off\n- Fix renames not working in cmd\n- Fix not supporting dollar signs in directory names\n- Files with trailing or leading spaces are now included in the file list\n- Fix execute action on Windows not being limited to certain file types\n- Fix execute action not showing confirmation dialog if enabled\n\n## Other\n\n- The fonts on macOS have been updated with the goal of better readability.\n  If you don't like the new font style, you can still select the old one in the appearance settings menu\n- Tailscale connections are now a top-level entry and can be synced via git\n- Add ability to specify gateways for direct RDP and VNC connections\n- Add two new connection colors with cyan and purple\n- Add new option to prefer available monochrome icons instead of colored variants\n- Add vietnamese translations\n- Improve local shell fallback handling and docs\n- Add more documentation links to the settings menu\n- Proxmox entries are now ordered by their vmid\n- Kitty and WezTerm now also support the tabs or windows settings option for terminals\n- Any small changes such as a change in color other connection are now synced instantly\n- XPipe will now clean any temp files more often\n\n## Fixes\n\n- Fix application freeze when having password manager identities while password manager was not running\n- Fix apt and rpm package manager installs failing due to two download redirects causing a wrong checksum\n- Fix automatic tunnel restart sometimes resulting in an invalid tunnel state\n- Fix BSD and Solaris systems not being recognized correctly\n- Fix performance issue when opening connection chooser dropdown\n- Fix small parts of the UI moving a bit on hover\n- Fix Bitwarden integration in some cases requiring the master password every time\n- Fix restart button not working on Linux\n- Fix license check becoming invalid if xpipe was left running for more than a week\n- Fix git vaultversion conflict not being solved automatically\n- Fix container service entries not showing the port when added initially\n\nThere are also many other small changes that are not listed explicitly here.\n"
  },
  {
    "path": "dist/changelog/18.3_incremental.md",
    "content": "- Fix docker compose container grouping not working when compose file name different from running compose project name\n- Fix SSH config file import not autofilling key info when detected\n- Fix SSH key spaces breaking config connections\n- Fix SSH config breaking with unknown WarnWeakCrypto option\n- Fix SSH config permissions being wrong on connection update with certain user configurations\n- Fix terminal open failing when rm=\"rm -i\" alias was configured on system\n- Update chinese translations (Thanks to @Xiaomckedou233 for the contribution)\n- Add support to open VSCodium workspaces in addition to all other VSCode applications\n- Add Proxmox PBS dashboard service entry if available\n- Add Windows ARM download links to website as they are now considered stable\n- Show warning when x64 Windows build is used on ARM systems\n"
  },
  {
    "path": "dist/changelog/18.4.md",
    "content": "XPipe 18 ventures into many new areas. It comes with the first cloud provider integration for Hetzner cloud to kickstart the process of integrating common cloud providers directly into XPipe. Furthermore, it comes with a new MCP server functionality to seamlessly integrate XPipe into your AI agent workflows. On top of everything, there are also many new convenience features like the network scan, multiple hostnames, SSH improvements, many bug fixes, and much more.\n\n## MCP server\n\nThere is now an MCP server available for XPipe which allows you to perform many actions in an agentic workflow via your favourite MCP client, for example Cursor. The MCP server feature is disabled by default has to be enabled in the MCP settings menu.\n\n[Documentation page](https://docs.xpipe.io/guide/mcp)\n\nHere is how it looks in Cursor:\n\n![MCP config](https://xpipe.io/assets/images/BlogPage/cursor-mcp.png)\n\nHere is how a chat that uses the XPipe MCP server looks like:\n\n![MCP screenshot](https://xpipe.io/assets/images/BlogPage/cursor-chat.png)\n\n## Hetzner cloud\n\nThis release introduces support for Hetzner cloud servers via the hcloud CLI tool. You can list all your service automatically and then access them normally as SSH connections. This is the first of hopefully many integrations for cloud providers and will serve as a good proof of concept. This integration is available in the Professional plan.\n\n![hcloud](https://xpipe.io/assets/images/BlogPage/hcloud.png)\n\n## Network scan\n\nThere is now the option to automatically search the local network for any listening SSH servers and add them automatically as new connections. This also works for remote systems and their networks.\n\nYou can find the network scan in the newly organized menu at New -> Network scan.\n\n![scan](https://xpipe.io/assets/images/BlogPage/network-scan.png)\n\n## Host addresses\n\nYou can now configure multiple addresses for a host. This allows you to quickly switch between different addresses if needed. \n\n![addresses](https://xpipe.io/assets/images/BlogPage/addresses.png)\n\n## SSH\n\n- Rework SSH timeout options to hopefully better handle unexpected connection disruptions\n- There is now a new settings option and improved handling of SSH MOTDs to control whether they should be shown or not\n- Fix SSH ProxyCommand not being executed for custom connections\n- Fix SSH agent public key setting not working on Linux and macOS due to permissions issue\n- The custom SSH agent setting now works differently. To use the agent socket specified in the settings menu,\n  you will now have to select the custom SSH agent for a connection. The default OpenSSH agent will no longer use this option\n- Fix custom SSH agent socket location setting not applying correctly\n- Fix SSH gateways not working on systems where username contained a dot\n- Fix some terminal connections asking for passwords even if it was entered before if the connection was edited\n- Add support to launch VsCode Insiders and Trae as well in the VsCode SSH launch menu\n\n## File browser\n\n- The initial transfer size calculation for large directories is now dynamically updated and shown\n- Fix script arguments not being passed with file selection\n- Fix file listing not working on Solaris systems\n- Fix refresh of single file selections failing on Windows systems\n- Fix conflict dialog being cut off\n- Fix renames not working in cmd\n- Fix not supporting dollar signs in directory names\n- Files with trailing or leading spaces are now included in the file list\n- Fix execute action on Windows not being limited to certain file types\n- Fix execute action not showing confirmation dialog if enabled\n\n## Other\n\n- The fonts on macOS have been updated with the goal of better readability.\n  If you don't like the new font style, you can still select the old one in the appearance settings menu\n- Tailscale connections are now a top-level entry and can be synced via git\n- Add ability to specify gateways for direct RDP and VNC connections\n- Add two new connection colors with cyan and purple\n- Add new option to prefer available monochrome icons instead of colored variants\n- Add vietnamese translations\n- Improve local shell fallback handling and docs\n- Add more documentation links to the settings menu\n- Proxmox entries are now ordered by their vmid\n- Kitty and WezTerm now also support the tabs or windows settings option for terminals\n- Any small changes such as a change in color other connection are now synced instantly\n- XPipe will now clean any temp files more often\n\n## Fixes\n\n- Fix application freeze when having password manager identities while password manager was not running\n- Fix apt and rpm package manager installs failing due to two download redirects causing a wrong checksum\n- Fix automatic tunnel restart sometimes resulting in an invalid tunnel state\n- Fix BSD and Solaris systems not being recognized correctly\n- Fix performance issue when opening connection chooser dropdown\n- Fix small parts of the UI moving a bit on hover\n- Fix Bitwarden integration in some cases requiring the master password every time\n- Fix restart button not working on Linux\n- Fix license check becoming invalid if xpipe was left running for more than a week\n- Fix git vaultversion conflict not being solved automatically\n- Fix container service entries not showing the port when added initially\n\nThere are also many other small changes that are not listed explicitly here.\n"
  },
  {
    "path": "dist/changelog/18.4_incremental.md",
    "content": "- Fix desktop shortcuts for workspaces and actions not working on Linux and macOS\n- Fix computed directory file size being off by a factor of 1024 on Linux\n- Fix license check for entries limited in amount, e.g. Proxmox, potentially disabling all entries and not only one\n- Fix custom git icon sources not persisting after restart\n- Fix docker integration not elevating with sudo if context config required root permissions\n- Fix window possibly entering invalid state on Windows and not showing anymore\n- Fix tunnel session restart not applying any changes made to connection config\n- Show warning when invalid SSH gateway chain is configured\n- Reshow any existing configuration dialog if possible for connection when editing it\n- Improve error handling on Windows when registry library load fails\n- Automatically open connection configuration dialog when cloning a connection\n- Derive custom git icon source directory name from repository URL\n"
  },
  {
    "path": "dist/changelog/18.5.md",
    "content": "XPipe 18 ventures into many new areas. It comes with the first cloud provider integration for Hetzner cloud to kickstart the process of integrating common cloud providers directly into XPipe. Furthermore, it comes with a new MCP server functionality to seamlessly integrate XPipe into your AI agent workflows. On top of everything, there are also many new convenience features like the network scan, multiple hostnames, SSH improvements, many bug fixes, and much more.\n\n## MCP server\n\nThere is now an MCP server available for XPipe which allows you to perform many actions in an agentic workflow via your favourite MCP client, for example Cursor. The MCP server feature is disabled by default has to be enabled in the MCP settings menu.\n\n[Documentation page](https://docs.xpipe.io/guide/mcp)\n\nHere is how it looks in Cursor:\n\n![MCP config](https://xpipe.io/assets/images/BlogPage/cursor-mcp.png)\n\nHere is how a chat that uses the XPipe MCP server looks like:\n\n![MCP screenshot](https://xpipe.io/assets/images/BlogPage/cursor-chat.png)\n\n## Hetzner cloud\n\nThis release introduces support for Hetzner cloud servers via the hcloud CLI tool. You can list all your service automatically and then access them normally as SSH connections. This is the first of hopefully many integrations for cloud providers and will serve as a good proof of concept. This integration is available in the Professional plan.\n\n![hcloud](https://xpipe.io/assets/images/BlogPage/hcloud.png)\n\n## Network scan\n\nThere is now the option to automatically search the local network for any listening SSH servers and add them automatically as new connections. This also works for remote systems and their networks.\n\nYou can find the network scan in the newly organized menu at New -> Network scan.\n\n![scan](https://xpipe.io/assets/images/BlogPage/network-scan.png)\n\n## Host addresses\n\nYou can now configure multiple addresses for a host. This allows you to quickly switch between different addresses if needed. \n\n![addresses](https://xpipe.io/assets/images/BlogPage/addresses.png)\n\n## SSH\n\n- Rework SSH timeout options to hopefully better handle unexpected connection disruptions\n- There is now a new settings option and improved handling of SSH MOTDs to control whether they should be shown or not\n- Fix SSH ProxyCommand not being executed for custom connections\n- Fix SSH agent public key setting not working on Linux and macOS due to permissions issue\n- The custom SSH agent setting now works differently. To use the agent socket specified in the settings menu,\n  you will now have to select the custom SSH agent for a connection. The default OpenSSH agent will no longer use this option\n- Fix custom SSH agent socket location setting not applying correctly\n- Fix SSH gateways not working on systems where username contained a dot\n- Fix some terminal connections asking for passwords even if it was entered before if the connection was edited\n- Add support to launch VsCode Insiders and Trae as well in the VsCode SSH launch menu\n\n## File browser\n\n- The initial transfer size calculation for large directories is now dynamically updated and shown\n- Fix script arguments not being passed with file selection\n- Fix file listing not working on Solaris systems\n- Fix refresh of single file selections failing on Windows systems\n- Fix conflict dialog being cut off\n- Fix renames not working in cmd\n- Fix not supporting dollar signs in directory names\n- Files with trailing or leading spaces are now included in the file list\n- Fix execute action on Windows not being limited to certain file types\n- Fix execute action not showing confirmation dialog if enabled\n\n## Other\n\n- The fonts on macOS have been updated with the goal of better readability.\n  If you don't like the new font style, you can still select the old one in the appearance settings menu\n- Tailscale connections are now a top-level entry and can be synced via git\n- Add ability to specify gateways for direct RDP and VNC connections\n- Add two new connection colors with cyan and purple\n- Add new option to prefer available monochrome icons instead of colored variants\n- Add vietnamese translations\n- Improve local shell fallback handling and docs\n- Add more documentation links to the settings menu\n- Proxmox entries are now ordered by their vmid\n- Kitty and WezTerm now also support the tabs or windows settings option for terminals\n- Any small changes such as a change in color other connection are now synced instantly\n- XPipe will now clean any temp files more often\n\n## Fixes\n\n- Fix application freeze when having password manager identities while password manager was not running\n- Fix apt and rpm package manager installs failing due to two download redirects causing a wrong checksum\n- Fix automatic tunnel restart sometimes resulting in an invalid tunnel state\n- Fix BSD and Solaris systems not being recognized correctly\n- Fix performance issue when opening connection chooser dropdown\n- Fix small parts of the UI moving a bit on hover\n- Fix Bitwarden integration in some cases requiring the master password every time\n- Fix restart button not working on Linux\n- Fix license check becoming invalid if xpipe was left running for more than a week\n- Fix git vaultversion conflict not being solved automatically\n- Fix container service entries not showing the port when added initially\n\nThere are also many other small changes that are not listed explicitly here.\n"
  },
  {
    "path": "dist/changelog/18.5_incremental.md",
    "content": "- Fix self-update not working when shown after XPipe restart\n- Fix some startup performance slowdowns\n- Fix AUR package info not being updated\n- Fix file renames with only case differences not applying on macOS\n- Fix browser delete action not working in quick access menu\n- Fix Windows registry query errors if registry access was blocked\n- Add option to rename synced key file in case one with the same name already exists\n- Add additional check to verify valid /tmp permissions on Linux\n"
  },
  {
    "path": "dist/changelog/18.6.md",
    "content": "XPipe 18 ventures into many new areas. It comes with the first cloud provider integration for Hetzner cloud to kickstart the process of integrating common cloud providers directly into XPipe. Furthermore, it comes with a new MCP server functionality to seamlessly integrate XPipe into your AI agent workflows. On top of everything, there are also many new convenience features like the network scan, multiple hostnames, SSH improvements, many bug fixes, and much more.\n\n## MCP server\n\nThere is now an MCP server available for XPipe which allows you to perform many actions in an agentic workflow via your favourite MCP client, for example Cursor. The MCP server feature is disabled by default has to be enabled in the MCP settings menu.\n\n[Documentation page](https://docs.xpipe.io/guide/mcp)\n\nHere is how it looks in Cursor:\n\n![MCP config](https://xpipe.io/assets/images/BlogPage/cursor-mcp.png)\n\nHere is how a chat that uses the XPipe MCP server looks like:\n\n![MCP screenshot](https://xpipe.io/assets/images/BlogPage/cursor-chat.png)\n\n## Hetzner cloud\n\nThis release introduces support for Hetzner cloud servers via the hcloud CLI tool. You can list all your service automatically and then access them normally as SSH connections. This is the first of hopefully many integrations for cloud providers and will serve as a good proof of concept. This integration is available in the Professional plan.\n\n![hcloud](https://xpipe.io/assets/images/BlogPage/hcloud.png)\n\n## Network scan\n\nThere is now the option to automatically search the local network for any listening SSH servers and add them automatically as new connections. This also works for remote systems and their networks.\n\nYou can find the network scan in the newly organized menu at New -> Network scan.\n\n![scan](https://xpipe.io/assets/images/BlogPage/network-scan.png)\n\n## Host addresses\n\nYou can now configure multiple addresses for a host. This allows you to quickly switch between different addresses if needed. \n\n![addresses](https://xpipe.io/assets/images/BlogPage/addresses.png)\n\n## SSH\n\n- Rework SSH timeout options to hopefully better handle unexpected connection disruptions\n- There is now a new settings option and improved handling of SSH MOTDs to control whether they should be shown or not\n- Fix SSH ProxyCommand not being executed for custom connections\n- Fix SSH agent public key setting not working on Linux and macOS due to permissions issue\n- The custom SSH agent setting now works differently. To use the agent socket specified in the settings menu,\n  you will now have to select the custom SSH agent for a connection. The default OpenSSH agent will no longer use this option\n- Fix custom SSH agent socket location setting not applying correctly\n- Fix SSH gateways not working on systems where username contained a dot\n- Fix some terminal connections asking for passwords even if it was entered before if the connection was edited\n- Add support to launch VsCode Insiders and Trae as well in the VsCode SSH launch menu\n\n## File browser\n\n- The initial transfer size calculation for large directories is now dynamically updated and shown\n- Fix script arguments not being passed with file selection\n- Fix file listing not working on Solaris systems\n- Fix refresh of single file selections failing on Windows systems\n- Fix conflict dialog being cut off\n- Fix renames not working in cmd\n- Fix not supporting dollar signs in directory names\n- Files with trailing or leading spaces are now included in the file list\n- Fix execute action on Windows not being limited to certain file types\n- Fix execute action not showing confirmation dialog if enabled\n\n## Other\n\n- The fonts on macOS have been updated with the goal of better readability.\n  If you don't like the new font style, you can still select the old one in the appearance settings menu\n- Tailscale connections are now a top-level entry and can be synced via git\n- Add ability to specify gateways for direct RDP and VNC connections\n- Add two new connection colors with cyan and purple\n- Add new option to prefer available monochrome icons instead of colored variants\n- Add vietnamese translations\n- Improve local shell fallback handling and docs\n- Add more documentation links to the settings menu\n- Proxmox entries are now ordered by their vmid\n- Kitty and WezTerm now also support the tabs or windows settings option for terminals\n- Any small changes such as a change in color other connection are now synced instantly\n- XPipe will now clean any temp files more often\n\n## Fixes\n\n- Fix application freeze when having password manager identities while password manager was not running\n- Fix apt and rpm package manager installs failing due to two download redirects causing a wrong checksum\n- Fix automatic tunnel restart sometimes resulting in an invalid tunnel state\n- Fix BSD and Solaris systems not being recognized correctly\n- Fix performance issue when opening connection chooser dropdown\n- Fix small parts of the UI moving a bit on hover\n- Fix Bitwarden integration in some cases requiring the master password every time\n- Fix restart button not working on Linux\n- Fix license check becoming invalid if xpipe was left running for more than a week\n- Fix git vaultversion conflict not being solved automatically\n- Fix container service entries not showing the port when added initially\n\nThere are also many other small changes that are not listed explicitly here.\n"
  },
  {
    "path": "dist/changelog/18.6_incremental.md",
    "content": "- Fix some types of connection entries being moved to default category after their configuration was edited\n- Fix bitwarden login issues when using custom server\n- Fix some NullPointers\n- The file browser address bar and file list will now update at the same time\n- Rotate service URLs to avoid browser cache issues when running multiple similar services on localhost\n"
  },
  {
    "path": "dist/changelog/18.7.md",
    "content": "XPipe 18 ventures into many new areas. It comes with the first cloud provider integration for Hetzner cloud to kickstart the process of integrating common cloud providers directly into XPipe. Furthermore, it comes with a new MCP server functionality to seamlessly integrate XPipe into your AI agent workflows. On top of everything, there are also many new convenience features like the network scan, multiple hostnames, SSH improvements, many bug fixes, and much more.\n\n## MCP server\n\nThere is now an MCP server available for XPipe which allows you to perform many actions in an agentic workflow via your favourite MCP client, for example Cursor. The MCP server feature is disabled by default has to be enabled in the MCP settings menu.\n\n[Documentation page](https://docs.xpipe.io/guide/mcp)\n\nHere is how it looks in Cursor:\n\n![MCP config](https://xpipe.io/assets/images/BlogPage/cursor-mcp.png)\n\nHere is how a chat that uses the XPipe MCP server looks like:\n\n![MCP screenshot](https://xpipe.io/assets/images/BlogPage/cursor-chat.png)\n\n## Hetzner cloud\n\nThis release introduces support for Hetzner cloud servers via the hcloud CLI tool. You can list all your service automatically and then access them normally as SSH connections. This is the first of hopefully many integrations for cloud providers and will serve as a good proof of concept. This integration is available in the Professional plan.\n\n![hcloud](https://xpipe.io/assets/images/BlogPage/hcloud.png)\n\n## Network scan\n\nThere is now the option to automatically search the local network for any listening SSH servers and add them automatically as new connections. This also works for remote systems and their networks.\n\nYou can find the network scan in the newly organized menu at New -> Network scan.\n\n![scan](https://xpipe.io/assets/images/BlogPage/network-scan.png)\n\n## Host addresses\n\nYou can now configure multiple addresses for a host. This allows you to quickly switch between different addresses if needed. \n\n![addresses](https://xpipe.io/assets/images/BlogPage/addresses.png)\n\n## SSH\n\n- Rework SSH timeout options to hopefully better handle unexpected connection disruptions\n- There is now a new settings option and improved handling of SSH MOTDs to control whether they should be shown or not\n- Fix SSH ProxyCommand not being executed for custom connections\n- Fix SSH agent public key setting not working on Linux and macOS due to permissions issue\n- The custom SSH agent setting now works differently. To use the agent socket specified in the settings menu,\n  you will now have to select the custom SSH agent for a connection. The default OpenSSH agent will no longer use this option\n- Fix custom SSH agent socket location setting not applying correctly\n- Fix SSH gateways not working on systems where username contained a dot\n- Fix some terminal connections asking for passwords even if it was entered before if the connection was edited\n- Add support to launch VsCode Insiders and Trae as well in the VsCode SSH launch menu\n\n## File browser\n\n- The initial transfer size calculation for large directories is now dynamically updated and shown\n- Fix script arguments not being passed with file selection\n- Fix file listing not working on Solaris systems\n- Fix refresh of single file selections failing on Windows systems\n- Fix conflict dialog being cut off\n- Fix renames not working in cmd\n- Fix not supporting dollar signs in directory names\n- Files with trailing or leading spaces are now included in the file list\n- Fix execute action on Windows not being limited to certain file types\n- Fix execute action not showing confirmation dialog if enabled\n\n## Other\n\n- The fonts on macOS have been updated with the goal of better readability.\n  If you don't like the new font style, you can still select the old one in the appearance settings menu\n- Tailscale connections are now a top-level entry and can be synced via git\n- Add ability to specify gateways for direct RDP and VNC connections\n- Add two new connection colors with cyan and purple\n- Add new option to prefer available monochrome icons instead of colored variants\n- Add vietnamese translations\n- Improve local shell fallback handling and docs\n- Add more documentation links to the settings menu\n- Proxmox entries are now ordered by their vmid\n- Kitty and WezTerm now also support the tabs or windows settings option for terminals\n- Any small changes such as a change in color other connection are now synced instantly\n- XPipe will now clean any temp files more often\n\n## Fixes\n\n- Fix application freeze when having password manager identities while password manager was not running\n- Fix apt and rpm package manager installs failing due to two download redirects causing a wrong checksum\n- Fix automatic tunnel restart sometimes resulting in an invalid tunnel state\n- Fix BSD and Solaris systems not being recognized correctly\n- Fix performance issue when opening connection chooser dropdown\n- Fix small parts of the UI moving a bit on hover\n- Fix Bitwarden integration in some cases requiring the master password every time\n- Fix restart button not working on Linux\n- Fix license check becoming invalid if xpipe was left running for more than a week\n- Fix git vaultversion conflict not being solved automatically\n- Fix container service entries not showing the port when added initially\n\nThere are also many other small changes that are not listed explicitly here.\n"
  },
  {
    "path": "dist/changelog/18.7_incremental.md",
    "content": "- Fix sudo elevation requiring a different approach on the newly released Ubuntu 25.10 due to sudo being replaced by a rust sudo variant\n- Fix host address text field sometimes entering invalid state and not applying hostname changes\n- Fix exception when changing synced key scope to per-user\n- Fix macOS Tahoe display name containing version twice\n- Fix inaccurate error message when a parent of a service did not support tunneling\n- Add setting to customize markdown notes template for connections\n- Remove confirm dialog when skipping connection validation to allow for faster connection creation\n"
  },
  {
    "path": "dist/changelog/19.0.1.md",
    "content": "## Netbird support\n\nYou can now connect to devices in your Netbird network via SSH and your locally installed netbird command-line client.\n\n![Netbird](https://xpipe.io/assets/images/BlogPage/netbird.png)\n\n## Legacy system support\n\nUp until now, the testing was done on *relatively* up-to-date machines that were not considered EOL. However, in practice, legacy systems are still used. With XPipe 19, the handling of older Unix-based systems has been greatly improved. As long as you can connect to a system via SSH somehow, it should work now regardless of how old the system is. \n\nThe following things have been implemented to improve legacy system support:\n- The unsupported SSH host key / key / MAC dialog will now search for matches that the client supports as well. This is to fix situations in which a server lists crypto algorithms that are so old that a modern SSH clients doesn't even know them anymore\n- Fix file listing not working on older/limited versions of `find`\n- Fix various SSH timeout options being too aggressive for older versions, resulting in connect timeouts\n- Fix tar browser actions not working on older tar versions\n- Fix systems where `set -u` was configured breaking xpipe's shell handling\n- Fix file transfer failing on systems with non-GNU `dd` to work around missing `fullblock` option\n- Fix various commands on systems where `sleep` command did not accept decimals\n- Improve non-Linux Unix-based system name formatting\n- Add full recognition for AIX system versions\n\n![AIX](https://xpipe.io/assets/images/BlogPage/aix.png)\n\n![HP-UX](https://xpipe.io/assets/images/BlogPage/hpux.png)\n\nYou are running old systems? Then feel free to test how XPipe 19 handles for you, and report anything that doesn't work yet.\n\n## Abstract hosts and connection organization\n\nUp until now, the handling of systems which did not support shell connections has been limited. While you could add a direct VNC / RDP connection entry, the organization wasn't as refined as with shell-based connections. For those hosts, you can now make use of the improved handling that was added for services and VNC/RDP connections.\n\nEssentially, you can now specify the target address inline or choose an existing host from xpipe. XPipe will then automatically adapt the connection based on what is possible. E.g. if you want to open a web service directly on a device without shell access, e.g. some embedded device, just specify the hostname and optional gateway. You can then open the website in the browser while also having a nice grouping of various connection entries for an abstract host in case you have more connections added for the same host:\n\n![Abstract host config](https://xpipe.io/assets/images/BlogPage/abstract-host-config.png)\n\nYou can also quickly convert existing connections entries like services and direct RDP/VNC connections to an abstract host entry to allow for better organization.\n\n![Abstract host](https://xpipe.io/assets/images/BlogPage/abstract-host.png)\n\n## SFTP support\n\nThere is now support for systems which only support pure SFTP and no SSH shell sessions. These can be opened in the file browser as normal, although with limited functionality as there is no way to execute commands on these systems.\n\n![SFTP](https://xpipe.io/assets/images/BlogPage/sftp.png)\n\n## Other\n\n- Add support for flatpak variants of various editors and terminals\n- Add ability to read files via sudo if current user did not have read permission for a file\n- Improve browser drag-and-drop to also work on the navigation bar to move between directories\n- Add ability to move categories to different parents\n- The accent color of your system appearance now applies to all UI elements\n- Make tunnels close instantly even if there was still traffic going through\n- Bump to JDK 25 and JavaFX 25\n- Rework password choice order\n- Add setting for mstsc RDP client to use smart sizing\n- Add traditional chinese translations\n- Add support for Zed editor on Windows\n- Rework gpg git initialization on Windows due to severe gpg agent slowdowns\n- Improve caching of various shell operations for speed improvements\n- The nixpkg package now also supports macOS and has been reworked as a flake. If you use XPipe on Nix, take a look at https://github.com/xpipe-io/nixpkg for the changes\n- Don't accept 7zip drag-and-drop to prevent confusion about non-existent files\n- The custom icon cache now always registers when it is out of date, prompting you to refresh\n- You can now automatically start service tunnels as well when XPipe is launched\n- Add support to automatically add unsupported SSH MAC type\n- Improve default browser detection for URL opens\n- The file browser can now automatically open UNC paths when using cmd by opening a new PowerShell tab\n- Add option to control application behavior on system hibernation\n- Add possible workaround when default beacon HTTP server port was not usable\n- Improve reliability of shell session restarts when a connection was interrupted\n- There is also now a checkout page for chinese users, which supports common chinese payment methods.\n\n## Fixes\n\n- Fix connection entry validity state sometimes not updating immediately when edited\n- Fix SSH connection host key type / kex type override not applying changes instantly\n- Fix misleading error messages when a file transfer was interrupted or permissions were missing\n- Fix Remmina RDP integration not supporting user domain prefix\n- Fix Linux FreeRDP not using FreeRDP v3 by default\n- Fix identities being moved to initial category when being editing\n- Fix system state info for some VMs and containers not showing\n- Fix fish shell v4 with init scripts failing to launch\n- Fix SSH MOTD errors with fish as a login shell\n- Fix vietnamese translations not being listed\n- Fix kitty launch race conditions sometimes breaking socket\n- Fix synced identities not syncing when no other categories were synced\n- Fix bitwarden sync not refreshing xpipe cache for bitwarden passwords\n- Fix sudo auth failing on legacy systems where openssl did not support certain options\n- Fix Remmina integration failing to encrypt passwords with special characters\n- Fix AppImage workspaces and app restart not working\n- Fix native file manager open not properly selecting files\n- Fix connection entries being able to be renamed to a blank string\n- Fix apple container integration freezing when recommended kernel was not installed\n- Fix local KVM integration constantly bringing up polkit auth window\n- Fix super key not being handled in VNC client\n- Fix Proxmox VNC action not setting empty password\n- Fix opened as root browser tab not launching correct shell in terminal for fish\n- Fix VNC scan adding localhost as a connection on macOS\n- Fix file git sync button sometimes being enabled too early\n- Fix errors when configuring an invalid git account password\n- Fix freeze on macOS with 1password agent when starting in background\n- Fix various NullPointers\n"
  },
  {
    "path": "dist/changelog/19.0.2_incremental.md",
    "content": "- Fix startup failure when git sync SSH was enabled and the app was configured to start in background\n"
  },
  {
    "path": "dist/changelog/19.0.md",
    "content": "## Netbird support\n\nYou can now connect to devices in your Netbird network via SSH and your locally installed netbird command-line client.\n\n![Netbird](https://xpipe.io/assets/images/BlogPage/netbird.png)\n\n## Legacy system support\n\nUp until now, the testing was done on *relatively* up-to-date machines that were not considered EOL. However, in practice, legacy systems are still used. With XPipe 19, the handling of older Unix-based systems has been greatly improved. As long as you can connect to a system via SSH somehow, it should work now regardless of how old the system is. \n\nThe following things have been implemented to improve legacy system support:\n- The unsupported SSH host key / key / MAC dialog will now search for matches that the client supports as well. This is to fix situations in which a server lists crypto algorithms that are so old that a modern SSH clients doesn't even know them anymore\n- Fix file listing not working on older/limited versions of `find`\n- Fix various SSH timeout options being too aggressive for older versions, resulting in connect timeouts\n- Fix tar browser actions not working on older tar versions\n- Fix systems where `set -u` was configured breaking xpipe's shell handling\n- Fix file transfer failing on systems with non-GNU `dd` to work around missing `fullblock` option\n- Fix various commands on systems where `sleep` command did not accept decimals\n- Improve non-Linux Unix-based system name formatting\n- Add full recognition for AIX system versions\n\n![AIX](https://xpipe.io/assets/images/BlogPage/aix.png)\n\n![HP-UX](https://xpipe.io/assets/images/BlogPage/hpux.png)\n\nYou are running old systems? Then feel free to test how XPipe 19 handles for you, and report anything that doesn't work yet.\n\n## Abstract hosts and connection organization\n\nUp until now, the handling of systems which did not support shell connections has been limited. While you could add a direct VNC / RDP connection entry, the organization wasn't as refined as with shell-based connections. For those hosts, you can now make use of the improved handling that was added for services and VNC/RDP connections.\n\nEssentially, you can now specify the target address inline or choose an existing host from xpipe. XPipe will then automatically adapt the connection based on what is possible. E.g. if you want to open a web service directly on a device without shell access, e.g. some embedded device, just specify the hostname and optional gateway. You can then open the website in the browser while also having a nice grouping of various connection entries for an abstract host in case you have more connections added for the same host:\n\n![Abstract host config](https://xpipe.io/assets/images/BlogPage/abstract-host-config.png)\n\nYou can also quickly convert existing connections entries like services and direct RDP/VNC connections to an abstract host entry to allow for better organization.\n\n![Abstract host](https://xpipe.io/assets/images/BlogPage/abstract-host.png)\n\n## SFTP support\n\nThere is now support for systems which only support pure SFTP and no SSH shell sessions. These can be opened in the file browser as normal, although with limited functionality as there is no way to execute commands on these systems.\n\n![SFTP](https://xpipe.io/assets/images/BlogPage/sftp.png)\n\n## Other\n\n- Add support for flatpak variants of various editors and terminals\n- Add ability to read files via sudo if current user did not have read permission for a file\n- Improve browser drag-and-drop to also work on the navigation bar to move between directories\n- Add ability to move categories to different parents\n- The accent color of your system appearance now applies to all UI elements\n- Make tunnels close instantly even if there was still traffic going through\n- Bump to JDK 25 and JavaFX 25\n- Rework password choice order\n- Add setting for mstsc RDP client to use smart sizing\n- Add traditional chinese translations\n- Add support for Zed editor on Windows\n- Rework gpg git initialization on Windows due to severe gpg agent slowdowns\n- Improve caching of various shell operations for speed improvements\n- The nixpkg package now also supports macOS and has been reworked as a flake. If you use XPipe on Nix, take a look at https://github.com/xpipe-io/nixpkg for the changes\n- Don't accept 7zip drag-and-drop to prevent confusion about non-existent files\n- The custom icon cache now always registers when it is out of date, prompting you to refresh\n- You can now automatically start service tunnels as well when XPipe is launched\n- Add support to automatically add unsupported SSH MAC type\n- Improve default browser detection for URL opens\n- The file browser can now automatically open UNC paths when using cmd by opening a new PowerShell tab\n- Add option to control application behavior on system hibernation\n- Add possible workaround when default beacon HTTP server port was not usable\n- Improve reliability of shell session restarts when a connection was interrupted\n- There is also now a checkout page for chinese users, which supports common chinese payment methods.\n\n## Fixes\n\n- Fix connection entry validity state sometimes not updating immediately when edited\n- Fix SSH connection host key type / kex type override not applying changes instantly\n- Fix misleading error messages when a file transfer was interrupted or permissions were missing\n- Fix Remmina RDP integration not supporting user domain prefix\n- Fix Linux FreeRDP not using FreeRDP v3 by default\n- Fix identities being moved to initial category when being editing\n- Fix system state info for some VMs and containers not showing\n- Fix fish shell v4 with init scripts failing to launch\n- Fix SSH MOTD errors with fish as a login shell\n- Fix vietnamese translations not being listed\n- Fix kitty launch race conditions sometimes breaking socket\n- Fix synced identities not syncing when no other categories were synced\n- Fix bitwarden sync not refreshing xpipe cache for bitwarden passwords\n- Fix sudo auth failing on legacy systems where openssl did not support certain options\n- Fix Remmina integration failing to encrypt passwords with special characters\n- Fix AppImage workspaces and app restart not working\n- Fix native file manager open not properly selecting files\n- Fix connection entries being able to be renamed to a blank string\n- Fix apple container integration freezing when recommended kernel was not installed\n- Fix local KVM integration constantly bringing up polkit auth window\n- Fix super key not being handled in VNC client\n- Fix Proxmox VNC action not setting empty password\n- Fix opened as root browser tab not launching correct shell in terminal for fish\n- Fix VNC scan adding localhost as a connection on macOS\n- Fix file git sync button sometimes being enabled too early\n- Fix errors when configuring an invalid git account password\n- Fix freeze on macOS with 1password agent when starting in background\n- Fix various NullPointers\n"
  },
  {
    "path": "dist/changelog/19.1_incremental.md",
    "content": "- Fix Windows crashing issue on some systems when system did not have full AVX support\n- Fix symlinks not being in shown in file browser in 19.0\n- Add support for the new Google Antigravity editor\n- Bump kasm workspaces registry version support to 1.18\n- Add file browser menu button to open explicit SFTP session for improved transfer speeds\n- Fix occasional NullPointer race condition on startup on macOS\n- Fix root file browser tab name being duplicated\n- Fix occasional error when associating KeePassXC database\n- Fix AUR package not being updated properly\n- Fix various small translation issues\n- Fix various other small errors\n"
  },
  {
    "path": "dist/changelog/19.2_incremental.md",
    "content": "- Re-enable AVX optimizations as much as possible without causing crashes\n- You can now also filter for connection type / connection information like the os name\n- Add Ctrl+F shortcut to focus search field\n- There is now a warning when you type into a password field and capslock is active\n- Fix RDP client setting not persisting\n- Fix SSH PKCS11Provider option breaking on systems with very old SSH client\n- Fix GNU tools on macOS (e.g. from homebrew or nix) breaking some file browser features\n- Fix file browser drag hover trying to cd into files as well\n- Fix RDP tunnel connections not automatically taking credentials from parent entry\n- Fix file browser context menu not closing on keyboard shortcut (Thanks for @nillpoe for the PR)\n- Fix connection timeout not taking password prompts and others into consideration\n- Fix local file system speeds being slow due to wrong buffer sizes\n- Fix some Keeper password manager error messages not being shown\n- Fix full Keeper record URLs not being accepted\n- Fix SFTP open browser menu option not showing for VMs\n- Fix auto-updater download breaking with dashes in username on Windows\n- Fix issues when opening terminal for a fish v4 login shell\n- Fix integrated VNC client failing to start when shell connection was not possible\n- Fix some issues with VNC handling for libvirt VMs\n- Fix various NullPointers\n"
  },
  {
    "path": "dist/changelog/19.3.1_incremental.md",
    "content": "- Fix SSH failing with bad configuration error when a custom IdentityFile option was used as additional SSH options\n"
  },
  {
    "path": "dist/changelog/19.3_incremental.md",
    "content": "- Fix SSH failing with bad configuration error when a connection had custom SSH options configured\n- Fix file browser symlinks to directories not being treated as directories in some cases\n- Improve yes/no toggle for category configuration to be less confusing in its neutral state\n- Add automatic fallback to Remmina RDP integration to use FreeRDP for RemoteApps when FreeRDP is installed as well\n"
  },
  {
    "path": "dist/changelog/19.4_incremental.md",
    "content": "- Fix URLs not opening in browser on current Debian sid and potentially other Linux systems\n- Fix file browser defaulting to SFTP for connections with no saved state, e.g. when they were synced the first time via git\n- Fix password field sometimes throwing errors when capslock was active\n- Fix connection filter string match count sometimes being wrong\n- Fix connection filter always switching category even if it was not necessary\n- Fix errors in SFTP session when right-clicking certain files\n- Improve Korean translations (Thanks to @nillpoe)\n"
  },
  {
    "path": "dist/changelog/19.5_incremental.md",
    "content": "- Fix file browser transfer size being displayed as twice the actual size for directory transfers\n- Fix SSH PKCS11Provider option breaking ssh on Windows 10 LTSC (again)\n- Fix various broken documentation links\n- Fix potential stack overflow when transferring directory with 100+ layers of nested directories\n- Fix MSYS2 shell environment name formatting\n- Fix category selection always changing when editing connection configurations\n- Fix Remmina window overlapping taskbar in fullscreen\n- Show connection name of SSH askpass prompt dialog\n- Improve interface for custom SSH connections with multiple hosts to always show target\n- Improve category config dialog description for sync option of special categories\n"
  },
  {
    "path": "dist/changelog/19.6_incremental.md",
    "content": "- Fix Linux executable being linked to a too new GLIBC version, causing it to not run on old Linux systems\n- Fix various other issues with the SSH client in Windows 10 LTSC\n- Fix scrollbar flicker when scrolling connection list fast\n- Fix in-place key not always replacing CRLF with LF\n- Fix Warp terminal sessions on Windows not registering exit properly\n- Fix fish terminal session not properly applying working directory\n"
  },
  {
    "path": "dist/changelog/20.0.1.md",
    "content": "- Fix issues with macOS .pkg installer\n"
  },
  {
    "path": "dist/changelog/20.0.md",
    "content": "## AWS support\n\nYou can now connect to your AWS systems from within XPipe. Currently, EC2 systems and S3 buckets are supported. The integration works on top of the AWS CLI, meaning that it is required to be installed on your system. The usage of the AWS CLI allows the integration to work very flexibly on any existing CLI setup if you already use the CLI. You can use any IAM access keys and authentication methods with it.\n\nThe EC2 support works as with any other remote system in XPipe. The connections can be established via SSH with optional AWS Systems Manager (SSM) support. The SSM support allows you to also reach systems that do not have a security group configured to allow external access to the normal SSH port.\n\nThe S3 support is available both for AWS buckets and other external buckets that are compatible with the S3 protocol. More enhancements to the performance and features of s3 file systems are planned.\n\n![AWS](https://xpipe.io/assets/images/BlogPage/aws.png)\n\nThis integration will be available in the Professional plan, but is also available for free for a month after release to iron out any issues first.\n\n## SSH keygen\n\nYou can now generate new SSH keys from within XPipe. The keys are generated via the installed OpenSSH `ssh-keygen` CLI tool, so you can be assured that the keys are generated in a cryptographically secure manner. You can generate keys with this button:\n\n![Keygen](https://xpipe.io/assets/images/BlogPage/keygen.png)\n\nThis keygen right now supports RSA, ED25519, and ED25519 + FIDO2:\n\n![Keygen](https://xpipe.io/assets/images/BlogPage/keygen-fido2.png)\n\nKeys of identities can now also be automatically applied to systems, allowing you to perform a quick key rotation when needed:\n\n![Identity Apply](https://xpipe.io/assets/images/BlogPage/id-apply.png)\n\nThe process of changing the authentication configuration of a system is not always one simple step. So the dialog is a comprehensive overview of what is needed to apply a certain identity to a remote system, with various quick-action buttons and notes. This gives you still full manual control of what should be done and an overview of what is required prior to doing so.\n\n![Identity Apply Dialog](https://xpipe.io/assets/images/BlogPage/id-apply-dialog.png)\n\nAs adding new authorized identities to systems requires the public key, there is now the functionality to generate and store the public key for identities based on the existing private key:\n\n![Public Keygen](https://xpipe.io/assets/images/BlogPage/pubkeygen.png)\n\n## Tags\n\nYou can now create and add tags to connection entries. This allows you to have a more structured workflow when filtering individual connections.\n\n![Tags](https://xpipe.io/assets/images/BlogPage/tags.png)\n\n## SSH jump servers\n\nThe jump server / gateway configuration has been reworked to make the configuration more intuitive. You can now control the jump server behaviour right at the gateway field for a connection. When a gateway is set, XPipe now defaults to using it as a jump server unless the option is explicitly disabled, which is a change from the previous version where it was the other way around.\n\n![Jump Servers](https://xpipe.io/assets/images/BlogPage/jump.png)\n\n## Tailscale\n\nThe tailscale integration has been updated with the recent improvements that were implemented for Netbird and AWS. It no longer requires the Tailscale SSH auth and instead gives you the option to use the automatic Tailscale SSH auth or connect normally to an SSH server, which requires password / key authentication. When configuring a tailscale system, you can choose the authentication type.\n\n![Tailscale](https://xpipe.io/assets/images/BlogPage/tailscale-auth.png)\n\nFurthermore, you can now use Tailscale connection entries as the base for other types of connections, e.g. direct RDP / VNC connections.\n\n## Split terminals\n\nThere is now a new batch action to open multiple systems in a split terminal pane instead of individual tabs. This action is only supported for terminals that support this, which currently includes: Windows Terminal, Kitty, and WezTerm. In addition, this is also supported when using any other terminal and a terminal multiplexer like tmux or zellij.\n\n![Split Action](https://xpipe.io/assets/images/BlogPage/split-bar.png)\n\nThis allows you to also use a feature like broadcast mode of your terminal to type one command into multiple terminal panes at the same time.\n\n![Split Terminal](https://xpipe.io/assets/images/BlogPage/split.png)\n\n## Group vaults\n\nIn addition to existing team vaults with multiple users, you can now also set up team group vaults:\n\n![Group Vaults](https://xpipe.io/assets/images/BlogPage/group-vault.png)\n\nThey work basically the same as user vaults, only the authentication/unlock workflow differs. Each group has a shared group secret, which can automatically be retrieved in one of multiple ways like a password manager, a custom command, or a secret file. This gives organization administrators the ability to control access to connections from a central location without users having to supply any passwords themselves. You can flexibly convert existing team vaults into group vaults if needed.\n\n## Other\n\n- The network scan now supports adding RDP and VNC systems as well in addition to SSH\n- You can use multiple KeePassXC databases at the same time\n- Add toggle to control whether a service should tunnel a remote port or not\n- Several fixes to be able to run the application in the Android Linux Terminal app without issues\n- Add WezTerm support for tabs on Windows\n- The entire interface has been reworked to better work with screen readers and other accessibility tools\n- Add refresh action in connection context menu to easily refresh the system state\n- The vault unlock dialog will now provide more feedback on failed unlocks\n- Improve fallback shell startup checks\n\n## Fixes\n\n- Fix flatpak application selections not persisting in the settings\n- Fix various file browser actions not working properly for symlink entries\n- Fix local system proxy configuration not being used for http requests, e.g. for license api requests\n- Fix browser navigation bar not showing busy state properly\n- Fix other Unix file types like sockets, block devices, and more not being listed in the file browser\n- Fix homebrew paths not being set on macOS when using another default shell like fish\n- Fix temp file paths to better handle xpipe being launched by multiple users\n- Fix race condition causing early startup errors sometimes not to be shown\n- Fix app not respecting system display scale in KDE\n- Fix zellij tab layout on initial terminal launch not being correct\n- Fix browser duplicate renames not working for directories\n- Fix latest bitwarden update causing XPipe to ask for the master password every time\n- Fix occasional text input delay\n- Fix host address for cloud systems resetting to unknown in configuration dialog when minimized\n- Fix sftp file system not handling invalid paths properly\n- Fix custom icon source directory causing errors when set to a root directory\n"
  },
  {
    "path": "dist/changelog/20.1.md",
    "content": "## Gateway adjustments\n\nThe SSH gateway setting has been adjusted again to be more clear on what it does. The documentation has also been updated. The default value has also been reverted to use gateway tunnels instead of ProxyJump again as gateway tunnels turn out to still work better than jump servers.\n\n![Gateway choice](https://xpipe.io/assets/images/BlogPage/gateway-choice.png)\n\nFurthermore, various fixes have been implemented for SSH gateways and EC2 gateways.\n\n## Changes\n\n- Add support for the passbolt password manager\n- Add proper connection dropdown in Windows SSH settings for X11 forward WSL instance\n- Format key passphrase prompts to not include the full file path to be shorter\n\n## Fixes\n\n- Fix disable TLS verification setting for HTTP requests not applying properly\n- Fix various NullPointers\n- Fix occasional application freeze due to deadlock\n- Fix various typos\n"
  },
  {
    "path": "dist/changelog/20.2.md",
    "content": "## Connection creation\n\nThe connection creation / configuration dialog has been reworked to allow for a more efficient workflow. When validating a connection, the window is no longer blocked, and you can continue editing the connection or also minimize it meanwhile.\n\n## Command rework\n\nThe custom command entries that you can create at `New` -> `Command` have been completely reworked. You can now create runnable commands / scripts to be run in a terminal or in the background without having to depend on creating scripts.\n\n## Remote SSH configs\n\nThis update adds back support for SSH configs located on a remote system. You can now add entries from SSH config files by searching for available connections on a remote system. If any host is configured in the config file, it will be added automatically to XPipe.\n\n## Network scan\n\nThe network scan has been improved. You can now also explicitly select detected systems even when there was no listening server running on them. Furthermore, the check whether an SSH server is listening has been improved as there were a few false negatives.\n\n## Updated names\n\nThe names and location of SSH connections in the creation menu have been updated. There is now no longer a simple SSH connection but instead just an SSH connection.\n\n## Fixes\n\n- Fix various daemon errors on certain Linux systems where user management was done with active directory and others\n- Fix custom icons sometimes not loading properly on startup\n- Fix various performance issues when using a display scale other than 100%\n- Fix file browser icons being blurry on scaled up high dpi displays\n- Fix autoupdater on Windows not fully updating all necessary files in some circumstances\n- Fix shell environment default state not being synced via git\n- Fix website open action in the webtop container not returning and greying out entries\n- Fix terminal tracker not tracking all terminal sessions properly, leading to some inconsistencies\n- Fix WezTerm on Windows not being focused when a new tab is opened\n- Fix putty install detection for serial connections\n- Fix various typos\n"
  },
  {
    "path": "dist/changelog/20.3.md",
    "content": "- Fix SSH connections being refused after some time for OpenSSH 9.8p1+ servers for VMs due to multiple connection attempts\n- Fix old OpenSSH clients not accepting \"none\" for \"PKCS11Provider\", causing dlopen issues on those systems \n- Add option to open AWS web dashboard from XPipe\n- Improve fallback behaviour when local shell failed to start\n- Fix various performance issues\n- Fix HTTP API not supplying CORS headers for web-based API clients\n- Fix hibernation behaviour setting not persisting\n- Fix category collapsed state always resetting\n- Fix laggy user interface when resizing window while the settings menu was open\n- Fix SSH config identity detection not working for patterns in host entries\n- Fix connection entry index not being able to be cleared\n- Fix various NullPointers\n- Fix connection index changes not syncing properly\n- Fix custom SSH agent setting not always overriding other agents configured in SSH config\n- Fix mstsc certificates trust status not persisting for tunneled RDP connections\n- Fix vscode-based editors not working when cmd.exe was disabed on the system\n"
  },
  {
    "path": "dist/changelog/20.4.md",
    "content": "- Fix powershell runnable scripts with named parameters failing to execute\n"
  },
  {
    "path": "dist/changelog/21.0.md",
    "content": "## Terminal docking\n\nOn Windows, you can now make use of docked terminal windows right inside XPipe. This works for basically all terminals on Windows, meaning that you can achieve almost any combination of tools.\n\nHere is a docked WezTerm instance running the zellij multiplexer through WSL:\n\n![Dock](https://xpipe.io/assets/images/BlogPage/dock.png)\n\nHere is a Windows Terminal instance with 4 split tabs that were launched through the split terminal feature of XPipe 20:\n\n![Split Dock](https://xpipe.io/assets/images/BlogPage/dock-split.png)\n\nA docked terminal is embedded into the XPipe window but can also be detached from it. You can also minimize the terminal normally, plus also toggle the terminal with the terminal dock button that will show up at the bottom right when a terminal is active:\n\n![Dock toggle](https://xpipe.io/assets/images/BlogPage/dock-toggle.png)\n\nIf you want to disable the terminal docking completely, you can do so in the settings menu:\n\n![Dock setting](https://xpipe.io/assets/images/BlogPage/dock-setting.png)\n\n## Cisco switch integration\n\nThere is now a new integration for Cisco switches. It will automatically detect when you connect to any device running Cisco IOS and will add entries for each available port:\n\n![Switch](https://xpipe.io/assets/images/BlogPage/switch.png)\n\nSearching for available connections on a cisco device which was added via SSH should make the network switch ports show up. From the interface you can then:\n- See the status of all ports and refresh them at any time\n- Filter out irrelevant ports\n- Open / shut / reset ports\n- Monitor individual interfaces for accumulated errors with the built-in ability to clear counters\n- Restart the device\n\nThis integration will be available in the Professional plan, but is also available for free for a few weeks after release. If the switch integration is well-received, support can also be expanded to other vendors and devices in the future.\n\n## Proxmox improvements\n\n- Add full support for Proxmox container networking. They now support services and tunnels in addition to normal VMs:\n\n![PCT](https://xpipe.io/assets/images/BlogPage/pct-networking.png)\n\n- Add support to open VMs with virt-viewer/remote-viewer via SPICE\n\n- The dashboard service now automatically determines whether it actually needs to be tunneled to localhost or not. This improves handling with the HTTPS certificate. You can refresh the Proxmox installation entry to apply these changes\n\n![Dashboard](https://xpipe.io/assets/images/BlogPage/pve-dashboard.png)\n\n## Scripting rework\n\nThe scripting system has been completely reworked with the goal of becoming simpler and more powerful at the same time.\n\n- You can now add custom script sources from an external URLs, e.g. a git repository. These source entries can then be used to quickly add script entries to xpipe automatically via an import functionality. It also supports fetching remote script files directory via HTTP urls if needed.\n\n![Script Source](https://xpipe.io/assets/images/BlogPage/script-source.png)\n\n![Script Ref](https://xpipe.io/assets/images/BlogPage/script-ref.png)\n\n- The script handling for different shell types has been improved. You can now run scripts on a remote system independently of the login shell type as long as the specified shell is installed on the system. This means that you can run for example zsh scripts now on any system with a bash login shell without issues.\n\n- Scripts groups were removed as they didn't provide a lot of value but made the organization more complicated than it needed to be.\n\n## Sync improvements\n\n- Add option to change the sync frequency for the git sync to once per session or fully manual. This reduces the amount of pushed commits and gives more control on how and when changes are synced. Multiple commits are also squashed together here when pushed\n- The git commit messages now show the actual names of deleted entries instead of just IDs\n- Add support to sync to a plain local directory without requiring a git repository\n- The git sync can now also work with read-only remote permissions\n- Any git credentials specified in the settings menu now take precedence over the default git CLI credentials. The custom git credentials setting is now also available on Windows\n\n## SSH config improvements\n\n- Fix various entries like SSH connections or SSH config entries sometimes disappearing from the list on restart\n- Fix SSH config write, e.g. for vscode SSH, not properly passing all configured options\n- Fix SSH config identity detection not working for patterns in host entries\n- Fix custom SSH agent setting not always overriding other agents configured in SSH config\n- Fix SSH config files include wildcards failing to parse in some cases\n- Fix SSH config import not removing identical aliases for host entries\n\n## Other\n\n- The RAM usage of the application has been improved by a lot\n- Replace various old icons with more modern variants\n- Add option to open AWS web dashboard for an AWS CLI profile from XPipe\n- Incomplete VM entries can now be used for services and tunnels\n- Improve serial support across the board\n- Add support to automatically wait in terminal until serial port is connected\n- Creating a new category will now automatically focus the text field to rename it\n- Add support for Yakuake terminal\n- Add support for virt-viewer as a VNC client\n- Improve fallback behaviour when local shell failed to start\n- Improve handling of workspace creation to not require an immediate restart\n- Add automatic detection feature for the use gateway setting for VMs\n- The WinSCP open action can now automatically generate .ppk keys if required\n- Add support for CoolRetroTerm\n\n## Fixes\n\n- Fix docker refresh action taking very long when compose projects were present\n- Fix ordering for some child connections being random after a restart\n- Fix mstsc certificates trust status not persisting for tunneled RDP connections\n- Fix various issues with the Keeper password manager and add 2FA support\n- Fix hetzner cloud integration using invalid context names when multiple CLIs contexts were present\n- Fix hetzner cloud integration sometimes not stripping away subnet mask from determined IP address\n- Fix predefined categories being able to be moved and causing breakages\n- Fix terminal session titles not applying for Konsole\n- Fix rbash shell detection not working\n- Fix SFTP failing with files with single quotes in their name\n- Fix WinSCP open action requiring an existing ppk key and only working with external key files, not in-place keys\n- Fix batch mode selection not working for incomplete connections, like newly added VMs without credentials\n- Fix batch action confirmation setting requiring a double confirmation for each individual connection\n- Fix hibernation behavior setting not applying when locking the system\n- Fix vscode-based editors not working when cmd.exe was disabled on the system\n- Fix various performance issues\n"
  },
  {
    "path": "dist/changelog/21.1.1.md",
    "content": "- Fix Warp terminal hyperlinks being opened in browser as well\n"
  },
  {
    "path": "dist/changelog/21.1.md",
    "content": "- Fix various hyperlinks and files being opened in web browser instead of associated application\n- Fix NullPointer when opening split terminal\n- Fix continuous NullPointer when git vault was enabled but no remote repository was specified\n- Fix OutOfBounds issue for the cisco switch version detection\n- Fix SSH config entries always using default id_rsa key if none was specified\n- Fix SSH config entries not preferring custom identity set in XPipe\n- Fix NullPointers with SSH configs on remote systems\n- Fix local custom icons directory not being created by default\n- Don't enable terminal docking with mixed display scale screens due to wrong translated window coordinates\n"
  },
  {
    "path": "dist/changelog/21.2.1.md",
    "content": "- Fix Pageant and custom SSH agents on Windows not working\n- Fix Pageant integration using wrong named pipe when multiple users were logged in on Windows\n- Fix plain directory sync not working for OneDrive directories\n- Fix terminal docking not working after window had been minimized to system tray\n- Fix Windows vault lock and hibernation lock not working after window had been minimized to system tray\n- Fix NullPointer when launching docked terminal via a desktop shortcut when XPipe was not running yet\n- Fix desktop shortcuts for actions not applying any custom workspace directories\n- Fix various other NullPointers\n"
  },
  {
    "path": "dist/changelog/21.2.md",
    "content": "- Fix various SSH agents not working if the socket path contained spaces\n- Fix terminal docking not working properly on multiple displays with different scale factor\n- Fix terminal docking sometimes not automatically reattaching windows that were moved back to the dock\n- The terminal dock indicator icon will now show a different icon to indicate when the terminal is detached \n- Automatically delete corrupted git index file when detected\n- Fix terminal test button not showing for the terminal selection in the settings menu\n- Fix network switch ports being included in total connection count\n- Fix various NullPointers\n- Fix some broken documentation links\n"
  },
  {
    "path": "dist/changelog/21.3.md",
    "content": "## Terminal docking\n\n- Restore proper window borders once a terminal is undocked\n- Fix terminals randomly undocking\n- Fix docked terminals in background sometimes being larger than main window\n- The connection hub button will now always hide the terminal when clicked\n- Fix file browser docking tabbing being inconsistent\n- Fix dock being broken after application window has been hidden once\n\n## Shell detection\n\nAdd new experimental heuristic for detecting custom shells of limited/embedded systems. Now, even if XPipe does not know the shell type and can't communicate with it, it will use heuristics to determine whether the shell is responding properly or the connection timeouted.\n\nThis should make the process of adding connections to custom systems that don't run a common operating system much easier. \n\n## MCP\n\nThe MCP server integration has been improved. The connection query by name for agents has been improved to reduce ambiguous results. Furthermore, the MCP instructions haven been augmented for various popular clients:\n\n![](https://xpipe.io/assets/images/BlogPage/mcp-instructions.png)\n\nThere is now also the option to pass additional context to any agent to apply consistent rules and information:\n\n![](https://xpipe.io/assets/images/BlogPage/mcp-context.png)\n\nThe MCP integration can also now be used to manage cisco switches via the new integration:\n\n![](https://xpipe.io/assets/images/BlogPage/mcp-switch.png)\n\n## Other\n\n- Add support for all other Keeper password manager 2FA methods (you have to reconfigure this in the settings menu if you already use Keeper with 2FA)\n- Fix vault sync for plain directories failing in OneDrive directories\n- Fix vault sync for plain directories not working well with immediate sync mode\n- Improve rendering performance on Windows\n- Add support for neovim editor (Thanks to @leycm)\n- Fix WezTerm not using tabs on Linux (Thanks to @leycm)\n- Fix WezTerm tab titles not being overridden (Thanks to @leycm)\n- Add browser context menu action for gradlew files to run tasks (Thanks to @leycm)\n- Fix AUR update failing if git core.autocrlf was set to true (Thanks to @leycm)\n- Fix SSH config update for VsCode integration creating connection duplicates\n- Fix various terminal updaters instantly closing when update failed\n- Fix VsCode not launching in Webtop due to sandbox restrictions\n- Add custom theming to Webtop\n- Fix various NullPointers\n"
  },
  {
    "path": "dist/changelog/21.4.md",
    "content": "- Add button to test custom SSH agent socket in the settings menu\n- Add support for Cisco IOS-XE devices for cisco integration\n- Add sync button for bitwarden password manager in the settings menu\n- Make bitwarden integration automatically apply external sync changes instantly\n- Make connection state and notes available to MCP client to be able to pass additional context\n- Fix Windows codesign timestamps not being set for signed execuables\n- Fix auxiliary DLLs not being codesigned on Windows\n- Fix docked terminals not being properly restored when XPipe is closed\n- Fix docked terminals not being properly hidden when minimized\n- Fix various terminal docking state inconsistencies\n- Fix startup issues when system temp dir variable was not properly set on Windows\n- Fix custom icon choice dialog being slow\n- Fix sudo shell environments not properly handling permissions for local machine in file browser\n- Fix Proxmox LXC integration failing to determine IP if container had multiple IPs assigned\n- Fix various cisco integration bugs\n- Fix ptyxis terminal tabs not having titles (Thanks to @Ehsan-U)\n- Fix AUR package not including desktop icons in all resolutions and sometimes not showing icons\n- Fix PowerShell Remote session handling on non-Windows systems\n- Fix blurry images when multiple screens had different display scale\n- Fix Termius integration launching web browser to open URL\n- Fix various NullPointers\n"
  },
  {
    "path": "dist/changelog/21.5.md",
    "content": "## Cisco device support\n\nThis release adds support for Cisco NX-OS devices:\n\n![NX-OS](https://xpipe.io/assets/images/BlogPage/nxos.png)\n\nThe existing cisco integration has been reworked, meaning that you will have to delete and re-add the network port entries again.\n\nThis completes the integration for Cisco network devices. If there is demand, support for other network device vendors can be added as well.\n\n## Fixes\n\n- Fix system detection being broken for shells with allocated tty/pty\n- Fix identities being moved to other categories when edited\n"
  },
  {
    "path": "dist/changelog/21.6.md",
    "content": "- Fix keygen for public key complaining about an invalid format when the key did not have a trailing newline\n- Fix MCP session in claude code timing out for long-running commands\n- Fix MCP list_systems tool not working properly when MCP client used absolute paths instead of just connection names\n- Fix SSH VsCode and SFTP actions being shown for containers/VMs that are not configured for SSH yet\n- Fix bitwarden integration not syncing properly in a new xpipe install\n"
  },
  {
    "path": "dist/changelog/8.0-6.md",
    "content": "## Temporary containers\n\nYou can now run a temporary docker container using a specified image that will get automatically removed once it is stopped. The container will keep running even if the image does not have any command specified that will run.\n\nThis can be useful if you quickly want to set up a certain environment by using a certain container image, e.g. a simple `ubuntu` image. You can then enter the container as normal in XPipe, perform your operations, and stop the container once it's no longer needed. It is then removed automatically.\n\n## macOS tray and dock handling\n\nDue to some confusion, XPipe will no longer use the system tray in macOS as an option when minimizing. It will instead conform to the usual macOS app handling that allows to reopen the window by clicking on the dock icon.\n\n## Other changes\n\n- Add option to skip connection validation\n- Introduce new changelog implementation that will be able to display the changelog relevant when upgrading from you current version, including all intermediate versions\n- Auto expand connections display when a new child is added \n- Fix opnsense and PFsense systems not working\n- Fix elevation not working in some cases and throwing errors\n- Fix debug mode not working"
  },
  {
    "path": "dist/changelog/8.0.1.md",
    "content": "- Fix self-updater not working on Linux and macOS. If you are on 8.0, you might have to update manually by downloading and installing it from https://github.com/xpipe-io/xpipe/releases\n"
  },
  {
    "path": "dist/changelog/8.0.md",
    "content": "This is this biggest update yet and includes many changes that are necessary going forward to allow for many future features to come. These new implementations take everything into account learned so far and are more intuitive and robust. Especially when considering the long-term timeline, these changes will come in handy.\n\nThe versioning scheme has also been changed to simplify version numbers. So we are going straight from 1.7 to 8.0!\n\nNote that on Windows the automatic updater still has a few issues with race conditions if you are upgrading from 1.7.16. If the automatic update fails, you can still install 8.0 manually by downloading and installing it from https://github.com/xpipe-io/xpipe/releases.\n\n## New terminal launcher\n\nThe terminal launcher functionality got completely reworked with the goal to make it more flexible and improve the terminal startup performance. You will quickly notice the new implementation whenever you launch any connection in your terminal.\n\n## Proxmox integration (Professional feature)\n\nThere is now support to directly query all VMs and containers located on a Proxmox system via the `pct` and `qm` tools. The containers can be accessed directly as any other containers while the VMs can be accessed via SSH. In case no SSH server is running in a vm, you can also choose to start one with XPipe.\n\nThis feature will be available in the professional version, but is also available in the free professional edition preview after release.\n\n## Improved professional edition preview\n\nAny new professional features, such as the Proxmox support, will be available for free for a couple of weeks after their initial release. There is now a new dialog available to allow you to quickly set up the XPipe professional preview plan.\n\nThis allows anyone interested in playing around with new features to do so without limitation and no commitment.\n\n## Git For Windows shell environments\n\nThe git installation on Windows comes with its own posix environment, which some people use to make use of standard Linux functionalities on Windows if they have not moved to WSL yet. This update brings full support to add this shell environment as well via the automatic detection functionality.\n\n## File browser improvements\n\nThe file browser has been reworked in terms of performance and reliability. File transfers of many files or now faster and any errors that can occur are now handled better.\n\nIn terms of the interface, there is also now a progress indicator for files being transferred. For any file conflicts, there is now a new dialog to choose how to resolve any conflict when copying or moving files.\n\nThere are also a couple more changes included:\n- Fix files in file browser not reloading content when trying to edit them multiple times in the session\n- Add Open with ... action to open files with an arbitrary program\n- The transfer pane now also allows file drops from outside the window to make it more intuitive\n\n## Kubernetes configs and namespaces\n\nThis update adds support to also add connections from other kubeconfig files.\n\nFurthermore, you can also choose to use any namespace you want. This is useful in cases where you have not set up a context for every namespace you have.\n\nThe Kubernetes support is also now available in the pro preview after release.\n\n## Settings rework\n\nThis update comes with a complete rework of the settings menu. Many options have been added and existing ones have been improved, with a focus on providing more control over security settings. Make sure to give them a read to discover new options.\n\nThere has been a big focus on providing finer-grained control over security settings, which can be especially useful in enterprise contexts.\n\n## Per-Vault settings\n\nPreviously all settings were stored on a per-system basis. This caused some problems with git vaults, as all relevant settings that should persist across systems were not synced. From now on, all options that should be the same on all synced systems are automatically included in the git vault.\n\n## Authentication improvements\n\nThis update comes with a newly created system for handling authentication that is better suited for arbitrary authentication prompts. This allows for better support for things like 2FA and other keyboard interactive authentications schemes. The sudo elevation authentication also has been reworked to be more intuitive and mirror the behavior of the system in regard to password prompts.\n\nYou also now have finer control over the caching behaviour of passwords and the sudo behaviour via additional settings.\n\n## Temporary containers\n\nYou can now run a temporary docker container using a specified image that will get automatically removed once it is stopped. The container will keep running even if the image does not have any command specified that will run.\n\nThis can be useful if you quickly want to set up a certain environment by using a certain container image, e.g. a simple `ubuntu` image. You can then enter the container as normal in XPipe, perform your operations, and stop the container once it's no longer needed. It is then removed automatically.\n\n## Fish and dumb shells\n\nUp until now, connecting to fish shells or various dumb shells you typically find in devices like routers and links, did not work as there was no proper support for them. The shell handling implementation has now been rewritten to support fish login shells (after some timeout).\n\nThe implementation also now supports dumb shells that can be reached via ssh for example, but support still has to be implemented manually. Since I currently don't own any typical hardware like various routers and links, e.g. from cisco, I did not have the ability yet to do this. So if you own any hardware you would like to see supported, open an issue and share the typical output that is printed upon connection, and it can easily be supported.\n\nFor now, it should work with MikroTik routers at least.\n\n## macOS tray and dock handling\n\nDue to some confusion, XPipe will no longer use the system tray in macOS as an option when minimizing. It will instead conform to the usual macOS app handling that allows to reopen the window by clicking on the dock icon.\n\n## PowerShell fallback\n\nSome Windows admins disable cmd on their systems for security reasons. Previously this would have caused XPipe to not function on these systems as it relied on cmd. From now on, it can also dynamically fall back to PowerShell if needed without utilizing cmd at all.\n\n## Bundled OpenSSH on Windows\n\nOne common problem in the past has been to fact that Microsoft ships relatively outdated OpenSSH versions on Windows, which do not support newer features like FIDO2 keys. Due to the permissive license of OpenSSH and its Windows fork, XPipe can bundle the latest OpenSSH versions on Windows. There is now an option the settings menu to use the latest bundled OpenSSH version.\n\n## Dependency upgrades\n\nAll dependencies have been upgraded to the latest version, coming with a few fixes and some new features. In particular, the JavaFX version has been bumped, which now allows for native system theme observation and the usage of accent colors. Around 10 dependency libraries have been removed as they are no longer necessary.\n\n## Timeout handling\n\nThe timeout model has been reworked. It is now set to a fixed amount of 30s while any active password prompts do no longer count towards it, meaning you can take your time when typing your passwords. An increased timeout value also allows for better handling of third party authentication schemes that XPipe has no control over, e.g. ones that will open a website in your browser for authentication.\n\n## Other changes\n\n- Add option to skip connection validation\n- Add ability to easily add files to the git vault data directory\n- Introduce new changelog implementation that will be able to display the changelog relevant when upgrading from you current version, including all intermediate versions\n- Auto expand connections display when a new child is added\n- Fix elevation not working in some cases and throwing errors\n- Improve git vault performance\n- Fix macOS updater and installation script sometimes only uninstalling existing version without installing new one\n- Fix scaling issues on Linux by providing a separate scaling option\n- Fix possible encoding issues on Windows with passwords that contained non-ASCII characters\n- Support opening ssh: URLs without username as well\n- Fix Linux OS logo sometimes showing wrongly or not at all\n"
  },
  {
    "path": "dist/changelog/8.1.md",
    "content": "This is this biggest update yet and includes many changes that are necessary going forward to allow for many future features to come. These new implementations take everything into account learned so far and are more intuitive and robust. Especially when considering the long-term timeline, these changes will come in handy.\n\nThe versioning scheme has also been changed to simplify version numbers. So we are going straight from 1.7 to 8.0!\n\n## Note on updating\n\nThe last few versions of XPipe from 1.7.16 to 8.0.1 all had a self-updater on Windows that was not working properly. This was caused by a newly introduced JDK bug. This is now fixed from 8.1 onwards. To upgrade to 8.1+ on Windows, you have to do it manually by downloading and installing it from https://github.com/xpipe-io/xpipe/releases. There shouldn't be any more problems with 8.1+ after that.\n\nNote that versions <8.1 do not contain version information in the git vault. If you're on multiple systems that are synced with git, the git vault format can be updated on one system and being pulled on another a system that is running an older version. This can lead to data corruption. If this happens to you, you should be able to reset the git repository to a previous ref.\n\n## New terminal launcher\n\nThe terminal launcher functionality got completely reworked with the goal to make it more flexible and improve the terminal startup performance. You will quickly notice the new implementation whenever you launch any connection in your terminal.\n\n## Proxmox integration (Professional feature)\n\nThere is now support to directly query all VMs and containers located on a Proxmox system via the `pct` and `qm` tools. The containers can be accessed directly as any other containers while the VMs can be accessed via SSH. In case no SSH server is running in a vm, you can also choose to start one with XPipe.\n\nThis feature will be available in the professional version, but is also available in the free professional edition preview after release.\n\n## Improved professional edition preview\n\nAny new professional features, such as the Proxmox support, will be available for free for a couple of weeks after their initial release. There is now a new dialog available to allow you to quickly set up the XPipe professional preview plan.\n\nThis allows anyone interested in playing around with new features to do so without limitation and no commitment.\n\n## Git For Windows shell environments\n\nThe git installation on Windows comes with its own posix environment, which some people use to make use of standard Linux functionalities on Windows if they have not moved to WSL yet. This update brings full support to add this shell environment as well via the automatic detection functionality.\n\n## File browser improvements\n\nThe file browser has been reworked in terms of performance and reliability. File transfers of many files or now faster and any errors that can occur are now handled better.\n\nIn terms of the interface, there is also now a progress indicator for files being transferred. For any file conflicts, there is now a new dialog to choose how to resolve any conflict when copying or moving files.\n\n## Kubernetes configs and namespaces\n\nThis update adds support to also add connections from other kubeconfig files.\n\nFurthermore, you can also choose to use any namespace you want. This is useful in cases where you have not set up a context for every namespace you have.\n\nThe Kubernetes support is also now available in the pro preview after release.\n\n## Settings rework\n\nThis update comes with a complete rework of the settings menu. Many options have been added and existing ones have been improved, with a focus on providing more control over security settings. Make sure to give them a read to discover new options.\n\nThere has been a big focus on providing finer-grained control over security settings, which can be especially useful in enterprise contexts.\n\n## Per-Vault settings\n\nPreviously all settings were stored on a per-system basis. This caused some problems with git vaults, as all relevant settings that should persist across systems were not synced. From now on, all options that should be the same on all synced systems are automatically included in the git vault.\n\n## Authentication improvements\n\nThis update comes with a newly created system for handling authentication that is better suited for arbitrary authentication prompts. This allows for better support for things like 2FA and other keyboard interactive authentications schemes. The sudo elevation authentication also has been reworked to be more intuitive and mirror the behavior of the system in regard to password prompts.\n\nYou also now have finer control over the caching behaviour of passwords and the sudo behaviour via additional settings.\n\n## Temporary containers\n\nYou can now run a temporary docker container using a specified image that will get automatically removed once it is stopped. The container will keep running even if the image does not have any command specified that will run.\n\nThis can be useful if you quickly want to set up a certain environment by using a certain container image, e.g. a simple `ubuntu` image. You can then enter the container as normal in XPipe, perform your operations, and stop the container once it's no longer needed. It is then removed automatically.\n\n## Fish and dumb shells\n\nUp until now, connecting to fish shells or various dumb shells you typically find in devices like routers and links, did not work as there was no proper support for them. The shell handling implementation has now been rewritten to support fish login shells (after some timeout).\n\nThe implementation also now supports dumb shells that can be reached via ssh for example, but support still has to be implemented manually. Since I currently don't own any typical hardware like various routers and links, e.g. from cisco, I did not have the ability yet to do this. So if you own any hardware you would like to see supported, open an issue and share the typical output that is printed upon connection, and it can easily be supported.\n\nFor now, it should work with MikroTik routers at least.\n\n## macOS tray and dock handling\n\nDue to some confusion, XPipe will no longer use the system tray in macOS as an option when minimizing. It will instead conform to the usual macOS app handling that allows to reopen the window by clicking on the dock icon.\n\n## PowerShell fallback\n\nSome Windows admins disable cmd on their systems for security reasons. Previously this would have caused XPipe to not function on these systems as it relied on cmd. From now on, it can also dynamically fall back to PowerShell if needed without utilizing cmd at all.\n\n## Bundled OpenSSH on Windows\n\nOne common problem in the past has been to fact that Microsoft ships relatively outdated OpenSSH versions on Windows, which do not support newer features like FIDO2 keys. Due to the permissive license of OpenSSH and its Windows fork, XPipe can bundle the latest OpenSSH versions on Windows. There is now an option the settings menu to use the latest bundled OpenSSH version.\n\n## Timeout handling\n\nThe timeout model has been reworked. It is now set to a fixed amount of 30s while any active password prompts do no longer count towards it, meaning you can take your time when typing your passwords. An increased timeout value also allows for better handling of third party authentication schemes that XPipe has no control over, e.g. ones that will open a website in your browser for authentication.\n\n## Other changes\n\n- Add option to skip connection validation\n- Add ability to easily add files to the git vault data directory\n- Introduce new changelog implementation that will be able to display the changelog relevant when upgrading from you current version, including all intermediate versions\n- Auto expand connections display when a new child is added\n- Fix elevation not working in some cases and throwing errors\n- Improve git vault performance\n- Fix macOS updater and installation script sometimes only uninstalling existing version without installing new one\n- Fix scaling issues on Linux by providing a separate scaling option\n- Fix possible encoding issues on Windows with passwords that contained non-ASCII characters\n- Support opening ssh: URLs without username as well\n- Fix Linux OS logo sometimes showing wrongly or not at all\n"
  },
  {
    "path": "dist/changelog/8.1_incremental.md",
    "content": "## Windows updater issues\n\nThe last few versions of XPipe from 1.7.16 to 8.0.1 all had a self-updater on Windows that was not working properly. This was caused by a newly introduced JDK bug. This is now fixed from 8.1 onwards.\n\nTo upgrade to 8.1+, you have to do it manually by downloading and installing it from https://github.com/xpipe-io/xpipe/releases. There shouldn't be any more problems with 8.1+ after that.\n\n## Git Vault Versioning\n\nWhen upgrading XPipe versions across many systems, one problem could have been the git vault format being updated on one system and being pulled on another a system that is running an older version. This could have led to data corruption. From now on, there will be a warning shown when the versions do not match up. From there you can choose to temporarily disable the git vault during a session until you upgrade to the matching version on that system as well.\n\nThis check however only works from 8.1 onwards. Older git vaults do not contain version information. So if this happens to you while updating from a previous version, e.g. 1.7.16, you should be able to reset the git repository to a previous ref.\n\n## Filtering for hosts\n\nYou can now search for IPs and hostnames in addition to the connection names to filter your connection list. The connection display when a filter is active has also been improved.\n\n## File browser transfer fixes\n\nThere was a regression in transfer speed in 8.0 causing transfers of large files being very slow. This is now fixed.\n\n## Open directories in WSL\n\nThere is now a new action available in the file browser for directories on Windows systems that allows you to open that directory in a WSL session. This makes it easier to quickly use Linux tools in a certain directory you're currently in when on Windows.\n\n## Other changes\n\n- Fix fallback shell action throwing some errors initially\n- Properly set TERM variable for powershell environments\n- Improve styling in some areas\n- Better validate invalid settings values on startup\n- Fix concurrency issues when querying multiple secrets at the same time\n- Fix red validation markers appearing in front of other UI elements\n- Fix msys2, cygwin, and gitforwindows shell environments being shown for the wrong parent connection when located on remote systems\n- Fix transferred files with BOM sometimes getting corrupted on Windows systems\n- Fix SSH askpass throwing errors on Windows systems where username contained special characters due to an OpenSSH bug\n- Fix some null pointers"
  },
  {
    "path": "dist/changelog/8.2.md",
    "content": "This is this biggest update yet and includes many changes that are necessary going forward to allow for many future features to come. These new implementations take everything into account learned so far and are more intuitive and robust. Especially when considering the long-term timeline, these changes will come in handy.\n\nThe versioning scheme has also been changed to simplify version numbers. So we are going straight from 1.7 to 8.0!\n\n## Note on updating\n\nThe last few versions of XPipe from 1.7.16 to 8.0.1 all had a self-updater on Windows that was not working properly. This was caused by a newly introduced JDK bug. This is now fixed from 8.1 onwards. To upgrade to 8.1+ on Windows, you have to do it manually by downloading and installing it from https://github.com/xpipe-io/xpipe/releases. There shouldn't be any more problems with 8.1+ after that.\n\nNote that versions <8.1 do not contain version information in the git vault. If you're on multiple systems that are synced with git, the git vault format can be updated on one system and being pulled on another a system that is running an older version. This can lead to data corruption. If this happens to you, you should be able to reset the git repository to a previous ref.\n\n## New terminal launcher\n\nThe terminal launcher functionality got completely reworked with the goal to make it more flexible and improve the terminal startup performance. You will quickly notice the new implementation whenever you launch any connection in your terminal.\n\n## Proxmox integration (Professional feature)\n\nThere is now support to directly query all VMs and containers located on a Proxmox system via the `pct` and `qm` tools. The containers can be accessed directly as any other containers while the VMs can be accessed via SSH. In case no SSH server is running in a vm, you can also choose to start one with XPipe.\n\nThis feature will be available in the professional version, but is also available in the free professional edition preview after release.\n\n## Improved professional edition preview\n\nAny new professional features, such as the Proxmox support, will be available for free for a couple of weeks after their initial release. There is now a new dialog available to allow you to quickly set up the XPipe professional preview plan.\n\nThis allows anyone interested in playing around with new features to do so without limitation and no commitment.\n\n## Git For Windows shell environments\n\nThe git installation on Windows comes with its own posix environment, which some people use to make use of standard Linux functionalities on Windows if they have not moved to WSL yet. This update brings full support to add this shell environment as well via the automatic detection functionality.\n\n## File browser improvements\n\nThe file browser has been reworked in terms of performance and reliability. File transfers of many files or now faster and any errors that can occur are now handled better.\n\nIn terms of the interface, there is also now a progress indicator for files being transferred. For any file conflicts, there is now a new dialog to choose how to resolve any conflict when copying or moving files.\n\n## Kubernetes configs and namespaces\n\nThis update adds support to also add connections from other kubeconfig files.\n\nFurthermore, you can also choose to use any namespace you want. This is useful in cases where you have not set up a context for every namespace you have.\n\nThe Kubernetes support is also now available in the pro preview after release.\n\n## Settings rework\n\nThis update comes with a complete rework of the settings menu. Many options have been added and existing ones have been improved, with a focus on providing more control over security settings. Make sure to give them a read to discover new options.\n\nThere has been a big focus on providing finer-grained control over security settings, which can be especially useful in enterprise contexts.\n\n## Per-Vault settings\n\nPreviously all settings were stored on a per-system basis. This caused some problems with git vaults, as all relevant settings that should persist across systems were not synced. From now on, all options that should be the same on all synced systems are automatically included in the git vault.\n\n## Authentication improvements\n\nThis update comes with a newly created system for handling authentication that is better suited for arbitrary authentication prompts. This allows for better support for things like 2FA and other keyboard interactive authentications schemes. The sudo elevation authentication also has been reworked to be more intuitive and mirror the behavior of the system in regard to password prompts.\n\nYou also now have finer control over the caching behaviour of passwords and the sudo behaviour via additional settings.\n\n## Temporary containers\n\nYou can now run a temporary docker container using a specified image that will get automatically removed once it is stopped. The container will keep running even if the image does not have any command specified that will run.\n\nThis can be useful if you quickly want to set up a certain environment by using a certain container image, e.g. a simple `ubuntu` image. You can then enter the container as normal in XPipe, perform your operations, and stop the container once it's no longer needed. It is then removed automatically.\n\n## Fish and dumb shells\n\nUp until now, connecting to fish shells or various dumb shells you typically find in devices like routers and links, did not work as there was no proper support for them. The shell handling implementation has now been rewritten to support fish login shells (after some timeout).\n\nThe implementation also now supports dumb shells that can be reached via ssh for example, but support still has to be implemented manually. Since I currently don't own any typical hardware like various routers and links, e.g. from cisco, I did not have the ability yet to do this. So if you own any hardware you would like to see supported, open an issue and share the typical output that is printed upon connection, and it can easily be supported.\n\nFor now, it should work with MikroTik routers at least.\n\n## macOS tray and dock handling\n\nDue to some confusion, XPipe will no longer use the system tray in macOS as an option when minimizing. It will instead conform to the usual macOS app handling that allows to reopen the window by clicking on the dock icon.\n\n## PowerShell fallback\n\nSome Windows admins disable cmd on their systems for security reasons. Previously this would have caused XPipe to not function on these systems as it relied on cmd. From now on, it can also dynamically fall back to PowerShell if needed without utilizing cmd at all.\n\n## Bundled OpenSSH on Windows\n\nOne common problem in the past has been to fact that Microsoft ships relatively outdated OpenSSH versions on Windows, which do not support newer features like FIDO2 keys. Due to the permissive license of OpenSSH and its Windows fork, XPipe can bundle the latest OpenSSH versions on Windows. There is now an option the settings menu to use the latest bundled OpenSSH version.\n\n## Timeout handling\n\nThe timeout model has been reworked. It is now set to a fixed amount of 30s while any active password prompts do no longer count towards it, meaning you can take your time when typing your passwords. An increased timeout value also allows for better handling of third party authentication schemes that XPipe has no control over, e.g. ones that will open a website in your browser for authentication.\n\n## Other changes\n\n- Add option to skip connection validation\n- Add ability to easily add files to the git vault data directory\n- Introduce new changelog implementation that will be able to display the changelog relevant when upgrading from you current version, including all intermediate versions\n- Auto expand connections display when a new child is added\n- Fix elevation not working in some cases and throwing errors\n- Improve git vault performance\n- Fix macOS updater and installation script sometimes only uninstalling existing version without installing new one\n- Fix scaling issues on Linux by providing a separate scaling option\n- Fix possible encoding issues on Windows with passwords that contained non-ASCII characters\n- Support opening ssh: URLs without username as well\n- Fix Linux OS logo sometimes showing wrongly or not at all\n"
  },
  {
    "path": "dist/changelog/8.2_incremental.md",
    "content": "## Quick access for connections\n\nOne common feedback that some users shared was that it could be quite cumbersome to access a specific nested connection as one would have to possibly expand several connections first. Expanded connections would then also take up a lot of space, leading to a lot of scrolling.\n\nThere is now a quick access button available for connections that enables you to quickly choose a connection in the hierarchy without having to expand any connection views.\n\n## Other changes\n\n- Fix terminal open not working sometimes on Windows\n- Fix terminals closing instantly without error in some cases when a connection startup error occurred\n- Fix local shell process not restarting if it exited unexpectedly before\n- Fix some null pointers"
  },
  {
    "path": "dist/changelog/8.3.md",
    "content": "This is this biggest update yet and includes many changes that are necessary going forward to allow for many future features to come. These new implementations take everything into account learned so far and are more intuitive and robust. Especially when considering the long-term timeline, these changes will come in handy.\n\nThe versioning scheme has also been changed to simplify version numbers. So we are going straight from 1.7 to 8.0!\n\n## Note on updating\n\nThe last few versions of XPipe from 1.7.16 to 8.0.1 all had a self-updater on Windows that was not working properly. This was caused by a newly introduced JDK bug. This is now fixed from 8.1 onwards. To upgrade to 8.1+ on Windows, you have to do it manually by downloading and installing it from https://github.com/xpipe-io/xpipe/releases. There shouldn't be any more problems with 8.1+ after that.\n\nNote that versions <8.1 do not contain version information in the git vault. If you're on multiple systems that are synced with git, the git vault format can be updated on one system and being pulled on another a system that is running an older version. This can lead to data corruption. If this happens to you, you should be able to reset the git repository to a previous ref.\n\n## New terminal launcher\n\nThe terminal launcher functionality got completely reworked with the goal to make it more flexible and improve the terminal startup performance. You will quickly notice the new implementation whenever you launch any connection in your terminal.\n\n## Proxmox integration (Professional feature)\n\nThere is now support to directly query all VMs and containers located on a Proxmox system via the `pct` and `qm` tools. The containers can be accessed directly as any other containers while the VMs can be accessed via SSH. In case no SSH server is running in a vm, you can also choose to start one with XPipe.\n\nThis feature will be available in the professional version, but is also available in the free professional edition preview after release.\n\n## Improved professional edition preview\n\nAny new professional features, such as the Proxmox support, will be available for free for a couple of weeks after their initial release. There is now a new dialog available to allow you to quickly set up the XPipe professional preview plan.\n\nThis allows anyone interested in playing around with new features to do so without limitation and no commitment.\n\n## Git For Windows shell environments\n\nThe git installation on Windows comes with its own posix environment, which some people use to make use of standard Linux functionalities on Windows if they have not moved to WSL yet. This update brings full support to add this shell environment as well via the automatic detection functionality.\n\n## File browser improvements\n\nThe file browser has been reworked in terms of performance and reliability. File transfers of many files or now faster and any errors that can occur are now handled better.\n\nIn terms of the interface, there is also now a progress indicator for files being transferred. For any file conflicts, there is now a new dialog to choose how to resolve any conflict when copying or moving files.\n\n## Kubernetes configs and namespaces\n\nThis update adds support to also add connections from other kubeconfig files.\n\nFurthermore, you can also choose to use any namespace you want. This is useful in cases where you have not set up a context for every namespace you have.\n\nThe Kubernetes support is also now available in the pro preview after release.\n\n## Settings rework\n\nThis update comes with a complete rework of the settings menu. Many options have been added and existing ones have been improved, with a focus on providing more control over security settings. Make sure to give them a read to discover new options.\n\nThere has been a big focus on providing finer-grained control over security settings, which can be especially useful in enterprise contexts.\n\n## Per-Vault settings\n\nPreviously all settings were stored on a per-system basis. This caused some problems with git vaults, as all relevant settings that should persist across systems were not synced. From now on, all options that should be the same on all synced systems are automatically included in the git vault.\n\n## Authentication improvements\n\nThis update comes with a newly created system for handling authentication that is better suited for arbitrary authentication prompts. This allows for better support for things like 2FA and other keyboard interactive authentications schemes. The sudo elevation authentication also has been reworked to be more intuitive and mirror the behavior of the system in regard to password prompts.\n\nYou also now have finer control over the caching behaviour of passwords and the sudo behaviour via additional settings.\n\n## Temporary containers\n\nYou can now run a temporary docker container using a specified image that will get automatically removed once it is stopped. The container will keep running even if the image does not have any command specified that will run.\n\nThis can be useful if you quickly want to set up a certain environment by using a certain container image, e.g. a simple `ubuntu` image. You can then enter the container as normal in XPipe, perform your operations, and stop the container once it's no longer needed. It is then removed automatically.\n\n## Fish and dumb shells\n\nUp until now, connecting to fish shells or various dumb shells you typically find in devices like routers and links, did not work as there was no proper support for them. The shell handling implementation has now been rewritten to support fish login shells (after some timeout).\n\nThe implementation also now supports dumb shells that can be reached via ssh for example, but support still has to be implemented manually. Since I currently don't own any typical hardware like various routers and links, e.g. from cisco, I did not have the ability yet to do this. So if you own any hardware you would like to see supported, open an issue and share the typical output that is printed upon connection, and it can easily be supported.\n\nFor now, it should work with MikroTik routers at least.\n\n## macOS tray and dock handling\n\nDue to some confusion, XPipe will no longer use the system tray in macOS as an option when minimizing. It will instead conform to the usual macOS app handling that allows to reopen the window by clicking on the dock icon.\n\n## PowerShell fallback\n\nSome Windows admins disable cmd on their systems for security reasons. Previously this would have caused XPipe to not function on these systems as it relied on cmd. From now on, it can also dynamically fall back to PowerShell if needed without utilizing cmd at all.\n\n## Bundled OpenSSH on Windows\n\nOne common problem in the past has been to fact that Microsoft ships relatively outdated OpenSSH versions on Windows, which do not support newer features like FIDO2 keys. Due to the permissive license of OpenSSH and its Windows fork, XPipe can bundle the latest OpenSSH versions on Windows. There is now an option the settings menu to use the latest bundled OpenSSH version.\n\n## Timeout handling\n\nThe timeout model has been reworked. It is now set to a fixed amount of 30s while any active password prompts do no longer count towards it, meaning you can take your time when typing your passwords. An increased timeout value also allows for better handling of third party authentication schemes that XPipe has no control over, e.g. ones that will open a website in your browser for authentication.\n\n## Other changes\n\n- Add option to skip connection validation\n- Add ability to easily add files to the git vault data directory\n- Introduce new changelog implementation that will be able to display the changelog relevant when upgrading from you current version, including all intermediate versions\n- Auto expand connections display when a new child is added\n- Fix elevation not working in some cases and throwing errors\n- Improve git vault performance\n- Fix macOS updater and installation script sometimes only uninstalling existing version without installing new one\n- Fix scaling issues on Linux by providing a separate scaling option\n- Fix possible encoding issues on Windows with passwords that contained non-ASCII characters\n- Support opening ssh: URLs without username as well\n- Fix Linux OS logo sometimes showing wrongly or not at all\n"
  },
  {
    "path": "dist/changelog/8.3_incremental.md",
    "content": "## SSH tunnel improvements\n\n- Fix tunnel connections not starting/stopping after being edited in a session\n- Fix default tunnel connection configuration not working with Windows OpenSSH servers\n- Improve tunnel configuration parameter names and descriptions to be more in line with other tools\n\n## Other changes\n\n- Fix null pointer when manually accepting new SSH host key\n- Fix search for connections dialog being shown even when connection validation has been skipped\n- Fix some passwords not being supplied correctly when using Windows servers as gateways\n- Fix sudo askpass message not including sudo prefix on macOS\n- Improve Warp terminal launching process on macOS\n- Add winget-pkg for package `xpipe-io.xpipe`\n"
  },
  {
    "path": "dist/changelog/8.4.md",
    "content": "This is this biggest update yet and includes many changes that are necessary going forward to allow for many future features to come. These new implementations take everything into account learned so far and are more intuitive and robust. Especially when considering the long-term timeline, these changes will come in handy.\n\nThe versioning scheme has also been changed to simplify version numbers. So we are going straight from 1.7 to 8.0!\n\n## Note on updating\n\nThe last few versions of XPipe from 1.7.16 to 8.0.1 all had a self-updater on Windows that was not working properly. This was caused by a newly introduced JDK bug. This is now fixed from 8.1 onwards. To upgrade to 8.1+ on Windows, you have to do it manually by downloading and installing it from https://github.com/xpipe-io/xpipe/releases. There shouldn't be any more problems with 8.1+ after that.\n\nNote that versions <8.1 do not contain version information in the git vault. If you're on multiple systems that are synced with git, the git vault format can be updated on one system and being pulled on another a system that is running an older version. This can lead to data corruption. If this happens to you, you should be able to reset the git repository to a previous ref.\n\n## New terminal launcher\n\nThe terminal launcher functionality got completely reworked with the goal to make it more flexible and improve the terminal startup performance. You will quickly notice the new implementation whenever you launch any connection in your terminal.\n\n## Proxmox integration (Professional feature)\n\nThere is now support to directly query all VMs and containers located on a Proxmox system via the `pct` and `qm` tools. The containers can be accessed directly as any other containers while the VMs can be accessed via SSH. In case no SSH server is running in a vm, you can also choose to start one with XPipe.\n\nThis feature will be available in the professional version, but is also available in the free professional edition preview after release.\n\n## Improved professional edition preview\n\nAny new professional features, such as the Proxmox support, will be available for free for a couple of weeks after their initial release. There is now a new dialog available to allow you to quickly set up the XPipe professional preview plan.\n\nThis allows anyone interested in playing around with new features to do so without limitation and no commitment.\n\n## Git For Windows shell environments\n\nThe git installation on Windows comes with its own posix environment, which some people use to make use of standard Linux functionalities on Windows if they have not moved to WSL yet. This update brings full support to add this shell environment as well via the automatic detection functionality.\n\n## File browser improvements\n\nThe file browser has been reworked in terms of performance and reliability. File transfers of many files or now faster and any errors that can occur are now handled better.\n\nIn terms of the interface, there is also now a progress indicator for files being transferred. For any file conflicts, there is now a new dialog to choose how to resolve any conflict when copying or moving files.\n\n## Kubernetes configs and namespaces\n\nThis update adds support to also add connections from other kubeconfig files.\n\nFurthermore, you can also choose to use any namespace you want. This is useful in cases where you have not set up a context for every namespace you have.\n\nThe Kubernetes support is also now available in the pro preview after release.\n\n## Settings rework\n\nThis update comes with a complete rework of the settings menu. Many options have been added and existing ones have been improved, with a focus on providing more control over security settings. Make sure to give them a read to discover new options.\n\nThere has been a big focus on providing finer-grained control over security settings, which can be especially useful in enterprise contexts.\n\n## Per-Vault settings\n\nPreviously all settings were stored on a per-system basis. This caused some problems with git vaults, as all relevant settings that should persist across systems were not synced. From now on, all options that should be the same on all synced systems are automatically included in the git vault.\n\n## Authentication improvements\n\nThis update comes with a newly created system for handling authentication that is better suited for arbitrary authentication prompts. This allows for better support for things like 2FA and other keyboard interactive authentications schemes. The sudo elevation authentication also has been reworked to be more intuitive and mirror the behavior of the system in regard to password prompts.\n\nYou also now have finer control over the caching behaviour of passwords and the sudo behaviour via additional settings.\n\n## Temporary containers\n\nYou can now run a temporary docker container using a specified image that will get automatically removed once it is stopped. The container will keep running even if the image does not have any command specified that will run.\n\nThis can be useful if you quickly want to set up a certain environment by using a certain container image, e.g. a simple `ubuntu` image. You can then enter the container as normal in XPipe, perform your operations, and stop the container once it's no longer needed. It is then removed automatically.\n\n## Fish and dumb shells\n\nUp until now, connecting to fish shells or various dumb shells you typically find in devices like routers and links, did not work as there was no proper support for them. The shell handling implementation has now been rewritten to support fish login shells (after some timeout).\n\nThe implementation also now supports dumb shells that can be reached via ssh for example, but support still has to be implemented manually. Since I currently don't own any typical hardware like various routers and links, e.g. from cisco, I did not have the ability yet to do this. So if you own any hardware you would like to see supported, open an issue and share the typical output that is printed upon connection, and it can easily be supported.\n\nFor now, it should work with MikroTik routers at least.\n\n## macOS tray and dock handling\n\nDue to some confusion, XPipe will no longer use the system tray in macOS as an option when minimizing. It will instead conform to the usual macOS app handling that allows to reopen the window by clicking on the dock icon.\n\n## PowerShell fallback\n\nSome Windows admins disable cmd on their systems for security reasons. Previously this would have caused XPipe to not function on these systems as it relied on cmd. From now on, it can also dynamically fall back to PowerShell if needed without utilizing cmd at all.\n\n## Bundled OpenSSH on Windows\n\nOne common problem in the past has been to fact that Microsoft ships relatively outdated OpenSSH versions on Windows, which do not support newer features like FIDO2 keys. Due to the permissive license of OpenSSH and its Windows fork, XPipe can bundle the latest OpenSSH versions on Windows. There is now an option the settings menu to use the latest bundled OpenSSH version.\n\n## Timeout handling\n\nThe timeout model has been reworked. It is now set to a fixed amount of 30s while any active password prompts do no longer count towards it, meaning you can take your time when typing your passwords. An increased timeout value also allows for better handling of third party authentication schemes that XPipe has no control over, e.g. ones that will open a website in your browser for authentication.\n\n## Other changes\n\n- Add option to skip connection validation\n- Add ability to easily add files to the git vault data directory\n- Introduce new changelog implementation that will be able to display the changelog relevant when upgrading from you current version, including all intermediate versions\n- Auto expand connections display when a new child is added\n- Fix elevation not working in some cases and throwing errors\n- Improve git vault performance\n- Fix macOS updater and installation script sometimes only uninstalling existing version without installing new one\n- Fix scaling issues on Linux by providing a separate scaling option\n- Fix possible encoding issues on Windows with passwords that contained non-ASCII characters\n- Support opening ssh: URLs without username as well\n- Fix Linux OS logo sometimes showing wrongly or not at all\n"
  },
  {
    "path": "dist/changelog/8.4_incremental.md",
    "content": "## Sudo password improvements\n\nThis update will restore the old behavior of sudo passwords being automatically sourced from connection details when possible, e.g. an SSH login password or a set WSL password.\nThis behavior can also be combined with the option to always confirm elevation access in the security settings if you don't want XPipe to automatically fill in your sudo password.\n\n## Other changes\n\n- Add support for PowerShell on Linux and macOS\n- Fix file browser being able to enter invalid state when the underlying connection dies, throwing a lot of errors for every action\n- Fix license check not properly updating dates and immediately throwing errors once the license validation failed. It now properly honours the offline grace period\n- Fix children connection not showing after refresh when another category was selected\n- Fix passwords not being properly cached when multiple prompts were required to log in\n- Fix askpass dialog frequently taking focus away from other applications while open\n- Fix rare git lock file issues\n- Fix terminal launch errors not showing\n- Fix errors when deleting parent connection while editing child\n- Fix trailing spaces in file name fields causing errors\n- Improve custom SSH connection description\n"
  },
  {
    "path": "dist/changelog/8.5.md",
    "content": "This is this biggest update yet and includes many changes that are necessary going forward to allow for many future features to come. These new implementations take everything into account learned so far and are more intuitive and robust. Especially when considering the long-term timeline, these changes will come in handy.\n\nThe versioning scheme has also been changed to simplify version numbers. So we are going straight from 1.7 to 8.0!\n\n## Note on updating\n\nThe last few versions of XPipe from 1.7.16 to 8.0.1 all had a self-updater on Windows that was not working properly. This was caused by a newly introduced JDK bug. This is now fixed from 8.1 onwards. To upgrade to 8.1+ on Windows, you have to do it manually by downloading and installing it from https://github.com/xpipe-io/xpipe/releases. There shouldn't be any more problems with 8.1+ after that.\n\nNote that versions <8.1 do not contain version information in the git vault. If you're on multiple systems that are synced with git, the git vault format can be updated on one system and being pulled on another a system that is running an older version. This can lead to data corruption. If this happens to you, you should be able to reset the git repository to a previous ref.\n\n## New terminal launcher\n\nThe terminal launcher functionality got completely reworked with the goal to make it more flexible and improve the terminal startup performance. You will quickly notice the new implementation whenever you launch any connection in your terminal.\n\n## Proxmox integration (Professional feature)\n\nThere is now support to directly query all VMs and containers located on a Proxmox system via the `pct` and `qm` tools. The containers can be accessed directly as any other containers while the VMs can be accessed via SSH. In case no SSH server is running in a vm, you can also choose to start one with XPipe.\n\nThis feature will be available in the professional version, but is also available in the free professional edition preview after release.\n\n## Improved professional edition preview\n\nAny new professional features, such as the Proxmox support, will be available for free for a couple of weeks after their initial release. There is now a new dialog available to allow you to quickly set up the XPipe professional preview plan.\n\nThis allows anyone interested in playing around with new features to do so without limitation and no commitment.\n\n## Git For Windows shell environments\n\nThe git installation on Windows comes with its own posix environment, which some people use to make use of standard Linux functionalities on Windows if they have not moved to WSL yet. This update brings full support to add this shell environment as well via the automatic detection functionality.\n\n## File browser improvements\n\nThe file browser has been reworked in terms of performance and reliability. File transfers of many files or now faster and any errors that can occur are now handled better.\n\nIn terms of the interface, there is also now a progress indicator for files being transferred. For any file conflicts, there is now a new dialog to choose how to resolve any conflict when copying or moving files.\n\n## Kubernetes configs and namespaces\n\nThis update adds support to also add connections from other kubeconfig files.\n\nFurthermore, you can also choose to use any namespace you want. This is useful in cases where you have not set up a context for every namespace you have.\n\nThe Kubernetes support is also now available in the pro preview after release.\n\n## Settings rework\n\nThis update comes with a complete rework of the settings menu. Many options have been added and existing ones have been improved, with a focus on providing more control over security settings. Make sure to give them a read to discover new options.\n\nThere has been a big focus on providing finer-grained control over security settings, which can be especially useful in enterprise contexts.\n\n## Per-Vault settings\n\nPreviously all settings were stored on a per-system basis. This caused some problems with git vaults, as all relevant settings that should persist across systems were not synced. From now on, all options that should be the same on all synced systems are automatically included in the git vault.\n\n## Authentication improvements\n\nThis update comes with a newly created system for handling authentication that is better suited for arbitrary authentication prompts. This allows for better support for things like 2FA and other keyboard interactive authentications schemes. The sudo elevation authentication also has been reworked to be more intuitive and mirror the behavior of the system in regard to password prompts.\n\nYou also now have finer control over the caching behaviour of passwords and the sudo behaviour via additional settings.\n\n## Temporary containers\n\nYou can now run a temporary docker container using a specified image that will get automatically removed once it is stopped. The container will keep running even if the image does not have any command specified that will run.\n\nThis can be useful if you quickly want to set up a certain environment by using a certain container image, e.g. a simple `ubuntu` image. You can then enter the container as normal in XPipe, perform your operations, and stop the container once it's no longer needed. It is then removed automatically.\n\n## Fish and dumb shells\n\nUp until now, connecting to fish shells or various dumb shells you typically find in devices like routers and links, did not work as there was no proper support for them. The shell handling implementation has now been rewritten to support fish login shells (after some timeout).\n\nThe implementation also now supports dumb shells that can be reached via ssh for example, but support still has to be implemented manually. Since I currently don't own any typical hardware like various routers and links, e.g. from cisco, I did not have the ability yet to do this. So if you own any hardware you would like to see supported, open an issue and share the typical output that is printed upon connection, and it can easily be supported.\n\nFor now, it should work with MikroTik routers at least.\n\n## macOS tray and dock handling\n\nDue to some confusion, XPipe will no longer use the system tray in macOS as an option when minimizing. It will instead conform to the usual macOS app handling that allows to reopen the window by clicking on the dock icon.\n\n## PowerShell fallback\n\nSome Windows admins disable cmd on their systems for security reasons. Previously this would have caused XPipe to not function on these systems as it relied on cmd. From now on, it can also dynamically fall back to PowerShell if needed without utilizing cmd at all.\n\n## Bundled OpenSSH on Windows\n\nOne common problem in the past has been to fact that Microsoft ships relatively outdated OpenSSH versions on Windows, which do not support newer features like FIDO2 keys. Due to the permissive license of OpenSSH and its Windows fork, XPipe can bundle the latest OpenSSH versions on Windows. There is now an option the settings menu to use the latest bundled OpenSSH version.\n\n## Timeout handling\n\nThe timeout model has been reworked. It is now set to a fixed amount of 30s while any active password prompts do no longer count towards it, meaning you can take your time when typing your passwords. An increased timeout value also allows for better handling of third party authentication schemes that XPipe has no control over, e.g. ones that will open a website in your browser for authentication.\n\n## Other changes\n\n- Add option to skip connection validation\n- Add ability to easily add files to the git vault data directory\n- Introduce new changelog implementation that will be able to display the changelog relevant when upgrading from you current version, including all intermediate versions\n- Auto expand connections display when a new child is added\n- Fix elevation not working in some cases and throwing errors\n- Improve git vault performance\n- Fix macOS updater and installation script sometimes only uninstalling existing version without installing new one\n- Fix scaling issues on Linux by providing a separate scaling option\n- Fix possible encoding issues on Windows with passwords that contained non-ASCII characters\n- Support opening ssh: URLs without username as well\n- Fix Linux OS logo sometimes showing wrongly or not at all\n"
  },
  {
    "path": "dist/changelog/8.5_incremental.md",
    "content": "## Professional edition restructuring\n\nThere was some feedback that the available plans for the professional edition were confusing. The monthly subscription and one time purchase could easily be confused as basically the same thing with a different timespan. Even the FAQ could still not eliminate all points of confusion as most readers were already familiar with plans from other tools, so it was difficult to properly break up the terms.\n\nWhile the monthly subscription did serve well as a cheap trial of some sorts, the community edition does almost the same job as most functionality is also available in there for noncommercial systems. Most people were also wary of subscriptions and interpreted the one-time purchase option as just a longer subscription even though that is not the case. There were also several other problems such as payment limitations with the subscription and no easy way to convert an existing subscription into the one-time purchase.\n\nWith this update, the monthly subscription will be retired. This makes it much easier to explain what you will get when purchasing the professional edition without having to differentiate between two completely different models. If you are currently using the monthly subscription, nothing will change for you. It will just not be possible for new customers to purchase it anymore.\n\nThe website at [https://xpipe.io](https://xpipe.io) has also been updated with the updated professional edition structure and will now hopefully explain everything better.\n\nThere's also now a lifetime professional edition available, which will include all future pro features. You can find the details for that on the pricing page in the FAQ if you are interested.\n\n## Other changes\n\n- Add support for OVH bastion systems (Professional feature)\n- Implement tab coloring for Windows Terminal in a better way\n- Fix sudo commands failing in `sh` shells with `Bad substitution`\n- Fix children connections always being included in parent category even when disabled in settings\n- Fix last used ordering not working correctly for connections\n- Fix skipped connection validation still searching for child connections\n- Fix connections failing when user home directory did not exist\n- Fix local file browser terminal open not initializing in the right directory\n- Fix temporary script directory permissions not being set correctly, leading to issues when logging into a system with multiple users\n- Fix application not using bundled fonts when local fonts failed to load\n- Fix automatic local shell fallback not working on Linux and macOS\n- Fix git password prompt getting stuck when called during shutdown and not pushing changes\n"
  },
  {
    "path": "dist/changelog/8.6.md",
    "content": "This is this biggest update yet and includes many changes that are necessary going forward to allow for many future features to come. These new implementations take everything into account learned so far and are more intuitive and robust. Especially when considering the long-term timeline, these changes will come in handy.\n\nThe versioning scheme has also been changed to simplify version numbers. So we are going straight from 1.7 to 8.0!\n\n## Note on updating\n\nThe last few versions of XPipe from 1.7.16 to 8.0.1 all had a self-updater on Windows that was not working properly. This was caused by a newly introduced JDK bug. This is now fixed from 8.1 onwards. To upgrade to 8.1+ on Windows, you have to do it manually by downloading and installing it from https://github.com/xpipe-io/xpipe/releases. There shouldn't be any more problems with 8.1+ after that.\n\nNote that versions <8.1 do not contain version information in the git vault. If you're on multiple systems that are synced with git, the git vault format can be updated on one system and being pulled on another a system that is running an older version. This can lead to data corruption. If this happens to you, you should be able to reset the git repository to a previous ref.\n\n## New terminal launcher\n\nThe terminal launcher functionality got completely reworked with the goal to make it more flexible and improve the terminal startup performance. You will quickly notice the new implementation whenever you launch any connection in your terminal.\n\n## Proxmox integration (Professional feature)\n\nThere is now support to directly query all VMs and containers located on a Proxmox system via the `pct` and `qm` tools. The containers can be accessed directly as any other containers while the VMs can be accessed via SSH. In case no SSH server is running in a vm, you can also choose to start one with XPipe.\n\nThis feature will be available in the professional version, but is also available in the free professional edition preview after release.\n\n## Improved professional edition preview\n\nAny new professional features, such as the Proxmox support, will be available for free for a couple of weeks after their initial release. There is now a new dialog available to allow you to quickly set up the XPipe professional preview plan.\n\nThis allows anyone interested in playing around with new features to do so without limitation and no commitment.\n\n## Git For Windows shell environments\n\nThe git installation on Windows comes with its own posix environment, which some people use to make use of standard Linux functionalities on Windows if they have not moved to WSL yet. This update brings full support to add this shell environment as well via the automatic detection functionality.\n\n## File browser improvements\n\nThe file browser has been reworked in terms of performance and reliability. File transfers of many files or now faster and any errors that can occur are now handled better.\n\nIn terms of the interface, there is also now a progress indicator for files being transferred. For any file conflicts, there is now a new dialog to choose how to resolve any conflict when copying or moving files.\n\n## Kubernetes configs and namespaces\n\nThis update adds support to also add connections from other kubeconfig files.\n\nFurthermore, you can also choose to use any namespace you want. This is useful in cases where you have not set up a context for every namespace you have.\n\nThe Kubernetes support is also now available in the pro preview after release.\n\n## Settings rework\n\nThis update comes with a complete rework of the settings menu. Many options have been added and existing ones have been improved, with a focus on providing more control over security settings. Make sure to give them a read to discover new options.\n\nThere has been a big focus on providing finer-grained control over security settings, which can be especially useful in enterprise contexts.\n\n## Per-Vault settings\n\nPreviously all settings were stored on a per-system basis. This caused some problems with git vaults, as all relevant settings that should persist across systems were not synced. From now on, all options that should be the same on all synced systems are automatically included in the git vault.\n\n## Authentication improvements\n\nThis update comes with a newly created system for handling authentication that is better suited for arbitrary authentication prompts. This allows for better support for things like 2FA and other keyboard interactive authentications schemes. The sudo elevation authentication also has been reworked to be more intuitive and mirror the behavior of the system in regard to password prompts.\n\nYou also now have finer control over the caching behaviour of passwords and the sudo behaviour via additional settings.\n\n## Temporary containers\n\nYou can now run a temporary docker container using a specified image that will get automatically removed once it is stopped. The container will keep running even if the image does not have any command specified that will run.\n\nThis can be useful if you quickly want to set up a certain environment by using a certain container image, e.g. a simple `ubuntu` image. You can then enter the container as normal in XPipe, perform your operations, and stop the container once it's no longer needed. It is then removed automatically.\n\n## Fish and dumb shells\n\nUp until now, connecting to fish shells or various dumb shells you typically find in devices like routers and links, did not work as there was no proper support for them. The shell handling implementation has now been rewritten to support fish login shells (after some timeout).\n\nThe implementation also now supports dumb shells that can be reached via ssh for example, but support still has to be implemented manually. Since I currently don't own any typical hardware like various routers and links, e.g. from cisco, I did not have the ability yet to do this. So if you own any hardware you would like to see supported, open an issue and share the typical output that is printed upon connection, and it can easily be supported.\n\nFor now, it should work with MikroTik routers at least.\n\n## macOS tray and dock handling\n\nDue to some confusion, XPipe will no longer use the system tray in macOS as an option when minimizing. It will instead conform to the usual macOS app handling that allows to reopen the window by clicking on the dock icon.\n\n## PowerShell fallback\n\nSome Windows admins disable cmd on their systems for security reasons. Previously this would have caused XPipe to not function on these systems as it relied on cmd. From now on, it can also dynamically fall back to PowerShell if needed without utilizing cmd at all.\n\n## Bundled OpenSSH on Windows\n\nOne common problem in the past has been to fact that Microsoft ships relatively outdated OpenSSH versions on Windows, which do not support newer features like FIDO2 keys. Due to the permissive license of OpenSSH and its Windows fork, XPipe can bundle the latest OpenSSH versions on Windows. There is now an option the settings menu to use the latest bundled OpenSSH version.\n\n## Timeout handling\n\nThe timeout model has been reworked. It is now set to a fixed amount of 30s while any active password prompts do no longer count towards it, meaning you can take your time when typing your passwords. An increased timeout value also allows for better handling of third party authentication schemes that XPipe has no control over, e.g. ones that will open a website in your browser for authentication.\n\n## Other changes\n\n- Add option to skip connection validation\n- Add ability to easily add files to the git vault data directory\n- Introduce new changelog implementation that will be able to display the changelog relevant when upgrading from you current version, including all intermediate versions\n- Auto expand connections display when a new child is added\n- Fix elevation not working in some cases and throwing errors\n- Improve git vault performance\n- Fix macOS updater and installation script sometimes only uninstalling existing version without installing new one\n- Fix scaling issues on Linux by providing a separate scaling option\n- Fix possible encoding issues on Windows with passwords that contained non-ASCII characters\n- Support opening ssh: URLs without username as well\n- Fix Linux OS logo sometimes showing wrongly or not at all\n"
  },
  {
    "path": "dist/changelog/8.6_incremental.md",
    "content": "## Education professional licenses\n\nThere is now the possibility to use XPipe professional for free for students and faculty from accredited educational institutions (high schools, colleges, and universities). Just send an email to hello@xpipe.io with your official email address of your educational institution.\n\n## Other changes\n\n- Add new quick access context menu for directories in the file browser. This allows you to quickly navigate into a directory structure\n- Fix passwords not being properly supplied to some CLI programs like ssh or git if xpipe was started from a terminal. The programs prompted the parent terminal session instead of the xpipe askpass. This is now fixed by calling setsid.\n- Fix elevation prompt requiring unnecessary confirmation the first time when the setting to always require elevation confirmation was active\n- Fix error messages being wrong when an external application could not be found\n- Fix browser transfer progress not showing MBs after more than 1GB had been transferred\n- Fix browser file icons sometimes getting mixed up after a change\n- Improve file browser display performance\n"
  },
  {
    "path": "dist/changelog/9.0.1.md",
    "content": "## Coherent desktops\n\nXPipe now comes with support for remote desktop connections. VNC connections are fully handled over SSH and can therefore be established on top of any existing SSH connection you have in XPipe. RDP support is realized similar to the terminal support, i.e. by launching your preferred RDP client with the connection information. X11-forwarding for SSH is also now supported.\n\nWith support for remote graphical desktop connection methods as well now in XPipe 9, the big picture idea is to implement the concept of coherent desktops. Essentially, you can launch predefined desktop applications, terminals, and scripts on any remote desktop connection, regardless of the underlying connection implementation. In combination with the improved SSH tunnel and background session support, you can launch graphical remote applications with one click in the same unified way for VNC over SSH connections, RDP connections, and X11-forwarded SSH connections.\n\nThe general implementation and concept will be refined over the next updates.\n\n## SSH connection improvements\n\n- Tunneled and X11-forwarded custom SSH connections are now properly detected and can be toggled on and off to run in the background as normal tunnels. This applies to normal connections and also SSH configs\n\n- The connection establishment has been reworked to reduce the amount of double prompts, e.g. for smartcards or 2FA, where user input is required twice. \n\n- The custom SSH connections now properly apply all configuration options of your user configuration file. They also now correctly apply multiple options for the same key correctly.\n\n- Any value specified for the `RemoteCommand` config option will now be properly applied when launching a terminal. This allows you to still use your preexisting init command setup, e.g. with tmux.\n\n- There is now support defining multiple host entries in place in a custom SSH connection. This is useful for cases where you want to use ProxyJump hosts in place without having to define them elsewhere.\n\n- A host key acceptance notification is now displayed properly in case your system doesn't automatically accept new host keys\n\n## SSH for unknown shells (Professional feature)\n\nThere's now an option to not let XPipe interact with the system. In case a system that does not run a known command shell, e.g. a router, link, or some IOT device, XPipe was previously unable to detect the shell type and errored out after some time. This option fixes this problem. This feature is available in the professional edition preview for two weeks.\n\n## SSH X11 Forwarding on Windows via WSL\n\nYou can now enable X11 forwarding for an SSH connection.\n\nXPipe allows you to use the WSL2 X11 capabilities on Windows for your SSH connection. The only thing you need for this is a [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) distribution installed on your local system. XPipe it will automatically choose a compatible installed distribution if possible, but you can also use another one in the settings menu.\n\nThis means that you don't need to install a separate X11 server on Windows. However, if you are using one anyway, XPipe will detect that and use the currently running X11 server.\n\n## Translations\n\nXPipe 9 now comes with translations for the user interface. These were initially generated with DeepL and can be easily improved and corrected by anyone on GitHub. You can check them out in action and if there is any translation you don't like, submit a quick pull request to fix it. For instructions on how to do this, see https://github.com/xpipe-io/xpipe/tree/master/lang.\n\n## Terminal improvements\n\nThe terminal integrations have been reworked across the board. To better show which terminals are well supported and which aren't, there is now a status indicator for every available terminal. This will show you how good the XPipe integration with each one is and which terminals are recommended to be used with XPipe. \n\nThe kitty terminal is now fully supported with tabs on both Linux and macOS. The Warp terminal integration now correctly enables all Warp features on remote shells. On macOS, other third-party prompts also now work properly in the launched terminals.\n\n## Password manager improvements\n\nThe password manager handling has been improved and some potential sources of errors and confusion have been eliminated. There are also now a few command templates available for established password managers to quickly get started.\n\n## Improved keyboard control\n\nIt is a goal to be able to use XPipe only with a keyboard either for productivity or for accessibility reasons. XPipe 9 introduces improved keyboard support with new shortcuts and improved focus control for navigating with the arrow keys, tab, space, and enter.\n\n## Improved logo\n\nThe application logo has been improved with of regards to contrast and visibility, which often was a problem on dark backgrounds. It should now stand out on any background color.\n\n## Other changes\n\nThere have been countless small bug fixes across the board. They are not listed individually here, but hopefully you will notice some of them.\n"
  },
  {
    "path": "dist/changelog/9.0.1_incremental.md",
    "content": "- Fix errors when trying to launch Tabby terminal\n- Fix terminal sessions closing instantly after completion\n- Fix choco package not being built\n- Fix a few broken translation strings\n"
  },
  {
    "path": "dist/changelog/9.0.md",
    "content": "## Coherent desktops\n\nXPipe now comes with support for remote desktop connections. VNC connections are fully handled over SSH and can therefore be established on top of any existing SSH connection you have in XPipe. RDP support is realized similar to the terminal support, i.e. by launching your preferred RDP client with the connection information. X11-forwarding for SSH is also now supported.\n\nWith support for remote graphical desktop connection methods as well now in XPipe 9, the big picture idea is to implement the concept of coherent desktops. Essentially, you can launch predefined desktop applications, terminals, and scripts on any remote desktop connection, regardless of the underlying connection implementation. In combination with the improved SSH tunnel and background session support, you can launch graphical remote applications with one click in the same unified way for VNC over SSH connections, RDP connections, and X11-forwarded SSH connections.\n\nThe general implementation and concept will be refined over the next updates.\n\n## SSH connection improvements\n\n- Tunneled and X11-forwarded custom SSH connections are now properly detected and can be toggled on and off to run in the background as normal tunnels. This applies to normal connections and also SSH configs\n\n- The connection establishment has been reworked to reduce the amount of double prompts, e.g. for smartcards or 2FA, where user input is required twice. \n\n- The custom SSH connections now properly apply all configuration options of your user configuration file. They also now correctly apply multiple options for the same key correctly.\n\n- Any value specified for the `RemoteCommand` config option will now be properly applied when launching a terminal. This allows you to still use your preexisting init command setup, e.g. with tmux.\n\n- There is now support defining multiple host entries in place in a custom SSH connection. This is useful for cases where you want to use ProxyJump hosts in place without having to define them elsewhere.\n\n- A host key acceptance notification is now displayed properly in case your system doesn't automatically accept new host keys\n\n## SSH for unknown shells (Professional feature)\n\nThere's now an option to not let XPipe interact with the system. In case a system that does not run a known command shell, e.g. a router, link, or some IOT device, XPipe was previously unable to detect the shell type and errored out after some time. This option fixes this problem. This feature is available in the professional edition preview for two weeks.\n\n## SSH X11 Forwarding on Windows via WSL\n\nYou can now enable X11 forwarding for an SSH connection.\n\nXPipe allows you to use the WSL2 X11 capabilities on Windows for your SSH connection. The only thing you need for this is a [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) distribution installed on your local system. XPipe it will automatically choose a compatible installed distribution if possible, but you can also use another one in the settings menu.\n\nThis means that you don't need to install a separate X11 server on Windows. However, if you are using one anyway, XPipe will detect that and use the currently running X11 server.\n\n## Translations\n\nXPipe 9 now comes with translations for the user interface. These were initially generated with DeepL and can be easily improved and corrected by anyone on GitHub. You can check them out in action and if there is any translation you don't like, submit a quick pull request to fix it. For instructions on how to do this, see https://github.com/xpipe-io/xpipe/tree/master/lang.\n\n## Terminal improvements\n\nThe terminal integrations have been reworked across the board. To better show which terminals are well supported and which aren't, there is now a status indicator for every available terminal. This will show you how good the XPipe integration with each one is and which terminals are recommended to be used with XPipe. \n\nThe kitty terminal is now fully supported with tabs on both Linux and macOS. The Warp terminal integration now correctly enables all Warp features on remote shells. On macOS, other third-party prompts also now work properly in the launched terminals.\n\n## Password manager improvements\n\nThe password manager handling has been improved and some potential sources of errors and confusion have been eliminated. There are also now a few command templates available for established password managers to quickly get started.\n\n## Improved keyboard control\n\nIt is a goal to be able to use XPipe only with a keyboard either for productivity or for accessibility reasons. XPipe 9 introduces improved keyboard support with new shortcuts and improved focus control for navigating with the arrow keys, tab, space, and enter.\n\n## Improved logo\n\nThe application logo has been improved with of regards to contrast and visibility, which often was a problem on dark backgrounds. It should now stand out on any background color.\n\n## Other changes\n\nThere have been countless small bug fixes across the board. They are not listed individually here, but hopefully you will notice some of them.\n"
  },
  {
    "path": "dist/changelog/9.1.md",
    "content": "## Coherent desktops\n\nXPipe now comes with support for remote desktop connections. VNC connections are fully handled over SSH and can therefore be established on top of any existing SSH connection you have in XPipe. RDP support is realized similar to the terminal support, i.e. by launching your preferred RDP client with the connection information. X11-forwarding for SSH is also now supported.\n\nWith support for remote graphical desktop connection methods as well now in XPipe 9, the big picture idea is to implement the concept of coherent desktops. Essentially, you can launch predefined desktop applications, terminals, and scripts on any remote desktop connection, regardless of the underlying connection implementation. In combination with the improved SSH tunnel and background session support, you can launch graphical remote applications with one click in the same unified way for VNC over SSH connections, RDP connections, and X11-forwarded SSH connections.\n\nThe general implementation and concept will be refined over the next updates.\n\n## SSH connection improvements\n\n- Tunneled and X11-forwarded custom SSH connections are now properly detected and can be toggled on and off to run in the background as normal tunnels. This applies to normal connections and also SSH configs\n\n- The connection establishment has been reworked to reduce the amount of double prompts, e.g. for smartcards or 2FA, where user input is required twice. \n\n- The custom SSH connections now properly apply all configuration options of your user configuration file. They also now correctly apply multiple options for the same key correctly.\n\n- Any value specified for the `RemoteCommand` config option will now be properly applied when launching a terminal. This allows you to still use your preexisting init command setup, e.g. with tmux.\n\n- There is now support defining multiple host entries in place in a custom SSH connection. This is useful for cases where you want to use ProxyJump hosts in place without having to define them elsewhere.\n\n- A host key acceptance notification is now displayed properly in case your system doesn't automatically accept new host keys\n\n## SSH for unknown shells (Professional feature)\n\nThere's now an option to not let XPipe interact with the system. In case a system that does not run a known command shell, e.g. a router, link, or some IOT device, XPipe was previously unable to detect the shell type and errored out after some time. This option fixes this problem. This feature is available in the professional edition preview for two weeks.\n\n## SSH X11 Forwarding on Windows via WSL\n\nYou can now enable X11 forwarding for an SSH connection.\n\nXPipe allows you to use the WSL2 X11 capabilities on Windows for your SSH connection. The only thing you need for this is a [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) distribution installed on your local system. XPipe it will automatically choose a compatible installed distribution if possible, but you can also use another one in the settings menu.\n\nThis means that you don't need to install a separate X11 server on Windows. However, if you are using one anyway, XPipe will detect that and use the currently running X11 server.\n\n## Translations\n\nXPipe 9 now comes with translations for the user interface. These were initially generated with DeepL and can be easily improved and corrected by anyone on GitHub. You can check them out in action and if there is any translation you don't like, submit a quick pull request to fix it. For instructions on how to do this, see https://github.com/xpipe-io/xpipe/tree/master/lang.\n\n## Terminal improvements\n\nThe terminal integrations have been reworked across the board. To better show which terminals are well supported and which aren't, there is now a status indicator for every available terminal. This will show you how good the XPipe integration with each one is and which terminals are recommended to be used with XPipe. \n\nThe kitty terminal is now fully supported with tabs on both Linux and macOS. The Warp terminal integration now correctly enables all Warp features on remote shells. On macOS, other third-party prompts also now work properly in the launched terminals.\n\n## Password manager improvements\n\nThe password manager handling has been improved and some potential sources of errors and confusion have been eliminated. There are also now a few command templates available for established password managers to quickly get started.\n\n## Improved keyboard control\n\nIt is a goal to be able to use XPipe only with a keyboard either for productivity or for accessibility reasons. XPipe 9 introduces improved keyboard support with new shortcuts and improved focus control for navigating with the arrow keys, tab, space, and enter.\n\n## Improved logo\n\nThe application logo has been improved with of regards to contrast and visibility, which often was a problem on dark backgrounds. It should now stand out on any background color.\n\n## Other changes\n\nThere have been countless small bug fixes across the board. They are not listed individually here, but hopefully you will notice some of them.\n"
  },
  {
    "path": "dist/changelog/9.1_incremental.md",
    "content": "- Add ability to drop files into an active VNC session to transfer them to the system\n- Rework VNC connections to distinguish between VNC server host and actual target system host for cases like VMs where the server host and controlled target system might be different\n- Add new setting to automatically lock vault when the local system goes into hibernation/sleep mode and a custom vault passphrase was set\n- Fix sudo elevation password not being filled automatically when launching some remote connection in a terminal\n- Fix git sometimes complaining about an unknown author identity when cloning on new systems\n- Fix search for connections dialog sometimes throwing errors\n- Fix NullPointer when launching an SFTP Client/Termius/VSCode for an SSH connection without a password set\n- Fix macOS terminal and editor app recognition to be more accurate\n- Fix file browser overview buttons both opening the same directory\n- Fix exception on Linux when desktop directory did not exist\n- Fix out of bounds error for certain VNC key input\n- Fix NullPointers when launching a desktop environment for an X11 SSH connection\n- Fix some NullPointers in the file browser\n- Fix some styling issues\n"
  },
  {
    "path": "dist/changelog/9.2.md",
    "content": "## Coherent desktops\n\nXPipe now comes with support for remote desktop connections. VNC connections are fully handled over SSH and can therefore be established on top of any existing SSH connection you have in XPipe. RDP support is realized similar to the terminal support, i.e. by launching your preferred RDP client with the connection information. X11-forwarding for SSH is also now supported.\n\nWith support for remote graphical desktop connection methods as well now in XPipe 9, the big picture idea is to implement the concept of coherent desktops. Essentially, you can launch predefined desktop applications, terminals, and scripts on any remote desktop connection, regardless of the underlying connection implementation. In combination with the improved SSH tunnel and background session support, you can launch graphical remote applications with one click in the same unified way for VNC over SSH connections, RDP connections, and X11-forwarded SSH connections.\n\nThe general implementation and concept will be refined over the next updates.\n\n## SSH connection improvements\n\n- Tunneled and X11-forwarded custom SSH connections are now properly detected and can be toggled on and off to run in the background as normal tunnels. This applies to normal connections and also SSH configs\n\n- The connection establishment has been reworked to reduce the amount of double prompts, e.g. for smartcards or 2FA, where user input is required twice. \n\n- The custom SSH connections now properly apply all configuration options of your user configuration file. They also now correctly apply multiple options for the same key correctly.\n\n- Any value specified for the `RemoteCommand` config option will now be properly applied when launching a terminal. This allows you to still use your preexisting init command setup, e.g. with tmux.\n\n- There is now support defining multiple host entries in place in a custom SSH connection. This is useful for cases where you want to use ProxyJump hosts in place without having to define them elsewhere.\n\n- A host key acceptance notification is now displayed properly in case your system doesn't automatically accept new host keys\n\n## SSH for unknown shells (Professional feature)\n\nThere's now an option to not let XPipe interact with the system. In case a system that does not run a known command shell, e.g. a router, link, or some IOT device, XPipe was previously unable to detect the shell type and errored out after some time. This option fixes this problem. This feature is available in the professional edition preview for two weeks.\n\n## SSH X11 Forwarding on Windows via WSL\n\nYou can now enable X11 forwarding for an SSH connection.\n\nXPipe allows you to use the WSL2 X11 capabilities on Windows for your SSH connection. The only thing you need for this is a [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) distribution installed on your local system. XPipe it will automatically choose a compatible installed distribution if possible, but you can also use another one in the settings menu.\n\nThis means that you don't need to install a separate X11 server on Windows. However, if you are using one anyway, XPipe will detect that and use the currently running X11 server.\n\n## Translations\n\nXPipe 9 now comes with translations for the user interface. These were initially generated with DeepL and can be easily improved and corrected by anyone on GitHub. You can check them out in action and if there is any translation you don't like, submit a quick pull request to fix it. For instructions on how to do this, see https://github.com/xpipe-io/xpipe/tree/master/lang.\n\n## Terminal improvements\n\nThe terminal integrations have been reworked across the board. To better show which terminals are well supported and which aren't, there is now a status indicator for every available terminal. This will show you how good the XPipe integration with each one is and which terminals are recommended to be used with XPipe. \n\nThe kitty terminal is now fully supported with tabs on both Linux and macOS. The Warp terminal integration now correctly enables all Warp features on remote shells. On macOS, other third-party prompts also now work properly in the launched terminals.\n\n## Password manager improvements\n\nThe password manager handling has been improved and some potential sources of errors and confusion have been eliminated. There are also now a few command templates available for established password managers to quickly get started.\n\n## Improved keyboard control\n\nIt is a goal to be able to use XPipe only with a keyboard either for productivity or for accessibility reasons. XPipe 9 introduces improved keyboard support with new shortcuts and improved focus control for navigating with the arrow keys, tab, space, and enter.\n\n## Improved logo\n\nThe application logo has been improved with of regards to contrast and visibility, which often was a problem on dark backgrounds. It should now stand out on any background color.\n\n## Other changes\n\nThere have been countless small bug fixes across the board. They are not listed individually here, but hopefully you will notice some of them.\n"
  },
  {
    "path": "dist/changelog/9.2_incremental.md",
    "content": "## Fixes\n\n- Fix custom scripts not properly applying\n- Fix closing application window while XPipe was saving not properly applying all changes\n- Fix race condition when loading file icons\n- Fix state corruption of local shell, leading to NullPointers once a shell connection had to be killed\n- Fix error handling in case powershell failed to start up\n- Fix a corrupted PATH leading to cmd or powershell not being able to be started\n- Fix headless system error message not being printed when application failed to start up\n- Fix offline licenses not properly applying\n- Fix WMClass not being properly set on Linux\n- Fix file browser files being dragged into macOS finder creating raw clipboard file\n- Fix SSH gateway not updating when choosing key file on another host\n- Fix file browser failing to connect if target system did not have id command available\n- Fix git share file button not jumping to correct settings menu\n\n## File browser improvements\n\nThe file browser has been reworked to support many new keyboard shortcuts, plus the general user experience has been improved:\n\n- There is now a duration estimate when transferring large files\n- Files that are right-clicked are now also included in the selection\n- The quick access menu will now shift focus properly\n- The file list can be navigated with the arrow keys. CTRL and SHIFT can be used to multiple select files\n- Any files you drag can now be explicitly moved by holding ALT\n- Renaming files will now preserve the selection\n- *RIGHT* will open the quick access menu tree for directories\n- *CTRL+W* closes the current file browser tab\n- *CTRL+SHIFT+W* closes all file browser tabs\n- *CTRL+Q* closes the window\n- *CTRL+F* will now properly toggle the find text field\n- *CTRL+L* will now focus the path location text field\n- *ALT+HOME* will go to the file system overview page\n- *ALT+H* shows the browsing history\n- *ALT-UP* navigates to the parent directory\n- *ESCAPE* clears the selection\n- *SPACE* shows the context menu for the selection\n- \n## Git handling improvements\n\nThe git error actions have been reworked. In case any merge conflict or similar occurs, the possible actions are now handled better:\n- They are properly highlighted to distinguish them from the normal error dialog window\n- They now work for all git client localizations\n- They are less likely to cause git accidents. Any possible destructive action has to be confirmed now\n"
  },
  {
    "path": "dist/changelog/9.3.md",
    "content": "## Coherent desktops\n\nXPipe now comes with support for remote desktop connections. VNC connections are fully handled over SSH and can therefore be established on top of any existing SSH connection you have in XPipe. RDP support is realized similar to the terminal support, i.e. by launching your preferred RDP client with the connection information. X11-forwarding for SSH is also now supported.\n\nWith support for remote graphical desktop connection methods as well now in XPipe 9, the big picture idea is to implement the concept of coherent desktops. Essentially, you can launch predefined desktop applications, terminals, and scripts on any remote desktop connection, regardless of the underlying connection implementation. In combination with the improved SSH tunnel and background session support, you can launch graphical remote applications with one click in the same unified way for VNC over SSH connections, RDP connections, and X11-forwarded SSH connections.\n\nThe general implementation and concept will be refined over the next updates.\n\n## SSH connection improvements\n\n- Tunneled and X11-forwarded custom SSH connections are now properly detected and can be toggled on and off to run in the background as normal tunnels. This applies to normal connections and also SSH configs\n\n- The connection establishment has been reworked to reduce the amount of double prompts, e.g. for smartcards or 2FA, where user input is required twice. \n\n- The custom SSH connections now properly apply all configuration options of your user configuration file. They also now correctly apply multiple options for the same key correctly.\n\n- Any value specified for the `RemoteCommand` config option will now be properly applied when launching a terminal. This allows you to still use your preexisting init command setup, e.g. with tmux.\n\n- There is now support defining multiple host entries in place in a custom SSH connection. This is useful for cases where you want to use ProxyJump hosts in place without having to define them elsewhere.\n\n- A host key acceptance notification is now displayed properly in case your system doesn't automatically accept new host keys\n\n## SSH for unknown shells (Professional feature)\n\nThere's now an option to not let XPipe interact with the system. In case a system that does not run a known command shell, e.g. a router, link, or some IOT device, XPipe was previously unable to detect the shell type and errored out after some time. This option fixes this problem. This feature is available in the professional edition preview for two weeks.\n\n## SSH X11 Forwarding on Windows via WSL\n\nYou can now enable X11 forwarding for an SSH connection.\n\nXPipe allows you to use the WSL2 X11 capabilities on Windows for your SSH connection. The only thing you need for this is a [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) distribution installed on your local system. XPipe it will automatically choose a compatible installed distribution if possible, but you can also use another one in the settings menu.\n\nThis means that you don't need to install a separate X11 server on Windows. However, if you are using one anyway, XPipe will detect that and use the currently running X11 server.\n\n## Translations\n\nXPipe 9 now comes with translations for the user interface. These were initially generated with DeepL and can be easily improved and corrected by anyone on GitHub. You can check them out in action and if there is any translation you don't like, submit a quick pull request to fix it. For instructions on how to do this, see https://github.com/xpipe-io/xpipe/tree/master/lang.\n\n## Terminal improvements\n\nThe terminal integrations have been reworked across the board. To better show which terminals are well supported and which aren't, there is now a status indicator for every available terminal. This will show you how good the XPipe integration with each one is and which terminals are recommended to be used with XPipe. \n\nThe kitty terminal is now fully supported with tabs on both Linux and macOS. The Warp terminal integration now correctly enables all Warp features on remote shells. On macOS, other third-party prompts also now work properly in the launched terminals.\n\n## Password manager improvements\n\nThe password manager handling has been improved and some potential sources of errors and confusion have been eliminated. There are also now a few command templates available for established password managers to quickly get started.\n\n## Improved keyboard control\n\nIt is a goal to be able to use XPipe only with a keyboard either for productivity or for accessibility reasons. XPipe 9 introduces improved keyboard support with new shortcuts and improved focus control for navigating with the arrow keys, tab, space, and enter.\n\n## Improved logo\n\nThe application logo has been improved with of regards to contrast and visibility, which often was a problem on dark backgrounds. It should now stand out on any background color.\n\n## Other changes\n\nThere have been countless small bug fixes across the board. They are not listed individually here, but hopefully you will notice some of them.\n"
  },
  {
    "path": "dist/changelog/9.3_incremental.md",
    "content": "## Git vault improvements\n\n- The generated repository readme file has been improved. It will now show a proper tree for all connections. It also now contains troubleshooting information and instructions in case something is not working as expected.\n- Fix git integration on Windows not committing anything when GPG signing was required\n- Fix git askpass not properly working for SSH git connections when the SSH connection required user input, e.g. if a used key file had a passphrase\n- Some git performance improvements\n\n## Other\n\n- Add support for Windows Terminal Canary\n- Proxmox systems can now also be added for users other than root if sudo is available\n- Fix actions to accept new ssh host key and to fix key permissions not showing up\n- Fix windows sometimes showing in the top left corner on Linux\n- Fix popup windows being cut off in xfce and i3 desktop environments\n- Fix k8s license check failing when a cluster permissions error occurred\n- Explicitly set language variables on Linux and macOS to keep internal terminal commands in english\n- Don't show nonfunctional translated directory links on Windows systems\n- The password manager command in the settings men you now support multi-line commands\n- Fix out of bounds error for file chooser with files already in the git vault\n- Fix NullPointer error when clearing a host connection chooser\n- Fix some NullPointers in the file browser\n"
  },
  {
    "path": "dist/changelog/9.4.1.md",
    "content": "## Coherent desktops\n\nXPipe now comes with support for remote desktop connections. VNC connections are fully handled over SSH and can therefore be established on top of any existing SSH connection you have in XPipe. RDP support is realized similar to the terminal support, i.e. by launching your preferred RDP client with the connection information. X11-forwarding for SSH is also now supported.\n\nWith support for remote graphical desktop connection methods as well now in XPipe 9, the big picture idea is to implement the concept of coherent desktops. Essentially, you can launch predefined desktop applications, terminals, and scripts on any remote desktop connection, regardless of the underlying connection implementation. In combination with the improved SSH tunnel and background session support, you can launch graphical remote applications with one click in the same unified way for VNC over SSH connections, RDP connections, and X11-forwarded SSH connections.\n\nThe general implementation and concept will be refined over the next updates.\n\n## SSH connection improvements\n\n- Tunneled and X11-forwarded custom SSH connections are now properly detected and can be toggled on and off to run in the background as normal tunnels. This applies to normal connections and also SSH configs\n\n- The connection establishment has been reworked to reduce the amount of double prompts, e.g. for smartcards or 2FA, where user input is required twice. \n\n- The custom SSH connections now properly apply all configuration options of your user configuration file. They also now correctly apply multiple options for the same key correctly.\n\n- Any value specified for the `RemoteCommand` config option will now be properly applied when launching a terminal. This allows you to still use your preexisting init command setup, e.g. with tmux.\n\n- There is now support defining multiple host entries in place in a custom SSH connection. This is useful for cases where you want to use ProxyJump hosts in place without having to define them elsewhere.\n\n- A host key acceptance notification is now displayed properly in case your system doesn't automatically accept new host keys\n\n## SSH for unknown shells (Professional feature)\n\nThere's now an option to not let XPipe interact with the system. In case a system that does not run a known command shell, e.g. a router, link, or some IOT device, XPipe was previously unable to detect the shell type and errored out after some time. This option fixes this problem. This feature is available in the professional edition preview for two weeks.\n\n## SSH X11 Forwarding on Windows via WSL\n\nYou can now enable X11 forwarding for an SSH connection.\n\nXPipe allows you to use the WSL2 X11 capabilities on Windows for your SSH connection. The only thing you need for this is a [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) distribution installed on your local system. XPipe it will automatically choose a compatible installed distribution if possible, but you can also use another one in the settings menu.\n\nThis means that you don't need to install a separate X11 server on Windows. However, if you are using one anyway, XPipe will detect that and use the currently running X11 server.\n\n## Translations\n\nXPipe 9 now comes with translations for the user interface. These were initially generated with DeepL and can be easily improved and corrected by anyone on GitHub. You can check them out in action and if there is any translation you don't like, submit a quick pull request to fix it. For instructions on how to do this, see https://github.com/xpipe-io/xpipe/tree/master/lang.\n\n## Terminal improvements\n\nThe terminal integrations have been reworked across the board. To better show which terminals are well supported and which aren't, there is now a status indicator for every available terminal. This will show you how good the XPipe integration with each one is and which terminals are recommended to be used with XPipe. \n\nThe kitty terminal is now fully supported with tabs on both Linux and macOS. The Warp terminal integration now correctly enables all Warp features on remote shells. On macOS, other third-party prompts also now work properly in the launched terminals.\n\n## Password manager improvements\n\nThe password manager handling has been improved and some potential sources of errors and confusion have been eliminated. There are also now a few command templates available for established password managers to quickly get started.\n\n## Improved keyboard control\n\nIt is a goal to be able to use XPipe only with a keyboard either for productivity or for accessibility reasons. XPipe 9 introduces improved keyboard support with new shortcuts and improved focus control for navigating with the arrow keys, tab, space, and enter.\n\n## Improved logo\n\nThe application logo has been improved with of regards to contrast and visibility, which often was a problem on dark backgrounds. It should now stand out on any background color.\n\n## Other changes\n\nThere have been countless small bug fixes across the board. They are not listed individually here, but hopefully you will notice some of them.\n"
  },
  {
    "path": "dist/changelog/9.4.1_incremental.md",
    "content": "- Make passwords for SSH config connections with set identity file automatically default to none, saving some manual configuration\n- Fix terminal installation detection being broken on macOS, always defaulting to kitty.app\n- Some small file IO performance improvements\n"
  },
  {
    "path": "dist/changelog/9.4.md",
    "content": "## Coherent desktops\n\nXPipe now comes with support for remote desktop connections. VNC connections are fully handled over SSH and can therefore be established on top of any existing SSH connection you have in XPipe. RDP support is realized similar to the terminal support, i.e. by launching your preferred RDP client with the connection information. X11-forwarding for SSH is also now supported.\n\nWith support for remote graphical desktop connection methods as well now in XPipe 9, the big picture idea is to implement the concept of coherent desktops. Essentially, you can launch predefined desktop applications, terminals, and scripts on any remote desktop connection, regardless of the underlying connection implementation. In combination with the improved SSH tunnel and background session support, you can launch graphical remote applications with one click in the same unified way for VNC over SSH connections, RDP connections, and X11-forwarded SSH connections.\n\nThe general implementation and concept will be refined over the next updates.\n\n## SSH connection improvements\n\n- Tunneled and X11-forwarded custom SSH connections are now properly detected and can be toggled on and off to run in the background as normal tunnels. This applies to normal connections and also SSH configs\n\n- The connection establishment has been reworked to reduce the amount of double prompts, e.g. for smartcards or 2FA, where user input is required twice. \n\n- The custom SSH connections now properly apply all configuration options of your user configuration file. They also now correctly apply multiple options for the same key correctly.\n\n- Any value specified for the `RemoteCommand` config option will now be properly applied when launching a terminal. This allows you to still use your preexisting init command setup, e.g. with tmux.\n\n- There is now support defining multiple host entries in place in a custom SSH connection. This is useful for cases where you want to use ProxyJump hosts in place without having to define them elsewhere.\n\n- A host key acceptance notification is now displayed properly in case your system doesn't automatically accept new host keys\n\n## SSH for unknown shells (Professional feature)\n\nThere's now an option to not let XPipe interact with the system. In case a system that does not run a known command shell, e.g. a router, link, or some IOT device, XPipe was previously unable to detect the shell type and errored out after some time. This option fixes this problem. This feature is available in the professional edition preview for two weeks.\n\n## SSH X11 Forwarding on Windows via WSL\n\nYou can now enable X11 forwarding for an SSH connection.\n\nXPipe allows you to use the WSL2 X11 capabilities on Windows for your SSH connection. The only thing you need for this is a [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) distribution installed on your local system. XPipe it will automatically choose a compatible installed distribution if possible, but you can also use another one in the settings menu.\n\nThis means that you don't need to install a separate X11 server on Windows. However, if you are using one anyway, XPipe will detect that and use the currently running X11 server.\n\n## Translations\n\nXPipe 9 now comes with translations for the user interface. These were initially generated with DeepL and can be easily improved and corrected by anyone on GitHub. You can check them out in action and if there is any translation you don't like, submit a quick pull request to fix it. For instructions on how to do this, see https://github.com/xpipe-io/xpipe/tree/master/lang.\n\n## Terminal improvements\n\nThe terminal integrations have been reworked across the board. To better show which terminals are well supported and which aren't, there is now a status indicator for every available terminal. This will show you how good the XPipe integration with each one is and which terminals are recommended to be used with XPipe. \n\nThe kitty terminal is now fully supported with tabs on both Linux and macOS. The Warp terminal integration now correctly enables all Warp features on remote shells. On macOS, other third-party prompts also now work properly in the launched terminals.\n\n## Password manager improvements\n\nThe password manager handling has been improved and some potential sources of errors and confusion have been eliminated. There are also now a few command templates available for established password managers to quickly get started.\n\n## Improved keyboard control\n\nIt is a goal to be able to use XPipe only with a keyboard either for productivity or for accessibility reasons. XPipe 9 introduces improved keyboard support with new shortcuts and improved focus control for navigating with the arrow keys, tab, space, and enter.\n\n## Improved logo\n\nThe application logo has been improved with of regards to contrast and visibility, which often was a problem on dark backgrounds. It should now stand out on any background color.\n\n## Other changes\n\nThere have been countless small bug fixes across the board. They are not listed individually here, but hopefully you will notice some of them.\n"
  },
  {
    "path": "dist/changelog/9.4_incremental.md",
    "content": "## Connection notes\n\nThere is now the new option to add notes to any connection. These notes are written in markdown, and the full markdown spec is supported.\n\n## File transfer reliability improvements\n\nThe file transfer mechanism when editing files had some flaws, which under rare conditions caused the data not being fully transferred or the file browser session to timeout/die. This was especially prevalent when saving a file multiple times in quick succession or when using VSCode on Windows, which performs multiple file writes on save (for whatever reason).\n\nThe entire transfer implementation has been rewritten to iron out these issues and increase reliability. Other file browser actions have also been made more reliable.\n\nThere seems to be another separate issue with a PowerShell bug when connecting to a Windows system, causing file uploads to be slow. For now, xpipe can fall back to pwsh if it is installed to work around this issue.\n\n## Git vault improvements\n\nThe conflict resolution has been improved\n- When setting up the git vault on another system, there will no longer be an initial merge conflict that has to be handled\n- In case of a merge conflict, overwriting local changes will now preserve all connections that are not added to the git vault, including local connections\n- You now have the option to force push changes when a conflict occurs while XPipe is saving while running, not requiring a restart anymore\n\n## Terminal improvements\n\nThe terminal integration got reworked for some terminals:\n- iTerm can now launch tabs instead of individual windows. There were also a few issues fixed that prevented it from launching sometimes\n- WezTerm now supports tabs on Linux and macOS. The Windows installation detection has been improved to detect all installed versions\n- Terminal.app will now launch faster\n\n## Other\n\n- You can now add simple RDP connections without a file\n- Fix VMware Player/Workstation and MSYS2 not being detected on Windows. Now simply searching for connections should add them automatically if they are installed\n- The file browser sidebar now only contains connections that can be opened in it, reducing the amount of connection shown\n- Clarify error message for RealVNC servers, highlighting that RealVNC uses a proprietary protocol spec that can't be supported by third-party VNC clients like xpipe\n- Fix Linux builds containing unnecessary debug symbols\n- Fix AUR package also installing a debug package\n- Fix application restart not working properly on macOS\n- Fix possibility of selecting own children connections as hosts, causing a stack overflow. Please don't try to create cycles in your connection graphs\n- Fix vault secrets not correctly updating unless restarted when changing vault passphrase\n- Fix connection launcher desktop shortcuts and URLs not properly executing if xpipe is not running\n- Fix move to ... menu sometimes not ordering categories correctly\n- Fix SSH command failing on macOS with homebrew openssh package installed\n- Fix SSH connections not opening the correct shell environment on Windows systems when username contained spaces due to an OpenSSH bug\n- Fix newly added connections not having the correct order\n- Fix error messages of external editor programs not being shown when they failed to start\n"
  },
  {
    "path": "dist/debug/debug_arguments.txt",
    "content": "-Dio.xpipe.app.writeSysOut=true\n-Dio.xpipe.app.writeLogs=false\n-Dio.xpipe.app.logLevel=trace\n-Dio.xpipe.app.developerMode=true\n-Dio.xpipe.beacon.debugOutput=true\n-Dio.xpipe.app.useVirtualThreads=false"
  },
  {
    "path": "dist/debug/linux/xpiped_debug.sh",
    "content": "#!/bin/bash\n\nDIR=\"${0%/*}\"\nEXTRA_ARGS=(JVM-ARGS)\nexport CDS_JVM_OPTS=\"${EXTRA_ARGS[*]}\"\n\n\"$DIR/../lib/runtime/bin/xpiped\" \"$@\"\n\nread -rsp \"Press any key to close\" -n 1 key\n"
  },
  {
    "path": "dist/debug/mac/xpiped_debug.sh",
    "content": "#!/bin/bash\n\nDIR=\"${0%/*}\"\nEXTRA_ARGS=(JVM-ARGS)\nexport CDS_JVM_OPTS=\"${EXTRA_ARGS[*]}\"\n\n\"$DIR/../../runtime/Contents/Home/bin/xpiped\" \"$@\"\n\nread -rsp \"Press any key to close\" -n 1 key\n"
  },
  {
    "path": "dist/debug/windows/xpiped_debug.bat",
    "content": "@echo off\r\nset CDS_JVM_OPTS=JVM-ARGS\r\nchcp 65001 > NUL\r\nCALL \"%~dp0\\..\\runtime\\bin\\xpiped.bat\" %*\r\npause\r\n"
  },
  {
    "path": "dist/fonts/allfonts.properties",
    "content": "family.0=Inter\nfont.0=Inter Regular\nfile.0=Inter-Regular.ttf"
  },
  {
    "path": "dist/fonts/logicalfonts.properties",
    "content": "sans.regular.0.font=Inter Regular\nsans.regular.0.file=Inter-Regular.ttf"
  },
  {
    "path": "dist/jpackage/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleAllowMixedLocalizations</key>\n\t<true/>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>English</string>\n\t<key>CFBundleExecutable</key>\n\t<string>xpiped</string>\n\t<key>CFBundleIconFile</key>\n\t<string>xpipe</string>\n\t<key>CFBundleIconName</key>\n\t<string>xpipe</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>__BUNDLE__</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>__NAME__</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleURLTypes</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>CFBundleURLName</key>\n\t\t\t<string>io.xpipe.URLScheme</string>\n\t\t\t<key>CFBundleURLSchemes</key>\n\t\t\t<array>\n\t\t\t\t<string>xpipe</string>\n\t\t\t\t<string>ssh</string>\n\t\t\t\t<string>sftp</string>\n\t\t\t\t<string>s3</string>\n\t\t\t</array>\n\t\t</dict>\n\t</array>\n\t<key>LSApplicationCategoryType</key>\n\t<string>public.app-category.developer-tools</string>\n\t<key>LSMinimumSystemVersion</key>\n\t<string>10.11</string>\n\t<key>NSHighResolutionCapable</key>\n\t<string>true</string>\n\t<key>NSHumanReadableCopyright</key>\n\t<string>Copyright (C) 2025</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>__VERSION__</string>\n\t<key>CFBundleVersion</key>\n\t<string>__VERSION__</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "dist/jpackage.gradle",
    "content": "apply from: \"$rootDir/gradle/gradle_scripts/javafx.gradle\"\napply from: \"$rootDir/gradle/gradle_scripts/jna.gradle\"\n\ndef distDir = \"${project.layout.buildDirectory.get()}/dist\"\n\n// To remove warnings, the plugin probably does not expect the JPackage tasks to be in a separate project\napplication {\n    mainModule = packageName(null)\n    mainClass = packageName('Main')\n}\n\ndef extModules = project.allExtensions\ndef extLibrariesTasks = extModules.stream().map { it.getTasksByName('createExtOutput', true)[0] }.toList()\n\ndependencies {\n    implementation project(':app')\n    if (!useBundledJavaFx) {\n        configurations.javafx.getAsFileTree().getFiles().forEach {\n            implementation files(it)\n        }\n    }\n    if (!useBundledJna) {\n        configurations.jna.getAsFileTree().getFiles().forEach {\n            implementation files(it)\n        }\n    }\n}\n\n// Mac does not like a zero major version\ndef macVersion = canonicalVersionString\nif (Integer.parseInt(macVersion.substring(0, 1)) == 0) {\n    macVersion = \"1\" + macVersion.substring(1)\n}\n\njlink {\n    imageDir = file(\"${project.layout.buildDirectory.get()}/image\")\n    options = [\n            // Disable this as this removes line numbers from stack traces!\n            // '--strip-debug',\n            '--no-header-files',\n            '--no-man-pages',\n            '--compress', 'zip-9',\n            '--ignore-signing-information'\n    ]\n\n    if (os.isLinux()) {\n        options.addAll('--strip-native-debug-symbols', 'exclude-debuginfo-files')\n    }\n\n    if (customJavaFxJmodsPath != null) {\n        addExtraModulePath(customJavaFxJmodsPath.toString())\n    } else if (useBundledJavaFx && !bundledJdkJavaFx) {\n        addExtraModulePath(layout.projectDirectory.dir(\"javafx/${platformName}/${arch}\").toString())\n    }\n\n    if (useBundledJna) {\n        addExtraModulePath(layout.projectDirectory.dir(\"jna/${platformName}/${arch}\").toString())\n    }\n\n    for (def extProject : extModules) {\n        def dir = \"${extProject.buildDir}/libs_ext\"\n        addExtraModulePath(dir)\n        addOptions(\"--add-modules\", groupName + \".ext.${extProject.name}\")\n    }\n\n    if (!ci || (!isStage && !isFullRelease)) {\n        addOptions(\"--add-modules\", \"jdk.jdwp.agent\")\n    }\n\n    launcher {\n        moduleName = packageName(null)\n        mainClass = packageName('Main')\n        name = jpackageExecutableName\n        jvmArgs = jpackageReleaseArguments\n    }\n\n    jpackage {\n        imageOutputDir = file(\"$distDir/jpackage\")\n        imageName = jpackageExecutableName\n        if (os.isWindows()) {\n            icon = \"$rootDir/dist/logo/logo.ico\"\n            appVersion = os.isWindows() ? windowsSchemaCanonicalVersion : canonicalVersionString\n        } else if (os.isLinux()) {\n            icon = \"$rootDir/dist/logo/logo.png\"\n            appVersion = canonicalVersionString\n        } else {\n            icon = \"$rootDir/dist/logo/logo.icns\"\n            resourceDir = file(\"${project.layout.buildDirectory.get()}/macos_resources\")\n            appVersion = macVersion\n        }\n        vendor = publisher\n        skipInstaller = true\n    }\n}\n\ntasks.named('jlink').get().dependsOn(rootProject.getTasksByName(\"jar\", true))\ntasks.named('jlink').get().dependsOn(extLibrariesTasks)\n\ndef outputName = os.isMacOsX() ? \"${jpackageExecutableName}.app/Contents/Resources\" : jpackageExecutableName\ntasks.register('copyBundledExtensions', DefaultTask) {\n    dependsOn extLibrariesTasks\n    doLast {\n        for (def extProject : extModules) {\n            def dir = \"${extProject.buildDir}/libs_ext\"\n            if (file(dir).exists()) {\n                copy {\n                    from(dir)\n                    into \"$distDir/jpackage/$outputName/extensions/${extProject.name}\"\n                    include '*.jar'\n                }\n            }\n        }\n    }\n}\n\ntasks.register('prepareMacOSResources', DefaultTask) {\n    doLast {\n        file(\"${project.layout.buildDirectory.get()}/macos_resources\").mkdirs()\n        copy {\n            from replaceVariablesInFile(\"$projectDir/jpackage/Info.plist\",\n                                        Map.of('__NAME__',\n                                               productName,\n                                               '__VERSION__',\n                                               versionString,\n                                               '__BUNDLE__',\n                                               jpackageMacOsBundleName))\n            into file(\"${project.layout.buildDirectory.get()}/macos_resources\")\n        }\n    }\n}\n\ntasks.register('finalizeMacOSResources', DefaultTask) {\n    doLast {\n        copy {\n            from file(\"$projectDir/logo/Assets.car\")\n            into file(\"$distDir/jpackage/$outputName\")\n        }\n\n        file(\"$distDir/jpackage/$outputName/${jpackageExecutableName}.icns\").renameTo(file(\"$distDir/jpackage/$outputName/xpipe.icns\"))\n    }\n}\n\nif (os.isMacOsX()) {\n    jpackageImage.finalizedBy(finalizeMacOSResources)\n    jpackageImage.dependsOn(prepareMacOSResources)\n}\n\ntasks.jpackage.finalizedBy(copyBundledExtensions)\n"
  },
  {
    "path": "dist/licenses/antlr.license",
    "content": "Copyright (c) 2012-2022 The ANTLR Project. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright\nnotice, this list of conditions and the following disclaimer in the\ndocumentation and/or other materials provided with the distribution.\n\n3. Neither name of copyright holders nor the names of its contributors\nmay be used to endorse or promote products derived from this software\nwithout specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "dist/licenses/antlr.properties",
    "content": "name=antlr4\nversion=3.3\nlicense=BSD 3-Clause\nlink=https://github.com/antlr/antlr4"
  },
  {
    "path": "dist/licenses/atlantafx.license",
    "content": "MIT License\n\nCopyright (c) [2022] [mkpaz]\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "dist/licenses/atlantafx.properties",
    "content": "name=AtlantaFX\nversion=2.0.1\nlicense=MIT License\nlink=https://github.com/mkpaz/atlantafx"
  },
  {
    "path": "dist/licenses/bc-java.license",
    "content": "Copyright (c) 2000-2024 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org). Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sub license, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "dist/licenses/bc-java.properties",
    "content": "name=Bouncy Castle Crypto Package For Java\nversion=1.81\nlicense=MIT License\nlink=https://github.com/bcgit/bc-java"
  },
  {
    "path": "dist/licenses/commons-io.license",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "dist/licenses/commons-io.properties",
    "content": "name=Commons IO\nversion=2.21.0\nlicense=Apache License 2.0\nlink=https://commons.apache.org/proper/commons-io/"
  },
  {
    "path": "dist/licenses/commons-lang.license",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "dist/licenses/commons-lang.properties",
    "content": "name=Commons Lang\nversion=3.20.0\nlicense=Apache License 2.0\nlink=https://commons.apache.org/proper/commons-lang/"
  },
  {
    "path": "dist/licenses/flexmark.license",
    "content": "Copyright (c) 2015-2016, Atlassian Pty Ltd\nAll rights reserved.\n\nCopyright (c) 2016-2018, Vladimir Schneider,\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "dist/licenses/flexmark.properties",
    "content": "name=flexmark\nversion=0.64.0\nlicense=BSD 2-Clause\nlink=https://github.com/vsch/flexmark-java"
  },
  {
    "path": "dist/licenses/fx-builders.license",
    "content": "MIT License\n\nCopyright (c) 2025 John Hendrikx\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE."
  },
  {
    "path": "dist/licenses/fx-builders.properties",
    "content": "name=fx-builders\nversion=0.4\nlicense=MIT License\nlink=https://github.com/int4-org/FX"
  },
  {
    "path": "dist/licenses/github-markdown-css.license",
    "content": "MIT License\n\nCopyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "dist/licenses/github-markdown-css.properties",
    "content": "name=github-markdown-css\nversion=5.1.0\nlicense=MIT License\nlink=https://github.com/sindresorhus/github-markdown-css"
  },
  {
    "path": "dist/licenses/graalvm.license",
    "content": "The GNU General Public License (GPL)\n\nVersion 2, June 1991\n\nCopyright (C) 1989, 1991 Free Software Foundation, Inc.\n51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA\n\nEveryone is permitted to copy and distribute verbatim copies of this license\ndocument, but changing it is not allowed.\n\nPreamble\n\nThe licenses for most software are designed to take away your freedom to share\nand change it.  By contrast, the GNU General Public License is intended to\nguarantee your freedom to share and change free software--to make sure the\nsoftware is free for all its users.  This General Public License applies to\nmost of the Free Software Foundation's software and to any other program whose\nauthors commit to using it.  (Some other Free Software Foundation software is\ncovered by the GNU Library General Public License instead.) You can apply it to\nyour programs, too.\n\nWhen we speak of free software, we are referring to freedom, not price.  Our\nGeneral Public Licenses are designed to make sure that you have the freedom to\ndistribute copies of free software (and charge for this service if you wish),\nthat you receive source code or can get it if you want it, that you can change\nthe software or use pieces of it in new free programs; and that you know you\ncan do these things.\n\nTo protect your rights, we need to make restrictions that forbid anyone to deny\nyou these rights or to ask you to surrender the rights.  These restrictions\ntranslate to certain responsibilities for you if you distribute copies of the\nsoftware, or if you modify it.\n\nFor example, if you distribute copies of such a program, whether gratis or for\na fee, you must give the recipients all the rights that you have.  You must\nmake sure that they, too, receive or can get the source code.  And you must\nshow them these terms so they know their rights.\n\nWe protect your rights with two steps: (1) copyright the software, and (2)\noffer you this license which gives you legal permission to copy, distribute\nand/or modify the software.\n\nAlso, for each author's protection and ours, we want to make certain that\neveryone understands that there is no warranty for this free software.  If the\nsoftware is modified by someone else and passed on, we want its recipients to\nknow that what they have is not the original, so that any problems introduced\nby others will not reflect on the original authors' reputations.\n\nFinally, any free program is threatened constantly by software patents.  We\nwish to avoid the danger that redistributors of a free program will\nindividually obtain patent licenses, in effect making the program proprietary.\nTo prevent this, we have made it clear that any patent must be licensed for\neveryone's free use or not licensed at all.\n\nThe precise terms and conditions for copying, distribution and modification\nfollow.\n\nTERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n0. This License applies to any program or other work which contains a notice\nplaced by the copyright holder saying it may be distributed under the terms of\nthis General Public License.  The \"Program\", below, refers to any such program\nor work, and a \"work based on the Program\" means either the Program or any\nderivative work under copyright law: that is to say, a work containing the\nProgram or a portion of it, either verbatim or with modifications and/or\ntranslated into another language.  (Hereinafter, translation is included\nwithout limitation in the term \"modification\".) Each licensee is addressed as\n\"you\".\n\nActivities other than copying, distribution and modification are not covered by\nthis License; they are outside its scope.  The act of running the Program is\nnot restricted, and the output from the Program is covered only if its contents\nconstitute a work based on the Program (independent of having been made by\nrunning the Program).  Whether that is true depends on what the Program does.\n\n1. You may copy and distribute verbatim copies of the Program's source code as\nyou receive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice and\ndisclaimer of warranty; keep intact all the notices that refer to this License\nand to the absence of any warranty; and give any other recipients of the\nProgram a copy of this License along with the Program.\n\nYou may charge a fee for the physical act of transferring a copy, and you may\nat your option offer warranty protection in exchange for a fee.\n\n2. You may modify your copy or copies of the Program or any portion of it, thus\nforming a work based on the Program, and copy and distribute such modifications\nor work under the terms of Section 1 above, provided that you also meet all of\nthese conditions:\n\n    a) You must cause the modified files to carry prominent notices stating\n    that you changed the files and the date of any change.\n\n    b) You must cause any work that you distribute or publish, that in whole or\n    in part contains or is derived from the Program or any part thereof, to be\n    licensed as a whole at no charge to all third parties under the terms of\n    this License.\n\n    c) If the modified program normally reads commands interactively when run,\n    you must cause it, when started running for such interactive use in the\n    most ordinary way, to print or display an announcement including an\n    appropriate copyright notice and a notice that there is no warranty (or\n    else, saying that you provide a warranty) and that users may redistribute\n    the program under these conditions, and telling the user how to view a copy\n    of this License.  (Exception: if the Program itself is interactive but does\n    not normally print such an announcement, your work based on the Program is\n    not required to print an announcement.)\n\nThese requirements apply to the modified work as a whole.  If identifiable\nsections of that work are not derived from the Program, and can be reasonably\nconsidered independent and separate works in themselves, then this License, and\nits terms, do not apply to those sections when you distribute them as separate\nworks.  But when you distribute the same sections as part of a whole which is a\nwork based on the Program, the distribution of the whole must be on the terms\nof this License, whose permissions for other licensees extend to the entire\nwhole, and thus to each and every part regardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest your\nrights to work written entirely by you; rather, the intent is to exercise the\nright to control the distribution of derivative or collective works based on\nthe Program.\n\nIn addition, mere aggregation of another work not based on the Program with the\nProgram (or with a work based on the Program) on a volume of a storage or\ndistribution medium does not bring the other work under the scope of this\nLicense.\n\n3. You may copy and distribute the Program (or a work based on it, under\nSection 2) in object code or executable form under the terms of Sections 1 and\n2 above provided that you also do one of the following:\n\n    a) Accompany it with the complete corresponding machine-readable source\n    code, which must be distributed under the terms of Sections 1 and 2 above\n    on a medium customarily used for software interchange; or,\n\n    b) Accompany it with a written offer, valid for at least three years, to\n    give any third party, for a charge no more than your cost of physically\n    performing source distribution, a complete machine-readable copy of the\n    corresponding source code, to be distributed under the terms of Sections 1\n    and 2 above on a medium customarily used for software interchange; or,\n\n    c) Accompany it with the information you received as to the offer to\n    distribute corresponding source code.  (This alternative is allowed only\n    for noncommercial distribution and only if you received the program in\n    object code or executable form with such an offer, in accord with\n    Subsection b above.)\n\nThe source code for a work means the preferred form of the work for making\nmodifications to it.  For an executable work, complete source code means all\nthe source code for all modules it contains, plus any associated interface\ndefinition files, plus the scripts used to control compilation and installation\nof the executable.  However, as a special exception, the source code\ndistributed need not include anything that is normally distributed (in either\nsource or binary form) with the major components (compiler, kernel, and so on)\nof the operating system on which the executable runs, unless that component\nitself accompanies the executable.\n\nIf distribution of executable or object code is made by offering access to copy\nfrom a designated place, then offering equivalent access to copy the source\ncode from the same place counts as distribution of the source code, even though\nthird parties are not compelled to copy the source along with the object code.\n\n4. You may not copy, modify, sublicense, or distribute the Program except as\nexpressly provided under this License.  Any attempt otherwise to copy, modify,\nsublicense or distribute the Program is void, and will automatically terminate\nyour rights under this License.  However, parties who have received copies, or\nrights, from you under this License will not have their licenses terminated so\nlong as such parties remain in full compliance.\n\n5. You are not required to accept this License, since you have not signed it.\nHowever, nothing else grants you permission to modify or distribute the Program\nor its derivative works.  These actions are prohibited by law if you do not\naccept this License.  Therefore, by modifying or distributing the Program (or\nany work based on the Program), you indicate your acceptance of this License to\ndo so, and all its terms and conditions for copying, distributing or modifying\nthe Program or works based on it.\n\n6. Each time you redistribute the Program (or any work based on the Program),\nthe recipient automatically receives a license from the original licensor to\ncopy, distribute or modify the Program subject to these terms and conditions.\nYou may not impose any further restrictions on the recipients' exercise of the\nrights granted herein.  You are not responsible for enforcing compliance by\nthird parties to this License.\n\n7. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues), conditions\nare imposed on you (whether by court order, agreement or otherwise) that\ncontradict the conditions of this License, they do not excuse you from the\nconditions of this License.  If you cannot distribute so as to satisfy\nsimultaneously your obligations under this License and any other pertinent\nobligations, then as a consequence you may not distribute the Program at all.\nFor example, if a patent license would not permit royalty-free redistribution\nof the Program by all those who receive copies directly or indirectly through\nyou, then the only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Program.\n\nIf any portion of this section is held invalid or unenforceable under any\nparticular circumstance, the balance of the section is intended to apply and\nthe section as a whole is intended to apply in other circumstances.\n\nIt is not the purpose of this section to induce you to infringe any patents or\nother property right claims or to contest validity of any such claims; this\nsection has the sole purpose of protecting the integrity of the free software\ndistribution system, which is implemented by public license practices.  Many\npeople have made generous contributions to the wide range of software\ndistributed through that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing to\ndistribute software through any other system and a licensee cannot impose that\nchoiceElement.\n\nThis section is intended to make thoroughly clear what is believed to be a\nconsequence of the rest of this License.\n\n8. If the distribution and/or use of the Program is restricted in certain\ncountries either by patents or by copyrighted interfaces, the original\ncopyright holder who places the Program under this License may add an explicit\ngeographical distribution limitation excluding those countries, so that\ndistribution is permitted only in or among countries not thus excluded.  In\nsuch case, this License incorporates the limitation as if written in the body\nof this License.\n\n9. The Free Software Foundation may publish revised and/or new versions of the\nGeneral Public License from time to time.  Such new versions will be similar in\nspirit to the present version, but may differ in detail to address new problems\nor concerns.\n\nEach version is given a distinguishing version number.  If the Program\nspecifies a version number of this License which applies to it and \"any later\nversion\", you have the option of following the terms and conditions either of\nthat version or of any later version published by the Free Software Foundation.\nIf the Program does not specify a version number of this License, you may\nchoose any version ever published by the Free Software Foundation.\n\n10. If you wish to incorporate parts of the Program into other free programs\nwhose distribution conditions are different, write to the author to ask for\npermission.  For software which is copyrighted by the Free Software Foundation,\nwrite to the Free Software Foundation; we sometimes make exceptions for this.\nOur decision will be guided by the two goals of preserving the free status of\nall derivatives of our free software and of promoting the sharing and reuse of\nsoftware generally.\n\nNO WARRANTY\n\n11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR\nTHE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN OTHERWISE\nSTATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE\nPROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED,\nINCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND\nFITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND\nPERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE,\nYOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL\nANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE\nPROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR\nINABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA\nBEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A\nFAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER\nOR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\nEND OF TERMS AND CONDITIONS\n\nHow to Apply These Terms to Your New Programs\n\nIf you develop a new program, and you want it to be of the greatest possible\nuse to the public, the best way to achieve this is to make it free software\nwhich everyone can redistribute and change under these terms.\n\nTo do so, attach the following notices to the program.  It is safest to attach\nthem to the start of each source file to most effectively convey the exclusion\nof warranty; and each file should have at least the \"copyright\" line and a\npointer 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\n    Copyright (C) <year> <name of author>\n\n    This program is free software; you can redistribute it and/or modify it\n    under the terms of the GNU General Public License as published by the Free\n    Software Foundation; either version 2 of the License, or (at your option)\n    any later version.\n\n    This program is distributed in the hope that it will be useful, but WITHOUT\n    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for\n    more details.\n\n    You should have received a copy of the GNU General Public License along\n    with this program; if not, write to the Free Software Foundation, Inc.,\n    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program is interactive, make it output a short notice like this when it\nstarts in an interactive mode:\n\n    Gnomovision version 69, Copyright (C) year name of author Gnomovision comes\n    with ABSOLUTELY NO WARRANTY; for details type 'show w'.  This is free\n    software, and you are welcome to redistribute it under certain conditions;\n    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, the commands you use may be\ncalled something other than 'show w' and 'show c'; they could even be\nmouse-clicks or menu items--whatever suits your program.\n\nYou should also get your employer (if you work as a programmer) or your school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.  Here\nis a sample; alter the names:\n\n    Yoyodyne, Inc., hereby disclaims all copyright interest in the program\n    'Gnomovision' (which makes passes at compilers) written by James Hacker.\n\n    signature of Ty Coon, 1 April 1989\n\n    Ty Coon, President of Vice\n\nThis General Public License does not permit incorporating your program into\nproprietary programs.  If your program is a subroutine library, you may\nconsider it more useful to permit linking proprietary applications with the\nlibrary.  If this is what you want to do, use the GNU Library General Public\nLicense instead of this License.\n\n\n\"CLASSPATH\" EXCEPTION TO THE GPL\n\nCertain source files distributed by Oracle America and/or its affiliates are\nsubject to the following clarification and special exception to the GPL, but\nonly where Oracle has expressly included in the particular source file's header\nthe words \"Oracle designates this particular file as subject to the \"Classpath\"\nexception as provided by Oracle in the LICENSE file that accompanied this code.\"\n\n    Linking this library statically or dynamically with other modules is making\n    a combined work based on this library.  Thus, the terms and conditions of\n    the GNU General Public License cover the whole combination.\n\n    As a special exception, the copyright holders of this library give you\n    permission to link this library with independent modules to produce an\n    executable, regardless of the license terms of these independent modules,\n    and to copy and distribute the resulting executable under terms of your\n    choiceElement, provided that you also meet, for each linked independent module,\n    the terms and conditions of the license of that module.  An independent\n    module is a module which is not derived from or based on this library.  If\n    you modify this library, you may extend this exception to your version of\n    the library, but you are not obligated to do so.  If you do not wish to do\n    so, delete this exception statement from your version.\n"
  },
  {
    "path": "dist/licenses/graalvm.properties",
    "content": "name=GraalVM Community\nversion=25.0.2\nlicense=GPL2 with the Classpath Exception\nlink=https://www.graalvm.org/"
  },
  {
    "path": "dist/licenses/ikonli.license",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "dist/licenses/ikonli.properties",
    "content": "name=Ikonli\nversion=12.2.0\nlicense=Apache License 2.0\nlink=https://github.com/kordamp/ikonli"
  },
  {
    "path": "dist/licenses/inter.license",
    "content": "Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttp://scripts.sil.org/OFL\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded,\nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION AND CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "dist/licenses/inter.properties",
    "content": "name=The Inter font family\nversion=4.2\nlicense=SIL Open Font License 1.1\nlink=https://github.com/rsms/inter"
  },
  {
    "path": "dist/licenses/jackson.license",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "dist/licenses/jackson.properties",
    "content": "name=Jackson Databind\nversion=2.21.0\nlicense=Apache License 2.0\nlink=https://github.com/FasterXML/jackson-databind"
  },
  {
    "path": "dist/licenses/java-annotations.license",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "dist/licenses/java-annotations.properties",
    "content": "name=JetBrains Annotations for JVM-based languages\nversion=24.0.1\nlicense=Apache License 2.0\nlink=https://github.com/JetBrains/java-annotations"
  },
  {
    "path": "dist/licenses/jna.license",
    "content": "SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1\n\nJava Native Access (JNA) is licensed under the LGPL, version 2.1\nor later, or (from version 4.0 onward) the Apache License,\nversion 2.0.\n\nYou can freely decide which license you want to apply to the project.\n\nYou may obtain a copy of the LGPL License at:\n\nhttp://www.gnu.org/licenses/licenses.html\n\nA copy is also included in the downloadable source code package\ncontaining JNA, in file \"LGPL2.1\", under the same directory\nas this file.\n\nYou may obtain a copy of the Apache License at:\n\nhttp://www.apache.org/licenses/\n\nA copy is also included in the downloadable source code package\ncontaining JNA, in file \"AL2.0\", under the same directory\nas this file.\n\nCommercial support may be available, please e-mail\ntwall[at]users[dot]sf[dot]net."
  },
  {
    "path": "dist/licenses/jna.properties",
    "content": "name=jna\nversion=5.17.0\nlicense=Apache License 2.0\nlink=https://github.com/java-native-access/jna/"
  },
  {
    "path": "dist/licenses/json-schema-validator.license",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "dist/licenses/json-schema-validator.properties",
    "content": "name=json-schema-validator\nversion=1.5.8\nlicense=Apache License 2.0\nlink=https://github.com/networknt/json-schema-validator"
  },
  {
    "path": "dist/licenses/jsvg.license",
    "content": "MIT License\n\nCopyright (c) 2021-2024 Jannis Weis\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "dist/licenses/jsvg.properties",
    "content": "name=JSVG\nversion=2.0.0\nlicense=MIT License\nlink=https://github.com/weisJ/jsvg"
  },
  {
    "path": "dist/licenses/lombok.license",
    "content": "Copyright (C) 2009-2021 The Project Lombok Authors.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\n==============================================================================\nLicenses for included components:\n\norg.ow2.asm:asm\norg.ow2.asm:asm-analysis\norg.ow2.asm:asm-commons\norg.ow2.asm:asm-tree\norg.ow2.asm:asm-util\nASM: a very small and fast Java bytecode manipulation framework\n Copyright (c) 2000-2011 INRIA, France Telecom\n All rights reserved.\n\n Redistribution and use in source and binary forms, with or without\n modification, are permitted provided that the following conditions\n are met:\n 1. Redistributions of source code must retain the above copyright\n    notice, this list of conditions and the following disclaimer.\n 2. Redistributions in binary form must reproduce the above copyright\n    notice, this list of conditions and the following disclaimer in the\n    documentation and/or other materials provided with the distribution.\n 3. Neither the name of the copyright holders nor the names of its\n    contributors may be used to endorse or promote products derived from\n    this software without specific prior written permission.\n\n THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF\n THE POSSIBILITY OF SUCH DAMAGE.\n\n------------------------------------------------------------------------------\nrzwitserloot/com.zwitserloot.cmdreader\n\n Copyright © 2010 Reinier Zwitserloot.\n\n Permission is hereby granted, free of charge, to any person obtaining a copy\n of this software and associated documentation files (the \"Software\"), to deal\n in the Software without restriction, including without limitation the rights\n to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the Software is\n furnished to do so, subject to the following conditions:\n\n The above copyright notice and this permission notice shall be included in\n all copies or substantial portions of the Software.\n\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n THE SOFTWARE.\n\n------------------------------------------------------------------------------\n\nprojectlombok/lombok.patcher\n\n Copyright (C) 2009-2021 The Project Lombok Authors.\n\n Permission is hereby granted, free of charge, to any person obtaining a copy\n of this software and associated documentation files (the \"Software\"), to deal\n in the Software without restriction, including without limitation the rights\n to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the Software is\n furnished to do so, subject to the following conditions:\n\n The above copyright notice and this permission notice shall be included in\n all copies or substantial portions of the Software.\n\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n THE SOFTWARE.\n\n------------------------------------------------------------------------------"
  },
  {
    "path": "dist/licenses/lombok.properties",
    "content": "name=lombok\nversion=1.18.38\nlicense=MIT License\nlink=https://projectlombok.org/"
  },
  {
    "path": "dist/licenses/material2.license",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "dist/licenses/material2.properties",
    "content": "name=Material2 Icons\nversion=20200820\nlicense=Apache License 2.0\nlink=https://github.com/material-icons/material-icons-font"
  },
  {
    "path": "dist/licenses/materialdesign2.license",
    "content": "Pictogrammers Free License\n--------------------------\n\nThis icon collection is released as free, open source, and GPL friendly by\nthe [Pictogrammers](http://pictogrammers.com/) icon group. You may use it\nfor commercial projects, open source projects, or anything really.\n\n# Icons: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)\nSome of the icons are redistributed under the Apache 2.0 license. All other\nicons are either redistributed under their respective licenses or are\ndistributed under the Apache 2.0 license.\n\n# Fonts: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)\nAll web and desktop fonts are distributed under the Apache 2.0 license. Web\nand desktop fonts contain some icons that are redistributed under the Apache\n2.0 license. All other icons are either redistributed under their respective\nlicenses or are distributed under the Apache 2.0 license.\n\n# Code: MIT (https://opensource.org/licenses/MIT)\nThe MIT license applies to all non-font and non-icon files."
  },
  {
    "path": "dist/licenses/materialdesign2.properties",
    "content": "name=MaterialDesign2\nversion=5.8.55\nlicense=Pictogrammers Free License\nlink=https://github.com/Templarian/MaterialDesign"
  },
  {
    "path": "dist/licenses/mcp-sdk.license",
    "content": "MIT License\n\nCopyright (c) 2025 the original author or authors.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "dist/licenses/mcp-sdk.properties",
    "content": "name=MCP Java SDK\nversion=0.17.2\nlicense=MIT License\nlink=https://github.com/modelcontextprotocol/java-sdk"
  },
  {
    "path": "dist/licenses/musl.license",
    "content": "musl as a whole is licensed under the following standard MIT license:\n\n----------------------------------------------------------------------\nCopyright © 2005-2014 Rich Felker, et al.\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n----------------------------------------------------------------------\n\nAuthors/contributors include:\n\nAnthony G. Basile\nArvid Picciani\nBobby Bingham\nBoris Brezillon\nChris Spiegel\nEmil Renner Berthing\nHiltjo Posthuma\nIsaac Dunham\nJens Gustedt\nJeremy Huntwork\nJohn Spencer\nJustin Cormack\nLuca Barbato\nLuka Perkov\nMichael Forney\nNicholas J. Kain\norc\nPascal Cuoq\nPierre Carrier\nRich Felker\nRichard Pennington\nSolar Designer\nStrake\nSzabolcs Nagy\nTimo Teräs\nValentin Ochs\nWilliam Haddon\n\nPortions of this software are derived from third-party works licensed\nunder terms compatible with the above MIT license:\n\nThe TRE regular expression implementation (src/regex/reg* and\nsrc/regex/tre*) is Copyright © 2001-2008 Ville Laurikari and licensed\nunder a 2-clause BSD license (license text in the source files). The\nincluded version has been heavily modified by Rich Felker in 2012, in\nthe interests of size, simplicity, and namespace cleanliness.\n\nMuch of the math library code (src/math/* and src/complex/*) is\nCopyright © 1993,2004 Sun Microsystems or\nCopyright © 2003-2011 David Schultz or\nCopyright © 2003-2009 Steven G. Kargl or\nCopyright © 2003-2009 Bruce D. Evans or\nCopyright © 2008 Stephen L. Moshier\nand labelled as such in comments in the individual source files. All\nhave been licensed under extremely permissive terms.\n\nThe ARM memcpy code (src/string/armel/memcpy.s) is Copyright © 2008\nThe Android Open Source Project and is licensed under a two-clause BSD\nlicense. It was taken from Bionic libc, used on Android.\n\nThe implementation of DES for crypt (src/misc/crypt_des.c) is\nCopyright © 1994 David Burren. It is licensed under a BSD license.\n\nThe implementation of blowfish crypt (src/misc/crypt_blowfish.c) was\noriginally written by Solar Designer and placed into the public\ndomain. The code also comes with a fallback permissive license for use\nin jurisdictions that may not recognize the public domain.\n\nThe smoothsort implementation (src/stdlib/qsort.c) is Copyright © 2011\nValentin Ochs and is licensed under an MIT-style license.\n\nThe BSD PRNG implementation (src/prng/random.c) and XSI search API\n(src/search/*.c) functions are Copyright © 2011 Szabolcs Nagy and\nlicensed under following terms: \"Permission to use, copy, modify,\nand/or distribute this code for any purpose with or without fee is\nhereby granted. There is no warranty.\"\n\nThe x86_64 port was written by Nicholas J. Kain. Several files (crt)\nwere released into the public domain; others are licensed under the\nstandard MIT license terms at the top of this file. See individual\nfiles for their copyright status.\n\nThe mips and microblaze ports were originally written by Richard\nPennington for use in the ellcc project. The original code was adapted\nby Rich Felker for build system and code conventions during upstream\nintegration. It is licensed under the standard MIT terms.\n\nThe powerpc port was also originally written by Richard Pennington,\nand later supplemented and integrated by John Spencer. It is licensed\nunder the standard MIT terms.\n\nAll other files which have no copyright comments are original works\nproduced specifically for use as part of this library, written either\nby Rich Felker, the main author of the library, or by one or more\ncontibutors listed above. Details on authorship of individual files\ncan be found in the git version control history of the project. The\nomission of copyright and license comments in each file is in the\ninterest of source tree size.\n\nAll public header files (include/* and arch/*/bits/*) should be\ntreated as Public Domain as they intentionally contain no content\nwhich can be covered by copyright. Some source modules may fall in\nthis category as well. If you believe that a file is so trivial that\nit should be in the Public Domain, please contact the authors and\nrequest an explicit statement releasing it from copyright.\n\nThe following files are trivial, believed not to be copyrightable in\nthe first place, and hereby explicitly released to the Public Domain:\n\nAll public headers: include/*, arch/*/bits/*\nStartup files: crt/*\n"
  },
  {
    "path": "dist/licenses/musl.properties",
    "content": "name=musl-libc\nversion=10\nlicense=MIT License\nlink=https://github.com/runtimejs/musl-libc"
  },
  {
    "path": "dist/licenses/openjfx.license",
    "content": "The GNU General Public License (GPL)\n\nVersion 2, June 1991\n\nCopyright (C) 1989, 1991 Free Software Foundation, Inc.\n51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA\n\nEveryone is permitted to copy and distribute verbatim copies of this license\ndocument, but changing it is not allowed.\n\nPreamble\n\nThe licenses for most software are designed to take away your freedom to share\nand change it.  By contrast, the GNU General Public License is intended to\nguarantee your freedom to share and change free software--to make sure the\nsoftware is free for all its users.  This General Public License applies to\nmost of the Free Software Foundation's software and to any other program whose\nauthors commit to using it.  (Some other Free Software Foundation software is\ncovered by the GNU Library General Public License instead.) You can apply it to\nyour programs, too.\n\nWhen we speak of free software, we are referring to freedom, not price.  Our\nGeneral Public Licenses are designed to make sure that you have the freedom to\ndistribute copies of free software (and charge for this service if you wish),\nthat you receive source code or can get it if you want it, that you can change\nthe software or use pieces of it in new free programs; and that you know you\ncan do these things.\n\nTo protect your rights, we need to make restrictions that forbid anyone to deny\nyou these rights or to ask you to surrender the rights.  These restrictions\ntranslate to certain responsibilities for you if you distribute copies of the\nsoftware, or if you modify it.\n\nFor example, if you distribute copies of such a program, whether gratis or for\na fee, you must give the recipients all the rights that you have.  You must\nmake sure that they, too, receive or can get the source code.  And you must\nshow them these terms so they know their rights.\n\nWe protect your rights with two steps: (1) copyright the software, and (2)\noffer you this license which gives you legal permission to copy, distribute\nand/or modify the software.\n\nAlso, for each author's protection and ours, we want to make certain that\neveryone understands that there is no warranty for this free software.  If the\nsoftware is modified by someone else and passed on, we want its recipients to\nknow that what they have is not the original, so that any problems introduced\nby others will not reflect on the original authors' reputations.\n\nFinally, any free program is threatened constantly by software patents.  We\nwish to avoid the danger that redistributors of a free program will\nindividually obtain patent licenses, in effect making the program proprietary.\nTo prevent this, we have made it clear that any patent must be licensed for\neveryone's free use or not licensed at all.\n\nThe precise terms and conditions for copying, distribution and modification\nfollow.\n\nTERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n0. This License applies to any program or other work which contains a notice\nplaced by the copyright holder saying it may be distributed under the terms of\nthis General Public License.  The \"Program\", below, refers to any such program\nor work, and a \"work based on the Program\" means either the Program or any\nderivative work under copyright law: that is to say, a work containing the\nProgram or a portion of it, either verbatim or with modifications and/or\ntranslated into another language.  (Hereinafter, translation is included\nwithout limitation in the term \"modification\".) Each licensee is addressed as\n\"you\".\n\nActivities other than copying, distribution and modification are not covered by\nthis License; they are outside its scope.  The act of running the Program is\nnot restricted, and the output from the Program is covered only if its contents\nconstitute a work based on the Program (independent of having been made by\nrunning the Program).  Whether that is true depends on what the Program does.\n\n1. You may copy and distribute verbatim copies of the Program's source code as\nyou receive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice and\ndisclaimer of warranty; keep intact all the notices that refer to this License\nand to the absence of any warranty; and give any other recipients of the\nProgram a copy of this License along with the Program.\n\nYou may charge a fee for the physical act of transferring a copy, and you may\nat your option offer warranty protection in exchange for a fee.\n\n2. You may modify your copy or copies of the Program or any portion of it, thus\nforming a work based on the Program, and copy and distribute such modifications\nor work under the terms of Section 1 above, provided that you also meet all of\nthese conditions:\n\n    a) You must cause the modified files to carry prominent notices stating\n    that you changed the files and the date of any change.\n\n    b) You must cause any work that you distribute or publish, that in whole or\n    in part contains or is derived from the Program or any part thereof, to be\n    licensed as a whole at no charge to all third parties under the terms of\n    this License.\n\n    c) If the modified program normally reads commands interactively when run,\n    you must cause it, when started running for such interactive use in the\n    most ordinary way, to print or display an announcement including an\n    appropriate copyright notice and a notice that there is no warranty (or\n    else, saying that you provide a warranty) and that users may redistribute\n    the program under these conditions, and telling the user how to view a copy\n    of this License.  (Exception: if the Program itself is interactive but does\n    not normally print such an announcement, your work based on the Program is\n    not required to print an announcement.)\n\nThese requirements apply to the modified work as a whole.  If identifiable\nsections of that work are not derived from the Program, and can be reasonably\nconsidered independent and separate works in themselves, then this License, and\nits terms, do not apply to those sections when you distribute them as separate\nworks.  But when you distribute the same sections as part of a whole which is a\nwork based on the Program, the distribution of the whole must be on the terms\nof this License, whose permissions for other licensees extend to the entire\nwhole, and thus to each and every part regardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest your\nrights to work written entirely by you; rather, the intent is to exercise the\nright to control the distribution of derivative or collective works based on\nthe Program.\n\nIn addition, mere aggregation of another work not based on the Program with the\nProgram (or with a work based on the Program) on a volume of a storage or\ndistribution medium does not bring the other work under the scope of this\nLicense.\n\n3. You may copy and distribute the Program (or a work based on it, under\nSection 2) in object code or executable form under the terms of Sections 1 and\n2 above provided that you also do one of the following:\n\n    a) Accompany it with the complete corresponding machine-readable source\n    code, which must be distributed under the terms of Sections 1 and 2 above\n    on a medium customarily used for software interchange; or,\n\n    b) Accompany it with a written offer, valid for at least three years, to\n    give any third party, for a charge no more than your cost of physically\n    performing source distribution, a complete machine-readable copy of the\n    corresponding source code, to be distributed under the terms of Sections 1\n    and 2 above on a medium customarily used for software interchange; or,\n\n    c) Accompany it with the information you received as to the offer to\n    distribute corresponding source code.  (This alternative is allowed only\n    for noncommercial distribution and only if you received the program in\n    object code or executable form with such an offer, in accord with\n    Subsection b above.)\n\nThe source code for a work means the preferred form of the work for making\nmodifications to it.  For an executable work, complete source code means all\nthe source code for all modules it contains, plus any associated interface\ndefinition files, plus the scripts used to control compilation and installation\nof the executable.  However, as a special exception, the source code\ndistributed need not include anything that is normally distributed (in either\nsource or binary form) with the major components (compiler, kernel, and so on)\nof the operating system on which the executable runs, unless that component\nitself accompanies the executable.\n\nIf distribution of executable or object code is made by offering access to copy\nfrom a designated place, then offering equivalent access to copy the source\ncode from the same place counts as distribution of the source code, even though\nthird parties are not compelled to copy the source along with the object code.\n\n4. You may not copy, modify, sublicense, or distribute the Program except as\nexpressly provided under this License.  Any attempt otherwise to copy, modify,\nsublicense or distribute the Program is void, and will automatically terminate\nyour rights under this License.  However, parties who have received copies, or\nrights, from you under this License will not have their licenses terminated so\nlong as such parties remain in full compliance.\n\n5. You are not required to accept this License, since you have not signed it.\nHowever, nothing else grants you permission to modify or distribute the Program\nor its derivative works.  These actions are prohibited by law if you do not\naccept this License.  Therefore, by modifying or distributing the Program (or\nany work based on the Program), you indicate your acceptance of this License to\ndo so, and all its terms and conditions for copying, distributing or modifying\nthe Program or works based on it.\n\n6. Each time you redistribute the Program (or any work based on the Program),\nthe recipient automatically receives a license from the original licensor to\ncopy, distribute or modify the Program subject to these terms and conditions.\nYou may not impose any further restrictions on the recipients' exercise of the\nrights granted herein.  You are not responsible for enforcing compliance by\nthird parties to this License.\n\n7. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues), conditions\nare imposed on you (whether by court order, agreement or otherwise) that\ncontradict the conditions of this License, they do not excuse you from the\nconditions of this License.  If you cannot distribute so as to satisfy\nsimultaneously your obligations under this License and any other pertinent\nobligations, then as a consequence you may not distribute the Program at all.\nFor example, if a patent license would not permit royalty-free redistribution\nof the Program by all those who receive copies directly or indirectly through\nyou, then the only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Program.\n\nIf any portion of this section is held invalid or unenforceable under any\nparticular circumstance, the balance of the section is intended to apply and\nthe section as a whole is intended to apply in other circumstances.\n\nIt is not the purpose of this section to induce you to infringe any patents or\nother property right claims or to contest validity of any such claims; this\nsection has the sole purpose of protecting the integrity of the free software\ndistribution system, which is implemented by public license practices.  Many\npeople have made generous contributions to the wide range of software\ndistributed through that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing to\ndistribute software through any other system and a licensee cannot impose that\nchoiceElement.\n\nThis section is intended to make thoroughly clear what is believed to be a\nconsequence of the rest of this License.\n\n8. If the distribution and/or use of the Program is restricted in certain\ncountries either by patents or by copyrighted interfaces, the original\ncopyright holder who places the Program under this License may add an explicit\ngeographical distribution limitation excluding those countries, so that\ndistribution is permitted only in or among countries not thus excluded.  In\nsuch case, this License incorporates the limitation as if written in the body\nof this License.\n\n9. The Free Software Foundation may publish revised and/or new versions of the\nGeneral Public License from time to time.  Such new versions will be similar in\nspirit to the present version, but may differ in detail to address new problems\nor concerns.\n\nEach version is given a distinguishing version number.  If the Program\nspecifies a version number of this License which applies to it and \"any later\nversion\", you have the option of following the terms and conditions either of\nthat version or of any later version published by the Free Software Foundation.\nIf the Program does not specify a version number of this License, you may\nchoose any version ever published by the Free Software Foundation.\n\n10. If you wish to incorporate parts of the Program into other free programs\nwhose distribution conditions are different, write to the author to ask for\npermission.  For software which is copyrighted by the Free Software Foundation,\nwrite to the Free Software Foundation; we sometimes make exceptions for this.\nOur decision will be guided by the two goals of preserving the free status of\nall derivatives of our free software and of promoting the sharing and reuse of\nsoftware generally.\n\nNO WARRANTY\n\n11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR\nTHE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN OTHERWISE\nSTATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE\nPROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED,\nINCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND\nFITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND\nPERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE,\nYOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL\nANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE\nPROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR\nINABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA\nBEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A\nFAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER\nOR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\nEND OF TERMS AND CONDITIONS\n\nHow to Apply These Terms to Your New Programs\n\nIf you develop a new program, and you want it to be of the greatest possible\nuse to the public, the best way to achieve this is to make it free software\nwhich everyone can redistribute and change under these terms.\n\nTo do so, attach the following notices to the program.  It is safest to attach\nthem to the start of each source file to most effectively convey the exclusion\nof warranty; and each file should have at least the \"copyright\" line and a\npointer 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\n    Copyright (C) <year> <name of author>\n\n    This program is free software; you can redistribute it and/or modify it\n    under the terms of the GNU General Public License as published by the Free\n    Software Foundation; either version 2 of the License, or (at your option)\n    any later version.\n\n    This program is distributed in the hope that it will be useful, but WITHOUT\n    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for\n    more details.\n\n    You should have received a copy of the GNU General Public License along\n    with this program; if not, write to the Free Software Foundation, Inc.,\n    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program is interactive, make it output a short notice like this when it\nstarts in an interactive mode:\n\n    Gnomovision version 69, Copyright (C) year name of author Gnomovision comes\n    with ABSOLUTELY NO WARRANTY; for details type 'show w'.  This is free\n    software, and you are welcome to redistribute it under certain conditions;\n    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, the commands you use may be\ncalled something other than 'show w' and 'show c'; they could even be\nmouse-clicks or menu items--whatever suits your program.\n\nYou should also get your employer (if you work as a programmer) or your school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.  Here\nis a sample; alter the names:\n\n    Yoyodyne, Inc., hereby disclaims all copyright interest in the program\n    'Gnomovision' (which makes passes at compilers) written by James Hacker.\n\n    signature of Ty Coon, 1 April 1989\n\n    Ty Coon, President of Vice\n\nThis General Public License does not permit incorporating your program into\nproprietary programs.  If your program is a subroutine library, you may\nconsider it more useful to permit linking proprietary applications with the\nlibrary.  If this is what you want to do, use the GNU Library General Public\nLicense instead of this License.\n\n\n\"CLASSPATH\" EXCEPTION TO THE GPL\n\nCertain source files distributed by Oracle America and/or its affiliates are\nsubject to the following clarification and special exception to the GPL, but\nonly where Oracle has expressly included in the particular source file's header\nthe words \"Oracle designates this particular file as subject to the \"Classpath\"\nexception as provided by Oracle in the LICENSE file that accompanied this code.\"\n\n    Linking this library statically or dynamically with other modules is making\n    a combined work based on this library.  Thus, the terms and conditions of\n    the GNU General Public License cover the whole combination.\n\n    As a special exception, the copyright holders of this library give you\n    permission to link this library with independent modules to produce an\n    executable, regardless of the license terms of these independent modules,\n    and to copy and distribute the resulting executable under terms of your\n    choiceElement, provided that you also meet, for each linked independent module,\n    the terms and conditions of the license of that module.  An independent\n    module is a module which is not derived from or based on this library.  If\n    you modify this library, you may extend this exception to your version of\n    the library, but you are not obligated to do so.  If you do not wish to do\n    so, delete this exception statement from your version.\n"
  },
  {
    "path": "dist/licenses/openjfx.properties",
    "content": "name=OpenJFX\nversion=26+ea-19\nlicense=GPL2 with the Classpath Exception\nlink=https://github.com/openjdk/jfx"
  },
  {
    "path": "dist/licenses/picocli.license",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"{}\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright {yyyy} {name of copyright owner}\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "dist/licenses/picocli.properties",
    "content": "name=picocli\nversion=4.7.7\nlicense=Apache 2.0 License\nlink=https://picocli.info/"
  },
  {
    "path": "dist/licenses/reactive-streams.license",
    "content": "MIT No Attribution\n\nCopyright 2014 Reactive Streams\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "dist/licenses/reactive-streams.properties",
    "content": "name=Reactive Streams\nversion=1.0.4\nlicense=MIT License\nlink=https://github.com/reactive-streams/reactive-streams-jvm"
  },
  {
    "path": "dist/licenses/reactor-core.license",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        https://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"{}\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright {yyyy} {name of copyright owner}\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       https://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n"
  },
  {
    "path": "dist/licenses/reactor-core.properties",
    "content": "name=Reactor Core\nversion=3.7.9\nlicense=Apache License 2.0\nlink=https://github.com/reactor/reactor-core"
  },
  {
    "path": "dist/licenses/roboto.license",
    "content": "Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded,\nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "dist/licenses/roboto.properties",
    "content": "name=The Roboto font family\nversion=3\nlicense=SIL Open Font License 1.1\nlink=https://github.com/googlefonts/roboto-3-classic"
  },
  {
    "path": "dist/licenses/sentry.license",
    "content": "MIT License\n\nCopyright (c) 2019 Sentry\nCopyright (c) 2015 Salomon BRYS for Android ANRWatchDog\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "dist/licenses/sentry.properties",
    "content": "name=Sentry Java\nversion=8.20.0\nlicense=MIT License\nlink=https://github.com/getsentry/sentry-java"
  },
  {
    "path": "dist/licenses/slf4j.license",
    "content": "\n Copyright (c) 2004-2017 QOS.ch\n All rights reserved.\n\n Permission is hereby granted, free  of charge, to any person obtaining\n a  copy  of this  software  and  associated  documentation files  (the\n \"Software\"), to  deal in  the Software without  restriction, including\n without limitation  the rights to  use, copy, modify,  merge, publish,\n distribute,  sublicense, and/or sell  copies of  the Software,  and to\n permit persons to whom the Software  is furnished to do so, subject to\n the following conditions:\n \n The  above  copyright  notice  and  this permission  notice  shall  be\n included in all copies or substantial portions of the Software.\n \n THE  SOFTWARE IS  PROVIDED  \"AS  IS\", WITHOUT  WARRANTY  OF ANY  KIND,\n EXPRESS OR  IMPLIED, INCLUDING  BUT NOT LIMITED  TO THE  WARRANTIES OF\n MERCHANTABILITY,    FITNESS    FOR    A   PARTICULAR    PURPOSE    AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n OF CONTRACT, TORT OR OTHERWISE,  ARISING FROM, OUT OF OR IN CONNECTION\n WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
  },
  {
    "path": "dist/licenses/slf4j.properties",
    "content": "name=SLF4J\nversion=2.0.17\nlicense=MIT License\nlink=https://www.slf4j.org/"
  },
  {
    "path": "dist/licenses/stringtemplate.license",
    "content": "[The \"BSD license\"]\nCopyright (c) 2011-2022 Terence Parr\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n 1. Redistributions of source code must retain the above copyright\n    notice, this list of conditions and the following disclaimer.\n 2. Redistributions in binary form must reproduce the above copyright\n    notice, this list of conditions and the following disclaimer in the\n    documentation and/or other materials provided with the distribution.\n 3. The name of the author may not be used to endorse or promote products\n    derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\nIMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\nOF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\nIN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\nINCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\nNOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\nTHIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "dist/licenses/stringtemplate.properties",
    "content": "name=stringtemplate4\nversion=4.0.2\nlicense=BSD 3-Clause\nlink=https://github.com/antlr/stringtemplate4"
  },
  {
    "path": "dist/licenses/validatorfx.license",
    "content": "BSD 3-Clause License\n\nCopyright (c) 2019, Robert Lichtenberger\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "dist/licenses/validatorfx.properties",
    "content": "name=ValidatorFX\nversion=0.4.2\nlicense=BSD 3-Clause\nlink=https://github.com/effad/ValidatorFX"
  },
  {
    "path": "dist/licenses/vernacular-vnc.license",
    "content": "Copyright (c) 2018 ShinyHut Solutions Ltd\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "dist/licenses/vernacular-vnc.properties",
    "content": "name=Vernacular VNC\nversion=1.15\nlicense=MIT License\nlink=https://github.com/xpipe-io/vernacular-vnc"
  },
  {
    "path": "dist/licenses/vscode-icons.license",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016 Roberto Huertas\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n\n\nAttribution-ShareAlike 4.0 International\n\n=======================================================================\n\nCreative Commons Corporation (\"Creative Commons\") is not a law firm and\ndoes not provide legal services or legal advice. Distribution of\nCreative Commons public licenses does not create a lawyer-client or\nother relationship. Creative Commons makes its licenses and related\ninformation available on an \"as-is\" basis. Creative Commons gives no\nwarranties regarding its licenses, any material licensed under their\nterms and conditions, or any related information. Creative Commons\ndisclaims all liability for damages resulting from their use to the\nfullest extent possible.\n\nUsing Creative Commons Public Licenses\n\nCreative Commons public licenses provide a standard set of terms and\nconditions that creators and other rights holders may use to share\noriginal works of authorship and other material subject to copyright\nand certain other rights specified in the public license below. The\nfollowing considerations are for informational purposes only, are not\nexhaustive, and do not form part of our licenses.\n\n     Considerations for licensors: Our public licenses are\n     intended for use by those authorized to give the public\n     permission to use material in ways otherwise restricted by\n     copyright and certain other rights. Our licenses are\n     irrevocable. Licensors should read and understand the terms\n     and conditions of the license they choose before applying it.\n     Licensors should also secure all rights necessary before\n     applying our licenses so that the public can reuse the\n     material as expected. Licensors should clearly mark any\n     material not subject to the license. This includes other CC-\n     licensed material, or material used under an exception or\n     limitation to copyright. More considerations for licensors:\n    wiki.creativecommons.org/Considerations_for_licensors\n\n     Considerations for the public: By using one of our public\n     licenses, a licensor grants the public permission to use the\n     licensed material under specified terms and conditions. If\n     the licensor's permission is not necessary for any reason--for\n     example, because of any applicable exception or limitation to\n     copyright--then that use is not regulated by the license. Our\n     licenses grant only permissions under copyright and certain\n     other rights that a licensor has authority to grant. Use of\n     the licensed material may still be restricted for other\n     reasons, including because others have copyright or other\n     rights in the material. A licensor may make special requests,\n     such as asking that all changes be marked or described.\n     Although not required by our licenses, you are encouraged to\n     respect those requests where reasonable. More considerations\n     for the public:\n    wiki.creativecommons.org/Considerations_for_licensees\n\n=======================================================================\n\nCreative Commons Attribution-ShareAlike 4.0 International Public\nLicense\n\nBy exercising the Licensed Rights (defined below), You accept and agree\nto be bound by the terms and conditions of this Creative Commons\nAttribution-ShareAlike 4.0 International Public License (\"Public\nLicense\"). To the extent this Public License may be interpreted as a\ncontract, You are granted the Licensed Rights in consideration of Your\nacceptance of these terms and conditions, and the Licensor grants You\nsuch rights in consideration of benefits the Licensor receives from\nmaking the Licensed Material available under these terms and\nconditions.\n\n\nSection 1 -- Definitions.\n\n  a. Adapted Material means material subject to Copyright and Similar\n     Rights that is derived from or based upon the Licensed Material\n     and in which the Licensed Material is translated, altered,\n     arranged, transformed, or otherwise modified in a manner requiring\n     permission under the Copyright and Similar Rights held by the\n     Licensor. For purposes of this Public License, where the Licensed\n     Material is a musical work, performance, or sound recording,\n     Adapted Material is always produced where the Licensed Material is\n     synched in timed relation with a moving image.\n\n  b. Adapter's License means the license You apply to Your Copyright\n     and Similar Rights in Your contributions to Adapted Material in\n     accordance with the terms and conditions of this Public License.\n\n  c. BY-SA Compatible License means a license listed at\n     creativecommons.org/compatiblelicenses, approved by Creative\n     Commons as essentially the equivalent of this Public License.\n\n  d. Copyright and Similar Rights means copyright and/or similar rights\n     closely related to copyright including, without limitation,\n     performance, broadcast, sound recording, and Sui Generis Database\n     Rights, without regard to how the rights are labeled or\n     categorized. For purposes of this Public License, the rights\n     specified in Section 2(b)(1)-(2) are not Copyright and Similar\n     Rights.\n\n  e. Effective Technological Measures means those measures that, in the\n     absence of proper authority, may not be circumvented under laws\n     fulfilling obligations under Article 11 of the WIPO Copyright\n     Treaty adopted on December 20, 1996, and/or similar international\n     agreements.\n\n  f. Exceptions and Limitations means fair use, fair dealing, and/or\n     any other exception or limitation to Copyright and Similar Rights\n     that applies to Your use of the Licensed Material.\n\n  g. License Elements means the license attributes listed in the name\n     of a Creative Commons Public License. The License Elements of this\n     Public License are Attribution and ShareAlike.\n\n  h. Licensed Material means the artistic or literary work, database,\n     or other material to which the Licensor applied this Public\n     License.\n\n  i. Licensed Rights means the rights granted to You subject to the\n     terms and conditions of this Public License, which are limited to\n     all Copyright and Similar Rights that apply to Your use of the\n     Licensed Material and that the Licensor has authority to license.\n\n  j. Licensor means the individual(s) or entity(ies) granting rights\n     under this Public License.\n\n  k. Share means to provide material to the public by any means or\n     process that requires permission under the Licensed Rights, such\n     as reproduction, public display, public performance, distribution,\n     dissemination, communication, or importation, and to make material\n     available to the public including in ways that members of the\n     public may access the material from a place and at a time\n     individually chosen by them.\n\n  l. Sui Generis Database Rights means rights other than copyright\n     resulting from Directive 96/9/EC of the European Parliament and of\n     the Council of 11 March 1996 on the legal protection of databases,\n     as amended and/or succeeded, as well as other essentially\n     equivalent rights anywhere in the world.\n\n  m. You means the individual or entity exercising the Licensed Rights\n     under this Public License. Your has a corresponding meaning.\n\n\nSection 2 -- Scope.\n\n  a. License grant.\n\n       1. Subject to the terms and conditions of this Public License,\n          the Licensor hereby grants You a worldwide, royalty-free,\n          non-sublicensable, non-exclusive, irrevocable license to\n          exercise the Licensed Rights in the Licensed Material to:\n\n            a. reproduce and Share the Licensed Material, in whole or\n               in part; and\n\n            b. produce, reproduce, and Share Adapted Material.\n\n       2. Exceptions and Limitations. For the avoidance of doubt, where\n          Exceptions and Limitations apply to Your use, this Public\n          License does not apply, and You do not need to comply with\n          its terms and conditions.\n\n       3. Term. The term of this Public License is specified in Section\n          6(a).\n\n       4. Media and formats; technical modifications allowed. The\n          Licensor authorizes You to exercise the Licensed Rights in\n          all media and formats whether now known or hereafter created,\n          and to make technical modifications necessary to do so. The\n          Licensor waives and/or agrees not to assert any right or\n          authority to forbid You from making technical modifications\n          necessary to exercise the Licensed Rights, including\n          technical modifications necessary to circumvent Effective\n          Technological Measures. For purposes of this Public License,\n          simply making modifications authorized by this Section 2(a)\n          (4) never produces Adapted Material.\n\n       5. Downstream recipients.\n\n            a. Offer from the Licensor -- Licensed Material. Every\n               recipient of the Licensed Material automatically\n               receives an offer from the Licensor to exercise the\n               Licensed Rights under the terms and conditions of this\n               Public License.\n\n            b. Additional offer from the Licensor -- Adapted Material.\n               Every recipient of Adapted Material from You\n               automatically receives an offer from the Licensor to\n               exercise the Licensed Rights in the Adapted Material\n               under the conditions of the Adapter's License You apply.\n\n            c. No downstream restrictions. You may not offer or impose\n               any additional or different terms or conditions on, or\n               apply any Effective Technological Measures to, the\n               Licensed Material if doing so restricts exercise of the\n               Licensed Rights by any recipient of the Licensed\n               Material.\n\n       6. No endorsement. Nothing in this Public License constitutes or\n          may be construed as permission to assert or imply that You\n          are, or that Your use of the Licensed Material is, connected\n          with, or sponsored, endorsed, or granted official status by,\n          the Licensor or others designated to receive attribution as\n          provided in Section 3(a)(1)(A)(i).\n\n  b. Other rights.\n\n       1. Moral rights, such as the right of integrity, are not\n          licensed under this Public License, nor are publicity,\n          privacy, and/or other similar personality rights; however, to\n          the extent possible, the Licensor waives and/or agrees not to\n          assert any such rights held by the Licensor to the limited\n          extent necessary to allow You to exercise the Licensed\n          Rights, but not otherwise.\n\n       2. Patent and trademark rights are not licensed under this\n          Public License.\n\n       3. To the extent possible, the Licensor waives any right to\n          collect royalties from You for the exercise of the Licensed\n          Rights, whether directly or through a collecting society\n          under any voluntary or waivable statutory or compulsory\n          licensing scheme. In all other cases the Licensor expressly\n          reserves any right to collect such royalties.\n\n\nSection 3 -- License Conditions.\n\nYour exercise of the Licensed Rights is expressly made subject to the\nfollowing conditions.\n\n  a. Attribution.\n\n       1. If You Share the Licensed Material (including in modified\n          form), You must:\n\n            a. retain the following if it is supplied by the Licensor\n               with the Licensed Material:\n\n                 i. identification of the creator(s) of the Licensed\n                    Material and any others designated to receive\n                    attribution, in any reasonable manner requested by\n                    the Licensor (including by pseudonym if\n                    designated);\n\n                ii. a copyright notice;\n\n               iii. a notice that refers to this Public License;\n\n                iv. a notice that refers to the disclaimer of\n                    warranties;\n\n                 v. a URI or hyperlink to the Licensed Material to the\n                    extent reasonably practicable;\n\n            b. indicate if You modified the Licensed Material and\n               retain an indication of any previous modifications; and\n\n            c. indicate the Licensed Material is licensed under this\n               Public License, and include the text of, or the URI or\n               hyperlink to, this Public License.\n\n       2. You may satisfy the conditions in Section 3(a)(1) in any\n          reasonable manner based on the medium, means, and context in\n          which You Share the Licensed Material. For example, it may be\n          reasonable to satisfy the conditions by providing a URI or\n          hyperlink to a resource that includes the required\n          information.\n\n       3. If requested by the Licensor, You must remove any of the\n          information required by Section 3(a)(1)(A) to the extent\n          reasonably practicable.\n\n  b. ShareAlike.\n\n     In addition to the conditions in Section 3(a), if You Share\n     Adapted Material You produce, the following conditions also apply.\n\n       1. The Adapter's License You apply must be a Creative Commons\n          license with the same License Elements, this version or\n          later, or a BY-SA Compatible License.\n\n       2. You must include the text of, or the URI or hyperlink to, the\n          Adapter's License You apply. You may satisfy this condition\n          in any reasonable manner based on the medium, means, and\n          context in which You Share Adapted Material.\n\n       3. You may not offer or impose any additional or different terms\n          or conditions on, or apply any Effective Technological\n          Measures to, Adapted Material that restrict exercise of the\n          rights granted under the Adapter's License You apply.\n\n\nSection 4 -- Sui Generis Database Rights.\n\nWhere the Licensed Rights include Sui Generis Database Rights that\napply to Your use of the Licensed Material:\n\n  a. for the avoidance of doubt, Section 2(a)(1) grants You the right\n     to extract, reuse, reproduce, and Share all or a substantial\n     portion of the contents of the database;\n\n  b. if You include all or a substantial portion of the database\n     contents in a database in which You have Sui Generis Database\n     Rights, then the database in which You have Sui Generis Database\n     Rights (but not its individual contents) is Adapted Material,\n     including for purposes of Section 3(b); and\n\n  c. You must comply with the conditions in Section 3(a) if You Share\n     all or a substantial portion of the contents of the database.\n\nFor the avoidance of doubt, this Section 4 supplements and does not\nreplace Your obligations under this Public License where the Licensed\nRights include other Copyright and Similar Rights.\n\n\nSection 5 -- Disclaimer of Warranties and Limitation of Liability.\n\n  a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE\n     EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS\n     AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF\n     ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,\n     IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,\n     WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR\n     PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,\n     ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT\n     KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT\n     ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.\n\n  b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE\n     TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,\n     NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,\n     INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,\n     COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR\n     USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN\n     ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR\n     DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR\n     IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.\n\n  c. The disclaimer of warranties and limitation of liability provided\n     above shall be interpreted in a manner that, to the extent\n     possible, most closely approximates an absolute disclaimer and\n     waiver of all liability.\n\n\nSection 6 -- Term and Termination.\n\n  a. This Public License applies for the term of the Copyright and\n     Similar Rights licensed here. However, if You fail to comply with\n     this Public License, then Your rights under this Public License\n     terminate automatically.\n\n  b. Where Your right to use the Licensed Material has terminated under\n     Section 6(a), it reinstates:\n\n       1. automatically as of the date the violation is cured, provided\n          it is cured within 30 days of Your discovery of the\n          violation; or\n\n       2. upon express reinstatement by the Licensor.\n\n     For the avoidance of doubt, this Section 6(b) does not affect any\n     right the Licensor may have to seek remedies for Your violations\n     of this Public License.\n\n  c. For the avoidance of doubt, the Licensor may also offer the\n     Licensed Material under separate terms or conditions or stop\n     distributing the Licensed Material at any time; however, doing so\n     will not terminate this Public License.\n\n  d. Sections 1, 5, 6, 7, and 8 survive termination of this Public\n     License.\n\n\nSection 7 -- Other Terms and Conditions.\n\n  a. The Licensor shall not be bound by any additional or different\n     terms or conditions communicated by You unless expressly agreed.\n\n  b. Any arrangements, understandings, or agreements regarding the\n     Licensed Material not stated herein are separate from and\n     independent of the terms and conditions of this Public License.\n\n\nSection 8 -- Interpretation.\n\n  a. For the avoidance of doubt, this Public License does not, and\n     shall not be interpreted to, reduce, limit, restrict, or impose\n     conditions on any use of the Licensed Material that could lawfully\n     be made without permission under this Public License.\n\n  b. To the extent possible, if any provision of this Public License is\n     deemed unenforceable, it shall be automatically reformed to the\n     minimum extent necessary to make it enforceable. If the provision\n     cannot be reformed, it shall be severed from this Public License\n     without affecting the enforceability of the remaining terms and\n     conditions.\n\n  c. No term or condition of this Public License will be waived and no\n     failure to comply consented to unless expressly agreed to by the\n     Licensor.\n\n  d. Nothing in this Public License constitutes or may be interpreted\n     as a limitation upon, or waiver of, any privileges and immunities\n     that apply to the Licensor or You, including from the legal\n     processes of any jurisdiction or authority.\n\n\n=======================================================================\n\nCreative Commons is not a party to its public\nlicenses. Notwithstanding, Creative Commons may elect to apply one of\nits public licenses to material it publishes and in those instances\nwill be considered the “Licensor.” The text of the Creative Commons\npublic licenses is dedicated to the public domain under the CC0 Public\nDomain Dedication. Except for the limited purpose of indicating that\nmaterial is shared under a Creative Commons public license or as\notherwise permitted by the Creative Commons policies published at\ncreativecommons.org/policies, Creative Commons does not authorize the\nuse of the trademark \"Creative Commons\" or any other trademark or logo\nof Creative Commons without its prior written consent including,\nwithout limitation, in connection with any unauthorized modifications\nto any of its public licenses or any other arrangements,\nunderstandings, or agreements concerning use of licensed material. For\nthe avoidance of doubt, this paragraph does not form part of the\npublic licenses.\n\nCreative Commons may be contacted at creativecommons.org.\n"
  },
  {
    "path": "dist/licenses/vscode-icons.properties",
    "content": "name=vscode-icons\nversion=1.2.0\nlicense=MIT License / Creative Commons - ShareAlike (CC BY-SA)\nlink=https://github.com/vscode-icons/vscode-icons"
  },
  {
    "path": "dist/licenses/zlib.license",
    "content": "Copyright (C) 1995-2024 Jean-loup Gailly and Mark Adler\n\nThis software is provided 'as-is', without any express or implied\nwarranty.  In no event will the authors be held liable for any damages\narising from the use of this software.\n\nPermission is granted to anyone to use this software for any purpose,\nincluding commercial applications, and to alter it and redistribute it\nfreely, subject to the following restrictions:\n\n1. The origin of this software must not be misrepresented; you must not\n claim that you wrote the original software. If you use this software\n in a product, an acknowledgment in the product documentation would be\n appreciated but is not required.\n2. Altered source versions must be plainly marked as such, and must not be\n misrepresented as being the original software.\n3. This notice may not be removed or altered from any source distribution.\n\nJean-loup Gailly        Mark Adler\njloup@gzip.org          madler@alumni.caltech.edu\n"
  },
  {
    "path": "dist/licenses/zlib.properties",
    "content": "name=zlib\nversion=1.3.1\nlicense=zlib License\nlink=https://www.zlib.net/"
  },
  {
    "path": "dist/logo/logo_composer.icon/icon.json",
    "content": "{\n  \"fill\" : {\n    \"linear-gradient\" : [\n      \"srgb:1.00000,1.00000,1.00000,1.00000\",\n      \"extended-gray:0.94078,1.00000\"\n    ]\n  },\n  \"groups\" : [\n    {\n      \"blend-mode-specializations\" : [\n        {\n          \"value\" : \"normal\"\n        },\n        {\n          \"appearance\" : \"dark\",\n          \"value\" : \"normal\"\n        },\n        {\n          \"appearance\" : \"tinted\",\n          \"value\" : \"plus-lighter\"\n        }\n      ],\n      \"blur-material-specializations\" : [\n        {\n          \"value\" : null\n        },\n        {\n          \"appearance\" : \"tinted\",\n          \"value\" : null\n        }\n      ],\n      \"layers\" : [\n        {\n          \"blend-mode-specializations\" : [\n            {\n              \"appearance\" : \"tinted\",\n              \"value\" : \"normal\"\n            }\n          ],\n          \"fill-specializations\" : [\n            {\n              \"appearance\" : \"tinted\",\n              \"value\" : \"automatic\"\n            }\n          ],\n          \"hidden\" : false,\n          \"image-name\" : \"logo.svg\",\n          \"name\" : \"logo\",\n          \"position\" : {\n            \"scale\" : 2,\n            \"translation-in-points\" : [\n              33.16796875,\n              -15.66015625\n            ]\n          }\n        }\n      ],\n      \"shadow-specializations\" : [\n        {\n          \"value\" : {\n            \"kind\" : \"neutral\",\n            \"opacity\" : 0.47\n          }\n        },\n        {\n          \"appearance\" : \"tinted\",\n          \"value\" : {\n            \"kind\" : \"neutral\",\n            \"opacity\" : 0.46\n          }\n        }\n      ],\n      \"translucency-specializations\" : [\n        {\n          \"value\" : {\n            \"enabled\" : true,\n            \"value\" : 0.03\n          }\n        },\n        {\n          \"appearance\" : \"tinted\",\n          \"value\" : {\n            \"enabled\" : true,\n            \"value\" : 0\n          }\n        }\n      ]\n    },\n    {\n      \"blend-mode-specializations\" : [\n        {\n          \"value\" : \"plus-darker\"\n        },\n        {\n          \"appearance\" : \"dark\",\n          \"value\" : \"darken\"\n        },\n        {\n          \"appearance\" : \"tinted\",\n          \"value\" : \"plus-lighter\"\n        }\n      ],\n      \"blur-material-specializations\" : [\n        {\n          \"value\" : 0.49\n        },\n        {\n          \"appearance\" : \"tinted\",\n          \"value\" : null\n        }\n      ],\n      \"layers\" : [\n        {\n          \"blend-mode\" : \"normal\",\n          \"hidden\" : false,\n          \"image-name\" : \"shadow.svg\",\n          \"name\" : \"shadow\",\n          \"opacity\" : 1,\n          \"position\" : {\n            \"scale\" : 2,\n            \"translation-in-points\" : [\n              0,\n              0\n            ]\n          }\n        }\n      ],\n      \"lighting-specializations\" : [\n        {\n          \"value\" : \"individual\"\n        },\n        {\n          \"appearance\" : \"tinted\",\n          \"value\" : \"individual\"\n        }\n      ],\n      \"opacity-specializations\" : [\n        {\n          \"value\" : 1\n        },\n        {\n          \"appearance\" : \"dark\",\n          \"value\" : 0.83\n        }\n      ],\n      \"position\" : {\n        \"scale\" : 1,\n        \"translation-in-points\" : [\n          29.921875,\n          3.359375\n        ]\n      },\n      \"shadow-specializations\" : [\n        {\n          \"value\" : {\n            \"kind\" : \"layer-color\",\n            \"opacity\" : 0.5\n          }\n        },\n        {\n          \"appearance\" : \"dark\",\n          \"value\" : {\n            \"kind\" : \"neutral\",\n            \"opacity\" : 0.5\n          }\n        },\n        {\n          \"appearance\" : \"tinted\",\n          \"value\" : {\n            \"kind\" : \"neutral\",\n            \"opacity\" : 0.49\n          }\n        }\n      ],\n      \"specular\" : true,\n      \"translucency-specializations\" : [\n        {\n          \"value\" : {\n            \"enabled\" : true,\n            \"value\" : 0.45\n          }\n        },\n        {\n          \"appearance\" : \"dark\",\n          \"value\" : {\n            \"enabled\" : true,\n            \"value\" : 0\n          }\n        },\n        {\n          \"appearance\" : \"tinted\",\n          \"value\" : {\n            \"enabled\" : true,\n            \"value\" : 0.3\n          }\n        }\n      ]\n    }\n  ],\n  \"supported-platforms\" : {\n    \"squares\" : [\n      \"macOS\"\n    ]\n  }\n}"
  },
  {
    "path": "ext/base/build.gradle",
    "content": "plugins {\n    id 'java'\n}\n\napply from: \"$rootDir/gradle/gradle_scripts/extension.gradle\"\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopApplicationStore.java",
    "content": "package io.xpipe.ext.base.desktop;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.Validators;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Value;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\n@Value\n@SuperBuilder\n@Jacksonized\n@JsonTypeName(\"desktopApplication\")\npublic class DesktopApplicationStore implements DataStore {\n\n    DataStoreEntryRef<DesktopBaseStore> desktop;\n    String path;\n    String arguments;\n\n    @Override\n    public void checkComplete() throws Throwable {\n        Validators.nonNull(desktop);\n        Validators.isType(desktop, DesktopBaseStore.class);\n        desktop.checkComplete();\n        Validators.nonNull(path);\n    }\n\n    public CommandBuilder getFullCommand() {\n        var builder = CommandBuilder.of().addFile(path).add(arguments != null ? \" \" + arguments : \"\");\n        builder = desktop.getStore().getUsedDesktopDialect().launchAsync(builder, true);\n        return builder;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopApplicationStoreProvider.java",
    "content": "package io.xpipe.ext.base.desktop;\n\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.hub.comp.StoreChoiceComp;\nimport io.xpipe.app.hub.comp.StoreEntryWrapper;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.hub.comp.SystemStateComp;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.storage.DataStoreCategory;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.core.FailableRunnable;\n\nimport javafx.beans.property.BooleanProperty;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.property.SimpleStringProperty;\n\nimport java.util.List;\n\npublic class DesktopApplicationStoreProvider implements DataStoreProvider {\n\n    @Override\n    public DocumentationLink getHelpLink() {\n        return DocumentationLink.DESKTOP_APPLICATIONS;\n    }\n\n    @Override\n    public FailableRunnable<Exception> launch(DataStoreEntry store) {\n        return () -> {\n            DesktopApplicationStore s = store.getStore().asNeeded();\n            var baseEntry = s.getDesktop().get();\n            var baseActivate = baseEntry.getProvider().activateAction(baseEntry);\n            if (baseActivate != null) {\n                baseActivate.run();\n            }\n            s.getDesktop().getStore().runDesktopApplication(store.getName(), s);\n        };\n    }\n\n    @Override\n    public FailableRunnable<Exception> launchBrowser(\n            BrowserFullSessionModel sessionModel, DataStoreEntry store, BooleanProperty busy) {\n        return launch(store);\n    }\n\n    @Override\n    public DataStoreCreationCategory getCreationCategory() {\n        return DataStoreCreationCategory.DESKTOP;\n    }\n\n    @Override\n    public DataStoreUsageCategory getUsageCategory() {\n        return DataStoreUsageCategory.DESKTOP;\n    }\n\n    @Override\n    public DataStoreEntry getDisplayParent(DataStoreEntry store) {\n        DesktopApplicationStore s = store.getStore().asNeeded();\n        return s.getDesktop().get();\n    }\n\n    @Override\n    public GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {\n        DesktopApplicationStore st = (DesktopApplicationStore) store.getValue();\n        var host = new SimpleObjectProperty<>(st.getDesktop());\n        var path = new SimpleStringProperty(st.getPath());\n        var args = new SimpleStringProperty(st.getArguments() != null ? st.getArguments() : null);\n        return new OptionsBuilder()\n                .nameAndDescription(\"desktopBase\")\n                .addComp(\n                        new StoreChoiceComp<>(\n                                entry,\n                                host,\n                                DesktopBaseStore.class,\n                                desktopStoreDataStoreEntryRef ->\n                                        desktopStoreDataStoreEntryRef.getStore().supportsDesktopAccess(),\n                                StoreViewState.get().getAllConnectionsCategory()),\n                        host)\n                .nonNull()\n                .nameAndDescription(\"desktopApplicationPath\")\n                .addString(path)\n                .nonNull()\n                .nameAndDescription(\"desktopApplicationArguments\")\n                .addString(args)\n                .bind(\n                        () -> {\n                            return DesktopApplicationStore.builder()\n                                    .desktop(host.get())\n                                    .path(path.get())\n                                    .arguments(args.get())\n                                    .build();\n                        },\n                        store)\n                .buildDialog();\n    }\n\n    @Override\n    public String getDisplayIconFileName(DataStore store) {\n        return \"base:desktopApplication_icon.svg\";\n    }\n\n    @Override\n    public int getOrderPriority() {\n        return 2;\n    }\n\n    @Override\n    public DataStore defaultStore(DataStoreCategory category) {\n        return DesktopApplicationStore.builder().build();\n    }\n\n    @Override\n    public String getId() {\n        return \"desktopApplication\";\n    }\n\n    @Override\n    public List<Class<?>> getStoreClasses() {\n        return List.of(DesktopApplicationStore.class);\n    }\n\n    @Override\n    public BaseRegionBuilder<?, ?> stateDisplay(StoreEntryWrapper w) {\n        return new SystemStateComp(SystemStateComp.State.SUCCESS);\n    }\n\n    @Override\n    public String summaryString(StoreEntryWrapper wrapper) {\n        var st = (DesktopApplicationStore) wrapper.getEntry().getStore();\n        return st.getPath() + (st.getArguments() != null ? \" \" + st.getArguments() : \"\");\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopBaseStore.java",
    "content": "package io.xpipe.ext.base.desktop;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.process.ShellDialect;\n\npublic interface DesktopBaseStore extends DataStore {\n\n    boolean supportsDesktopAccess();\n\n    void runDesktopApplication(String name, DesktopApplicationStore applicationStore) throws Exception;\n\n    ShellDialect getUsedDesktopDialect();\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/host/AbstractHostCreationActionProvider.java",
    "content": "package io.xpipe.ext.base.host;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class AbstractHostCreationActionProvider implements HubLeafProvider<AbstractHostTransformStore> {\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<AbstractHostTransformStore> store) {\n        return AppI18n.observable(\"abstractHostConvert\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<AbstractHostTransformStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2c-cog-transfer-outline\");\n    }\n\n    @Override\n    public Class<?> getApplicableClass() {\n        return AbstractHostTransformStore.class;\n    }\n\n    @Override\n    public boolean isApplicable(DataStoreEntryRef<AbstractHostTransformStore> o) {\n        return o.getStore().canConvertToAbstractHost();\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<AbstractHostTransformStore> {\n\n        @Override\n        public void executeImpl() {\n            var d = ref.getStore();\n            var ah = d.createAbstractHostStore();\n            var entry = DataStorage.get().addStoreIfNotPresent(ref.get().getName(), ah);\n            entry.setExpanded(true);\n            var newStore = d.withNewParent(entry.ref());\n            DataStorage.get().updateEntryStore(ref.get(), newStore);\n            entry.setChildrenCache(null);\n            StoreViewState.get().triggerStoreListUpdate();\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/host/AbstractHostStore.java",
    "content": "package io.xpipe.ext.base.host;\n\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.Validators;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.ToString;\nimport lombok.Value;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\n@Value\n@ToString(callSuper = true)\n@SuperBuilder\n@Jacksonized\n@JsonTypeName(\"abstractHost\")\npublic class AbstractHostStore implements DataStore, HostAddressStore, HostAddressGatewayStore {\n\n    String host;\n    DataStoreEntryRef<NetworkTunnelStore> gateway;\n\n    @Override\n    public void checkComplete() throws Throwable {\n        Validators.nonNull(host);\n    }\n\n    @Override\n    public HostAddress getHostAddress() {\n        return HostAddress.of(host);\n    }\n\n    @Override\n    public DataStoreEntryRef<NetworkTunnelStore> getTunnelGateway() {\n        return gateway;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/host/AbstractHostStoreProvider.java",
    "content": "package io.xpipe.ext.base.host;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.hub.comp.*;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.storage.DataStoreCategory;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.util.DocumentationLink;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.SneakyThrows;\n\nimport java.util.List;\n\npublic class AbstractHostStoreProvider implements DataStoreProvider {\n\n    @Override\n    public DocumentationLink getHelpLink() {\n        return DocumentationLink.ABSTRACT_HOSTS;\n    }\n\n    @Override\n    public int getOrderPriority() {\n        return 2;\n    }\n\n    @Override\n    public BaseRegionBuilder<?, ?> stateDisplay(StoreEntryWrapper w) {\n        return new SystemStateComp(new SimpleObjectProperty<>(SystemStateComp.State.SUCCESS));\n    }\n\n    @Override\n    public DataStoreCreationCategory getCreationCategory() {\n        return DataStoreCreationCategory.HOST;\n    }\n\n    @Override\n    public DataStoreUsageCategory getUsageCategory() {\n        return DataStoreUsageCategory.GROUP;\n    }\n\n    @Override\n    public ObservableValue<String> informationString(StoreSection section) {\n        return Bindings.createStringBinding(\n                () -> {\n                    var all = section.getAllChildren().getList();\n                    var shown = section.getShownChildren().getList();\n                    if (shown.size() == 0) {\n                        return null;\n                    }\n\n                    var string = all.size() == shown.size() ? all.size() : shown.size() + \"/\" + all.size();\n                    return all.size() > 0\n                            ? (all.size() == 1\n                                    ? AppI18n.get(\"hostHasConnection\", string)\n                                    : AppI18n.get(\"hostHasConnections\", string))\n                            : AppI18n.get(\"hostNoConnections\");\n                },\n                section.getShownChildren().getList(),\n                section.getAllChildren().getList(),\n                AppI18n.activeLanguage());\n    }\n\n    @SneakyThrows\n    @Override\n    public GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {\n        AbstractHostStore st = store.getValue().asNeeded();\n\n        var host = new SimpleObjectProperty<>(st.getHost());\n        var gateway = new SimpleObjectProperty<>(st.getTunnelGateway());\n\n        return new OptionsBuilder()\n                .nameAndDescription(\"abstractHostAddress\")\n                .addString(host)\n                .nonNull()\n                .nameAndDescription(\"abstractHostGateway\")\n                .addComp(\n                        new StoreChoiceComp<>(\n                                entry,\n                                gateway,\n                                NetworkTunnelStore.class,\n                                null,\n                                StoreViewState.get().getAllConnectionsCategory()),\n                        gateway)\n                .bind(\n                        () -> {\n                            return AbstractHostStore.builder()\n                                    .host(host.getValue())\n                                    .gateway(gateway.getValue())\n                                    .build();\n                        },\n                        store)\n                .buildDialog();\n    }\n\n    @Override\n    public String summaryString(StoreEntryWrapper wrapper) {\n        AbstractHostStore scriptStore = wrapper.getEntry().getStore().asNeeded();\n        return scriptStore.getHost();\n    }\n\n    @Override\n    public String getDisplayIconFileName(DataStore store) {\n        return \"base:abstractHost_icon.svg\";\n    }\n\n    @Override\n    public DataStore defaultStore(DataStoreCategory category) {\n        return AbstractHostStore.builder().build();\n    }\n\n    @Override\n    public String getId() {\n        return \"abstractHost\";\n    }\n\n    @Override\n    public List<Class<?>> getStoreClasses() {\n        return List.of(AbstractHostStore.class);\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/host/AbstractHostTransformStore.java",
    "content": "package io.xpipe.ext.base.host;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\npublic interface AbstractHostTransformStore extends DataStore {\n\n    boolean canConvertToAbstractHost();\n\n    AbstractHostStore createAbstractHostStore();\n\n    AbstractHostTransformStore withNewParent(DataStoreEntryRef<AbstractHostStore> newParent);\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressChoice.java",
    "content": "package io.xpipe.ext.base.host;\n\nimport io.xpipe.app.ext.HostAddress;\nimport io.xpipe.app.issue.TrackEvent;\nimport io.xpipe.app.platform.OptionsBuilder;\n\nimport javafx.beans.property.*;\nimport javafx.collections.FXCollections;\nimport javafx.collections.ListChangeListener;\n\nimport lombok.Builder;\nimport lombok.Value;\n\nimport java.util.ArrayList;\n\n@Builder\n@Value\npublic class HostAddressChoice {\n\n    Property<HostAddress> value;\n    boolean allowMutation;\n    String translationKey;\n    boolean includeDescription;\n\n    public OptionsBuilder build() {\n        var existing = value.getValue();\n        var val = new SimpleObjectProperty<>(existing != null ? existing.get() : null);\n        var list = FXCollections.observableArrayList(existing != null ? existing.getAvailable() : new ArrayList<>());\n        // For updating the options builder binding on list change, it doesn't support observable lists\n        var listHashProp = new SimpleIntegerProperty(0);\n        list.addListener((ListChangeListener<? super String>) c -> {\n            listHashProp.set(c.getList().hashCode());\n        });\n        var options = new OptionsBuilder();\n        if (includeDescription) {\n            options.nameAndDescription(this.translationKey);\n        } else {\n            options.name(translationKey);\n        }\n        options.addComp(new HostAddressChoiceComp(val, list, allowMutation));\n        options.addProperty(val);\n        options.nonNull();\n        options.addProperty(listHashProp);\n        options.bind(\n                () -> {\n                    var fullList = new ArrayList<>(list);\n                    if (val.getValue() != null && !fullList.contains(val.getValue())) {\n                        fullList.add(val.getValue());\n                    }\n\n                    TrackEvent.withTrace(\"Host address update\")\n                            .tag(\"address\", val.getValue())\n                            .tag(\"list\", fullList)\n                            .handle();\n\n                    return HostAddress.of(val.getValue(), fullList);\n                },\n                value);\n        return options;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressChoiceComp.java",
    "content": "package io.xpipe.ext.base.host;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.platform.MenuHelper;\n\nimport javafx.application.Platform;\nimport javafx.beans.property.ObjectProperty;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableBooleanValue;\nimport javafx.collections.ListChangeListener;\nimport javafx.collections.ObservableList;\nimport javafx.css.PseudoClass;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.ListCell;\nimport javafx.scene.control.skin.ComboBoxListViewSkin;\nimport javafx.scene.layout.HBox;\n\nimport atlantafx.base.controls.Spacer;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.ArrayList;\n\npublic class HostAddressChoiceComp extends RegionBuilder<HBox> {\n\n    private final ObjectProperty<String> currentAddress;\n    private final ObservableList<String> allAddresses;\n    private final boolean mutable;\n\n    public HostAddressChoiceComp(\n            ObjectProperty<String> currentAddress, ObservableList<String> allAddresses, boolean mutable) {\n        this.currentAddress = currentAddress;\n        this.allAddresses = allAddresses;\n        this.mutable = mutable;\n    }\n\n    @Override\n    public HBox createSimple() {\n        var adding = new SimpleBooleanProperty(false);\n        var combo = createComboBox(adding);\n\n        var addButton = new ButtonComp(null, new FontIcon(\"mdi2f-format-list-group-plus\"), () -> {\n            var toAdd = currentAddress.getValue();\n            if (toAdd == null) {\n                return;\n            }\n\n            adding.set(true);\n            if (!allAddresses.contains(toAdd)) {\n                allAddresses.addFirst(toAdd);\n            }\n            currentAddress.setValue(null);\n            adding.set(false);\n        });\n        addButton.describe(d -> d.nameKey(\"addAnotherHostName\"));\n\n        var nodes = new ArrayList<BaseRegionBuilder<?, ?>>();\n        nodes.add(combo);\n        if (mutable) {\n            nodes.add(addButton);\n        }\n\n        var layout = new InputGroupComp(nodes);\n        layout.setMainReference(combo);\n        layout.apply(struc -> struc.setFillHeight(true));\n        return layout.build();\n    }\n\n    private BaseRegionBuilder<?, ?> createComboBox(ObservableBooleanValue adding) {\n        var prop = new SimpleStringProperty();\n        currentAddress.subscribe(hostAddress -> {\n            prop.setValue(hostAddress);\n        });\n        prop.addListener((observable, oldValue, newValue) -> {\n            if (mutable) {\n                currentAddress.setValue(newValue);\n                // Update list as well\n                var index = allAddresses.indexOf(oldValue);\n                if (!adding.get() && index != -1) {\n                    Platform.runLater(() -> {\n                        if (newValue != null) {\n                            if (!allAddresses.contains(newValue)) {\n                                allAddresses.set(index, newValue);\n                            }\n                        } else {\n                            allAddresses.remove(index);\n                        }\n                    });\n                }\n            } else if (allAddresses.contains(newValue)) {\n                currentAddress.setValue(newValue);\n            }\n        });\n\n        var combo = new ComboTextFieldComp(prop, allAddresses, () -> {\n            return new ListCell<>() {\n\n                {\n                    setOnMouseClicked(event -> {\n                        getScene().getWindow().hide();\n                        event.consume();\n                    });\n                }\n\n                @Override\n                protected void updateItem(String item, boolean empty) {\n                    super.updateItem(item, empty);\n                    if (empty) {\n                        return;\n                    }\n\n                    var hbox = new HBox();\n                    hbox.getChildren().add(new Label(item));\n                    hbox.getChildren().add(new Spacer());\n                    if (mutable) {\n                        hbox.getChildren()\n                                .add(new IconButtonComp(\"mdi2t-trash-can-outline\", () -> {\n                                            allAddresses.remove(item);\n                                        })\n                                        .build());\n                    }\n\n                    setGraphic(hbox);\n                    setText(null);\n                }\n            };\n        });\n        combo.apply(struc -> {\n            var skin = new ComboBoxListViewSkin<>(struc);\n            MenuHelper.fixComboBoxSkin(skin);\n            struc.setSkin(skin);\n            skin.setHideOnClick(false);\n\n            // The focus seems to break on selection from the popup\n            struc.selectionModelProperty()\n                    .get()\n                    .selectedIndexProperty()\n                    .addListener((observable, oldValue, newValue) -> {\n                        Platform.runLater(() -> {\n                            struc.getParent().requestFocus();\n                        });\n                    });\n\n            allAddresses.addListener((ListChangeListener<? super String>) change -> {\n                struc.setVisibleRowCount(10);\n                if (!change.next()) {\n                    return;\n                }\n\n                if (change.wasReplaced()) {\n                    return;\n                }\n\n                if (struc.isShowing()) {\n                    struc.hide();\n                    if (allAddresses.size() > 0) {\n                        struc.show();\n                    }\n                } else {\n                    struc.requestFocus();\n                    if (allAddresses.size() > 0) {\n                        struc.show();\n                    }\n                }\n\n                struc.pseudoClassStateChanged(PseudoClass.getPseudoClass(\"empty\"), allAddresses.isEmpty());\n            });\n            struc.pseudoClassStateChanged(PseudoClass.getPseudoClass(\"empty\"), allAddresses.isEmpty());\n\n            currentAddress.addListener((observable, oldValue, newValue) -> {\n                if (newValue == null) {\n                    struc.requestFocus();\n                }\n            });\n        });\n        combo.hgrow();\n        combo.style(\"host-address-choice-comp\");\n        return combo;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressGatewayStore.java",
    "content": "package io.xpipe.ext.base.host;\n\nimport io.xpipe.app.ext.NetworkTunnelStore;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\npublic interface HostAddressGatewayStore extends HostAddressStore {\n\n    DataStoreEntryRef<NetworkTunnelStore> getTunnelGateway();\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressIdentityStore.java",
    "content": "package io.xpipe.ext.base.host;\n\nimport io.xpipe.ext.base.identity.IdentityValue;\n\npublic interface HostAddressIdentityStore extends HostAddressStore {\n\n    IdentityValue getIdentity();\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressStore.java",
    "content": "package io.xpipe.ext.base.host;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.HostAddress;\n\npublic interface HostAddressStore extends DataStore {\n\n    HostAddress getHostAddress();\n\n    default void refreshHostAddressOrThrow() throws Exception {}\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressSwitchBranchProvider.java",
    "content": "package io.xpipe.ext.base.host;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.HubBranchProvider;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.HubMenuItemProvider;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.property.ReadOnlyStringWrapper;\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\npublic class HostAddressSwitchBranchProvider implements HubBranchProvider<HostAddressSwitchStore> {\n\n    @Override\n    public List<HubMenuItemProvider<?>> getChildren(DataStoreEntryRef<HostAddressSwitchStore> store) {\n        return store.getStore().getHostAddress().getAvailable().stream()\n                .map(s -> {\n                    return new HostAddressProvider(\n                            s.equals(store.getStore().getHostAddress().get()), s);\n                })\n                .collect(Collectors.toList());\n    }\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.CONFIGURATION;\n    }\n\n    @Override\n    public boolean isApplicable(DataStoreEntryRef<HostAddressSwitchStore> o) {\n        return o.getStore().getHostAddress() != null\n                && !o.getStore().getHostAddress().isSingle();\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<HostAddressSwitchStore> store) {\n        return AppI18n.observable(\"switchHostAddress\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<HostAddressSwitchStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2f-format-list-group\");\n    }\n\n    @Override\n    public Class<HostAddressSwitchStore> getApplicableClass() {\n        return HostAddressSwitchStore.class;\n    }\n\n    private static class HostAddressProvider implements HubLeafProvider<HostAddressSwitchStore> {\n\n        private final boolean active;\n        private final String address;\n\n        private HostAddressProvider(boolean active, String address) {\n            this.active = active;\n            this.address = address;\n        }\n\n        @Override\n        public void execute(DataStoreEntryRef<HostAddressSwitchStore> ref) {\n            var newStore = ref.getStore().withAddress(address);\n            if (newStore.isPresent()) {\n                DataStorage.get().updateEntryStore(ref.get(), newStore.get());\n            }\n        }\n\n        @Override\n        public ObservableValue<String> getName(DataStoreEntryRef<HostAddressSwitchStore> store) {\n            return new ReadOnlyStringWrapper(address);\n        }\n\n        @Override\n        public LabelGraphic getIcon(DataStoreEntryRef<HostAddressSwitchStore> store) {\n            return active ? new LabelGraphic.IconGraphic(\"mdi2a-arrow-right\") : LabelGraphic.none();\n        }\n\n        @Override\n        public Class<HostAddressSwitchStore> getApplicableClass() {\n            return HostAddressSwitchStore.class;\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressSwitchStore.java",
    "content": "package io.xpipe.ext.base.host;\n\nimport io.xpipe.app.ext.HostAddress;\n\nimport java.util.Optional;\n\npublic interface HostAddressSwitchStore extends HostAddressStore {\n\n    HostAddress getHostAddress();\n\n    Optional<HostAddressSwitchStore> withAddress(String address);\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressTunnelStore.java",
    "content": "package io.xpipe.ext.base.host;\n\nimport io.xpipe.app.ext.NetworkTunnelStore;\n\npublic interface HostAddressTunnelStore extends HostAddressStore, NetworkTunnelStore {}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityApplyDialog.java",
    "content": "package io.xpipe.ext.base.identity;\n\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.browser.file.BrowserFileOpener;\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.hub.comp.StoreChoiceComp;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.BooleanScope;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.*;\nimport javafx.geometry.Insets;\n\nimport atlantafx.base.theme.Styles;\nimport lombok.Data;\n\nimport java.util.List;\n\npublic class IdentityApplyDialog {\n\n    @Data\n    private static class SystemState {\n\n        public static SystemState of(ShellControl sc, IdentityStore identity) throws Exception {\n            var s = new SystemState();\n            s.init(sc, identity);\n            return s;\n        }\n\n        boolean inAuthorizedKeys;\n        boolean keyAuthEnabled;\n        boolean keyAuthInMethods;\n        boolean passwordAuthEnabled;\n        boolean passwordAuthInMethods;\n        boolean rootLoginEnabled;\n        boolean mightRequireAdministratorAuthorizedKeys;\n\n        FilePath configFile;\n        FilePath authorizedKeysFile;\n\n        private void init(ShellControl sc, IdentityStore identity) throws Exception {\n            var hasPassword =\n                    identity.getPassword() != null && identity.getPassword().expectsQuery();\n            var hasIdentity = identity.getSshIdentity() != null\n                    && identity.getSshIdentity().getPublicKey() != null;\n\n            configFile = getSystemConfigPath(sc);\n            authorizedKeysFile = getAuthorizedKeysFile(sc);\n\n            var configContent = getSshdConfigContent(sc);\n            keyAuthEnabled = isSet(configContent, \"PubkeyAuthentication\", \"yes\", true, false);\n            passwordAuthEnabled = isSet(configContent, \"PasswordAuthentication\", \"yes\", true, false);\n            rootLoginEnabled = isSet(configContent, \"PermitRootLogin\", \"yes\", !hasPassword, false)\n                    || (!hasPassword\n                            && !isSet(configContent, \"PermitRootLogin\", \"forced-commands-only\", true, false)\n                            && isSet(configContent, \"PermitRootLogin\", \"prohibit-password\", true, true));\n            keyAuthInMethods = isSet(configContent, \"AuthenticationMethods\", \"publickey\", true, false);\n            passwordAuthInMethods = isSet(configContent, \"AuthenticationMethods\", \"password\", true, false);\n            mightRequireAdministratorAuthorizedKeys = sc.getOsType() == OsType.WINDOWS\n                    && isSet(configContent, \"Match\", \"Group administrators\", false, false)\n                    && isSet(configContent, \"AuthorizedKeysFile\", \"administrators_authorized_keys\", false, false);\n\n            if (hasIdentity) {\n                var authorizedKeysContent = getAuthorizedKeysContent(sc);\n\n                var publicKey = identity.getSshIdentity().getPublicKey();\n                var split = publicKey.split(\"\\\\s+\");\n                var basePublicKey = split[0] + \" \" + split[1];\n\n                inAuthorizedKeys = authorizedKeysContent.toLowerCase().contains(basePublicKey.toLowerCase());\n            } else {\n                inAuthorizedKeys = true;\n            }\n        }\n\n        private FilePath getSystemConfigPath(ShellControl sc) throws Exception {\n            if (sc.getOsType() == OsType.WINDOWS) {\n                var base = sc.view().getEnvironmentVariableOrThrow(\"programdata\");\n                return FilePath.of(base).join(\"ssh\", \"sshd_config\");\n            }\n\n            return FilePath.of(\"/etc/ssh/sshd_config\");\n        }\n\n        private FilePath getAuthorizedKeysFile(ShellControl sc) throws Exception {\n            var v = sc.view();\n            var authorizedKeysFile = v.userHome().join(\".ssh\", \"authorized_keys\");\n            return authorizedKeysFile;\n        }\n\n        private String getAuthorizedKeysContent(ShellControl sc) throws Exception {\n            var v = sc.view();\n            var authorizedKeysContent = v.fileExists(authorizedKeysFile) ? v.readTextFile(authorizedKeysFile) : \"\";\n            return authorizedKeysContent;\n        }\n\n        private String getSshdConfigContent(ShellControl sc) throws Exception {\n            var v = sc.view();\n            var configContent = v.fileExists(configFile) ? v.readTextFile(configFile) : \"\";\n            return configContent;\n        }\n\n        private boolean isSet(String config, String name, String value, boolean notFoundDef, boolean notSpecifiedDef) {\n            var found = config.lines()\n                    .filter(s -> {\n                        return !s.strip().startsWith(\"#\");\n                    })\n                    .filter(s -> {\n                        return s.toLowerCase().contains(name.toLowerCase());\n                    })\n                    .toList();\n            if (found.isEmpty()) {\n                return notFoundDef;\n            }\n\n            for (String line : found) {\n                var matches = line.toLowerCase().contains(value.toLowerCase());\n                if (matches) {\n                    return true;\n                }\n            }\n\n            return notSpecifiedDef;\n        }\n    }\n\n    private static void addPublicKey(SystemState systemState, ShellControl sc, String publicKey) throws Exception {\n        var v = sc.view();\n        var authorizedKeysFile = systemState.getAuthorizedKeysFile();\n        v.mkdir(authorizedKeysFile.getParent());\n        String authorizedKeysContent;\n        if (v.fileExists(authorizedKeysFile)) {\n            var text = v.readTextFile(authorizedKeysFile).strip();\n            authorizedKeysContent = text.isBlank() ? publicKey + \"\\n\" : text + \"\\n\" + publicKey + \"\\n\";\n        } else {\n            authorizedKeysContent = publicKey + \"\\n\";\n        }\n        v.writeTextFile(authorizedKeysFile, authorizedKeysContent);\n        if (sc.getOsType() != OsType.WINDOWS) {\n            sc.command(CommandBuilder.of().add(\"chmod\", \"600\").addFile(authorizedKeysFile))\n                    .execute();\n        }\n    }\n\n    private static BaseRegionBuilder<?, ?> success() {\n        var graphic = new LabelGraphic.IconGraphic(\"mdi2c-checkbox-marked-outline\");\n        return new LabelComp(AppI18n.observable(\"valid\"), new ReadOnlyObjectWrapper<>(graphic))\n                .style(Styles.SUCCESS)\n                .apply(struc -> {\n                    AppFontSizes.lg(struc);\n                });\n    }\n\n    private static BaseRegionBuilder<?, ?> warning() {\n        var graphic = new LabelGraphic.IconGraphic(\"mdi2a-alert-box-outline\");\n        return new LabelComp(AppI18n.observable(\"warning\"), new ReadOnlyObjectWrapper<>(graphic))\n                .style(Styles.WARNING)\n                .apply(struc -> {\n                    AppFontSizes.lg(struc);\n                });\n    }\n\n    private static BaseRegionBuilder<?, ?> fail(BaseRegionBuilder<?, ?> fixComp) {\n        var graphic = new LabelGraphic.IconGraphic(\"mdi2c-close-box-outline\");\n        var label = new LabelComp(AppI18n.observable(\"notValid\"), new ReadOnlyObjectWrapper<>(graphic))\n                .style(Styles.DANGER);\n        label.apply(struc -> {\n            AppFontSizes.lg(struc);\n        });\n        if (fixComp != null) {\n            var hbox = new HorizontalComp(List.of(label, fixComp, RegionBuilder.hspacer()));\n            hbox.spacing(10);\n            return hbox;\n        } else {\n            return label;\n        }\n    }\n\n    private static BaseRegionBuilder<?, ?> createAuthorizedKeysOptions(\n            Property<DataStoreEntryRef<ShellStore>> system,\n            ObjectProperty<SystemState> systemState,\n            IdentityStore identity,\n            BooleanProperty busy) {\n        var showAddAuthorizedHost = BindingsHelper.mapBoolean(systemState, s -> {\n            return s != null && !s.isInAuthorizedKeys();\n        });\n\n        var editButton = new ButtonComp(AppI18n.observable(\"identityApplyEditAuthorizedKeysButton\"), () -> {\n                    ThreadHelper.runFailableAsync(() -> {\n                        BooleanScope.executeExclusive(busy, () -> {\n                            var sc = system.getValue().getStore().getOrStartSession();\n                            var file = systemState.get().getAuthorizedKeysFile();\n                            var model = BrowserFullSessionModel.DEFAULT.openFileSystemSync(\n                                    system.getValue(), null, m -> file.getParent(), null, false);\n                            var found = model.findFile(file);\n                            if (found.isEmpty()) {\n                                model.getFileSystem().touch(file);\n                                if (sc.getOsType() != OsType.WINDOWS) {\n                                    sc.command(CommandBuilder.of()\n                                                    .add(\"chmod\", \"600\")\n                                                    .addFile(file))\n                                            .execute();\n                                }\n                                model.refreshSync();\n                                found = model.findFile(file);\n                            }\n                            if (found.isPresent()) {\n                                BrowserFileOpener.openInTextEditor(model, found.get());\n                            }\n                        });\n                    });\n                })\n                .padding(new Insets(4, 8, 4, 8));\n\n        var addButton = new ButtonComp(AppI18n.observable(\"identityApplyAuthorizedHostButton\"), () -> {\n                    ThreadHelper.runFailableAsync(() -> {\n                        BooleanScope.executeExclusive(busy, () -> {\n                            var sc = system.getValue().getStore().getOrStartSession();\n                            addPublicKey(\n                                    systemState.get(),\n                                    sc,\n                                    identity.getSshIdentity().getPublicKey());\n                            systemState.setValue(SystemState.of(sc, identity));\n                        });\n                    });\n                })\n                .padding(new Insets(4, 8, 4, 8));\n\n        var options = new OptionsBuilder()\n                .nameAndDescription(\"identityApplyAuthorizedHost\")\n                .addComp(success())\n                .hide(showAddAuthorizedHost)\n                .nameAndDescription(\"identityApplyAuthorizedHost\")\n                .addComp(fail(addButton))\n                .hide(showAddAuthorizedHost.not())\n                .nameAndDescription(\"identityApplyEditAuthorizedKeys\")\n                .addComp(editButton);\n\n        return options.buildComp().hide(Bindings.isNull(systemState));\n    }\n\n    private static BaseRegionBuilder<?, ?> createConfigOptions(\n            Property<DataStoreEntryRef<ShellStore>> system,\n            Property<SystemState> systemState,\n            IdentityStore identity,\n            BooleanProperty busy) {\n        var showAdminWarning = BindingsHelper.mapBoolean(systemState, s -> {\n            return s != null\n                    && identity.getSshIdentity() != null\n                    && identity.getSshIdentity().providesKey()\n                    && s.isMightRequireAdministratorAuthorizedKeys();\n        });\n        var showPasswordEnabledWarning = BindingsHelper.mapBoolean(systemState, s -> {\n            return s != null\n                    && (identity.getPassword() == null\n                            || !identity.getPassword().expectsQuery())\n                    && s.isPasswordAuthEnabled()\n                    && s.isPasswordAuthInMethods();\n        });\n        var showPasswordDisabledWarning = BindingsHelper.mapBoolean(systemState, s -> {\n            return s != null\n                    && identity.getPassword() != null\n                    && identity.getPassword().expectsQuery()\n                    && (!s.isPasswordAuthEnabled() || !s.isPasswordAuthInMethods());\n        });\n        var showKeyEnabledWarning = BindingsHelper.mapBoolean(systemState, s -> {\n            return s != null\n                    && (identity.getSshIdentity() == null\n                            || !identity.getSshIdentity().providesKey())\n                    && s.keyAuthEnabled;\n        });\n        var showKeyDisabledWarning = BindingsHelper.mapBoolean(systemState, s -> {\n            return s != null\n                    && identity.getSshIdentity() != null\n                    && identity.getSshIdentity().providesKey()\n                    && !s.keyAuthEnabled;\n        });\n        var showRootDisabledWarning = BindingsHelper.mapBoolean(systemState, s -> {\n            return s != null\n                    && identity.getUsername()\n                            .getFixedUsername()\n                            .map(u -> u.equals(\"root\"))\n                            .orElse(false)\n                    && !s.rootLoginEnabled;\n        });\n        var showConfigSection = showKeyEnabledWarning\n                .or(showRootDisabledWarning)\n                .or(showPasswordEnabledWarning)\n                .or(showPasswordDisabledWarning)\n                .or(showKeyDisabledWarning)\n                .or(showAdminWarning);\n\n        var editButton = new ButtonComp(AppI18n.observable(\"identityApplyEditConfigButton\"), () -> {\n                    ThreadHelper.runFailableAsync(() -> {\n                        BooleanScope.executeExclusive(busy, () -> {\n                            var file = systemState.getValue().getConfigFile();\n                            var model = BrowserFullSessionModel.DEFAULT.openFileSystemSync(\n                                    system.getValue(), null, m -> file.getParent(), null, false);\n                            var found = model.findFile(file);\n                            if (found.isEmpty()) {\n                                return;\n                            }\n                            BrowserFileOpener.openInTextEditor(model, found.get());\n                        });\n                    });\n                })\n                .padding(new Insets(4, 8, 4, 8));\n\n        var options = new OptionsBuilder()\n                .nameAndDescription(\"identityApplyConfigPasswordEnabled\")\n                .addComp(warning())\n                .hide(showPasswordEnabledWarning.not())\n                .nameAndDescription(\"identityApplyConfigPasswordDisabled\")\n                .addComp(warning())\n                .hide(showPasswordDisabledWarning.not())\n                .nameAndDescription(\"identityApplyConfigKeyEnabled\")\n                .addComp(warning())\n                .hide(showKeyEnabledWarning.not())\n                .nameAndDescription(\"identityApplyConfigKeyDisabled\")\n                .addComp(warning())\n                .hide(showKeyDisabledWarning.not())\n                .nameAndDescription(\"identityApplyConfigRootDisabledWarning\")\n                .addComp(fail(null))\n                .hide(showRootDisabledWarning.not())\n                .nameAndDescription(\"identityApplyConfigAdminWarning\")\n                .documentationLink(\n                        \"https://learn.microsoft.com/en-us/windows-server/administration/openssh/openssh_keymanagement#administrative-user\")\n                .addComp(warning())\n                .hide(showAdminWarning.not())\n                .nameAndDescription(\"identityApplyEditConfig\")\n                .addComp(editButton)\n                .buildComp()\n                .hide(showConfigSection.not());\n        return options;\n    }\n\n    public static void show(DataStoreEntryRef<IdentityStore> identity) {\n        var busy = new SimpleBooleanProperty();\n        var system = new SimpleObjectProperty<DataStoreEntryRef<ShellStore>>();\n        var systemState = new SimpleObjectProperty<SystemState>();\n        var showSetIdentityButton = new SimpleBooleanProperty();\n        var showIdentityAlreadySet = new SimpleBooleanProperty();\n        system.addListener((observable, oldValue, newValue) -> {\n            systemState.setValue(null);\n            showSetIdentityButton.set(false);\n            showIdentityAlreadySet.set(false);\n\n            if (newValue == null) {\n                return;\n            }\n\n            ThreadHelper.runFailableAsync(() -> {\n                BooleanScope.executeExclusive(busy, () -> {\n                    var sc = newValue.getStore().getOrStartSession();\n                    systemState.setValue(SystemState.of(sc, identity.getStore()));\n                    showSetIdentityButton.set(newValue.getStore() instanceof IdentitySwitchStore iss\n                            && !iss.getIdentity().unwrap().equals(identity.getStore()));\n                    showIdentityAlreadySet.set(newValue.getStore() instanceof IdentitySwitchStore iss\n                            && iss.getIdentity().unwrap().equals(identity.getStore()));\n                });\n            });\n        });\n\n        var systemChoice =\n                new StoreChoiceComp<>(\n                        null,\n                        system,\n                        ShellStore.class,\n                        null,\n                        StoreViewState.get().getAllConnectionsCategory()) {\n\n                    @Override\n                    protected String toName(DataStoreEntry entry) {\n                        if (entry == null) {\n                            return null;\n                        }\n\n                        return DataStorage.get().getStoreEntryDisplayName(entry) + \" -> \"\n                                + IdentitySummary.createSummary(identity.getStore());\n                    }\n                };\n        var systemChoiceBusy = new LoadingOverlayComp(systemChoice, busy, false);\n\n        var applyButton = new ButtonComp(AppI18n.observable(\"identityApplySetStoreIdentityButton\"), () -> {\n            DataStorage.get()\n                    .updateEntryStore(\n                            system.get().get(),\n                            ((IdentitySwitchStore) system.get().getStore())\n                                    .withIdentity(new IdentityValue.Ref(identity)));\n            showSetIdentityButton.set(false);\n            showIdentityAlreadySet.set(true);\n        });\n        applyButton.padding(new Insets(4, 8, 4, 8));\n\n        var options = new OptionsBuilder()\n                .nameAndDescription(\"identityApplyTargetHost\")\n                .documentationLink(DocumentationLink.IDENTITY_APPLY)\n                .addComp(systemChoiceBusy, system)\n                .addComp(createAuthorizedKeysOptions(system, systemState, identity.getStore(), busy))\n                .addComp(createConfigOptions(system, systemState, identity.getStore(), busy))\n                .nameAndDescription(\"identityApplySetStoreIdentity\")\n                .addComp(success())\n                .hide(showIdentityAlreadySet.not())\n                .nameAndDescription(\"identityApplySetStoreIdentity\")\n                .addComp(fail(applyButton))\n                .hide(showSetIdentityButton.not());\n\n        var modal = ModalOverlay.of(\n                \"identityApplyTitle\", options.buildComp().prefWidth(600).prefHeight(500));\n        modal.persist();\n        modal.addButton(ModalButton.cancel());\n        modal.addButton(ModalButton.ok().augment(button -> {\n            button.disableProperty().bind(PlatformThread.sync(busy));\n        }));\n        modal.show();\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityApplyHubLeafProvider.java",
    "content": "package io.xpipe.ext.base.identity;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.hub.comp.StoreCreationDialog;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.process.ShellTtyState;\nimport io.xpipe.app.process.SystemState;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.ext.base.identity.ssh.NoIdentityStrategy;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class IdentityApplyHubLeafProvider implements HubLeafProvider<IdentityStore> {\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.OPEN;\n    }\n\n    @Override\n    public boolean isMajor() {\n        return true;\n    }\n\n    @Override\n    public boolean isApplicable(DataStoreEntryRef<IdentityStore> o) {\n        var state = o.get().getStorePersistentState();\n        if (state instanceof SystemState systemState) {\n            return (systemState.getShellDialect() == null\n                            || systemState.getShellDialect().getDumbMode().supportsAnyPossibleInteraction())\n                    && (systemState.getTtyState() == null || systemState.getTtyState() == ShellTtyState.NONE);\n        } else {\n            return true;\n        }\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<IdentityStore> store) {\n        return AppI18n.observable(\"applyIdentityToHost\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<IdentityStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2e-export\");\n    }\n\n    @Override\n    public Class<?> getApplicableClass() {\n        return IdentityStore.class;\n    }\n\n    @Override\n    public AbstractAction createAction(DataStoreEntryRef<IdentityStore> ref) {\n        return Action.builder().ref(ref).build();\n    }\n\n    @Override\n    public String getId() {\n        return \"applyIdentity\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<IdentityStore> {\n\n        @Override\n        public void executeImpl() {\n            if (ref.getStore().getSshIdentity() != null\n                    && !(ref.getStore().getSshIdentity() instanceof NoIdentityStrategy)\n                    && ref.getStore().getSshIdentity().getPublicKey() == null) {\n                AppDialog.confirm(\"identityApplyMissingPublicKey\");\n                StoreCreationDialog.showEdit(ref.get());\n                return;\n            }\n\n            IdentityApplyDialog.show(ref);\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityChoiceBuilder.java",
    "content": "package io.xpipe.ext.base.identity;\n\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.ButtonComp;\nimport io.xpipe.app.comp.base.InputGroupComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.platform.OptionsChoiceBuilder;\nimport io.xpipe.app.secret.EncryptedValue;\nimport io.xpipe.app.secret.SecretRetrievalStrategy;\nimport io.xpipe.app.secret.SecretStrategyChoiceConfig;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStorageUserHandler;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.*;\nimport io.xpipe.ext.base.identity.ssh.SshIdentityStrategy;\nimport io.xpipe.ext.base.identity.ssh.SshIdentityStrategyChoiceConfig;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.*;\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Value;\n\nimport java.util.List;\n\n@Value\n@Builder\n@AllArgsConstructor\npublic class IdentityChoiceBuilder {\n\n    ObjectProperty<IdentityValue> identity;\n    boolean allowCustomUserInput;\n    boolean requireUserInput;\n    boolean requirePassword;\n    boolean keyInput;\n    boolean requireKeyInput;\n    boolean allowAgentForward;\n    String userChoiceTranslationKey;\n    ObservableValue<String> passwordChoiceTranslationKey;\n    ObservableValue<DataStoreEntryRef<ShellStore>> fileSystem;\n\n    public IdentityChoiceBuilder(\n            ObjectProperty<IdentityValue> identity,\n            boolean allowCustomUserInput,\n            boolean requireUserInput,\n            boolean requirePassword,\n            boolean keyInput,\n            boolean requireKeyInput,\n            boolean allowAgentForward,\n            String userChoiceTranslationKey,\n            String passwordChoiceTranslationKey) {\n        this.identity = identity;\n        this.allowCustomUserInput = allowCustomUserInput;\n        this.requireUserInput = requireUserInput;\n        this.requirePassword = requirePassword;\n        this.keyInput = keyInput;\n        this.requireKeyInput = requireKeyInput;\n        this.allowAgentForward = allowAgentForward;\n        this.userChoiceTranslationKey = userChoiceTranslationKey;\n        this.passwordChoiceTranslationKey = new ReadOnlyStringWrapper(passwordChoiceTranslationKey);\n        this.fileSystem = new ReadOnlyObjectWrapper<>(DataStorage.get().local().ref());\n    }\n\n    public static OptionsBuilder ssh(ObjectProperty<IdentityValue> identity, boolean requireUser) {\n        var i = new IdentityChoiceBuilder(\n                identity, true, requireUser, true, true, true, true, \"identityChoice\", \"passwordAuthentication\");\n        return i.build();\n    }\n\n    public static OptionsBuilder container(ObjectProperty<IdentityValue> identity) {\n        var i = new IdentityChoiceBuilder(\n                identity, true, false, false, false, false, false, \"customUsername\", \"customUsernamePassword\");\n        return i.build();\n    }\n\n    public static OptionsBuilder keyAuthChoice(\n            Property<SshIdentityStrategy> identity, SshIdentityStrategyChoiceConfig config) {\n        return OptionsChoiceBuilder.builder()\n                .allowNull(false)\n                .property(identity)\n                .customConfiguration(config)\n                .available(SshIdentityStrategy.getSubclasses())\n                .transformer(entryComboBox -> {\n                    var button = new ButtonComp(null, new LabelGraphic.IconGraphic(\"mdi2k-key-plus\"), () -> {\n                        ProcessControlProvider.get().showSshKeygenDialog(null, identity);\n                    });\n                    button.describe(d -> d.nameKey(\"generateKey\"));\n                    var comboComp = RegionBuilder.of(() -> entryComboBox);\n                    var hbox = new InputGroupComp(List.of(comboComp, button));\n                    hbox.setMainReference(comboComp);\n                    return hbox.build();\n                })\n                .build()\n                .build();\n    }\n\n    public OptionsBuilder build() {\n        var existing = identity.getValue();\n        var user = new SimpleStringProperty(\n                existing instanceof IdentityValue.InPlace inPlace && inPlace.unwrap() != null\n                        ? inPlace.unwrap().getUsername().get()\n                        : null);\n        var pass = new SimpleObjectProperty<>(\n                existing instanceof IdentityValue.InPlace inPlace && inPlace.unwrap() != null\n                        ? inPlace.unwrap().getPassword()\n                        : null);\n        var identityStrategy = new SimpleObjectProperty<>(\n                existing instanceof IdentityValue.InPlace inPlace && inPlace.unwrap() != null\n                        ? inPlace.unwrap().getSshIdentity()\n                        : null);\n        var ref = new SimpleObjectProperty<>(existing instanceof IdentityValue.Ref r ? r.getRef() : null);\n        var inPlaceSelected = ref.isNull();\n        var refSelected = ref.isNotNull();\n\n        var passwordChoice = OptionsChoiceBuilder.builder()\n                .allowNull(false)\n                .property(pass)\n                .customConfiguration(\n                        SecretStrategyChoiceConfig.builder().allowNone(true).build())\n                .available(SecretRetrievalStrategy.getClasses())\n                .build()\n                .build();\n\n        var options = new OptionsBuilder()\n                .nameAndDescription(userChoiceTranslationKey)\n                .addComp(new IdentitySelectComp(ref, user, pass, identityStrategy, allowCustomUserInput), user)\n                .nonNullIf(inPlaceSelected.and(new SimpleBooleanProperty(requireUserInput)))\n                .name(Bindings.createStringBinding(\n                        () -> {\n                            return AppI18n.get(passwordChoiceTranslationKey.getValue());\n                        },\n                        passwordChoiceTranslationKey,\n                        AppI18n.activeLanguage()))\n                .description(Bindings.createStringBinding(\n                        () -> {\n                            return AppI18n.get(passwordChoiceTranslationKey.getValue() + \"Description\");\n                        },\n                        passwordChoiceTranslationKey,\n                        AppI18n.activeLanguage()))\n                .sub(passwordChoice, pass)\n                .nonNullIf(inPlaceSelected.and(new SimpleBooleanProperty(requirePassword)))\n                .hide(refSelected)\n                .addProperty(ref);\n\n        var sshIdentityChoiceConfig = SshIdentityStrategyChoiceConfig.builder()\n                .allowAgentForward(allowAgentForward)\n                .allowKeyFileSync(true)\n                .perUserKeyFileCheck(() -> false)\n                .fileSystem(fileSystem)\n                .build();\n\n        if (keyInput) {\n            options.name(\"keyAuthentication\")\n                    .description(\"keyAuthenticationDescription\")\n                    .documentationLink(DocumentationLink.SSH_KEYS)\n                    .sub(keyAuthChoice(identityStrategy, sshIdentityChoiceConfig), identityStrategy)\n                    .nonNullIf(inPlaceSelected.and(new ReadOnlyBooleanWrapper(requireKeyInput)))\n                    .hide(refSelected);\n        }\n        options.bind(\n                () -> {\n                    if (ref.get() != null) {\n                        return IdentityValue.Ref.builder().ref(ref.get()).build();\n                    } else {\n                        var u = user.get();\n                        // In case of team vaults, identities shouldn't really be specified inline anyway\n                        // If they are, we use the vault key to make it accessible for all users\n                        var useUserKey = DataStorageUserHandler.getInstance().getUserCount() <= 1;\n                        var p = useUserKey\n                                ? EncryptedValue.CurrentKey.of(pass.get())\n                                : EncryptedValue.VaultKey.of(pass.get());\n                        EncryptedValue<SshIdentityStrategy> i = keyInput\n                                ? (useUserKey\n                                        ? EncryptedValue.CurrentKey.of(identityStrategy.get())\n                                        : EncryptedValue.VaultKey.of(identityStrategy.get()))\n                                : null;\n                        if (u == null && p == null && i == null) {\n                            return null;\n                        } else {\n                            return IdentityValue.InPlace.builder()\n                                    .identityStore(LocalIdentityStore.builder()\n                                            .username(u)\n                                            .password(p)\n                                            .sshIdentity(i)\n                                            .build())\n                                    .build();\n                        }\n                    }\n                },\n                identity);\n        return options;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityMigrationDeserializer.java",
    "content": "package io.xpipe.ext.base.identity;\n\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.JsonDeserializer;\nimport com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer;\nimport com.fasterxml.jackson.databind.jsontype.TypeDeserializer;\nimport com.fasterxml.jackson.databind.node.JsonNodeFactory;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.fasterxml.jackson.databind.node.TreeTraversingParser;\n\nimport java.io.IOException;\n\npublic class IdentityMigrationDeserializer extends DelegatingDeserializer {\n\n    public IdentityMigrationDeserializer(JsonDeserializer<?> d) {\n        super(d);\n    }\n\n    @Override\n    protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {\n        return new IdentityMigrationDeserializer(newDelegatee);\n    }\n\n    @Override\n    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {\n        return super.deserialize(restructure(p), ctxt);\n    }\n\n    @Override\n    public Object deserialize(JsonParser p, DeserializationContext ctxt, Object intoValue) throws IOException {\n        return super.deserialize(restructure(p), ctxt, intoValue);\n    }\n\n    public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer typeDeserializer)\n            throws IOException {\n        return super.deserializeWithType(restructure(jp), ctxt, typeDeserializer);\n    }\n\n    public JsonParser restructure(JsonParser p) throws IOException {\n        var node = p.readValueAsTree();\n        if (node == null) {\n            return p;\n        }\n\n        if (!node.isObject()) {\n            var newJsonParser = new TreeTraversingParser((ObjectNode) node, p.getCodec());\n            newJsonParser.nextToken();\n            return newJsonParser;\n        }\n\n        migrate((ObjectNode) node);\n        var newJsonParser = new TreeTraversingParser((ObjectNode) node, p.getCodec());\n        newJsonParser.nextToken();\n        return newJsonParser;\n    }\n\n    private void migrate(ObjectNode containerNode) {\n        var user = containerNode.get(\"user\");\n        var password = containerNode.get(\"password\");\n        var identity = containerNode.get(\"identityStrategy\");\n        if (identity == null) {\n            var vmIdentity = containerNode.get(\"identity\");\n            if (vmIdentity != null && !vmIdentity.has(\"username\")) {\n                identity = vmIdentity;\n            }\n        }\n        if (identity == null) {\n            var additional = containerNode.get(\"additionalIdentity\");\n            if (additional != null) {\n                identity = additional;\n            }\n        }\n\n        if (user == null) {\n            user = containerNode.get(\"username\");\n        }\n\n        if (password != null && password.isObject() && identity != null && identity.isObject()) {\n            var identityStore = JsonNodeFactory.instance.objectNode();\n            identityStore.put(\"type\", \"localIdentity\");\n            if (user != null && user.isTextual()) {\n                identityStore.set(\"username\", user);\n            }\n            identityStore.set(\"password\", password);\n            identityStore.set(\"sshIdentity\", identity);\n\n            var inPlace = JsonNodeFactory.instance.objectNode();\n            inPlace.put(\"type\", \"inPlace\");\n            inPlace.set(\"identityStore\", identityStore);\n\n            containerNode.set(\"identity\", inPlace);\n        } else if (password != null) {\n            var identityStore = JsonNodeFactory.instance.objectNode();\n            identityStore.put(\"type\", \"localIdentity\");\n            if (user != null && user.isTextual()) {\n                identityStore.set(\"username\", user);\n            }\n            identityStore.set(\"password\", password);\n\n            var inPlace = JsonNodeFactory.instance.objectNode();\n            inPlace.put(\"type\", \"inPlace\");\n            inPlace.set(\"identityStore\", identityStore);\n\n            containerNode.set(\"identity\", inPlace);\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/IdentitySelectComp.java",
    "content": "package io.xpipe.ext.base.identity;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.RegionBuilder;\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppFontSizes;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.DataStoreCreationCategory;\nimport io.xpipe.app.hub.comp.*;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.MenuHelper;\nimport io.xpipe.app.platform.PlatformThread;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.secret.EncryptedValue;\nimport io.xpipe.app.secret.SecretNoneStrategy;\nimport io.xpipe.app.secret.SecretRetrievalStrategy;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.ext.base.identity.ssh.NoIdentityStrategy;\nimport io.xpipe.ext.base.identity.ssh.SshIdentityStrategy;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.ObjectProperty;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.FXCollections;\nimport javafx.collections.ListChangeListener;\nimport javafx.scene.control.ListCell;\nimport javafx.scene.control.skin.ComboBoxListViewSkin;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.input.KeyEvent;\nimport javafx.scene.layout.AnchorPane;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Region;\n\nimport atlantafx.base.theme.Styles;\n\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\n\npublic class IdentitySelectComp extends RegionBuilder<HBox> {\n\n    private final ObjectProperty<DataStoreEntryRef<IdentityStore>> selectedReference;\n    private final Property<String> inPlaceUser;\n    private final ObservableValue<SecretRetrievalStrategy> password;\n    private final ObservableValue<SshIdentityStrategy> identityStrategy;\n    private final boolean allowUserInput;\n\n    public IdentitySelectComp(\n            ObjectProperty<DataStoreEntryRef<IdentityStore>> selectedReference,\n            Property<String> inPlaceUser,\n            ObservableValue<SecretRetrievalStrategy> password,\n            ObservableValue<SshIdentityStrategy> identityStrategy,\n            boolean allowUserInput) {\n        this.selectedReference = selectedReference;\n        this.inPlaceUser = inPlaceUser;\n        this.password = password;\n        this.identityStrategy = identityStrategy;\n        this.allowUserInput = allowUserInput;\n    }\n\n    private void addNamedIdentity() {\n        var hasPwMan = AppPrefs.get().passwordManager().getValue() != null;\n        var pwManIdentity = DataStorage.get().getStoreEntries().stream()\n                .map(entry -> entry.getStore() instanceof PasswordManagerIdentityStore p ? p : null)\n                .filter(s -> s != null)\n                .findFirst();\n        var hasPassword = password.getValue() != null && !(password.getValue() instanceof SecretNoneStrategy);\n        var hasSshIdentity =\n                identityStrategy.getValue() != null && !(identityStrategy.getValue() instanceof NoIdentityStrategy);\n        if (hasPwMan && pwManIdentity.isPresent() && !hasPassword && !hasSshIdentity) {\n            var perUser = pwManIdentity.get().isPerUser();\n            var id = PasswordManagerIdentityStore.builder()\n                    .key(inPlaceUser.getValue())\n                    .perUser(perUser)\n                    .build();\n            showIdentityCreation(id);\n            return;\n        }\n\n        var synced = DataStorage.get().getStoreEntries().stream()\n                .map(entry -> entry.getStore() instanceof SyncedIdentityStore p ? p : null)\n                .filter(s -> s != null)\n                .findFirst();\n        if (synced.isPresent()) {\n            var pass = EncryptedValue.VaultKey.of(password.getValue());\n            if (pass == null) {\n                pass = EncryptedValue.VaultKey.of(new SecretNoneStrategy());\n            }\n            var ssh = EncryptedValue.VaultKey.of(identityStrategy.getValue());\n            if (ssh == null) {\n                ssh = EncryptedValue.VaultKey.of(new NoIdentityStrategy());\n            }\n            var id = SyncedIdentityStore.builder()\n                    .username(inPlaceUser.getValue())\n                    .password(pass)\n                    .sshIdentity(ssh)\n                    .build();\n            showIdentityCreation(id);\n            return;\n        }\n\n        var pass = EncryptedValue.CurrentKey.of(password.getValue());\n        if (pass == null) {\n            pass = EncryptedValue.CurrentKey.of(new SecretNoneStrategy());\n        }\n        var ssh = EncryptedValue.CurrentKey.of(identityStrategy.getValue());\n        if (ssh == null) {\n            ssh = EncryptedValue.CurrentKey.of(new NoIdentityStrategy());\n        }\n        var id = LocalIdentityStore.builder()\n                .username(inPlaceUser.getValue())\n                .password(pass)\n                .sshIdentity(ssh)\n                .build();\n        showIdentityCreation(id);\n    }\n\n    private void showIdentityCreation(IdentityStore store) {\n        StoreCreationDialog.showCreation(\n                null,\n                store,\n                DataStoreCreationCategory.IDENTITY,\n                dataStoreEntry -> {\n                    PlatformThread.runLaterIfNeeded(() -> {\n                        applyRef(dataStoreEntry.ref());\n                    });\n                },\n                false);\n    }\n\n    private void editNamedIdentity() {\n        var id = selectedReference.get();\n        if (id == null) {\n            return;\n        }\n\n        StoreCreationDialog.showEdit(id.get());\n    }\n\n    @Override\n    public HBox createSimple() {\n        ObservableValue<LabelGraphic> icon = Bindings.createObjectBinding(\n                () -> {\n                    return selectedReference.get() != null\n                            ? new LabelGraphic.IconGraphic(\"mdi2a-account-edit\")\n                            : new LabelGraphic.IconGraphic(\"mdi2a-account-multiple-plus\");\n                },\n                selectedReference);\n        var addButton = new ButtonComp(null, icon, () -> {\n            if (selectedReference.get() != null) {\n                editNamedIdentity();\n            } else {\n                addNamedIdentity();\n            }\n        });\n        addButton.describe(d -> d.nameKey(\"addReusableIdentity\"));\n\n        var nodes = new ArrayList<BaseRegionBuilder<?, ?>>();\n        nodes.add(createComboBox());\n        nodes.add(addButton);\n        var layout = new InputGroupComp(nodes).setMainReference(0).apply(struc -> struc.setFillHeight(true));\n\n        layout.apply(struc -> {\n            struc.focusedProperty().addListener((observable, oldValue, newValue) -> {\n                Platform.runLater(() -> {\n                    struc.getChildren().getFirst().requestFocus();\n                });\n            });\n        });\n\n        return layout.build();\n    }\n\n    private String formatName(DataStoreEntry storeEntry) {\n        IdentityStore id = storeEntry.getStore().asNeeded();\n        var suffix = id instanceof LocalIdentityStore\n                ? AppI18n.get(\"localIdentity\")\n                : id instanceof PasswordManagerIdentityStore\n                        ? AppI18n.get(\"passwordManagerIdentity\")\n                        : id instanceof SyncedIdentityStore && storeEntry.isPerUserStore()\n                                ? AppI18n.get(\"userIdentity\")\n                                : AppI18n.get(\"globalIdentity\");\n        return storeEntry.getName() + \" (\" + suffix + \")\";\n    }\n\n    private void applyRef(DataStoreEntryRef<IdentityStore> newRef) {\n        this.selectedReference.setValue(newRef);\n    }\n\n    private BaseRegionBuilder<?, ?> createComboBox() {\n        var map = new LinkedHashMap<String, DataStoreEntryRef<IdentityStore>>();\n        for (DataStoreEntry storeEntry : DataStorage.get().getStoreEntries()) {\n            if (storeEntry.getValidity().isUsable() && storeEntry.getStore() instanceof IdentityStore) {\n                map.put(formatName(storeEntry), storeEntry.ref());\n            }\n        }\n\n        StoreViewState.get().getAllEntries().getList().addListener((ListChangeListener<? super StoreEntryWrapper>)\n                c -> {\n                    map.clear();\n                    for (DataStoreEntry storeEntry : DataStorage.get().getStoreEntries()) {\n                        if (storeEntry.getValidity().isUsable() && storeEntry.getStore() instanceof IdentityStore) {\n                            map.put(formatName(storeEntry), storeEntry.ref());\n                        }\n                    }\n                });\n\n        var prop = new SimpleStringProperty();\n        if (inPlaceUser.getValue() != null) {\n            prop.setValue(inPlaceUser.getValue());\n        } else if (selectedReference.getValue() != null) {\n            prop.setValue(formatName(selectedReference.getValue().get()));\n        }\n\n        prop.addListener((observable, oldValue, newValue) -> {\n            var ex = map.get(newValue);\n            applyRef(ex);\n\n            if (ex == null) {\n                inPlaceUser.setValue(newValue);\n            } else {\n                inPlaceUser.setValue(null);\n            }\n        });\n\n        selectedReference.addListener((observable, oldValue, newValue) -> {\n            if (newValue != null) {\n                PlatformThread.runLaterIfNeeded(() -> {\n                    var s = formatName(newValue.get());\n                    prop.setValue(s);\n                });\n            } else {\n                prop.setValue(null);\n            }\n        });\n\n        var combo = new ComboTextFieldComp(\n                prop, FXCollections.observableList(map.keySet().stream().toList()), () -> {\n                    return new ListCell<>() {\n                        @Override\n                        protected void updateItem(String item, boolean empty) {\n                            super.updateItem(item, empty);\n                            if (empty) {\n                                return;\n                            }\n\n                            setText(item);\n\n                            if (item != null) {\n                                var store = map.get(item);\n                                if (store != null) {\n                                    var provider = store.get().getProvider();\n                                    var image = provider.getDisplayIconFileName(store.getStore());\n                                    setGraphic(PrettyImageHelper.ofFixedSize(image, 16, 16)\n                                            .build());\n                                }\n                            } else {\n                                setGraphic(null);\n                            }\n                        }\n                    };\n                });\n        combo.apply(struc -> struc.setEditable(allowUserInput));\n        combo.style(Styles.LEFT_PILL);\n\n        combo.apply(struc -> {\n            var binding = Bindings.createStringBinding(\n                    () -> {\n                        if (selectedReference.get() != null) {\n                            return selectedReference.get().get().getName();\n                        }\n\n                        return AppI18n.get(\"defineNewIdentityOrSelect\");\n                    },\n                    AppI18n.activeLanguage(),\n                    selectedReference);\n            struc.promptTextProperty().bind(binding);\n        });\n\n        combo.apply(struc -> {\n            struc.addEventFilter(KeyEvent.KEY_PRESSED, event -> {\n                if (event.getCode() == KeyCode.ESCAPE && !allowUserInput) {\n                    selectedReference.setValue(null);\n                    prop.setValue(null);\n                    event.consume();\n                }\n            });\n        });\n\n        combo.apply(struc -> {\n            var popover = new StoreChoicePopover<>(\n                    null,\n                    selectedReference,\n                    IdentityStore.class,\n                    null,\n                    StoreViewState.get().getAllIdentitiesCategory(),\n                    null,\n                    true,\n                    \"selectIdentity\",\n                    \"noCompatibleIdentity\");\n\n            popover.withPopover(po -> {\n                ((Region) po.getContentNode()).setMaxHeight(350);\n                po.showingProperty().addListener((o, oldValue, newValue) -> {\n                    if (!newValue) {\n                        struc.hide();\n                    }\n                });\n            });\n\n            var skin = new ComboBoxListViewSkin<>(struc) {\n                @Override\n                public void show() {\n                    popover.show(struc);\n                }\n\n                @Override\n                public void hide() {\n                    popover.hide();\n                }\n            };\n            MenuHelper.fixComboBoxSkin(skin);\n            struc.setSkin(skin);\n        });\n\n        combo.apply(struc -> {\n            struc.getEditor().focusedProperty().addListener((observable, oldValue, newValue) -> {\n                if (newValue && selectedReference.get() != null) {\n                    Platform.runLater(() -> {\n                        if (struc.isShowing()) {\n                            return;\n                        }\n\n                        struc.getEditor().selectAll();\n                    });\n                }\n            });\n        });\n\n        var clearButton = new IconButtonComp(\"mdi2c-close\", () -> {\n            selectedReference.setValue(null);\n            inPlaceUser.setValue(null);\n        });\n        clearButton.style(Styles.FLAT);\n        clearButton.hide(selectedReference.isNull());\n        clearButton.apply(struc -> {\n            struc.setOpacity(0.7);\n            struc.getStyleClass().add(\"clear-button\");\n            AppFontSizes.xs(struc);\n            AnchorPane.setRightAnchor(struc, 30.0);\n            AnchorPane.setTopAnchor(struc, 3.0);\n            AnchorPane.setBottomAnchor(struc, 3.0);\n        });\n\n        var stack = new AnchorComp(List.of(combo, clearButton));\n        stack.style(\"identity-select-comp\");\n        stack.hgrow();\n        stack.apply(struc -> {\n            var comboRegion = (Region) struc.getChildren().getFirst();\n            struc.prefWidthProperty().bind(comboRegion.prefWidthProperty());\n            struc.prefHeightProperty().bind(comboRegion.prefHeightProperty());\n            AnchorPane.setLeftAnchor(comboRegion, 0.0);\n            AnchorPane.setRightAnchor(comboRegion, 0.0);\n            struc.focusedProperty().addListener((observable, oldValue, newValue) -> {\n                if (newValue) {\n                    struc.getChildren().getFirst().requestFocus();\n                }\n            });\n        });\n\n        return stack;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityStore.java",
    "content": "package io.xpipe.ext.base.identity;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.SelfReferentialStore;\nimport io.xpipe.app.secret.SecretRetrievalStrategy;\nimport io.xpipe.ext.base.identity.ssh.SshIdentityStrategy;\n\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.ToString;\nimport lombok.experimental.SuperBuilder;\n\n@SuperBuilder\n@EqualsAndHashCode\n@ToString\n@Getter\npublic abstract class IdentityStore implements SelfReferentialStore, DataStore {\n\n    public abstract UsernameStrategy getUsername();\n\n    public abstract SecretRetrievalStrategy getPassword();\n\n    public abstract SshIdentityStrategy getSshIdentity();\n\n    @Override\n    public void checkComplete() throws Throwable {\n        if (getPassword() != null) {\n            getPassword().checkComplete();\n        }\n        if (getSshIdentity() != null) {\n            getSshIdentity().checkComplete();\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityStoreProvider.java",
    "content": "package io.xpipe.ext.base.identity;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.hub.comp.StoreEntryWrapper;\nimport io.xpipe.app.hub.comp.StoreSection;\nimport io.xpipe.app.hub.comp.SystemStateComp;\n\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\n\npublic abstract class IdentityStoreProvider implements DataStoreProvider {\n\n    @Override\n    public List<String> getSearchableTerms(DataStore store) {\n        IdentityStore s = store.asNeeded();\n        var name = s.getUsername().getFixedUsername();\n        return name.isPresent() ? List.of(name.get()) : List.of();\n    }\n\n    @Override\n    public BaseRegionBuilder<?, ?> stateDisplay(StoreEntryWrapper w) {\n        return new SystemStateComp(new SimpleObjectProperty<>(SystemStateComp.State.SUCCESS));\n    }\n\n    @Override\n    public DataStoreCreationCategory getCreationCategory() {\n        return DataStoreCreationCategory.IDENTITY;\n    }\n\n    @Override\n    public DataStoreUsageCategory getUsageCategory() {\n        return DataStoreUsageCategory.IDENTITY;\n    }\n\n    @Override\n    public ObservableValue<String> informationString(StoreSection section) {\n        var st = (IdentityStore) section.getWrapper().getStore().getValue();\n        return new SimpleStringProperty(IdentitySummary.createSummary(st));\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/IdentitySummary.java",
    "content": "package io.xpipe.ext.base.identity;\n\nimport io.xpipe.app.secret.SecretNoneStrategy;\nimport io.xpipe.ext.base.identity.ssh.NoIdentityStrategy;\n\npublic class IdentitySummary {\n\n    public static String createSummary(IdentityStore st) {\n        var user = st.getUsername().hasUser()\n                ? st.getUsername().getFixedUsername().map(s -> \"User \" + s).orElse(\"User\")\n                : \"Anonymous user\";\n        var s = user\n                + (st.getPassword() == null || st.getPassword() instanceof SecretNoneStrategy ? \"\" : \" + password\")\n                + (st.getSshIdentity() == null || st.getSshIdentity() instanceof NoIdentityStrategy ? \"\" : \" + key\");\n        return s;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/IdentitySwitchStore.java",
    "content": "package io.xpipe.ext.base.identity;\n\nimport io.xpipe.ext.base.host.HostAddressStore;\n\npublic interface IdentitySwitchStore extends HostAddressStore {\n\n    IdentityValue getIdentity();\n\n    IdentitySwitchStore withIdentity(IdentityValue identity);\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityValue.java",
    "content": "package io.xpipe.ext.base.identity;\n\nimport io.xpipe.app.ext.ValidationException;\nimport io.xpipe.app.secret.EncryptedValue;\nimport io.xpipe.app.secret.SecretNoneStrategy;\nimport io.xpipe.app.secret.SecretRetrievalStrategy;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreCategory;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.Validators;\nimport io.xpipe.ext.base.identity.ssh.NoIdentityStrategy;\nimport io.xpipe.ext.base.identity.ssh.SshIdentityStrategy;\n\nimport com.fasterxml.jackson.annotation.JsonSubTypes;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\n@JsonSubTypes({\n    @JsonSubTypes.Type(value = IdentityValue.InPlace.class),\n    @JsonSubTypes.Type(value = IdentityValue.Ref.class)\n})\npublic interface IdentityValue {\n\n    static IdentityValue ofCategory(DataStoreCategory category) {\n        var effective = DataStorage.get().getEffectiveCategoryConfig(category);\n        if (effective.getDefaultIdentityStore() == null) {\n            return null;\n        }\n\n        var found = DataStorage.get().getStoreEntryIfPresent(effective.getDefaultIdentityStore());\n        if (found.isEmpty() || !(found.get().getStore() instanceof IdentityStore)) {\n            return null;\n        }\n\n        return new Ref(found.get().ref());\n    }\n\n    static IdentityValue ofBreakout(DataStoreEntry e) {\n        var s = DataStorage.get();\n        if (s == null) {\n            return null;\n        }\n\n        var cat = s.getStoreCategory(e);\n        var uuid = cat.getConfig().getDefaultIdentityStore();\n        var found = s.getStoreEntryIfPresent(uuid);\n        if (found.isEmpty() || !(found.get().getStore() instanceof IdentityStore)) {\n            return null;\n        }\n\n        return new Ref(found.get().ref());\n    }\n\n    static IdentityValue.InPlace of(LocalIdentityStore identityStore) {\n        return new InPlace(identityStore);\n    }\n\n    static IdentityValue.InPlace none() {\n        var s = LocalIdentityStore.builder()\n                .password(EncryptedValue.of(new SecretNoneStrategy()))\n                .sshIdentity(EncryptedValue.of(new NoIdentityStrategy()))\n                .build();\n        return of(s);\n    }\n\n    static IdentityValue.InPlace of(String user) {\n        return of(user, null, null);\n    }\n\n    static IdentityValue.InPlace of(String user, SecretRetrievalStrategy password) {\n        return of(user, password, null);\n    }\n\n    static IdentityValue.InPlace of(String user, SecretRetrievalStrategy password, SshIdentityStrategy sshIdentity) {\n        var s = LocalIdentityStore.builder()\n                .username(user)\n                .password(password != null ? EncryptedValue.of(password) : null)\n                .sshIdentity(sshIdentity != null ? EncryptedValue.of(sshIdentity) : null)\n                .build();\n        return of(s);\n    }\n\n    void checkComplete() throws ValidationException;\n\n    IdentityStore unwrap();\n\n    boolean isPerUser();\n\n    boolean isInPlace();\n\n    default void checkCompleteUser() throws ValidationException {\n        Validators.nonNull(unwrap().getUsername().hasUser() ? new Object() : null, \"Identity username\");\n    }\n\n    default void checkCompletePassword() throws ValidationException {\n        Validators.nonNull(unwrap().getPassword(), \"Identity password\");\n        unwrap().getPassword().checkComplete();\n    }\n\n    default void checkCompleteSshIdentity() throws ValidationException {\n        Validators.nonNull(unwrap().getSshIdentity(), \"Identity ssh key\");\n        unwrap().getSshIdentity().checkComplete();\n    }\n\n    @JsonTypeName(\"inPlace\")\n    @Value\n    @Jacksonized\n    @Builder\n    class InPlace implements IdentityValue {\n\n        LocalIdentityStore identityStore;\n\n        @Override\n        public void checkComplete() throws ValidationException {\n            Validators.nonNull(identityStore);\n        }\n\n        @Override\n        public LocalIdentityStore unwrap() {\n            return identityStore != null\n                    ? identityStore\n                    : LocalIdentityStore.builder().build();\n        }\n\n        @Override\n        public boolean isPerUser() {\n            return false;\n        }\n\n        @Override\n        public boolean isInPlace() {\n            return true;\n        }\n    }\n\n    @JsonTypeName(\"ref\")\n    @Value\n    @Jacksonized\n    @Builder\n    class Ref implements IdentityValue {\n\n        DataStoreEntryRef<IdentityStore> ref;\n\n        @Override\n        public void checkComplete() throws ValidationException {\n            Validators.nonNull(ref);\n            Validators.isType(ref, IdentityStore.class);\n        }\n\n        @Override\n        public IdentityStore unwrap() {\n            return ref != null && ref.getStore() != null\n                    ? ref.getStore()\n                    : LocalIdentityStore.builder().build();\n        }\n\n        @Override\n        public boolean isPerUser() {\n            return ref != null && ref.get().isPerUserStore();\n        }\n\n        @Override\n        public boolean isInPlace() {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/LocalIdentityConvertHubLeafProvider.java",
    "content": "package io.xpipe.ext.base.identity;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.window.AppMainWindow;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.hub.comp.StoreCreationDialog;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.secret.EncryptedValue;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.application.Platform;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.control.Button;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class LocalIdentityConvertHubLeafProvider implements HubLeafProvider<LocalIdentityStore> {\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.CUSTOM;\n    }\n\n    @Override\n    public boolean isMajor() {\n        return true;\n    }\n\n    @Override\n    public boolean isApplicable(DataStoreEntryRef<LocalIdentityStore> o) {\n        return DataStorage.get().supportsSync();\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<LocalIdentityStore> store) {\n        return AppI18n.observable(\"sync\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<LocalIdentityStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2g-git\");\n    }\n\n    @Override\n    public Class<?> getApplicableClass() {\n        return LocalIdentityStore.class;\n    }\n\n    @Override\n    public AbstractAction createAction(DataStoreEntryRef<LocalIdentityStore> ref) {\n        return Action.builder().ref(ref).build();\n    }\n\n    @Override\n    public String getId() {\n        return \"convertLocalIdentity\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<LocalIdentityStore> {\n\n        @Override\n        public void executeImpl() {\n            var st = ref.getStore();\n            var synced = SyncedIdentityStore.builder()\n                    .username(st.getUsername().get())\n                    .password(EncryptedValue.VaultKey.of(st.getPassword()))\n                    .sshIdentity(EncryptedValue.VaultKey.of(st.getSshIdentity()))\n                    .perUser(false)\n                    .build();\n            StoreCreationDialog.showEdit(ref.get(), synced, ignored -> {});\n\n            // Ugly solution to sync key file if needed\n            Platform.runLater(() -> {\n                var found = AppMainWindow.get().getStage().getScene().getRoot().lookupAll(\".git-sync-file-button\");\n                if (found.size() != 1) {\n                    return;\n                }\n\n                var first = found.iterator().next();\n                if (first instanceof Button b) {\n                    b.fire();\n                }\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/LocalIdentityStore.java",
    "content": "package io.xpipe.ext.base.identity;\n\nimport io.xpipe.app.secret.EncryptedValue;\nimport io.xpipe.app.secret.SecretRetrievalStrategy;\nimport io.xpipe.ext.base.identity.ssh.SshIdentityStrategy;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport lombok.Value;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\n@SuperBuilder\n@JsonTypeName(\"localIdentity\")\n@Jacksonized\n@Value\n@EqualsAndHashCode(callSuper = true)\n@ToString(callSuper = true)\npublic class LocalIdentityStore extends IdentityStore {\n\n    String username;\n    EncryptedValue<SecretRetrievalStrategy> password;\n    EncryptedValue<SshIdentityStrategy> sshIdentity;\n\n    public UsernameStrategy.Fixed getUsername() {\n        return new UsernameStrategy.Fixed(username);\n    }\n\n    @Override\n    public SecretRetrievalStrategy getPassword() {\n        return password != null ? password.getValue() : null;\n    }\n\n    @Override\n    public SshIdentityStrategy getSshIdentity() {\n        return sshIdentity != null ? sshIdentity.getValue() : null;\n    }\n\n    EncryptedValue<SecretRetrievalStrategy> getEncryptedPassword() {\n        return password;\n    }\n\n    EncryptedValue<SshIdentityStrategy> getEncryptedSshIdentity() {\n        return sshIdentity;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/LocalIdentityStoreProvider.java",
    "content": "package io.xpipe.ext.base.identity;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.GuiDialog;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.platform.OptionsChoiceBuilder;\nimport io.xpipe.app.secret.EncryptedValue;\nimport io.xpipe.app.secret.SecretNoneStrategy;\nimport io.xpipe.app.secret.SecretRetrievalStrategy;\nimport io.xpipe.app.secret.SecretStrategyChoiceConfig;\nimport io.xpipe.app.storage.*;\nimport io.xpipe.app.util.*;\nimport io.xpipe.ext.base.identity.ssh.NoIdentityStrategy;\nimport io.xpipe.ext.base.identity.ssh.SshIdentityStrategyChoiceConfig;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.property.SimpleStringProperty;\n\nimport java.util.List;\nimport java.util.UUID;\n\npublic class LocalIdentityStoreProvider extends IdentityStoreProvider {\n\n    @Override\n    public UUID getTargetCategory(DataStore store, UUID target) {\n        var cat = DataStorage.get().getStoreCategoryIfPresent(target).orElseThrow();\n        var inLocal = DataStorage.get().getCategoryParentHierarchy(cat).stream()\n                .anyMatch(dataStoreCategory ->\n                        dataStoreCategory.getUuid().equals(DataStorage.LOCAL_IDENTITIES_CATEGORY_UUID));\n        return inLocal ? target : DataStorage.LOCAL_IDENTITIES_CATEGORY_UUID;\n    }\n\n    @Override\n    public GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {\n        LocalIdentityStore st = (LocalIdentityStore) store.getValue();\n\n        var user = new SimpleStringProperty(st.getUsername().get());\n        var pass = new SimpleObjectProperty<>(st.getPassword());\n        var identity = new SimpleObjectProperty<>(st.getSshIdentity());\n\n        var sshIdentityChoiceConfig = SshIdentityStrategyChoiceConfig.builder()\n                .allowAgentForward(true)\n                .allowKeyFileSync(false)\n                .perUserKeyFileCheck(() -> false)\n                .build();\n\n        var passwordChoice = OptionsChoiceBuilder.builder()\n                .allowNull(false)\n                .property(pass)\n                .customConfiguration(\n                        SecretStrategyChoiceConfig.builder().allowNone(true).build())\n                .available(SecretRetrievalStrategy.getClasses())\n                .build()\n                .build();\n\n        return new OptionsBuilder()\n                .nameAndDescription(\"username\")\n                .addString(user)\n                .name(\"passwordAuthentication\")\n                .description(\"passwordAuthenticationDescription\")\n                .sub(passwordChoice, pass)\n                .name(\"keyAuthentication\")\n                .description(\"keyAuthenticationDescription\")\n                .documentationLink(DocumentationLink.SSH_KEYS)\n                .sub(IdentityChoiceBuilder.keyAuthChoice(identity, sshIdentityChoiceConfig), identity)\n                .bind(\n                        () -> {\n                            return LocalIdentityStore.builder()\n                                    .username(user.get())\n                                    .password(\n                                            st.getEncryptedPassword() != null\n                                                    ? st.getEncryptedPassword().withValue(pass.get())\n                                                    : EncryptedValue.of(pass.get()))\n                                    .sshIdentity(\n                                            st.getEncryptedSshIdentity() != null\n                                                    ? st.getEncryptedSshIdentity()\n                                                            .withValue(identity.get())\n                                                    : EncryptedValue.of(identity.get()))\n                                    .build();\n                        },\n                        store)\n                .buildDialog();\n    }\n\n    @Override\n    public DataStore defaultStore(DataStoreCategory category) {\n        return LocalIdentityStore.builder()\n                .password(EncryptedValue.of(new SecretNoneStrategy()))\n                .sshIdentity(EncryptedValue.of(new NoIdentityStrategy()))\n                .build();\n    }\n\n    @Override\n    public String getId() {\n        return \"localIdentity\";\n    }\n\n    @Override\n    public List<Class<?>> getStoreClasses() {\n        return List.of(LocalIdentityStore.class);\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/PasswordManagerIdentityStore.java",
    "content": "package io.xpipe.ext.base.identity;\n\nimport io.xpipe.app.ext.InternalCacheDataStore;\nimport io.xpipe.app.ext.UserScopeStore;\nimport io.xpipe.app.ext.ValidatableStore;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.pwman.PasswordManager;\nimport io.xpipe.app.secret.SecretQuery;\nimport io.xpipe.app.secret.SecretQueryResult;\nimport io.xpipe.app.secret.SecretQueryState;\nimport io.xpipe.app.secret.SecretRetrievalStrategy;\nimport io.xpipe.app.util.*;\nimport io.xpipe.ext.base.identity.ssh.NoIdentityStrategy;\nimport io.xpipe.ext.base.identity.ssh.SshIdentityStrategy;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport lombok.Value;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.time.Duration;\nimport java.time.Instant;\n\n@SuperBuilder\n@JsonTypeName(\"passwordManagerIdentity\")\n@Jacksonized\n@Value\n@EqualsAndHashCode(callSuper = true)\n@ToString(callSuper = true)\npublic class PasswordManagerIdentityStore extends IdentityStore\n        implements InternalCacheDataStore, ValidatableStore, UserScopeStore {\n\n    String key;\n    boolean perUser;\n\n    private boolean checkOutdatedOrRefresh() {\n        var instant = getCache(\"lastQueried\", Instant.class, null);\n        if (instant != null) {\n            var now = Instant.now();\n            var pm = AppPrefs.get().passwordManager().getValue();\n            var cacheDuration = pm != null ? pm.getCacheDuration().toSeconds() : 15;\n            if (Duration.between(instant, now).toSeconds() < cacheDuration) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    private PasswordManager.CredentialResult retrieveCredentials() {\n        if (!checkOutdatedOrRefresh()) {\n            var credential = getCache(\"credential\", PasswordManager.CredentialResult.class, null);\n            if (credential != null) {\n                return credential;\n            }\n        }\n\n        if (AppPrefs.get() == null || AppPrefs.get().passwordManager().getValue() == null) {\n            return null;\n        }\n\n        var r = AppPrefs.get().passwordManager().getValue().retrieveCredentials(key);\n        if (r == null) {\n            throw ErrorEventFactory.expected(\n                    new UnsupportedOperationException(\"Credentials were requested but not supplied\"));\n        }\n\n        if (r.getUsername() == null) {\n            throw ErrorEventFactory.expected(\n                    new UnsupportedOperationException(\"Identity \" + key + \" does not provide a username\"));\n        }\n\n        if (r.getPassword() == null) {\n            throw ErrorEventFactory.expected(\n                    new UnsupportedOperationException(\"Identity \" + key + \" does not provide a password\"));\n        }\n\n        setCache(\"lastQueried\", Instant.now());\n        setCache(\"credential\", r);\n\n        return r;\n    }\n\n    public UsernameStrategy getUsername() {\n        return new UsernameStrategy.Dynamic(() -> {\n            var r = retrieveCredentials();\n            var effective = r != null && r.getUsername() != null ? r.getUsername() : \"unknown\";\n            return effective;\n        });\n    }\n\n    @Override\n    public SecretRetrievalStrategy getPassword() {\n        return new SecretRetrievalStrategy() {\n\n            @Override\n            public SecretQuery query() {\n                return new SecretQuery() {\n                    @Override\n                    public SecretQueryResult query(String prompt) {\n                        var r = retrieveCredentials();\n                        if (r == null || r.getPassword() == null) {\n                            return new SecretQueryResult(null, SecretQueryState.RETRIEVAL_FAILURE);\n                        }\n\n                        return new SecretQueryResult(r.getPassword(), SecretQueryState.NORMAL);\n                    }\n\n                    @Override\n                    public Duration cacheDuration() {\n                        return null;\n                    }\n\n                    @Override\n                    public boolean retryOnFail() {\n                        return false;\n                    }\n\n                    @Override\n                    public boolean requiresUserInteraction() {\n                        return false;\n                    }\n                };\n            }\n        };\n    }\n\n    @Override\n    public SshIdentityStrategy getSshIdentity() {\n        return new NoIdentityStrategy();\n    }\n\n    @Override\n    public void checkComplete() throws Throwable {\n        Validators.nonNull(key);\n    }\n\n    @Override\n    public void validate() {\n        retrieveCredentials();\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/PasswordManagerIdentityStoreProvider.java",
    "content": "package io.xpipe.ext.base.identity;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.DataStoreCreationCategory;\nimport io.xpipe.app.ext.GuiDialog;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.prefs.PasswordManagerTestComp;\nimport io.xpipe.app.storage.DataStorageUserHandler;\nimport io.xpipe.app.storage.DataStoreCategory;\nimport io.xpipe.app.storage.DataStoreEntry;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleStringProperty;\n\nimport java.util.List;\n\npublic class PasswordManagerIdentityStoreProvider extends IdentityStoreProvider {\n\n    @Override\n    public DataStoreCreationCategory getCreationCategory() {\n        return AppPrefs.get().passwordManager().getValue() != null ? DataStoreCreationCategory.IDENTITY : null;\n    }\n\n    @Override\n    public GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {\n        PasswordManagerIdentityStore st = (PasswordManagerIdentityStore) store.getValue();\n\n        var key = new SimpleStringProperty(st.getKey());\n        var perUser = new SimpleBooleanProperty(st.isPerUser());\n\n        var comp = new PasswordManagerTestComp(key, false);\n        return new OptionsBuilder()\n                .nameAndDescription(\"passwordManagerKey\")\n                .addComp(comp.hgrow(), key)\n                .nonNull()\n                .nameAndDescription(\n                        DataStorageUserHandler.getInstance().getActiveUser() != null\n                                ? \"identityPerUser\"\n                                : \"identityPerUserDisabled\")\n                .addToggle(perUser)\n                .hide(DataStorageUserHandler.getInstance().getActiveUser() == null)\n                .bind(\n                        () -> {\n                            return PasswordManagerIdentityStore.builder()\n                                    .key(key.get())\n                                    .build();\n                        },\n                        store)\n                .buildDialog();\n    }\n\n    @Override\n    public DataStore defaultStore(DataStoreCategory category) {\n        return PasswordManagerIdentityStore.builder().key(null).build();\n    }\n\n    @Override\n    public String getId() {\n        return \"passwordManagerIdentity\";\n    }\n\n    @Override\n    public List<Class<?>> getStoreClasses() {\n        return List.of(PasswordManagerIdentityStore.class);\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/SyncedIdentityStore.java",
    "content": "package io.xpipe.ext.base.identity;\n\nimport io.xpipe.app.ext.UserScopeStore;\nimport io.xpipe.app.ext.ValidationException;\nimport io.xpipe.app.secret.EncryptedValue;\nimport io.xpipe.app.secret.SecretRetrievalStrategy;\nimport io.xpipe.ext.base.identity.ssh.KeyFileStrategy;\nimport io.xpipe.ext.base.identity.ssh.SshIdentityStrategy;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport lombok.Value;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\n@SuperBuilder\n@JsonTypeName(\"syncedIdentity\")\n@Value\n@EqualsAndHashCode(callSuper = true)\n@ToString(callSuper = true)\n@Jacksonized\npublic class SyncedIdentityStore extends IdentityStore implements UserScopeStore {\n\n    String username;\n    // We can encrypt it with only the vault key as\n    // per user stores are additionally encrypted on the entry level\n    EncryptedValue.VaultKey<SecretRetrievalStrategy> password;\n    EncryptedValue.VaultKey<SshIdentityStrategy> sshIdentity;\n    boolean perUser;\n\n    public UsernameStrategy.Fixed getUsername() {\n        return new UsernameStrategy.Fixed(username);\n    }\n\n    @Override\n    public SecretRetrievalStrategy getPassword() {\n        return password != null ? password.getValue() : null;\n    }\n\n    @Override\n    public SshIdentityStrategy getSshIdentity() {\n        return sshIdentity != null ? sshIdentity.getValue() : null;\n    }\n\n    @Override\n    public void checkComplete() throws Throwable {\n        super.checkComplete();\n        if (getSshIdentity() instanceof KeyFileStrategy f) {\n            if (!f.getFile().isInDataDirectory()) {\n                throw new ValidationException(\"Key file is not synced\");\n            }\n        }\n    }\n\n    EncryptedValue.VaultKey<SecretRetrievalStrategy> getEncryptedPassword() {\n        return password;\n    }\n\n    EncryptedValue.VaultKey<SshIdentityStrategy> getEncryptedSshIdentity() {\n        return sshIdentity;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/SyncedIdentityStoreProvider.java",
    "content": "package io.xpipe.ext.base.identity;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.DataStoreCreationCategory;\nimport io.xpipe.app.ext.GuiDialog;\nimport io.xpipe.app.hub.comp.StoreEntryWrapper;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.platform.OptionsChoiceBuilder;\nimport io.xpipe.app.platform.Validator;\nimport io.xpipe.app.prefs.VaultAuthentication;\nimport io.xpipe.app.secret.EncryptedValue;\nimport io.xpipe.app.secret.SecretNoneStrategy;\nimport io.xpipe.app.secret.SecretRetrievalStrategy;\nimport io.xpipe.app.secret.SecretStrategyChoiceConfig;\nimport io.xpipe.app.storage.*;\nimport io.xpipe.app.util.*;\nimport io.xpipe.ext.base.identity.ssh.KeyFileStrategy;\nimport io.xpipe.ext.base.identity.ssh.NoIdentityStrategy;\nimport io.xpipe.ext.base.identity.ssh.SshIdentityStrategyChoiceConfig;\n\nimport javafx.beans.property.*;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.UUID;\n\npublic class SyncedIdentityStoreProvider extends IdentityStoreProvider {\n\n    @Override\n    public DataStoreCreationCategory getCreationCategory() {\n        return DataStorage.get().supportsSync() ? DataStoreCreationCategory.IDENTITY : null;\n    }\n\n    @Override\n    public UUID getTargetCategory(DataStore store, UUID target) {\n        var cat = DataStorage.get().getStoreCategoryIfPresent(target).orElseThrow();\n        var inSynced = DataStorage.get().getCategoryParentHierarchy(cat).stream()\n                .anyMatch(dataStoreCategory ->\n                        dataStoreCategory.getUuid().equals(DataStorage.SYNCED_IDENTITIES_CATEGORY_UUID));\n        return inSynced ? target : DataStorage.SYNCED_IDENTITIES_CATEGORY_UUID;\n    }\n\n    @Override\n    public GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {\n        SyncedIdentityStore st = (SyncedIdentityStore) store.getValue();\n\n        var user = new SimpleStringProperty(st.getUsername().get());\n        var pass = new SimpleObjectProperty<>(st.getPassword());\n        var identity = new SimpleObjectProperty<>(st.getSshIdentity());\n        var perUser = new SimpleBooleanProperty(st.isPerUser());\n        perUser.addListener((observable, oldValue, newValue) -> {\n            if (!(identity.getValue() instanceof KeyFileStrategy f)\n                    || f.getFile() == null\n                    || !f.getFile().isInDataDirectory()) {\n                return;\n            }\n\n            var source = Path.of(f.getFile().toAbsoluteFilePath(null).toString());\n            var target = DataStorage.get()\n                    .getDataDir()\n                    .resolve(\"keys\", f.getFile().toAbsoluteFilePath(null).getFileName());\n            DataStorageSyncHandler.getInstance().addDataFile(source, target, newValue);\n\n            var pub = Path.of(source + \".pub\");\n            var pubTarget = DataStorage.get()\n                    .getDataDir()\n                    .resolve(\"keys\", f.getFile().toAbsoluteFilePath(null).getFileName() + \".pub\");\n            if (Files.exists(pub)) {\n                DataStorageSyncHandler.getInstance().addDataFile(pub, pubTarget, newValue);\n            }\n        });\n\n        var sshIdentityChoiceConfig = SshIdentityStrategyChoiceConfig.builder()\n                .allowAgentForward(true)\n                .allowKeyFileSync(true)\n                .perUserKeyFileCheck(() -> perUser.get())\n                .build();\n\n        var passwordChoice = OptionsChoiceBuilder.builder()\n                .allowNull(false)\n                .property(pass)\n                .customConfiguration(\n                        SecretStrategyChoiceConfig.builder().allowNone(true).build())\n                .available(SecretRetrievalStrategy.getClasses())\n                .build()\n                .build();\n\n        var handler = DataStorageUserHandler.getInstance();\n        return new OptionsBuilder()\n                .nameAndDescription(\"username\")\n                .addString(user)\n                .name(\"passwordAuthentication\")\n                .description(\"passwordAuthenticationDescription\")\n                .sub(passwordChoice, pass)\n                .name(\"keyAuthentication\")\n                .description(\"keyAuthenticationDescription\")\n                .documentationLink(DocumentationLink.SSH_KEYS)\n                .sub(IdentityChoiceBuilder.keyAuthChoice(identity, sshIdentityChoiceConfig), identity)\n                .check(val -> Validator.create(val, AppI18n.observable(\"keyNotSynced\"), identity, i -> {\n                    var wrong = i instanceof KeyFileStrategy f\n                            && f.getFile() != null\n                            && !f.getFile().isInDataDirectory();\n                    return !wrong;\n                }))\n                .nameAndDescription(\n                        handler.getActiveUser() != null\n                                ? (handler.getVaultAuthenticationType() == VaultAuthentication.GROUP\n                                        ? \"identityPerGroup\"\n                                        : \"identityPerUser\")\n                                : \"identityPerUserDisabled\")\n                .addToggle(perUser)\n                .disable(handler.getActiveUser() == null)\n                .bind(\n                        () -> {\n                            return SyncedIdentityStore.builder()\n                                    .username(user.get())\n                                    .password(\n                                            st.getEncryptedPassword() != null\n                                                    ? st.getEncryptedPassword().withValue(pass.get())\n                                                    : EncryptedValue.VaultKey.of(pass.get()))\n                                    .sshIdentity(\n                                            st.getEncryptedSshIdentity() != null\n                                                    ? st.getEncryptedSshIdentity()\n                                                            .withValue(identity.get())\n                                                    : EncryptedValue.VaultKey.of(identity.get()))\n                                    .password(EncryptedValue.VaultKey.of(pass.get()))\n                                    .sshIdentity(EncryptedValue.VaultKey.of(identity.get()))\n                                    .perUser(perUser.get())\n                                    .build();\n                        },\n                        store)\n                .buildDialog();\n    }\n\n    @Override\n    public String summaryString(StoreEntryWrapper wrapper) {\n        return wrapper.getEntry().isPerUserStore() ? AppI18n.get(\"userIdentity\") : AppI18n.get(\"globalIdentity\");\n    }\n\n    @Override\n    public DataStore defaultStore(DataStoreCategory category) {\n        return SyncedIdentityStore.builder()\n                .password(EncryptedValue.VaultKey.of(new SecretNoneStrategy()))\n                .sshIdentity(EncryptedValue.VaultKey.of(new NoIdentityStrategy()))\n                .perUser(false)\n                .build();\n    }\n\n    @Override\n    public String getId() {\n        return \"syncedIdentity\";\n    }\n\n    @Override\n    public List<Class<?>> getStoreClasses() {\n        return List.of(SyncedIdentityStore.class);\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/UsernameStrategy.java",
    "content": "package io.xpipe.ext.base.identity;\n\nimport io.xpipe.core.FailableSupplier;\n\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\n\nimport java.util.Optional;\n\npublic interface UsernameStrategy {\n\n    boolean hasUser();\n\n    Optional<String> getFixedUsername();\n\n    String retrieveUsername() throws Exception;\n\n    @EqualsAndHashCode\n    @ToString\n    class None implements UsernameStrategy {\n\n        @Override\n        public boolean hasUser() {\n            return false;\n        }\n\n        @Override\n        public Optional<String> getFixedUsername() {\n            return Optional.empty();\n        }\n\n        @Override\n        public String retrieveUsername() {\n            return null;\n        }\n    }\n\n    @EqualsAndHashCode\n    @ToString\n    final class Fixed implements UsernameStrategy {\n\n        private final String username;\n\n        public Fixed(String username) {\n            this.username = username;\n        }\n\n        public String get() {\n            return username;\n        }\n\n        @Override\n        public boolean hasUser() {\n            return getFixedUsername().isPresent();\n        }\n\n        @Override\n        public Optional<String> getFixedUsername() {\n            return Optional.ofNullable(username);\n        }\n\n        @Override\n        public String retrieveUsername() {\n            return getFixedUsername().orElseThrow();\n        }\n    }\n\n    final class Dynamic implements UsernameStrategy {\n\n        private final FailableSupplier<String> username;\n\n        public Dynamic(FailableSupplier<String> username) {\n            this.username = username;\n        }\n\n        @Override\n        public int hashCode() {\n            return getClass().hashCode();\n        }\n\n        @Override\n        public boolean equals(Object obj) {\n            return obj instanceof Dynamic;\n        }\n\n        @Override\n        public String toString() {\n            return \"<dynamic>\";\n        }\n\n        @Override\n        public boolean hasUser() {\n            return true;\n        }\n\n        @Override\n        public Optional<String> getFixedUsername() {\n            return Optional.empty();\n        }\n\n        @Override\n        public String retrieveUsername() throws Exception {\n            var r = username.get();\n            return r;\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/ssh/CustomAgentStrategy.java",
    "content": "package io.xpipe.ext.base.identity.ssh;\n\nimport io.xpipe.app.comp.base.ButtonComp;\nimport io.xpipe.app.comp.base.HorizontalComp;\nimport io.xpipe.app.comp.base.TextFieldComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.platform.Validator;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.core.KeyValue;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.geometry.Insets;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@JsonTypeName(\"customAgent\")\n@Value\n@Jacksonized\n@Builder\npublic class CustomAgentStrategy implements SshIdentityStrategy {\n\n    @SuppressWarnings(\"unused\")\n    public static OptionsBuilder createOptions(\n            Property<CustomAgentStrategy> p, SshIdentityStrategyChoiceConfig config) {\n        var forward =\n                new SimpleBooleanProperty(p.getValue() != null && p.getValue().isForwardAgent());\n        var publicKey =\n                new SimpleStringProperty(p.getValue() != null ? p.getValue().getPublicKey() : null);\n\n        var socketBinding = Bindings.createObjectBinding(() -> {\n            var agent = AppPrefs.get().sshAgentSocket().getValue();\n            if (agent == null) {\n                agent = AppPrefs.get().defaultSshAgentSocket().getValue();\n            }\n            return agent != null ? agent.toString() : AppI18n.get(\"agentSocketNotConfigured\");\n        }, AppPrefs.get().defaultSshAgentSocket(), AppPrefs.get().sshAgentSocket());\n        var socketProp = new SimpleStringProperty();\n        socketProp.bind(socketBinding);\n        var socketDisplay = new HorizontalComp(List.of(\n                        new TextFieldComp(socketProp)\n                                .apply(struc -> struc.setEditable(false))\n                                .hgrow(),\n                        new ButtonComp(null, new FontIcon(\"mdomz-settings\"), () -> {\n                                    AppPrefs.get().selectCategory(\"ssh\");\n                                })\n                                .padding(new Insets(7))))\n                .spacing(9);\n\n        return new OptionsBuilder()\n                .nameAndDescription(\"agentSocket\")\n                .addComp(socketDisplay)\n                .check(val -> Validator.create(\n                        val,\n                        AppI18n.observable(\"agentSocketNotConfigured\"), Bindings.createObjectBinding(() -> {\n                            var agent = AppPrefs.get().sshAgentSocket().getValue();\n                            if (agent == null) {\n                                agent = AppPrefs.get().defaultSshAgentSocket().getValue();\n                            }\n                            return agent;\n                        }, AppPrefs.get().sshAgentSocket(), AppPrefs.get().defaultSshAgentSocket()),\n                        i -> {\n                            return i != null;\n                        }))\n                .nameAndDescription(\"forwardAgent\")\n                .addToggle(forward)\n                .nonNull()\n                .hide(!config.isAllowAgentForward())\n                .nameAndDescription(\"publicKey\")\n                .addComp(\n                        new TextFieldComp(publicKey)\n                                .apply(struc -> struc.setPromptText(\n                                        \"ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBmhLUTJiP...== Your Comment\")),\n                        publicKey)\n                .bind(\n                        () -> {\n                            return new CustomAgentStrategy(forward.get(), publicKey.get());\n                        },\n                        p);\n    }\n\n    boolean forwardAgent;\n    String publicKey;\n\n    @Override\n    public void prepareParent(ShellControl parent) throws Exception {\n        if (parent.isLocal()) {\n            var agent = AppPrefs.get().sshAgentSocket().getValue();\n            if (agent == null) {\n                agent = AppPrefs.get().defaultSshAgentSocket().getValue();\n            }\n            SshIdentityStateManager.prepareLocalCustomAgent(\n                    parent, agent);\n        }\n    }\n\n    @Override\n    public void buildCommand(CommandBuilder builder) {}\n\n    private String getIdentityAgent(ShellControl sc) throws Exception {\n        if (!sc.isLocal() || sc.getOsType() == OsType.WINDOWS) {\n            return null;\n        }\n\n        if (AppPrefs.get() != null) {\n            var agent = AppPrefs.get().sshAgentSocket().getValue();\n            if (agent == null) {\n                agent = AppPrefs.get().defaultSshAgentSocket().getValue();\n            }\n            if (agent != null) {\n                return agent.resolveTildeHome(sc.view().userHome()).toString();\n            }\n        }\n\n        return null;\n    }\n\n    @Override\n    public List<KeyValue> configOptions(ShellControl sc) throws Exception {\n        var file = SshIdentityStrategy.getPublicKeyPath(sc, publicKey);\n        var l = new ArrayList<>(List.of(\n                new KeyValue(\"IdentitiesOnly\", file.isPresent() ? \"yes\" : \"no\"),\n                new KeyValue(\"ForwardAgent\", forwardAgent ? \"yes\" : \"no\"),\n                new KeyValue(\"IdentityFile\", file.isPresent() ? file.get().toString() : \"none\"),\n                new KeyValue(\"PKCS11Provider\", \"none\")));\n\n        var agent = getIdentityAgent(sc);\n        if (agent != null) {\n            l.add(new KeyValue(\"IdentityAgent\", \"\\\"\" + agent + \"\\\"\"));\n        }\n\n        return l;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/ssh/CustomPkcs11LibraryStrategy.java",
    "content": "package io.xpipe.ext.base.identity.ssh;\n\nimport io.xpipe.app.comp.base.ContextualFileReferenceChoiceComp;\nimport io.xpipe.app.ext.ValidationException;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.util.LicenseProvider;\nimport io.xpipe.app.util.Validators;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.KeyValue;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.ReadOnlyObjectWrapper;\nimport javafx.beans.property.SimpleObjectProperty;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.io.IOException;\nimport java.util.List;\n\n@Value\n@Jacksonized\n@Builder\n@JsonTypeName(\"customPkcs11\")\n@AllArgsConstructor\npublic class CustomPkcs11LibraryStrategy implements SshIdentityStrategy {\n\n    @SuppressWarnings(\"unused\")\n    public static String getOptionsNameKey() {\n        return \"customPkcs11Library\";\n    }\n\n    @SuppressWarnings(\"unused\")\n    public static OptionsBuilder createOptions(\n            Property<CustomPkcs11LibraryStrategy> p, SshIdentityStrategyChoiceConfig config) {\n\n        var file =\n                new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getFile() : null);\n        return new OptionsBuilder()\n                .nameAndDescription(\"pkcs11Library\")\n                .addComp(\n                        new ContextualFileReferenceChoiceComp(\n                                config.getFileSystem() != null\n                                        ? config.getFileSystem()\n                                        : new ReadOnlyObjectWrapper<>(\n                                                DataStorage.get().local().ref()),\n                                file,\n                                null,\n                                List.of(),\n                                e -> {\n                                    if (config.getFileSystem() == null) {\n                                        return e.equals(DataStorage.get().local());\n                                    }\n\n                                    var fs = config.getFileSystem().getValue();\n                                    if (fs == null) {\n                                        return e.equals(DataStorage.get().local());\n                                    } else {\n                                        return e.equals(fs.get());\n                                    }\n                                },\n                                false),\n                        file)\n                .nonNull()\n                .bind(\n                        () -> {\n                            return new CustomPkcs11LibraryStrategy(file.get());\n                        },\n                        p);\n    }\n\n    FilePath file;\n\n    @Override\n    public void checkComplete() throws ValidationException {\n        Validators.nonNull(file);\n    }\n\n    @Override\n    public void prepareParent(ShellControl parent) throws Exception {\n        parent.requireLicensedFeature(LicenseProvider.get().getFeature(\"pkcs11Identity\"));\n\n        if (!parent.getShellDialect()\n                .createFileExistsCommand(parent, file.toString())\n                .executeAndCheck()) {\n            throw ErrorEventFactory.expected(new IOException(\"PKCS11 library at \" + file + \" not found\"));\n        }\n    }\n\n    @Override\n    public void buildCommand(CommandBuilder builder) {\n        builder.setup(sc -> {\n            var dir = file.getParent();\n            if (sc.getOsType() == OsType.WINDOWS) {\n                builder.addToPath(dir, true);\n            } else {\n                builder.addToEnvironmentPath(\"LD_LIBRARY_PATH\", dir, true);\n            }\n        });\n    }\n\n    @Override\n    public List<KeyValue> configOptions(ShellControl sc) {\n        return List.of(\n                new KeyValue(\"IdentitiesOnly\", \"no\"),\n                new KeyValue(\"PKCS11Provider\", \"\\\"\" + file.toString() + \"\\\"\"),\n                new KeyValue(\"IdentityFile\", \"none\"),\n                new KeyValue(\"IdentityAgent\", \"none\"));\n    }\n\n    @Override\n    public String getPublicKey() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/ssh/GpgAgentStrategy.java",
    "content": "package io.xpipe.ext.base.identity.ssh;\n\nimport io.xpipe.app.comp.base.TextFieldComp;\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.util.LicenseProvider;\nimport io.xpipe.core.KeyValue;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleStringProperty;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.nio.file.Files;\nimport java.util.ArrayList;\nimport java.util.List;\n\n@Value\n@Jacksonized\n@Builder\n@JsonTypeName(\"gpgAgent\")\npublic class GpgAgentStrategy implements SshIdentityStrategy {\n\n    @SuppressWarnings(\"unused\")\n    public static OptionsBuilder createOptions(Property<GpgAgentStrategy> p, SshIdentityStrategyChoiceConfig config) {\n        var forward =\n                new SimpleBooleanProperty(p.getValue() != null && p.getValue().isForwardAgent());\n        var publicKey =\n                new SimpleStringProperty(p.getValue() != null ? p.getValue().getPublicKey() : null);\n        return new OptionsBuilder()\n                .nameAndDescription(\"forwardAgent\")\n                .addToggle(forward)\n                .nonNull()\n                .hide(!config.isAllowAgentForward())\n                .nameAndDescription(\"publicKey\")\n                .addComp(\n                        new TextFieldComp(publicKey)\n                                .apply(struc -> struc.setPromptText(\n                                        \"ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBmhLUTJiP...== Your Comment\")),\n                        publicKey)\n                .bind(\n                        () -> {\n                            return new GpgAgentStrategy(forward.get(), publicKey.get());\n                        },\n                        p);\n    }\n\n    private static Boolean supported;\n\n    public static boolean isSupported() {\n        if (supported != null) {\n            return supported;\n        }\n\n        try {\n            var found = LocalShell.getShell()\n                    .view()\n                    .findProgram(\"gpg-connect-agent\")\n                    .isPresent();\n            if (!found) {\n                return (supported = false);\n            }\n        } catch (Exception ex) {\n            return (supported = false);\n        }\n\n        if (OsType.ofLocal() == OsType.WINDOWS) {\n            var file = AppSystemInfo.ofWindows().getRoamingAppData().resolve(\"gnupg\", \"gpg-agent.conf\");\n            return (supported = Files.exists(file));\n        } else {\n            var file = AppSystemInfo.ofCurrent().getUserHome().resolve(\".gnupg\", \"gpg-agent.conf\");\n            return (supported = Files.exists(file));\n        }\n    }\n\n    boolean forwardAgent;\n    String publicKey;\n\n    @Override\n    public void prepareParent(ShellControl parent) throws Exception {\n        parent.requireLicensedFeature(LicenseProvider.get().getFeature(\"gpgAgent\"));\n        if (parent.isLocal()) {\n            SshIdentityStateManager.prepareLocalGpgAgent();\n        }\n    }\n\n    @Override\n    public void buildCommand(CommandBuilder builder) {}\n\n    private String getIdentityAgent(ShellControl sc) throws Exception {\n        if (sc.getOsType() == OsType.WINDOWS) {\n            return null;\n        }\n\n        var r = sc.command(\"gpgconf --list-dirs agent-ssh-socket\").readStdoutOrThrow();\n        if (r.isEmpty()) {\n            return null;\n        }\n\n        return r;\n    }\n\n    @Override\n    public List<KeyValue> configOptions(ShellControl sc) throws Exception {\n        var file = SshIdentityStrategy.getPublicKeyPath(sc, publicKey);\n        var l = new ArrayList<>(List.of(\n                new KeyValue(\"IdentitiesOnly\", file.isPresent() ? \"yes\" : \"no\"),\n                new KeyValue(\"ForwardAgent\", forwardAgent ? \"yes\" : \"no\"),\n                new KeyValue(\"IdentityFile\", file.isPresent() ? file.get().toString() : \"none\"),\n                new KeyValue(\"PKCS11Provider\", \"none\")));\n\n        var agent = getIdentityAgent(sc);\n        if (agent != null) {\n            l.add(new KeyValue(\"IdentityAgent\", \"\\\"\" + agent + \"\\\"\"));\n        }\n\n        return l;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/ssh/InPlaceKeyStrategy.java",
    "content": "package io.xpipe.ext.base.identity.ssh;\n\nimport io.xpipe.app.comp.base.ButtonComp;\nimport io.xpipe.app.comp.base.InputGroupComp;\nimport io.xpipe.app.comp.base.TextAreaComp;\nimport io.xpipe.app.comp.base.TextFieldComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.ext.ValidationException;\nimport io.xpipe.app.platform.ClipboardHelper;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.platform.OptionsChoiceBuilder;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.secret.SecretRetrievalStrategy;\nimport io.xpipe.app.secret.SecretStrategyChoiceConfig;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.app.util.LocalFileTracker;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.app.util.Validators;\nimport io.xpipe.core.*;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.nio.charset.StandardCharsets;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.stream.Collectors;\n\n@Value\n@Jacksonized\n@Builder\n@JsonTypeName(\"inPlaceKey\")\n@AllArgsConstructor\npublic class InPlaceKeyStrategy implements SshIdentityStrategy {\n\n    @SuppressWarnings(\"unused\")\n    public static OptionsBuilder createOptions(Property<InPlaceKeyStrategy> p, SshIdentityStrategyChoiceConfig config) {\n        var options = new OptionsBuilder();\n\n        var key = options.map(p, InPlaceKeyStrategy::getKey, SecretValue::getSecretValue);\n        var publicKey = options.map(p, InPlaceKeyStrategy::getPublicKey);\n        var keyPasswordProperty = options.map(p, InPlaceKeyStrategy::getPassword);\n\n        var passwordChoice = OptionsChoiceBuilder.builder()\n                .allowNull(false)\n                .property(keyPasswordProperty)\n                .customConfiguration(SecretStrategyChoiceConfig.builder()\n                        .allowNone(true)\n                        .passwordKey(\"passphrase\")\n                        .build())\n                .available(SecretRetrievalStrategy.getClasses())\n                .build()\n                .build();\n        var publicKeyField = new TextFieldComp(publicKey).apply(struc -> {\n            struc.promptTextProperty()\n                    .bind(Bindings.createStringBinding(\n                            () -> {\n                                return \"ssh-... ABCDEF.... (\" + AppI18n.get(\"publicKeyGenerateNotice\") + \")\";\n                            },\n                            AppI18n.activeLanguage()));\n            struc.setEditable(false);\n        });\n        var generateButton = new ButtonComp(null, new LabelGraphic.IconGraphic(\"mdi2c-cog-refresh-outline\"), () -> {\n                    ThreadHelper.runAsync(() -> {\n                        var generated = ProcessControlProvider.get()\n                                .generatePublicSshKey(InPlaceSecretValue.of(key.get()), keyPasswordProperty.get());\n                        if (generated != null) {\n                            publicKey.set(generated);\n                        }\n                    });\n                })\n                .describe(d -> d.nameKey(\"generatePublicKey\"))\n                .disable(key.isNull().or(publicKey.isNotNull()).or(keyPasswordProperty.isNull()));\n        var copyButton = new ButtonComp(null, new FontIcon(\"mdi2c-clipboard-multiple-outline\"), () -> {\n                    ClipboardHelper.copyText(publicKey.get());\n                })\n                .disable(publicKey.isNull())\n                .describe(d -> d.nameKey(\"copyPublicKey\"));\n\n        var publicKeyBox = new InputGroupComp(List.of(publicKeyField, copyButton, generateButton));\n        publicKeyBox.setMainReference(publicKeyField);\n\n        return options.nameAndDescription(\"inPlaceKeyText\")\n                .addComp(\n                        new TextAreaComp(key).applyStructure(struc -> {\n                            struc.getTextArea().setPromptText(\"\"\"\n                                                      -----BEGIN ... PRIVATE KEY-----\n\n\n                                                      -----END   ... PRIVATE KEY-----\n                                                      \"\"\");\n                            struc.getTextArea().setPrefRowCount(4);\n                        }),\n                        key)\n                .nonNull()\n                .name(\"keyPassword\")\n                .description(\"sshConfigHost.identityPassphraseDescription\")\n                .sub(passwordChoice, keyPasswordProperty)\n                .nonNull()\n                .nameAndDescription(\"inPlacePublicKey\")\n                .documentationLink(DocumentationLink.SSH_PUBLIC_KEY)\n                .addComp(publicKeyBox, publicKey)\n                .bind(\n                        () -> {\n                            return new InPlaceKeyStrategy(\n                                    key.getValue() != null ? InPlaceSecretValue.of(key.getValue()) : null,\n                                    publicKey.get(),\n                                    keyPasswordProperty.getValue());\n                        },\n                        p);\n    }\n\n    SecretValue key;\n    String publicKey;\n    SecretRetrievalStrategy password;\n\n    public void checkComplete() throws ValidationException {\n        Validators.nonNull(key);\n        Validators.nonNull(password);\n    }\n\n    @Override\n    public synchronized void prepareParent(ShellControl parent) throws Exception {\n        if (key == null) {\n            return;\n        }\n\n        var file = getTargetFilePath(parent);\n        if (parent.view().fileExists(file)) {\n            return;\n        }\n\n        parent.view().touch(file);\n        if (parent.getOsType() != OsType.WINDOWS) {\n            parent.command(CommandBuilder.of().add(\"chmod\", \"600\").addFile(file))\n                    .execute();\n        }\n        // Make sure that the line endings are in LF\n        // to support older SSH clients that break with CRLF\n        var bytes = (key.getSecretValue().lines().collect(Collectors.joining(\"\\n\")) + \"\\n\")\n                .getBytes(StandardCharsets.UTF_8);\n        parent.view().writeRawFile(file, bytes);\n        if (parent.getOsType() != OsType.WINDOWS) {\n            parent.command(CommandBuilder.of().add(\"chmod\", \"400\").addFile(file))\n                    .execute();\n        }\n\n        if (parent.isLocal()) {\n            LocalFileTracker.deleteOnExit(file.asLocalPath());\n        }\n    }\n\n    @Override\n    public void buildCommand(CommandBuilder builder) {}\n\n    @Override\n    public List<KeyValue> configOptions(ShellControl sc) {\n        return List.of(\n                new KeyValue(\"IdentitiesOnly\", \"yes\"),\n                new KeyValue(\"IdentityAgent\", \"none\"),\n                new KeyValue(\"IdentityFile\", \"\\\"\" + getTargetFilePath(sc) + \"\\\"\"),\n                new KeyValue(\"PKCS11Provider\", \"none\"));\n    }\n\n    @Override\n    public SecretRetrievalStrategy getAskpassStrategy() {\n        return password;\n    }\n\n    private FilePath getTargetFilePath(ShellControl sc) {\n        var temp = sc.getSystemTemporaryDirectory()\n                .join(\"xpipe-\"\n                        + Math.abs(Objects.hash(this, AppSystemInfo.ofCurrent().getUser())) + \".key\");\n        return temp;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/ssh/KeyFileStrategy.java",
    "content": "package io.xpipe.ext.base.identity.ssh;\n\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.ext.ValidationException;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.ClipboardHelper;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.platform.OptionsChoiceBuilder;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.secret.SecretRetrievalStrategy;\nimport io.xpipe.app.secret.SecretStrategyChoiceConfig;\nimport io.xpipe.app.storage.ContextualFileReference;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.app.util.Validators;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.InPlaceSecretValue;\nimport io.xpipe.core.KeyValue;\nimport io.xpipe.core.OsType;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.ReadOnlyObjectWrapper;\nimport javafx.beans.property.SimpleObjectProperty;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.List;\n\n@Value\n@Jacksonized\n@Builder\n@JsonTypeName(\"file\")\n@AllArgsConstructor\npublic class KeyFileStrategy implements SshIdentityStrategy {\n\n    @SuppressWarnings(\"unused\")\n    public static String getOptionsNameKey() {\n        return \"keyFile\";\n    }\n\n    @SuppressWarnings(\"unused\")\n    public static OptionsBuilder createOptions(Property<KeyFileStrategy> p, SshIdentityStrategyChoiceConfig config) {\n        var keyPath = new SimpleObjectProperty<>(\n                p.getValue() != null && p.getValue().getFile() != null\n                        ? p.getValue().getFile().toAbsoluteFilePath(null)\n                        : null);\n        p.addListener((observable, oldValue, newValue) -> {\n            if (keyPath.get() != null\n                    && newValue != null\n                    && !ContextualFileReference.of(keyPath.get()).equals(newValue.getFile())) {\n                return;\n            }\n\n            keyPath.setValue(\n                    newValue != null && newValue.getFile() != null\n                            ? newValue.getFile().toAbsoluteFilePath(null)\n                            : null);\n        });\n        var keyPasswordProperty =\n                new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getPassword() : null);\n        var publicKey =\n                new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getPublicKey() : null);\n\n        var sync = ContextualFileReferenceSync.of(\n                DataStorage.get().getDataDir().resolve(\"keys\"),\n                file -> file.getFileName().toString(),\n                config.getPerUserKeyFileCheck());\n\n        var passwordChoice = OptionsChoiceBuilder.builder()\n                .allowNull(false)\n                .property(keyPasswordProperty)\n                .customConfiguration(SecretStrategyChoiceConfig.builder()\n                        .allowNone(true)\n                        .passwordKey(\"passphrase\")\n                        .build())\n                .available(SecretRetrievalStrategy.getClasses())\n                .build()\n                .build();\n\n        var publicKeyField = new TextFieldComp(publicKey).apply(struc -> {\n            struc.promptTextProperty()\n                    .bind(Bindings.createStringBinding(\n                            () -> {\n                                return \"ssh-... ABCDEF.... (\" + AppI18n.get(\"publicKeyGenerateNotice\") + \")\";\n                            },\n                            AppI18n.activeLanguage()));\n            struc.setEditable(false);\n        });\n        var generateButton = new ButtonComp(null, new LabelGraphic.IconGraphic(\"mdi2c-cog-refresh-outline\"), () -> {\n                    ThreadHelper.runFailableAsync(() -> {\n                        var sc = config.getFileSystem() != null\n                                ? config.getFileSystem().getValue().getStore().getOrStartSession()\n                                : LocalShell.getShell();\n                        var path = keyPath.get();\n                        if (!sc.view().fileExists(path)) {\n                            return;\n                        }\n\n                        var pubKeyPath = FilePath.of(path + \".pub\");\n                        if (sc.view().fileExists(pubKeyPath)) {\n                            var contents = sc.view().readTextFile(pubKeyPath).strip();\n                            Platform.runLater(() -> {\n                                publicKey.set(contents);\n                            });\n                            return;\n                        }\n\n                        var contents = sc.view().readRawFile(path);\n                        var generated = ProcessControlProvider.get()\n                                .generatePublicSshKey(InPlaceSecretValue.of(contents), keyPasswordProperty.get());\n                        if (generated != null) {\n                            Platform.runLater(() -> {\n                                publicKey.set(generated);\n                            });\n                        }\n                    });\n                })\n                .describe(d -> d.nameKey(\"generatePublicKey\"))\n                .disable(keyPath.isNull().or(publicKey.isNotNull()).or(keyPasswordProperty.isNull()));\n        var copyButton = new ButtonComp(null, new FontIcon(\"mdi2c-clipboard-multiple-outline\"), () -> {\n                    ClipboardHelper.copyText(publicKey.get());\n                })\n                .disable(publicKey.isNull())\n                .describe(d -> d.nameKey(\"copyPublicKey\"));\n\n        var publicKeyBox = new InputGroupComp(List.of(publicKeyField, copyButton, generateButton));\n        publicKeyBox.setMainReference(publicKeyField);\n\n        return new OptionsBuilder()\n                .name(\"location\")\n                .description(\"locationDescription\")\n                .addComp(\n                        new ContextualFileReferenceChoiceComp(\n                                config.getFileSystem() != null\n                                        ? config.getFileSystem()\n                                        : new ReadOnlyObjectWrapper<>(\n                                                DataStorage.get().local().ref()),\n                                keyPath,\n                                config.isAllowKeyFileSync() ? sync : null,\n                                List.of(),\n                                e -> {\n                                    if (config.getFileSystem() == null) {\n                                        return e.equals(DataStorage.get().local());\n                                    }\n\n                                    var fs = config.getFileSystem().getValue();\n                                    if (fs == null) {\n                                        return e.equals(DataStorage.get().local());\n                                    } else {\n                                        return e.equals(fs.get());\n                                    }\n                                },\n                                false),\n                        keyPath)\n                .nonNull()\n                .name(\"keyPassword\")\n                .description(\"sshConfigHost.identityPassphraseDescription\")\n                .sub(passwordChoice, keyPasswordProperty)\n                .nonNull()\n                .nameAndDescription(\"inPlacePublicKey\")\n                .documentationLink(DocumentationLink.SSH_PUBLIC_KEY)\n                .addComp(publicKeyBox, publicKey)\n                .bind(\n                        () -> {\n                            return new KeyFileStrategy(\n                                    ContextualFileReference.of(keyPath.get()),\n                                    keyPasswordProperty.get(),\n                                    publicKey.get());\n                        },\n                        p);\n    }\n\n    ContextualFileReference file;\n    SecretRetrievalStrategy password;\n    String publicKey;\n\n    public void checkComplete() throws ValidationException {\n        Validators.nonNull(file);\n        Validators.nonNull(password);\n    }\n\n    @Override\n    public void prepareParent(ShellControl parent) throws Exception {\n        if (file == null) {\n            return;\n        }\n\n        var s = file.toAbsoluteFilePath(parent);\n        // The ~ is supported on all platforms, so manually replace it here for Windows\n        if (s.startsWith(\"~\")) {\n            s = s.resolveTildeHome(parent.view().userHome());\n        }\n        var resolved = parent.getShellDialect()\n                .evaluateExpression(parent, s.toString())\n                .readStdoutOrThrow();\n        if (!parent.getShellDialect().createFileExistsCommand(parent, resolved).executeAndCheck()) {\n            var systemName = parent.getSourceStore()\n                    .flatMap(shellStore -> DataStorage.get().getStoreEntryIfPresent(shellStore, false))\n                    .map(e -> DataStorage.get().getStoreEntryDisplayName(e));\n            var msg = \"Identity file \" + resolved + \" does not exist\"\n                    + (systemName.isPresent() ? \" on system \" + systemName.get() : \"\");\n            throw ErrorEventFactory.expected(new IllegalArgumentException(msg));\n        }\n\n        if (resolved.endsWith(\".ppk\")) {\n            var ex = new IllegalArgumentException(\"Identity file \" + resolved\n                    + \" is in non-standard PuTTY Private Key format (.ppk), which is not supported by OpenSSH. Please export/convert it to a \"\n                    + \"standard format like .pem via PuTTY\");\n            ErrorEventFactory.preconfigure(\n                    ErrorEventFactory.fromThrowable(ex).expected().link(\"https://www.puttygen.com/convert-pem-to-ppk\"));\n            throw ex;\n        }\n\n        if (resolved.endsWith(\".pub\")) {\n            throw ErrorEventFactory.expected(new IllegalArgumentException(\"Identity file \" + resolved\n                    + \" is marked to be a public key file, SSH authentication requires the private key\"));\n        }\n\n        if (parent.getOsType() != OsType.WINDOWS) {\n            // Try to preserve the same permission set\n            parent.command(CommandBuilder.of()\n                            .add(\"test\", \"-w\")\n                            .addFile(resolved)\n                            .add(\"&&\", \"chmod\", \"600\")\n                            .addFile(resolved)\n                            .add(\"||\", \"chmod\", \"400\")\n                            .addFile(resolved))\n                    .executeAndCheck();\n        }\n    }\n\n    @Override\n    public void buildCommand(CommandBuilder builder) {}\n\n    @Override\n    public List<KeyValue> configOptions(ShellControl sc) {\n        return List.of(\n                new KeyValue(\"IdentitiesOnly\", \"yes\"),\n                new KeyValue(\"IdentityAgent\", \"none\"),\n                new KeyValue(\"IdentityFile\", \"\\\"\" + resolveFilePath(sc).toString() + \"\\\"\"),\n                new KeyValue(\"PKCS11Provider\", \"none\"));\n    }\n\n    @Override\n    public SecretRetrievalStrategy getAskpassStrategy() {\n        return password;\n    }\n\n    private FilePath resolveFilePath(ShellControl sc) {\n        var s = file.toAbsoluteFilePath(sc);\n        // The ~ is supported on all platforms, so manually replace it here for Windows\n        if (s.startsWith(\"~\")) {\n            s = s.resolveTildeHome(FilePath.of(AppSystemInfo.ofCurrent().getUserHome()));\n        }\n        return s;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/ssh/NoIdentityStrategy.java",
    "content": "package io.xpipe.ext.base.identity.ssh;\n\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.core.KeyValue;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Value;\n\nimport java.util.List;\n\n@JsonTypeName(\"none\")\n@Value\npublic class NoIdentityStrategy implements SshIdentityStrategy {\n\n    @Override\n    public void prepareParent(ShellControl parent) {}\n\n    @Override\n    public void buildCommand(CommandBuilder builder) {}\n\n    @Override\n    public List<KeyValue> configOptions(ShellControl sc) {\n        // Don't use any agent keys to prevent too many authentication failures\n        return List.of(\n                new KeyValue(\"IdentitiesOnly\", \"yes\"),\n                new KeyValue(\"IdentityAgent\", \"none\"),\n                new KeyValue(\"IdentityFile\", \"none\"),\n                new KeyValue(\"PKCS11Provider\", \"none\"));\n    }\n\n    @Override\n    public String getPublicKey() {\n        return null;\n    }\n\n    @Override\n    public boolean providesKey() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/ssh/OpenSshAgentStrategy.java",
    "content": "package io.xpipe.ext.base.identity.ssh;\n\nimport io.xpipe.app.comp.base.TextFieldComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.core.KeyValue;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleStringProperty;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@JsonTypeName(\"sshAgent\")\n@Value\n@Jacksonized\n@Builder\npublic class OpenSshAgentStrategy implements SshIdentityStrategy {\n\n    @SuppressWarnings(\"unused\")\n    public static OptionsBuilder createOptions(\n            Property<OpenSshAgentStrategy> p, SshIdentityStrategyChoiceConfig config) {\n        var socket = AppPrefs.get().defaultSshAgentSocket().getValue();\n        var forward =\n                new SimpleBooleanProperty(p.getValue() != null && p.getValue().isForwardAgent());\n        var publicKey =\n                new SimpleStringProperty(p.getValue() != null ? p.getValue().getPublicKey() : null);\n        return new OptionsBuilder()\n                .nameAndDescription(\"agentSocket\")\n                .addStaticString(socket != null ? socket : AppI18n.get(\"agentSocketNotFound\"))\n                .hide(OsType.ofLocal() == OsType.WINDOWS)\n                .nameAndDescription(\"forwardAgent\")\n                .addToggle(forward)\n                .nonNull()\n                .hide(!config.isAllowAgentForward())\n                .nameAndDescription(\"publicKey\")\n                .addComp(\n                        new TextFieldComp(publicKey)\n                                .apply(struc -> struc.setPromptText(\n                                        \"ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBmhLUTJiP...== Your Comment\")),\n                        publicKey)\n                .bind(\n                        () -> {\n                            return new OpenSshAgentStrategy(forward.get(), publicKey.get());\n                        },\n                        p);\n    }\n\n    boolean forwardAgent;\n    String publicKey;\n\n    @Override\n    public void prepareParent(ShellControl parent) throws Exception {\n        if (parent.isLocal()) {\n            SshIdentityStateManager.prepareLocalOpenSshAgent(\n                    parent, AppPrefs.get().defaultSshAgentSocket().getValue());\n        }\n    }\n\n    @Override\n    public void buildCommand(CommandBuilder builder) {}\n\n    private String getIdentityAgent(ShellControl sc) throws Exception {\n        if (sc.getOsType() == OsType.WINDOWS) {\n            return null;\n        }\n\n        if (AppPrefs.get() != null) {\n            var socket = AppPrefs.get().defaultSshAgentSocket().getValue();\n            if (socket != null) {\n                return socket.resolveTildeHome(sc.view().userHome()).toString();\n            }\n        }\n\n        return null;\n    }\n\n    @Override\n    public List<KeyValue> configOptions(ShellControl sc) throws Exception {\n        var file = SshIdentityStrategy.getPublicKeyPath(sc, publicKey);\n        var l = new ArrayList<>(List.of(\n                new KeyValue(\"IdentitiesOnly\", file.isPresent() ? \"yes\" : \"no\"),\n                new KeyValue(\"ForwardAgent\", forwardAgent ? \"yes\" : \"no\"),\n                new KeyValue(\"IdentityFile\", file.isPresent() ? file.get().toString() : \"none\"),\n                new KeyValue(\"PKCS11Provider\", \"none\")));\n\n        var agent = getIdentityAgent(sc);\n        if (agent != null) {\n            l.add(new KeyValue(\"IdentityAgent\", \"\\\"\" + agent + \"\\\"\"));\n        }\n\n        return l;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/ssh/OtherExternalAgentStrategy.java",
    "content": "package io.xpipe.ext.base.identity.ssh;\n\nimport io.xpipe.app.comp.base.TextFieldComp;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.core.KeyValue;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleStringProperty;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\n\n@JsonTypeName(\"otherExternal\")\n@Value\n@Jacksonized\n@Builder\npublic class OtherExternalAgentStrategy implements SshIdentityStrategy {\n\n    @SuppressWarnings(\"unused\")\n    public static OptionsBuilder createOptions(\n            Property<OtherExternalAgentStrategy> p, SshIdentityStrategyChoiceConfig config) {\n        var forward =\n                new SimpleBooleanProperty(p.getValue() != null && p.getValue().isForwardAgent());\n        var publicKey =\n                new SimpleStringProperty(p.getValue() != null ? p.getValue().getPublicKey() : null);\n        return new OptionsBuilder()\n                .nameAndDescription(\"forwardAgent\")\n                .addToggle(forward)\n                .nonNull()\n                .hide(!config.isAllowAgentForward())\n                .nameAndDescription(\"publicKey\")\n                .addComp(\n                        new TextFieldComp(publicKey)\n                                .apply(struc -> struc.setPromptText(\n                                        \"ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBmhLUTJiP...== Your Comment\")),\n                        publicKey)\n                .bind(\n                        () -> {\n                            return new OtherExternalAgentStrategy(forward.get(), publicKey.get());\n                        },\n                        p);\n    }\n\n    boolean forwardAgent;\n    String publicKey;\n\n    @Override\n    public void prepareParent(ShellControl parent) throws Exception {\n        if (parent.isLocal()) {\n            SshIdentityStateManager.prepareLocalExternalAgent();\n        }\n    }\n\n    @Override\n    public void buildCommand(CommandBuilder builder) {}\n\n    @Override\n    public List<KeyValue> configOptions(ShellControl sc) throws Exception {\n        var file = SshIdentityStrategy.getPublicKeyPath(sc, publicKey);\n        return List.of(\n                new KeyValue(\"IdentitiesOnly\", file.isPresent() ? \"yes\" : \"no\"),\n                new KeyValue(\"ForwardAgent\", forwardAgent ? \"yes\" : \"no\"),\n                new KeyValue(\"IdentityFile\", file.isPresent() ? file.get().toString() : \"none\"),\n                new KeyValue(\"PKCS11Provider\", \"none\"));\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/ssh/PageantStrategy.java",
    "content": "package io.xpipe.ext.base.identity.ssh;\n\nimport io.xpipe.app.comp.base.TextFieldComp;\nimport io.xpipe.app.core.AppSystemInfo;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.LocalShell;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.core.KeyValue;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleStringProperty;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.sun.jna.Memory;\nimport com.sun.jna.platform.win32.Kernel32;\nimport com.sun.jna.platform.win32.WinBase;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@JsonTypeName(\"pageant\")\n@Value\n@Jacksonized\n@Builder\npublic class PageantStrategy implements SshIdentityStrategy {\n\n    @SuppressWarnings(\"unused\")\n    public static OptionsBuilder createOptions(Property<PageantStrategy> p, SshIdentityStrategyChoiceConfig config) {\n        var forward =\n                new SimpleBooleanProperty(p.getValue() != null && p.getValue().isForwardAgent());\n        var publicKey =\n                new SimpleStringProperty(p.getValue() != null ? p.getValue().getPublicKey() : null);\n        return new OptionsBuilder()\n                .nameAndDescription(\"forwardAgent\")\n                .addToggle(forward)\n                .nonNull()\n                .hide(!config.isAllowAgentForward())\n                .nameAndDescription(\"publicKey\")\n                .addComp(\n                        new TextFieldComp(publicKey)\n                                .apply(struc -> struc.setPromptText(\n                                        \"ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBmhLUTJiP...== Your Comment\")),\n                        publicKey)\n                .bind(\n                        () -> {\n                            return new PageantStrategy(forward.get(), publicKey.get());\n                        },\n                        p);\n    }\n\n    private static Boolean supported;\n\n    public static boolean isSupported() {\n        if (supported != null) {\n            return supported;\n        }\n\n        if (OsType.ofLocal() == OsType.WINDOWS) {\n            return true;\n        } else {\n            try {\n                var found = LocalShell.getShell().view().findProgram(\"pageant\").isPresent();\n                return (supported = found);\n            } catch (Exception ex) {\n                return (supported = false);\n            }\n        }\n    }\n\n    boolean forwardAgent;\n    String publicKey;\n\n    @Override\n    public void prepareParent(ShellControl parent) throws Exception {\n        if (parent.getOsType() != OsType.WINDOWS) {\n            var out = parent.executeSimpleStringCommand(\"pageant -l\");\n            if (out.isBlank()) {\n                throw ErrorEventFactory.expected(\n                        new IllegalStateException(\"Pageant is not running or has no identities\"));\n            }\n\n            var socket = AppPrefs.get().defaultSshAgentSocket().getValue();\n            if (socket == null || !socket.toString().contains(\"pageant\")) {\n                throw ErrorEventFactory.expected(new IllegalStateException(\n                        \"Pageant is not running as the primary agent via the $SSH_AUTH_SOCK variable.\"));\n            }\n        }\n    }\n\n    @Override\n    public void buildCommand(CommandBuilder builder) {}\n\n    private String getIdentityAgent(ShellControl sc) {\n        if (sc.isLocal() && sc.getOsType() == OsType.WINDOWS) {\n            return getPageantWindowsPipe();\n        }\n\n        return null;\n    }\n\n    @Override\n    public List<KeyValue> configOptions(ShellControl sc) throws Exception {\n        var file = SshIdentityStrategy.getPublicKeyPath(sc, publicKey);\n        var l = new ArrayList<>(List.of(\n                new KeyValue(\"IdentitiesOnly\", file.isPresent() ? \"yes\" : \"no\"),\n                new KeyValue(\"ForwardAgent\", forwardAgent ? \"yes\" : \"no\"),\n                new KeyValue(\"IdentityFile\", file.isPresent() ? file.get().toString() : \"none\"),\n                new KeyValue(\"PKCS11Provider\", \"none\")));\n\n        var agent = getIdentityAgent(sc);\n        if (agent != null) {\n            l.add(new KeyValue(\"IdentityAgent\", \"\\\"\" + agent + \"\\\"\"));\n        }\n\n        return l;\n    }\n\n    private String getPageantWindowsPipe() {\n        Memory p = new Memory(WinBase.WIN32_FIND_DATA.sizeOf());\n        var r = Kernel32.INSTANCE.FindFirstFile(\"\\\\\\\\.\\\\pipe\\\\*pageant.\" + AppSystemInfo.ofCurrent().getUser() + \"*\", p);\n        if (r == WinBase.INVALID_HANDLE_VALUE) {\n            throw ErrorEventFactory.expected(new IllegalStateException(\"Pageant is not running\"));\n        }\n\n        WinBase.WIN32_FIND_DATA fd = new WinBase.WIN32_FIND_DATA(p);\n        Kernel32.INSTANCE.FindClose(r);\n\n        var file = \"\\\\\\\\.\\\\pipe\\\\\" + fd.getFileName();\n        return file;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/ssh/SshIdentityStateManager.java",
    "content": "package io.xpipe.ext.base.identity.ssh;\n\nimport io.xpipe.app.issue.ErrorAction;\nimport io.xpipe.app.issue.ErrorEvent;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.*;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.OsType;\n\nimport com.sun.jna.Memory;\nimport com.sun.jna.platform.win32.Kernel32;\nimport com.sun.jna.platform.win32.WinBase;\n\nimport java.nio.file.Path;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\npublic class SshIdentityStateManager {\n\n    private static RunningAgent runningAgent;\n\n    private static boolean checkNamedPipeExists(Path path) {\n        Memory p = new Memory(WinBase.WIN32_FIND_DATA.sizeOf());\n        // This will not break the named pipe compared to using a normal exists check\n        var r = Kernel32.INSTANCE.FindFirstFile(path.toString(), p);\n        if (!WinBase.INVALID_HANDLE_VALUE.equals(r)) {\n            Kernel32.INSTANCE.FindClose(r);\n            return true;\n        } else {\n            return false;\n        }\n    }\n\n    private static void stopWindowsAgents(boolean openssh, boolean gpg, boolean external) throws Exception {\n        var pipePath = Path.of(\"\\\\\\\\.\\\\pipe\\\\openssh-ssh-agent\");\n        if (!checkNamedPipeExists(pipePath)) {\n            return;\n        }\n\n        try (var sc = LocalShell.getShell().start()) {\n            var gpgList = sc.executeSimpleStringCommand(\"TASKLIST /FI \\\"IMAGENAME eq gpg-agent.exe\\\"\");\n            var gpgRunning = gpgList.contains(\"gpg-agent.exe\");\n\n            var opensshList = sc.executeSimpleStringCommand(\"TASKLIST /FI \\\"IMAGENAME eq ssh-agent.exe\\\"\");\n            var opensshRunning = opensshList.contains(\"ssh-agent.exe\");\n\n            if (external && !gpgRunning && !opensshRunning) {\n                throw ErrorEventFactory.expected(new IllegalStateException(\n                        \"An external password manager agent is running, but XPipe requested to use another SSH agent. You have to disable the \"\n                                + \"password manager agent first.\"));\n            }\n\n            if (gpg && gpgRunning) {\n                // This sometimes takes a long time if the agent is not running. Why?\n                sc.executeSimpleCommand(CommandBuilder.of().add(\"gpg-connect-agent\", \"killagent\", \"/bye\"));\n            }\n\n            if (openssh && opensshRunning) {\n                var msg =\n                        \"The Windows OpenSSH agent is running. This will cause it to interfere with other agents. You have to manually stop the \"\n                                + \"running ssh-agent service to allow other agents to work\";\n                var r = new AtomicBoolean();\n                var event = ErrorEventFactory.fromMessage(msg).expected();\n                var shutdown = new ErrorAction() {\n                    @Override\n                    public String getName() {\n                        return \"Shut down ssh-agent service\";\n                    }\n\n                    @Override\n                    public String getDescription() {\n                        return \"Stop the agent service as an administrator\";\n                    }\n\n                    @Override\n                    public boolean handle(ErrorEvent event) {\n                        r.set(true);\n                        return true;\n                    }\n                };\n                event.customAction(shutdown).handle();\n\n                if (r.get()) {\n                    if (sc.getShellDialect() == ShellDialects.CMD) {\n                        sc.command(\n                                        \"powershell -Command \\\"Start-Process cmd -Wait -ArgumentList /c, sc, stop, ssh-agent -Verb runAs\\\"\")\n                                .executeAndCheck();\n                    } else {\n                        sc.command(\n                                        \"powershell -Command \\\"Start-Process cmd -Wait -ArgumentList /c, sc, stop, ssh-agent -Verb runAs\\\"\")\n                                .executeAndCheck();\n                    }\n                }\n            }\n        }\n    }\n\n    private static void checkLocalAgentIdentities(String socketEvn) throws Exception {\n        try (var sc = LocalShell.getShell().start()) {\n            checkAgentIdentities(sc, socketEvn);\n        }\n    }\n\n    public static synchronized void checkAgentIdentities(ShellControl sc, String authSock) throws Exception {\n        var found = sc.view().findProgram(\"ssh-add\");\n        if (found.isEmpty()) {\n            throw ErrorEventFactory.expected(new IllegalStateException(\n                    \"SSH agent tool ssh-add not found in PATH. Is the SSH agent correctly installed?\"));\n        }\n\n        try (var c = sc.command(CommandBuilder.of().add(\"ssh-add\", \"-l\").fixedEnvironment(\"SSH_AUTH_SOCK\", authSock))\n                .start()) {\n            var r = c.readStdoutAndStderr();\n            if (c.getExitCode() != 0) {\n                var posixMessage = sc.getOsType() != OsType.WINDOWS\n                        ? authSock != null\n                                ? \" and the socket \" + authSock\n                                : \" and the SSH agent socket in the settings menu\"\n                        : \"\";\n                var ex = new IllegalStateException(\"Unable to list agent identities via command ssh-add -l:\\n\" + r[0]\n                        + \"\\n\"\n                        + r[1]\n                        + \"\\nPlease check your SSH agent configuration%s.\".formatted(posixMessage));\n                var eventBuilder = ErrorEventFactory.fromThrowable(ex).expected();\n                ErrorEventFactory.preconfigure(eventBuilder);\n                throw ex;\n            }\n        } catch (ProcessOutputException ex) {\n            if (sc.getOsType() == OsType.WINDOWS && ex.getOutput().contains(\"No such file or directory\")) {\n                throw ProcessOutputException.withPrefix(\n                        \"Failed to connect to the OpenSSH agent service. Is the Windows OpenSSH feature enabled and the OpenSSH Authentication \"\n                                + \"Agent service running?\",\n                        ex);\n            } else {\n                throw ex;\n            }\n        }\n    }\n\n    public static synchronized void prepareLocalExternalAgent() throws Exception {\n        if (runningAgent == RunningAgent.EXTERNAL_AGENT) {\n            return;\n        }\n\n        if (OsType.ofLocal() == OsType.WINDOWS) {\n            stopWindowsAgents(true, true, false);\n\n            var pipePath = Path.of(\"\\\\\\\\.\\\\pipe\\\\openssh-ssh-agent\");\n            var pipeExists = checkNamedPipeExists(pipePath);\n            if (!pipeExists) {\n                // No agent is running\n                throw ErrorEventFactory.expected(new IllegalStateException(\n                        \"An external password manager agent is set for this connection, but no external SSH agent is running. Make sure that the \"\n                                + \"agent is started in your password manager\"));\n            }\n        }\n\n        checkLocalAgentIdentities(null);\n\n        runningAgent = RunningAgent.EXTERNAL_AGENT;\n    }\n\n    public static synchronized void prepareLocalGpgAgent() throws Exception {\n        if (runningAgent == RunningAgent.GPG_AGENT) {\n            return;\n        }\n\n        try (var sc = LocalShell.getShell().start()) {\n            CommandSupport.isInPathOrThrow(sc, \"gpg-connect-agent\", \"GPG connect agent executable\", null);\n\n            FilePath dir;\n            if (sc.getOsType() == OsType.WINDOWS) {\n                stopWindowsAgents(true, false, true);\n                var appdata = FilePath.of(sc.view().getEnvironmentVariableOrThrow(\"APPDATA\"))\n                        .join(\"gnupg\");\n                dir = appdata;\n            } else {\n                dir = sc.view().userHome().join(\".gnupg\");\n            }\n\n            sc.view().mkdir(dir);\n            var confFile = dir.join(\"gpg-agent.conf\");\n            var content = sc.view().fileExists(confFile) ? sc.view().readTextFile(confFile) : \"\";\n\n            if (sc.getOsType() == OsType.WINDOWS) {\n                if (!content.contains(\"enable-win32-openssh-support\")) {\n                    content += \"\\nenable-win32-openssh-support\\n\";\n                    sc.view().writeTextFile(confFile, content);\n                }\n                // reloadagent does not work correctly, so kill it\n                stopWindowsAgents(true, true, false);\n                sc.command(CommandBuilder.of().add(\"gpg-connect-agent\", \"/bye\")).execute();\n                checkLocalAgentIdentities(null);\n            } else {\n                if (!content.contains(\"enable-ssh-support\")) {\n                    content += \"\\nenable-ssh-support\\n\";\n                    sc.view().writeTextFile(confFile, content);\n                    sc.executeSimpleCommand(CommandBuilder.of().add(\"gpg-connect-agent\", \"reloadagent\", \"/bye\"));\n                } else {\n                    sc.executeSimpleCommand(CommandBuilder.of().add(\"gpg-connect-agent\", \"/bye\"));\n                }\n                var socketEnv =\n                        sc.command(\"gpgconf --list-dirs agent-ssh-socket\").readStdoutOrThrow();\n                checkLocalAgentIdentities(socketEnv);\n            }\n        }\n\n        runningAgent = RunningAgent.GPG_AGENT;\n    }\n\n    public static synchronized void prepareLocalOpenSshAgent(ShellControl sc, FilePath socket) throws Exception {\n        if (runningAgent == RunningAgent.SSH_AGENT) {\n            return;\n        }\n\n        if (sc.getOsType() == OsType.WINDOWS) {\n            CommandSupport.isInPathOrThrow(sc, \"ssh-agent\", \"SSH Agent\", null);\n            stopWindowsAgents(false, true, true);\n            sc.executeSimpleBooleanCommand(\"ssh-agent start\");\n            checkLocalAgentIdentities(null);\n        } else {\n            checkLocalAgentIdentities(\n                    socket != null\n                            ? socket.resolveTildeHome(sc.view().userHome()).toString()\n                            : null);\n        }\n\n        runningAgent = RunningAgent.SSH_AGENT;\n    }\n\n    public static synchronized void prepareLocalCustomAgent(ShellControl sc, FilePath socket) throws Exception {\n        if (runningAgent == RunningAgent.CUSTOM_AGENT) {\n            return;\n        }\n\n        if (sc.getOsType() == OsType.WINDOWS) {\n            checkLocalAgentIdentities(null);\n        } else {\n            checkLocalAgentIdentities(\n                    socket != null\n                            ? socket.resolveTildeHome(sc.view().userHome()).toString()\n                            : null);\n        }\n\n        runningAgent = RunningAgent.CUSTOM_AGENT;\n    }\n\n    private enum RunningAgent {\n        SSH_AGENT,\n        CUSTOM_AGENT,\n        GPG_AGENT,\n        EXTERNAL_AGENT\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/ssh/SshIdentityStrategy.java",
    "content": "package io.xpipe.ext.base.identity.ssh;\n\nimport io.xpipe.app.ext.ValidationException;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.OsFileSystem;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.secret.SecretNoneStrategy;\nimport io.xpipe.app.secret.SecretRetrievalStrategy;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.KeyValue;\nimport io.xpipe.core.OsType;\n\nimport com.fasterxml.jackson.annotation.JsonSubTypes;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\n@JsonSubTypes({\n    @JsonSubTypes.Type(value = NoIdentityStrategy.class),\n    @JsonSubTypes.Type(value = KeyFileStrategy.class),\n    @JsonSubTypes.Type(value = InPlaceKeyStrategy.class),\n    @JsonSubTypes.Type(value = OpenSshAgentStrategy.class),\n    @JsonSubTypes.Type(value = PageantStrategy.class),\n    @JsonSubTypes.Type(value = CustomAgentStrategy.class),\n    @JsonSubTypes.Type(value = GpgAgentStrategy.class),\n    @JsonSubTypes.Type(value = YubikeyPivStrategy.class),\n    @JsonSubTypes.Type(value = CustomPkcs11LibraryStrategy.class),\n    @JsonSubTypes.Type(value = OtherExternalAgentStrategy.class)\n})\npublic interface SshIdentityStrategy {\n\n    static List<Class<?>> getSubclasses() {\n        var l = new ArrayList<Class<?>>();\n        l.add(NoIdentityStrategy.class);\n        l.add(InPlaceKeyStrategy.class);\n        l.add(KeyFileStrategy.class);\n        l.add(OpenSshAgentStrategy.class);\n        if (OsType.ofLocal() != OsType.WINDOWS) {\n            l.add(CustomAgentStrategy.class);\n        }\n        if (GpgAgentStrategy.isSupported()) {\n            l.add(GpgAgentStrategy.class);\n        }\n        if (PageantStrategy.isSupported()) {\n            l.add(PageantStrategy.class);\n        }\n        l.add(YubikeyPivStrategy.class);\n        l.add(CustomPkcs11LibraryStrategy.class);\n        l.add(OtherExternalAgentStrategy.class);\n\n        return l;\n    }\n\n    static Optional<FilePath> getPublicKeyPath(ShellControl sc, String publicKey) throws Exception {\n        if (publicKey == null || publicKey.isBlank()) {\n            return Optional.empty();\n        }\n\n        var isFile = OsFileSystem.of(sc.getOsType()).isProbableFilePath(publicKey);\n        if (isFile && sc.view().fileExists(FilePath.of(publicKey))) {\n            return Optional.of(FilePath.of(publicKey));\n        }\n\n        try {\n            var base = sc.getSystemTemporaryDirectory().join(\"key.pub\");\n            var file = sc.view().writeTextFileDeterministic(base, publicKey.strip() + \"\\n\");\n\n            if (sc.getOsType() != OsType.WINDOWS) {\n                sc.command(CommandBuilder.of().add(\"chmod\", \"400\").addFile(file))\n                        .executeAndCheck();\n            }\n\n            return Optional.of(file);\n        } catch (Exception e) {\n            ErrorEventFactory.fromThrowable(e).handle();\n            return Optional.empty();\n        }\n    }\n\n    default boolean providesKey() {\n        return true;\n    }\n\n    default void checkComplete() throws ValidationException {}\n\n    void prepareParent(ShellControl parent) throws Exception;\n\n    void buildCommand(CommandBuilder builder);\n\n    List<KeyValue> configOptions(ShellControl sc) throws Exception;\n\n    default SecretRetrievalStrategy getAskpassStrategy() {\n        return new SecretNoneStrategy();\n    }\n\n    String getPublicKey();\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/ssh/SshIdentityStrategyChoiceConfig.java",
    "content": "package io.xpipe.ext.base.identity.ssh;\n\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.Builder;\nimport lombok.Value;\n\nimport java.util.function.Supplier;\n\n@Value\n@Builder\npublic class SshIdentityStrategyChoiceConfig {\n\n    Supplier<Boolean> perUserKeyFileCheck;\n    boolean allowKeyFileSync;\n    boolean allowAgentForward;\n    ObservableValue<DataStoreEntryRef<ShellStore>> fileSystem;\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/identity/ssh/YubikeyPivStrategy.java",
    "content": "package io.xpipe.ext.base.identity.ssh;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.util.LicenseProvider;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.KeyValue;\nimport io.xpipe.core.OsType;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\n\n@Value\n@Jacksonized\n@Builder\n@JsonTypeName(\"yubikeyPiv\")\n@AllArgsConstructor\npublic class YubikeyPivStrategy implements SshIdentityStrategy {\n\n    private String getFile(ShellControl sc) {\n        var file =\n                switch (sc.getOsType()) {\n                    case OsType.MacOs ignored -> \"/usr/local/lib/libykcs11.dylib\";\n                    case OsType.Windows ignored -> {\n                        var x64 = \"C:\\\\Program Files\\\\Yubico\\\\Yubico PIV Tool\\\\bin\\\\libykcs11.dll\";\n                        if (Files.exists(Path.of(x64))) {\n                            yield x64;\n                        }\n\n                        var x86 = \"C:\\\\Program Files (x86)\\\\Yubico\\\\Yubico PIV Tool\\\\bin\\\\libykcs11.dll\";\n                        if (Files.exists(Path.of(x86))) {\n                            yield x86;\n                        }\n\n                        yield x64;\n                    }\n                    default -> \"/usr/local/lib/libykcs11.so\";\n                };\n        return file;\n    }\n\n    @Override\n    public void prepareParent(ShellControl parent) throws Exception {\n        parent.requireLicensedFeature(LicenseProvider.get().getFeature(\"pkcs11Identity\"));\n\n        var file = getFile(parent);\n        if (!parent.getShellDialect().createFileExistsCommand(parent, file).executeAndCheck()) {\n            throw ErrorEventFactory.expected(new IOException(\"Yubikey PKCS11 library at \" + file + \" not found\"));\n        }\n    }\n\n    @Override\n    public void buildCommand(CommandBuilder builder) {\n        builder.setup(sc -> {\n            var file = getFile(sc);\n            var dir = FilePath.of(file).getParent();\n            if (sc.getOsType() == OsType.WINDOWS) {\n                builder.addToPath(dir, true);\n            } else {\n                builder.addToEnvironmentPath(\"LD_LIBRARY_PATH\", dir, true);\n            }\n        });\n    }\n\n    @Override\n    public List<KeyValue> configOptions(ShellControl sc) {\n        return List.of(\n                new KeyValue(\"IdentitiesOnly\", \"no\"),\n                new KeyValue(\"PKCS11Provider\", \"\\\"\" + getFile(sc) + \"\\\"\"),\n                new KeyValue(\"IdentityFile\", \"none\"),\n                new KeyValue(\"IdentityAgent\", \"none\"));\n    }\n\n    @Override\n    public String getPublicKey() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/PredefinedScriptStore.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.core.AppNames;\nimport io.xpipe.app.core.AppResources;\nimport io.xpipe.app.process.ShellDialects;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport lombok.Getter;\nimport lombok.Setter;\n\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.util.UUID;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.function.Supplier;\n\n@Getter\npublic enum PredefinedScriptStore {\n    APT_UPDATE(\"Apt upgrade\", () -> ScriptStore.builder()\n            .textSource(ScriptTextSource.InPlace.builder()\n                    .dialect(ShellDialects.SH)\n                    .text(file(\"apt_upgrade.sh\"))\n                    .build())\n            .shellScript(true)\n            .runnableScript(true)\n            .build()),\n    REMOVE_CR(\"CRLF to LF\", () -> ScriptStore.builder()\n            .textSource(ScriptTextSource.InPlace.builder()\n                    .dialect(ShellDialects.SH)\n                    .text(file(\"crlf_to_lf.sh\"))\n                    .build())\n            .fileScript(true)\n            .shellScript(true)\n            .build()),\n    DIFF(\"Diff\", () -> ScriptStore.builder()\n            .textSource(ScriptTextSource.InPlace.builder()\n                    .dialect(ShellDialects.SH)\n                    .text(file(\"diff.sh\"))\n                    .build())\n            .fileScript(true)\n            .build()),\n    GIT_CONFIG(\"Git Config\", () -> ScriptStore.builder()\n            .textSource(ScriptTextSource.InPlace.builder()\n                    .text(file(\"git_config.sh\"))\n                    .build())\n            .runnableScript(true)\n            .build()),\n    SYSTEM_HEALTH_STATUS(\"System health status\", () -> ScriptStore.builder()\n            .textSource(ScriptTextSource.InPlace.builder()\n                    .dialect(ShellDialects.SH)\n                    .text(file(\"system_health.sh\"))\n                    .build())\n            .initScript(true)\n            .build());\n\n    private final String name;\n    private final Supplier<ScriptStore> scriptStore;\n    private final UUID uuid;\n\n    @Setter\n    private DataStoreEntryRef<ScriptStore> entry;\n\n    PredefinedScriptStore(String name, Supplier<ScriptStore> scriptStore) {\n        this.name = name;\n        this.scriptStore = scriptStore;\n        this.uuid = UUID.nameUUIDFromBytes(name.getBytes(StandardCharsets.UTF_8));\n    }\n\n    public static ShellScript file(String name) {\n        AtomicReference<String> string = new AtomicReference<>();\n        AppResources.with(AppNames.extModuleName(\"base\"), \"scripts/\" + name, var1 -> {\n            string.set(Files.readString(var1));\n        });\n        return ShellScript.of(string.get());\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/RunBackgroundScriptActionProvider.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class RunBackgroundScriptActionProvider implements ActionProvider {\n\n    @Override\n    public String getId() {\n        return \"runBackgroundScript\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<ShellStore> {\n\n        private final DataStoreEntryRef<ScriptStore> scriptStore;\n\n        @Override\n        public void executeImpl() throws Exception {\n            var sc = ref.getStore().getOrStartSession();\n            var script = scriptStore.getStore().assembleScriptChain(sc, false);\n            if (script != null) {\n                sc.command(script).execute();\n            }\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/RunFileScriptMenuProvider.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.browser.file.BrowserEntry;\nimport io.xpipe.app.browser.file.BrowserFileSystemTabModel;\nimport io.xpipe.app.browser.menu.*;\nimport io.xpipe.app.comp.base.PrettyImageHelper;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.AppLayoutModel;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.process.CommandBuilder;\nimport io.xpipe.app.process.ScriptHelper;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.property.SimpleStringProperty;\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\n\npublic class RunFileScriptMenuProvider implements BrowserMenuBranchProvider {\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2c-code-greater-than\");\n    }\n\n    @Override\n    public BrowserMenuCategory getCategory() {\n        return BrowserMenuCategory.ACTION;\n    }\n\n    @Override\n    public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        return AppI18n.observable(\"runScript\");\n    }\n\n    @Override\n    public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        if (model.getFileSystem().getShell().isEmpty()) {\n            return false;\n        }\n\n        return model.getBrowserModel() instanceof BrowserFullSessionModel;\n    }\n\n    @Override\n    public List<? extends BrowserMenuItemProvider> getBranchingActions(\n            BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n        var config =\n                DataStorage.get().getEffectiveCategoryConfig(model.getEntry().get());\n        if (Boolean.TRUE.equals(config.getDontAllowScripts())) {\n            return List.of(new BrowserMenuLeafProvider() {\n                @Override\n                public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                    return AppI18n.observable(\"scriptsDisabled\");\n                }\n            });\n        }\n\n        var actions = createActionForScriptHierarchy(model, entries);\n        if (actions.isEmpty()) {\n            actions = List.of(new BrowserMenuLeafProvider() {\n                @Override\n                public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                    StoreViewState.get().getAllScriptsCategory().select();\n                    AppLayoutModel.get().selectConnections();\n                }\n\n                @Override\n                public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                    return AppI18n.observable(\"noScriptsAvailable\");\n                }\n            });\n        }\n        return actions;\n    }\n\n    private List<? extends BrowserMenuItemProvider> createActionForScriptHierarchy(\n            BrowserFileSystemTabModel model, List<BrowserEntry> selected) {\n        var sc = model.getFileSystem().getShell().orElseThrow();\n        var hierarchy = ScriptHierarchy.buildEnabledHierarchy(ref -> {\n            if (!ref.getStore().isFileScript()) {\n                return false;\n            }\n\n            if (!ref.getStore().isCompatible(sc)) {\n                return false;\n            }\n            return true;\n        });\n        return createActionForScriptHierarchy(hierarchy).getBranchingActions(model, selected);\n    }\n\n    private BrowserMenuBranchProvider createActionForScriptHierarchy(ScriptHierarchy hierarchy) {\n        if (hierarchy.isLeaf()) {\n            return createActionForScript(hierarchy.getScript());\n        }\n\n        var list = hierarchy.getChildren().stream()\n                .map(c -> createActionForScriptHierarchy(c))\n                .toList();\n        return new BrowserMenuBranchProvider() {\n            @Override\n            public LabelGraphic getIcon() {\n                if (!hierarchy.isLeaf()) {\n                    return null;\n                }\n\n                return new LabelGraphic.CompGraphic(PrettyImageHelper.ofFixedSize(\n                        hierarchy.getScript().get().getEffectiveIconFile(), 16, 16));\n            }\n\n            @Override\n            public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                return new SimpleStringProperty(hierarchy.getName());\n            }\n\n            @Override\n            public List<? extends BrowserMenuItemProvider> getBranchingActions(\n                    BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                return list;\n            }\n        };\n    }\n\n    private BrowserMenuBranchProvider createActionForScript(DataStoreEntryRef<ScriptStore> ref) {\n        return new MultiExecuteMenuProvider() {\n\n            @Override\n            public LabelGraphic getIcon() {\n                return new LabelGraphic.CompGraphic(\n                        PrettyImageHelper.ofFixedSize(ref.get().getEffectiveIconFile(), 16, 16));\n            }\n\n            @Override\n            public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                return new SimpleStringProperty(ref.get().getName());\n            }\n\n            @Override\n            protected List<CommandBuilder> createCommand(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {\n                var sc = model.getFileSystem().getShell().orElseThrow();\n                var content = ref.getStore().assembleScriptChain(sc, true);\n                if (content == null) {\n                    return List.of();\n                }\n\n                var script = ScriptHelper.createExecScript(sc, content.getValue());\n                var builder = CommandBuilder.of().add(sc.getShellDialect().runScriptCommand(sc, script.toString()));\n                for (BrowserEntry entry : entries) {\n                    builder.addFile(entry.getRawFileEntry().getPath());\n                }\n                return List.of(builder);\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/RunHubBatchScriptActionProvider.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.hub.action.MultiStoreAction;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.CommandDialog;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.ArrayList;\n\npublic class RunHubBatchScriptActionProvider implements ActionProvider {\n\n    @Override\n    public String getId() {\n        return \"runHubBatchScript\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends MultiStoreAction<ShellStore> {\n\n        private final DataStoreEntryRef<ScriptStore> scriptStore;\n\n        @Override\n        public void executeImpl() throws Exception {\n            var list = new ArrayList<CommandDialog.CommandEntry>();\n            for (DataStoreEntryRef<ShellStore> ref : refs) {\n                var sc = ref.getStore().getOrStartSession();\n                var script = scriptStore.getStore().assembleScriptChain(sc, false);\n                if (script == null) {\n                    continue;\n                }\n\n                var cmd = sc.command(script);\n                list.add(new CommandDialog.CommandEntry(ref.get().getName(), cmd));\n            }\n            CommandDialog.runMultipleAndShow(list);\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/RunHubScriptActionProvider.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.CommandDialog;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class RunHubScriptActionProvider implements ActionProvider {\n\n    @Override\n    public String getId() {\n        return \"runHubScript\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<ShellStore> {\n\n        DataStoreEntryRef<ScriptStore> scriptStore;\n\n        @Override\n        public void executeImpl() throws Exception {\n            var sc = ref.getStore().getOrStartSession();\n            var script = scriptStore.getStore().assembleScriptChain(sc, false);\n            if (script != null) {\n                var cmd = sc.command(script);\n                CommandDialog.runAndShow(cmd);\n            }\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/RunScriptActionProviderMenu.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.action.*;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.hub.action.*;\nimport io.xpipe.app.hub.action.impl.RefreshActionProvider;\nimport io.xpipe.app.hub.comp.StoreCategoryConfigComp;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.prefs.AppPrefs;\nimport io.xpipe.app.process.ShellTtyState;\nimport io.xpipe.app.process.SystemState;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.property.ReadOnlyObjectWrapper;\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.Value;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\npublic class RunScriptActionProviderMenu implements HubBranchProvider<ShellStore>, BatchHubProvider<ShellStore> {\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.CUSTOM;\n    }\n\n    @Override\n    public boolean isMajor() {\n        return true;\n    }\n\n    @Override\n    public boolean isApplicable(DataStoreEntryRef<ShellStore> o) {\n        var state = o.get().getStorePersistentState();\n        if (state instanceof SystemState systemState) {\n            return (systemState.getShellDialect() == null\n                            || systemState.getShellDialect().getDumbMode().supportsAnyPossibleInteraction())\n                    && (systemState.getTtyState() == null || systemState.getTtyState() == ShellTtyState.NONE);\n        } else {\n            return false;\n        }\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<ShellStore> store) {\n        return AppI18n.observable(\"runScript\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<ShellStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2p-play-box-multiple-outline\");\n    }\n\n    @Override\n    public Class<ShellStore> getApplicableClass() {\n        return ShellStore.class;\n    }\n\n    @Override\n    public List<HubMenuItemProvider<?>> getChildren(DataStoreEntryRef<ShellStore> store) {\n        if (Boolean.TRUE.equals(\n                DataStorage.get().getEffectiveCategoryConfig(store.get()).getDontAllowScripts())) {\n            return List.of(new ScriptsDisabledActionProvider());\n        }\n\n        var replacement = ProcessControlProvider.get().replace(store);\n        var state = replacement.get().getStorePersistentState();\n        if (!(state instanceof SystemState systemState) || systemState.getShellDialect() == null) {\n            return List.of(new NoStateActionProvider());\n        }\n\n        var hierarchy = ScriptHierarchy.buildEnabledHierarchy(ref -> {\n            if (!ref.getStore().isRunnableScript()) {\n                return false;\n            }\n\n            if (!ref.getStore().isCompatible(systemState.getShellDialect())) {\n                return false;\n            }\n\n            return true;\n        });\n        List<HubMenuItemProvider<?>> list = hierarchy.getChildren().stream()\n                .map(c -> new ScriptActionProvider(c))\n                .collect(Collectors.toList());\n        if (list.isEmpty()) {\n            return List.of(new NoScriptsActionProvider());\n        } else {\n            return list;\n        }\n    }\n\n    @Override\n    public ObservableValue<String> getName() {\n        return AppI18n.observable(\"runScript\");\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2p-play-box-multiple-outline\");\n    }\n\n    @Override\n    public List<ActionProvider> getChildren(List<DataStoreEntryRef<ShellStore>> batch) {\n        if (batch.stream()\n                .anyMatch(store -> Boolean.TRUE.equals(DataStorage.get()\n                        .getEffectiveCategoryConfig(store.get())\n                        .getDontAllowScripts()))) {\n            return List.of(new ScriptsDisabledActionProvider());\n        }\n\n        var stateMissing = batch.stream().anyMatch(ref -> {\n            var state = ref.get().getStorePersistentState();\n            if (state instanceof SystemState systemState) {\n                if (systemState.getShellDialect() == null) {\n                    return true;\n                }\n\n                if (systemState.getTtyState() == null || systemState.getTtyState() != ShellTtyState.NONE) {\n                    return true;\n                }\n            }\n            return false;\n        });\n\n        if (stateMissing) {\n            return List.of(new NoStateActionProvider());\n        }\n\n        var hierarchy = ScriptHierarchy.buildEnabledHierarchy(scriptRef -> {\n            var compatible = batch.stream().allMatch(ref -> {\n                var state = ref.get().getStorePersistentState();\n                if (state instanceof SystemState systemState) {\n                    return scriptRef.getStore().isCompatible(systemState.getShellDialect());\n                } else {\n                    return false;\n                }\n            });\n            if (!compatible) {\n                return false;\n            }\n\n            if (!scriptRef.getStore().isRunnableScript()) {\n                return false;\n            }\n\n            return true;\n        });\n        var list = hierarchy.getChildren().stream()\n                .<ActionProvider>map(c -> new ScriptActionProvider(c))\n                .toList();\n        if (list.isEmpty()) {\n            return List.of(new NoScriptsActionProvider());\n        } else {\n            return list;\n        }\n    }\n\n    @Value\n    private static class TerminalRunActionProvider\n            implements HubLeafProvider<ShellStore>, BatchHubProvider<ShellStore> {\n\n        ScriptHierarchy hierarchy;\n\n        @Override\n        public boolean runParallel() {\n            return true;\n        }\n\n        @Override\n        public AbstractAction createAction(DataStoreEntryRef<ShellStore> ref) {\n            return RunTerminalScriptActionProvider.Action.builder()\n                    .ref(ref)\n                    .scriptStore(hierarchy.getScript())\n                    .build();\n        }\n\n        @Override\n        public boolean requiresValidStore() {\n            return true;\n        }\n\n        @Override\n        public boolean isApplicable(DataStoreEntryRef<ShellStore> o) {\n            return true;\n        }\n\n        @Override\n        public ObservableValue<String> getName(DataStoreEntryRef<ShellStore> store) {\n            var t = AppPrefs.get().terminalType().getValue();\n            return AppI18n.observable(\n                    \"executeInTerminal\", t != null ? t.toTranslatedString().getValue() : \"?\");\n        }\n\n        @Override\n        public LabelGraphic getIcon(DataStoreEntryRef<ShellStore> store) {\n            return new LabelGraphic.IconGraphic(\"mdi2c-code-greater-than\");\n        }\n\n        @Override\n        public Class<?> getApplicableClass() {\n            return ShellStore.class;\n        }\n\n        @Override\n        public ObservableValue<String> getName() {\n            var t = AppPrefs.get().terminalType().getValue();\n            return AppI18n.observable(\n                    \"executeInTerminal\", t != null ? t.toTranslatedString().getValue() : \"?\");\n        }\n\n        @Override\n        public LabelGraphic getIcon() {\n            return new LabelGraphic.IconGraphic(\"mdi2c-code-greater-than\");\n        }\n\n        @Override\n        public RunTerminalScriptActionProvider.Action createBatchAction(DataStoreEntryRef<ShellStore> ref) {\n            return RunTerminalScriptActionProvider.Action.builder()\n                    .ref(ref)\n                    .scriptStore(hierarchy.getScript())\n                    .build();\n        }\n    }\n\n    @Value\n    private static class HubRunActionProvider implements HubLeafProvider<ShellStore>, BatchHubProvider<ShellStore> {\n\n        ScriptHierarchy hierarchy;\n\n        @Override\n        public boolean requiresValidStore() {\n            return true;\n        }\n\n        @Override\n        public AbstractAction createAction(DataStoreEntryRef<ShellStore> ref) {\n            return RunHubScriptActionProvider.Action.builder()\n                    .ref(ref)\n                    .scriptStore(hierarchy.getScript())\n                    .build();\n        }\n\n        @Override\n        public boolean isApplicable(DataStoreEntryRef<ShellStore> o) {\n            return true;\n        }\n\n        @Override\n        public ObservableValue<String> getName(DataStoreEntryRef<ShellStore> store) {\n            return AppI18n.observable(\"runInConnectionHub\");\n        }\n\n        @Override\n        public LabelGraphic getIcon(DataStoreEntryRef<ShellStore> store) {\n            return new LabelGraphic.IconGraphic(\"mdal-desktop_mac\");\n        }\n\n        @Override\n        public Class<?> getApplicableClass() {\n            return ShellStore.class;\n        }\n\n        @Override\n        public ObservableValue<String> getName() {\n            return AppI18n.observable(\"runInConnectionHub\");\n        }\n\n        @Override\n        public LabelGraphic getIcon() {\n            return new LabelGraphic.IconGraphic(\"mdal-desktop_mac\");\n        }\n\n        @Override\n        public AbstractAction createBatchAction(List<DataStoreEntryRef<ShellStore>> stores) {\n            return RunHubBatchScriptActionProvider.Action.builder()\n                    .refs(stores)\n                    .scriptStore(hierarchy.getScript())\n                    .build();\n        }\n\n        @Override\n        public RunHubScriptActionProvider.Action createBatchAction(DataStoreEntryRef<ShellStore> ref) {\n            return RunHubScriptActionProvider.Action.builder()\n                    .ref(ref)\n                    .scriptStore(hierarchy.getScript())\n                    .build();\n        }\n    }\n\n    @Value\n    private static class BackgroundRunActionProvider\n            implements HubLeafProvider<ShellStore>, BatchHubProvider<ShellStore> {\n\n        ScriptHierarchy hierarchy;\n\n        @Override\n        public boolean requiresValidStore() {\n            return true;\n        }\n\n        @Override\n        public AbstractAction createAction(DataStoreEntryRef<ShellStore> ref) {\n            return RunBackgroundScriptActionProvider.Action.builder()\n                    .ref(ref)\n                    .scriptStore(hierarchy.getScript())\n                    .build();\n        }\n\n        @Override\n        public boolean isApplicable(DataStoreEntryRef<ShellStore> o) {\n            return true;\n        }\n\n        @Override\n        public ObservableValue<String> getName(DataStoreEntryRef<ShellStore> store) {\n            return AppI18n.observable(\"executeInBackground\");\n        }\n\n        @Override\n        public LabelGraphic getIcon(DataStoreEntryRef<ShellStore> store) {\n            return new LabelGraphic.IconGraphic(\"mdi2f-flip-to-back\");\n        }\n\n        @Override\n        public Class<?> getApplicableClass() {\n            return ShellStore.class;\n        }\n\n        @Override\n        public ObservableValue<String> getName() {\n            return AppI18n.observable(\"executeInBackground\");\n        }\n\n        @Override\n        public LabelGraphic getIcon() {\n            return new LabelGraphic.IconGraphic(\"mdi2f-flip-to-back\");\n        }\n\n        @Override\n        public RunBackgroundScriptActionProvider.Action createBatchAction(DataStoreEntryRef<ShellStore> ref) {\n            return RunBackgroundScriptActionProvider.Action.builder()\n                    .ref(ref)\n                    .scriptStore(hierarchy.getScript())\n                    .build();\n        }\n\n        @Override\n        public boolean runParallel() {\n            return true;\n        }\n\n        @Override\n        public List<ActionProvider> getChildren(List<DataStoreEntryRef<ShellStore>> batch) {\n            return List.of();\n        }\n    }\n\n    @Value\n    private static class ScriptActionProvider implements HubBranchProvider<ShellStore>, BatchHubProvider<ShellStore> {\n\n        ScriptHierarchy hierarchy;\n\n        @Override\n        public ObservableValue<String> getName() {\n            return new ReadOnlyObjectWrapper<>(hierarchy.getName());\n        }\n\n        @Override\n        public LabelGraphic getIcon() {\n            if (hierarchy.isLeaf()) {\n                return new LabelGraphic.ImageGraphic(hierarchy.getScript().get().getEffectiveIconFile(), 16);\n            }\n\n            return new LabelGraphic.ImageGraphic(\"base:scriptGroup_icon.svg\", 16);\n        }\n\n        @Override\n        public List<? extends ActionProvider> getChildren(List<DataStoreEntryRef<ShellStore>> batch) {\n            return getChildren();\n        }\n\n        @Override\n        public boolean isApplicable(DataStoreEntryRef<ShellStore> o) {\n            return true;\n        }\n\n        @Override\n        public ObservableValue<String> getName(DataStoreEntryRef<ShellStore> store) {\n            return getName();\n        }\n\n        @Override\n        public LabelGraphic getIcon(DataStoreEntryRef<ShellStore> store) {\n            return getIcon();\n        }\n\n        @Override\n        public Class<ShellStore> getApplicableClass() {\n            return ShellStore.class;\n        }\n\n        @Override\n        public List<HubMenuItemProvider<?>> getChildren(DataStoreEntryRef<ShellStore> store) {\n            return getChildren();\n        }\n\n        private List<HubMenuItemProvider<?>> getChildren() {\n            if (hierarchy.isLeaf()) {\n                return List.of(\n                        new TerminalRunActionProvider(hierarchy),\n                        new HubRunActionProvider(hierarchy),\n                        new BackgroundRunActionProvider(hierarchy));\n            }\n\n            return hierarchy.getChildren().stream()\n                    .map(c -> new ScriptActionProvider(c))\n                    .collect(Collectors.toList());\n        }\n    }\n\n    private static class NoScriptsActionProvider implements HubLeafProvider<ShellStore>, BatchHubProvider<ShellStore> {\n\n        @Override\n        public boolean requiresValidStore() {\n            return true;\n        }\n\n        @Override\n        public void execute(DataStoreEntryRef<ShellStore> ref) {\n            var cat = StoreViewState.get().getAllScriptsCategory();\n            cat.select();\n        }\n\n        @Override\n        public boolean isApplicable(DataStoreEntryRef<ShellStore> o) {\n            return true;\n        }\n\n        @Override\n        public ObservableValue<String> getName(DataStoreEntryRef<ShellStore> store) {\n            return AppI18n.observable(\"noScriptsAvailable\");\n        }\n\n        @Override\n        public LabelGraphic getIcon(DataStoreEntryRef<ShellStore> store) {\n            return new LabelGraphic.IconGraphic(\"mdi2i-image-filter-none\");\n        }\n\n        @Override\n        public Class<?> getApplicableClass() {\n            return ShellStore.class;\n        }\n\n        @Override\n        public ObservableValue<String> getName() {\n            return AppI18n.observable(\"noScriptsAvailable\");\n        }\n\n        @Override\n        public LabelGraphic getIcon() {\n            return new LabelGraphic.IconGraphic(\"mdi2i-image-filter-none\");\n        }\n\n        @Override\n        public void execute(List<DataStoreEntryRef<ShellStore>> dataStoreEntryRefs) {\n            var cat = StoreViewState.get().getAllScriptsCategory();\n            cat.select();\n        }\n    }\n\n    private static class ScriptsDisabledActionProvider\n            implements HubLeafProvider<ShellStore>, BatchHubProvider<ShellStore> {\n\n        @Override\n        public boolean requiresValidStore() {\n            return true;\n        }\n\n        @Override\n        public void execute(DataStoreEntryRef<ShellStore> ref) {\n            var cat = StoreViewState.get().getCategoryWrapper(DataStorage.get().getStoreCategory(ref.get()));\n            StoreCategoryConfigComp.show(cat);\n        }\n\n        @Override\n        public boolean isApplicable(DataStoreEntryRef<ShellStore> o) {\n            return true;\n        }\n\n        @Override\n        public ObservableValue<String> getName(DataStoreEntryRef<ShellStore> store) {\n            return AppI18n.observable(\"scriptsDisabled\");\n        }\n\n        @Override\n        public LabelGraphic getIcon(DataStoreEntryRef<ShellStore> store) {\n            return new LabelGraphic.IconGraphic(\"mdi2b-block-helper\");\n        }\n\n        @Override\n        public Class<?> getApplicableClass() {\n            return ShellStore.class;\n        }\n\n        @Override\n        public ObservableValue<String> getName() {\n            return AppI18n.observable(\"scriptsDisabled\");\n        }\n\n        @Override\n        public LabelGraphic getIcon() {\n            return new LabelGraphic.IconGraphic(\"mdi2b-block-helper\");\n        }\n\n        @Override\n        public void execute(List<DataStoreEntryRef<ShellStore>> dataStoreEntryRefs) {\n            var cat = StoreViewState.get()\n                    .getCategoryWrapper(DataStorage.get()\n                            .getStoreCategory(dataStoreEntryRefs.getFirst().get()));\n            StoreCategoryConfigComp.show(cat);\n        }\n    }\n\n    private static class NoStateActionProvider implements HubLeafProvider<ShellStore>, BatchHubProvider<ShellStore> {\n\n        @Override\n        public boolean requiresValidStore() {\n            return true;\n        }\n\n        @Override\n        public boolean isApplicable(DataStoreEntryRef<ShellStore> o) {\n            return true;\n        }\n\n        @Override\n        public ObservableValue<String> getName(DataStoreEntryRef<ShellStore> store) {\n            return AppI18n.observable(\"noScriptStateAvailable\");\n        }\n\n        @Override\n        public LabelGraphic getIcon(DataStoreEntryRef<ShellStore> store) {\n            return new LabelGraphic.IconGraphic(\"mdi2i-image-filter-none\");\n        }\n\n        @Override\n        public Class<?> getApplicableClass() {\n            return ShellStore.class;\n        }\n\n        @Override\n        public ObservableValue<String> getName() {\n            return AppI18n.observable(\"noScriptStateAvailable\");\n        }\n\n        @Override\n        public LabelGraphic getIcon() {\n            return new LabelGraphic.IconGraphic(\"mdi2i-image-filter-none\");\n        }\n\n        @Override\n        public StoreAction<ShellStore> createBatchAction(DataStoreEntryRef<ShellStore> ref) {\n            return RefreshActionProvider.Action.builder()\n                    .ref(ref.asNeeded())\n                    .build()\n                    .asNeeded();\n        }\n\n        @Override\n        public StoreAction<ShellStore> createAction(DataStoreEntryRef<ShellStore> ref) {\n            return RefreshActionProvider.Action.builder()\n                    .ref(ref.asNeeded())\n                    .build()\n                    .asNeeded();\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/RunTerminalScriptActionProvider.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.terminal.TerminalLaunch;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class RunTerminalScriptActionProvider implements ActionProvider {\n\n    @Override\n    public String getId() {\n        return \"runTerminalStore\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<ShellStore> {\n\n        DataStoreEntryRef<ScriptStore> scriptStore;\n\n        @Override\n        public void executeImpl() throws Exception {\n            var sc = ref.getStore().getOrStartSession();\n            var script = scriptStore.getStore().assembleScriptChain(sc, false);\n            if (script == null) {\n                return;\n            }\n\n            TerminalLaunch.builder()\n                    .entry(ref.get())\n                    .title(scriptStore.get().getName())\n                    .command(sc.command(script))\n                    .pauseOnExit(true)\n                    .launch();\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/ScriptCollectionSource.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.comp.base.ContextualFileReferenceChoiceComp;\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.ext.ValidationException;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.util.Validators;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.UuidHelper;\n\nimport javafx.beans.property.*;\n\nimport com.fasterxml.jackson.annotation.JsonSubTypes;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.io.IOException;\nimport java.nio.file.FileVisitResult;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.SimpleFileVisitor;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.util.ArrayList;\nimport java.util.List;\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\n@JsonSubTypes({\n    @JsonSubTypes.Type(value = ScriptCollectionSource.Directory.class),\n    @JsonSubTypes.Type(value = ScriptCollectionSource.GitRepository.class)\n})\npublic interface ScriptCollectionSource {\n\n    @JsonTypeName(\"directory\")\n    @Value\n    @Jacksonized\n    @Builder\n    class Directory implements ScriptCollectionSource {\n\n        Path path;\n\n        @SuppressWarnings(\"unused\")\n        static OptionsBuilder createOptions(Property<Directory> property) {\n            var path = new SimpleObjectProperty<>(\n                    property.getValue().getPath() != null\n                            ? FilePath.of(property.getValue().getPath())\n                            : null);\n            return new OptionsBuilder()\n                    .nameAndDescription(\"scriptDirectory\")\n                    .addComp(\n                            new ContextualFileReferenceChoiceComp(\n                                    new ReadOnlyObjectWrapper<>(\n                                            DataStorage.get().local().ref()),\n                                    path,\n                                    null,\n                                    List.of(),\n                                    entry -> DataStorage.get().local().equals(entry),\n                                    true),\n                            path)\n                    .nonNull()\n                    .bind(\n                            () -> Directory.builder()\n                                    .path(path.get() != null ? path.get().asLocalPath() : null)\n                                    .build(),\n                            property);\n        }\n\n        @Override\n        public void checkComplete() throws ValidationException {\n            Validators.nonNull(path);\n        }\n\n        @Override\n        public void prepare() {\n            if (!Files.isDirectory(path)) {\n                throw ErrorEventFactory.expected(\n                        new IllegalStateException(\"Source directory \" + path + \" does not exist\"));\n            }\n        }\n\n        @Override\n        public Path getLocalPath() {\n            return path;\n        }\n\n        @Override\n        public String toSummary() {\n            return path.toString();\n        }\n\n        @Override\n        public String toName() {\n            return AppI18n.get(\"directorySource\");\n        }\n    }\n\n    @JsonTypeName(\"gitRepository\")\n    @Value\n    @Jacksonized\n    @Builder\n    class GitRepository implements ScriptCollectionSource {\n\n        String url;\n\n        @SuppressWarnings(\"unused\")\n        static OptionsBuilder createOptions(Property<GitRepository> property) {\n            var url = new SimpleStringProperty(property.getValue().getUrl());\n            return new OptionsBuilder()\n                    .nameAndDescription(\"scriptSourceUrl\")\n                    .addString(url)\n                    .nonNull()\n                    .bind(() -> GitRepository.builder().url(url.get()).build(), property);\n        }\n\n        private String getName() {\n            var name = FilePath.of(url).getFileName();\n            if (!name.isEmpty()) {\n                return name;\n            }\n\n            return UuidHelper.generateFromObject(url).toString();\n        }\n\n        @Override\n        public void checkComplete() throws ValidationException {\n            Validators.nonNull(url);\n        }\n\n        @Override\n        public void prepare() throws Exception {\n            if (Files.exists(getLocalPath())) {\n                ProcessControlProvider.get().pullRepository(getLocalPath());\n            } else {\n                ProcessControlProvider.get().cloneRepository(url, getLocalPath());\n            }\n        }\n\n        @Override\n        public Path getLocalPath() {\n            return AppCache.getBasePath().resolve(\"scripts\").resolve(getName());\n        }\n\n        @Override\n        public String toSummary() {\n            return url.replace(\"http://\", \"\")\n                    .replace(\"https://\", \"\")\n                    .replace(\"file://\", \"\")\n                    .replace(\"ssh://\", \"\");\n        }\n\n        @Override\n        public String toName() {\n            return AppI18n.get(\"gitRepositorySource\");\n        }\n    }\n\n    void checkComplete() throws ValidationException;\n\n    void prepare() throws Exception;\n\n    Path getLocalPath();\n\n    String toSummary();\n\n    String toName();\n\n    default List<ScriptCollectionSourceEntry> listScripts() {\n        var availableDialects = ScriptDialects.getSupported();\n        var l = new ArrayList<ScriptCollectionSourceEntry>();\n\n        if (!Files.exists(getLocalPath())) {\n            return l;\n        }\n\n        try {\n            Files.walkFileTree(getLocalPath(), new SimpleFileVisitor<>() {\n                @Override\n                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {\n                    var name = file.getFileName().toString();\n                    var dialect = availableDialects.stream()\n                            .filter(shellDialect -> {\n                                return name.endsWith(\".\" + shellDialect.getScriptFileEnding());\n                            })\n                            .findFirst();\n                    if (dialect.isEmpty()) {\n                        return FileVisitResult.CONTINUE;\n                    }\n\n                    var entry = ScriptCollectionSourceEntry.builder()\n                            .name(name)\n                            .source(ScriptCollectionSource.this)\n                            .dialect(dialect.get())\n                            .localFile(file)\n                            .build();\n                    l.add(entry);\n\n                    return FileVisitResult.CONTINUE;\n                }\n            });\n        } catch (IOException e) {\n            ErrorEventFactory.fromThrowable(e).expected().handle();\n        }\n        return l;\n    }\n\n    static List<Class<?>> getClasses() {\n        var l = new ArrayList<Class<?>>();\n        l.add(Directory.class);\n        l.add(GitRepository.class);\n        return l;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/ScriptCollectionSourceBrowseActionProvider.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.DesktopHelper;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class ScriptCollectionSourceBrowseActionProvider implements HubLeafProvider<ScriptCollectionSourceStore> {\n\n    @Override\n    public AbstractAction createAction(DataStoreEntryRef<ScriptCollectionSourceStore> ref) {\n        return Action.builder().ref(ref).build();\n    }\n\n    @Override\n    public boolean isMajor() {\n        return true;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<ScriptCollectionSourceStore> store) {\n        return AppI18n.observable(\"browse\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<ScriptCollectionSourceStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2f-folder-search-outline\");\n    }\n\n    @Override\n    public Class<ScriptCollectionSourceStore> getApplicableClass() {\n        return ScriptCollectionSourceStore.class;\n    }\n\n    @Override\n    public String getId() {\n        return \"browseScriptCollectionSource\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<ScriptCollectionSourceStore> {\n\n        @Override\n        public void executeImpl() {\n            DesktopHelper.browseFile(ref.getStore().getSource().getLocalPath());\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/ScriptCollectionSourceEntry.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.process.ShellDialect;\n\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.nio.file.Path;\n\n@Value\n@Builder\n@Jacksonized\npublic class ScriptCollectionSourceEntry {\n\n    String name;\n    ShellDialect dialect;\n    ScriptCollectionSource source;\n    Path localFile;\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/ScriptCollectionSourceImportDialog.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.comp.base.*;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.ShellDialectIcons;\nimport io.xpipe.app.hub.comp.*;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.BooleanScope;\nimport io.xpipe.app.util.ThreadHelper;\nimport io.xpipe.core.FilePath;\n\nimport javafx.application.Platform;\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.*;\nimport javafx.collections.FXCollections;\nimport javafx.collections.ObservableList;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.Priority;\n\nimport org.kordamp.ikonli.javafx.FontIcon;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class ScriptCollectionSourceImportDialog {\n\n    private final DataStoreEntryRef<ScriptCollectionSourceStore> source;\n    private final ObservableList<ScriptCollectionSourceEntry> available = FXCollections.observableArrayList();\n    private final ObservableList<ScriptCollectionSourceEntry> shown = FXCollections.observableArrayList();\n    private final ObservableList<ScriptCollectionSourceEntry> selected = FXCollections.observableArrayList();\n    private final StringProperty filter = new SimpleStringProperty();\n    private final BooleanProperty busy = new SimpleBooleanProperty();\n    private final ObjectProperty<StoreCategoryWrapper> targetCategory = new SimpleObjectProperty<>();\n\n    public ScriptCollectionSourceImportDialog(DataStoreEntryRef<ScriptCollectionSourceStore> source) {\n        this.source = source;\n        available.setAll(source.getStore().getSource().listScripts());\n        update();\n\n        filter.addListener((observable, oldValue, newValue) -> {\n            update();\n        });\n\n        targetCategory.set(findDefaultCategory());\n    }\n\n    private StoreCategoryWrapper findDefaultCategory() {\n        var all = StoreViewState.get().getSortedCategories(StoreViewState.get().getAllScriptsCategory())\n                .filtered(w -> w.getParent() != null &&\n                        !w.getCategory().getUuid().equals(DataStorage.PREDEFINED_SCRIPTS_CATEGORY_UUID) &&\n                        !w.getCategory().getUuid().equals(DataStorage.SCRIPT_SOURCES_CATEGORY_UUID));\n        return all.getList().size() > 0 ? all.getList().getFirst() : null;\n    }\n\n    public void show() {\n        var filterField = new FilterComp(filter).hgrow();\n        // Ugly solution to focus the filter on show\n        filterField.apply(r -> {\n            HBox.setHgrow(r, Priority.SOMETIMES);\n            r.sceneProperty().subscribe(s -> {\n                if (s != null) {\n                    Platform.runLater(() -> {\n                        Platform.runLater(() -> {\n                            r.requestFocus();\n                        });\n                    });\n                }\n            });\n        });\n\n        var refresh = new ButtonComp(null, new FontIcon(\"mdmz-refresh\"), () -> {\n                    ThreadHelper.runAsync(() -> {\n                        try (var ignored = new BooleanScope(busy).exclusive().start()) {\n                            source.getStore().getSource().prepare();\n                            var all = source.getStore().getSource().listScripts();\n                            available.setAll(all);\n                            update();\n                        } catch (Exception e) {\n                            ErrorEventFactory.fromThrowable(e).handle();\n                        }\n                    });\n                })\n                .maxHeight(100);\n\n        var notFound = new LabelComp(AppI18n.observable(\"noScriptsFound\"));\n        notFound.show(Bindings.isEmpty(shown).and(busy.not()));\n\n        var selector = new ListSelectorComp<>(\n                shown,\n                e -> e.getName() + \" [\" + e.getDialect().getDisplayName() + \"]\",\n                e -> new LabelGraphic.ImageGraphic(ShellDialectIcons.getImageName(e.getDialect()), 16),\n                selected,\n                e -> false,\n                () -> shown.size() > 0);\n        selector.disable(busy);\n\n        var stack = new StackComp(List.of(notFound, selector));\n        stack.prefWidth(600);\n        stack.prefHeight(650);\n\n        var catChoice = new DataStoreCategoryChoiceComp(\n                StoreViewState.get().getAllScriptsCategory(),\n                StoreViewState.get().getActiveCategory(),\n                targetCategory,\n                false,\n                w -> {\n                    return w.getParent() != null && !w.equals(StoreViewState.get().getScriptSourcesCategory());\n                });\n        catChoice.hgrow();\n        catChoice.maxHeight(100);\n\n        var modal = ModalOverlay.of(\n                Bindings.createStringBinding(\n                        () -> {\n                            return AppI18n.get(\"scriptSourceCollectionImportTitle\", selected.size(), available.size());\n                        },\n                        available,\n                        selected,\n                        AppI18n.activeLanguage()),\n                stack,\n                null);\n        modal.addButtonBarComp(refresh);\n        modal.addButtonBarComp(filterField);\n        modal.addButtonBarComp(catChoice);\n        modal.addButton(ModalButton.ok(() -> {\n                    ThreadHelper.runAsync(() -> {\n                        finish();\n                    });\n                }))\n                .augment(button ->\n                        button.disableProperty().bind(Bindings.isEmpty(selected).or(targetCategory.isNull())));\n        modal.show();\n    }\n\n    private void finish() {\n        StoreViewState.get().selectCategoryIntoViewIfNeeded(targetCategory.getValue());\n\n        var added = new ArrayList<DataStoreEntry>();\n        for (ScriptCollectionSourceEntry e : selected) {\n            var name = FilePath.of(e.getName()).getBaseName().toString();\n            var textSource = ScriptTextSource.SourceReference.builder()\n                    .ref(source)\n                    .name(e.getName())\n                    .build();\n\n            var alreadyAdded = DataStorage.get().getStoreEntries().stream()\n                    .anyMatch(entry ->\n                            entry.getStore() instanceof ScriptStore ss && textSource.equals(ss.getTextSource()));\n            if (alreadyAdded) {\n                continue;\n            }\n\n            var store = ScriptStore.builder().textSource(textSource).build();\n            var entry = DataStoreEntry.createNew(name, store);\n            entry.setCategoryUuid(targetCategory.getValue().getCategory().getUuid());\n            DataStorage.get().addStoreEntryIfNotPresent(entry);\n            added.add(entry);\n        }\n\n        if (added.size() == 1) {\n            StoreCreationDialog.showEdit(added.getFirst());\n        }\n    }\n\n    private void update() {\n        if (filter.get() == null) {\n            shown.setAll(available);\n            return;\n        }\n\n        var f = filter.get().toLowerCase();\n        var filtered = available.stream()\n                .filter(e -> e.getName().toLowerCase().contains(f))\n                .toList();\n        var newList = new ArrayList<ScriptCollectionSourceEntry>();\n        newList.addAll(selected);\n        newList.addAll(filtered);\n        shown.setAll(newList);\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/ScriptCollectionSourceImportHubProvider.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class ScriptCollectionSourceImportHubProvider implements HubLeafProvider<ScriptCollectionSourceStore> {\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.CUSTOM;\n    }\n\n    @Override\n    public boolean isMajor() {\n        return true;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<ScriptCollectionSourceStore> store) {\n        return AppI18n.observable(\"importScripts\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<ScriptCollectionSourceStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2i-import\");\n    }\n\n    @Override\n    public Class<?> getApplicableClass() {\n        return ScriptCollectionSourceStore.class;\n    }\n\n    @Override\n    public String getId() {\n        return \"importScriptCollection\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<ScriptCollectionSourceStore> {\n\n        @Override\n        public void executeImpl() {\n            var dialog = new ScriptCollectionSourceImportDialog(ref);\n            dialog.show();\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/ScriptCollectionSourceRefreshHubProvider.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class ScriptCollectionSourceRefreshHubProvider implements HubLeafProvider<ScriptCollectionSourceStore> {\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.CUSTOM;\n    }\n\n    @Override\n    public boolean isMajor() {\n        return true;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<ScriptCollectionSourceStore> store) {\n        return AppI18n.observable(\"refreshSource\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<ScriptCollectionSourceStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2r-refresh\");\n    }\n\n    @Override\n    public Class<?> getApplicableClass() {\n        return ScriptCollectionSourceStore.class;\n    }\n\n    @Override\n    public String getId() {\n        return \"refreshScriptCollection\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<ScriptCollectionSourceStore> {\n\n        @Override\n        public void executeImpl() throws Exception {\n            ref.getStore().refresh();\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/ScriptCollectionSourceStore.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.util.Validators;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.*;\nimport lombok.experimental.FieldDefaults;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\n\n@SuperBuilder(toBuilder = true)\n@Value\n@Jacksonized\n@JsonTypeName(\"scriptCollectionSource\")\npublic class ScriptCollectionSourceStore\n        implements DataStore, StatefulDataStore<ScriptCollectionSourceStore.State>, ValidatableStore {\n\n    ScriptCollectionSource source;\n\n    @Override\n    public void checkComplete() throws Throwable {\n        Validators.nonNull(source);\n        source.checkComplete();\n    }\n\n    public void refresh() throws Exception {\n        source.prepare();\n        var l = source.listScripts();\n        setState(State.builder().entries(l).build());\n    }\n\n    @Override\n    public void validate() throws Exception {\n        refresh();\n    }\n\n    @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)\n    @Getter\n    @EqualsAndHashCode(callSuper = true)\n    @SuperBuilder(toBuilder = true)\n    @Jacksonized\n    public static class State extends DataStoreState {\n\n        List<ScriptCollectionSourceEntry> entries;\n\n        @Override\n        public DataStoreState mergeCopy(DataStoreState newer) {\n            var s = (State) newer;\n            var b = toBuilder();\n            b.entries(useNewer(entries, s.entries));\n            return b.build();\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/ScriptCollectionSourceStoreProvider.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.hub.comp.*;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.platform.OptionsChoiceBuilder;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreCategory;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.app.util.StoreStateFormat;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.SneakyThrows;\n\nimport java.util.List;\nimport java.util.UUID;\n\npublic class ScriptCollectionSourceStoreProvider implements DataStoreProvider {\n\n    @Override\n    public int getOrderPriority() {\n        return 1;\n    }\n\n    @Override\n    public UUID getTargetCategory(DataStore store, UUID target) {\n        return DataStorage.SCRIPT_SOURCES_CATEGORY_UUID;\n    }\n\n    @Override\n    public DocumentationLink getHelpLink() {\n        return DocumentationLink.SCRIPTING;\n    }\n\n    @Override\n    public boolean canMoveCategories() {\n        return false;\n    }\n\n    @Override\n    public boolean shouldShowScan() {\n        return false;\n    }\n\n    @Override\n    public BaseRegionBuilder<?, ?> stateDisplay(StoreEntryWrapper w) {\n        return new SystemStateComp(new SimpleObjectProperty<>(SystemStateComp.State.SUCCESS));\n    }\n\n    @Override\n    public DataStoreCreationCategory getCreationCategory() {\n        return DataStoreCreationCategory.SCRIPT;\n    }\n\n    @SneakyThrows\n    @Override\n    public GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {\n        ScriptCollectionSourceStore st = store.getValue().asNeeded();\n\n        var source = new SimpleObjectProperty<>(st.getSource());\n\n        var sourceChoice = OptionsChoiceBuilder.builder()\n                .property(source)\n                .available(ScriptCollectionSource.getClasses())\n                .build();\n\n        return new OptionsBuilder()\n                .nameAndDescription(\"scriptCollectionSourceType\")\n                .sub(sourceChoice.build(), source)\n                .nonNull()\n                .bind(\n                        () -> {\n                            return ScriptCollectionSourceStore.builder()\n                                    .source(source.get())\n                                    .build();\n                        },\n                        store)\n                .buildDialog();\n    }\n\n    @Override\n    public String summaryString(StoreEntryWrapper wrapper) {\n        ScriptCollectionSourceStore st = wrapper.getEntry().getStore().asNeeded();\n        return st.getSource().toName();\n    }\n\n    @Override\n    public ObservableValue<String> informationString(StoreSection section) {\n        ScriptCollectionSourceStore st =\n                section.getWrapper().getEntry().getStore().asNeeded();\n        return Bindings.createStringBinding(\n                () -> {\n                    var s = st.getState();\n                    var summary = st.getSource().toSummary();\n                    var init = s.getEntries() != null;\n                    var format = new StoreStateFormat(\n                            List.of(),\n                            summary,\n                            init\n                                    ? AppI18n.get(\n                                            \"scriptsContained\", s.getEntries().size())\n                                    : null,\n                            !init ? AppI18n.get(\"notInitialized\") : null);\n                    return format.format();\n                },\n                section.getWrapper().getPersistentState(),\n                AppI18n.activeLanguage());\n    }\n\n    @Override\n    public DataStore defaultStore(DataStoreCategory category) {\n        return ScriptCollectionSourceStore.builder().build();\n    }\n\n    @Override\n    public String getId() {\n        return \"scriptCollectionSource\";\n    }\n\n    @Override\n    public List<Class<?>> getStoreClasses() {\n        return List.of(ScriptCollectionSourceStore.class);\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/ScriptDataStorageProvider.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.core.AppProperties;\nimport io.xpipe.app.ext.DataStorageExtensionProvider;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\n\npublic class ScriptDataStorageProvider extends DataStorageExtensionProvider {\n\n    @Override\n    public void storageInit() {\n        if (AppProperties.get().isNewBuildSession()) {\n            var legacyLeftovers = DataStorage.get().getStoreEntries().stream()\n                    .filter(entry -> {\n                        return entry.getValidity() == DataStoreEntry.Validity.LOAD_FAILED\n                                && (\"My scripts\".equals(entry.getName())\n                                        || \"Files\".equals(entry.getName())\n                                        || \"Management\".equals(entry.getName()));\n                    })\n                    .toList();\n            DataStorage.get().deleteWithChildren(legacyLeftovers.toArray(DataStoreEntry[]::new));\n        }\n\n        // Don't regenerate if the user deleted anything\n        if (!AppProperties.get().isInitialLaunch()) {\n            return;\n        }\n\n        if (AppProperties.get().isTest()) {\n            return;\n        }\n\n        for (PredefinedScriptStore value : PredefinedScriptStore.values()) {\n            var previous = DataStorage.get().getStoreEntryIfPresent(value.getUuid());\n            var store = value.getScriptStore().get();\n            if (previous.isPresent()) {\n                DataStorage.get().updateEntryStore(previous.get(), store);\n                value.setEntry(previous.get().ref());\n            } else {\n                var e = DataStoreEntry.createNew(\n                        value.getUuid(), DataStorage.PREDEFINED_SCRIPTS_CATEGORY_UUID, value.getName(), store);\n                DataStorage.get().addStoreEntryIfNotPresent(e);\n                value.setEntry(e.ref());\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/ScriptDialects.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.process.ShellDialect;\nimport io.xpipe.app.process.ShellDialects;\n\nimport java.util.List;\n\npublic class ScriptDialects {\n\n    public static List<ShellDialect> getSupported() {\n        var availableDialects = List.of(\n                ShellDialects.SH,\n                ShellDialects.BASH,\n                ShellDialects.ZSH,\n                ShellDialects.FISH,\n                ShellDialects.CMD,\n                ShellDialects.POWERSHELL,\n                ShellDialects.POWERSHELL_CORE);\n        return availableDialects;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/ScriptHierarchy.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreCategory;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport lombok.Value;\n\nimport java.util.*;\nimport java.util.function.Predicate;\n\n@Value\npublic class ScriptHierarchy {\n\n    String name;\n    DataStoreCategory category;\n    DataStoreEntryRef<ScriptStore> script;\n    List<ScriptHierarchy> children;\n\n    public static ScriptHierarchy buildEnabledHierarchy(Predicate<DataStoreEntryRef<ScriptStore>> include) {\n        var enabled =\n                ScriptStoreSetup.getEnabledScripts().stream().filter(include).toList();\n\n        var categories = new HashSet<DataStoreCategory>();\n        for (DataStoreEntryRef<ScriptStore> ref : enabled) {\n            var cat = DataStorage.get().getStoreCategory(ref.get());\n            var catParents = DataStorage.get().getCategoryParentHierarchy(cat);\n            categories.addAll(catParents);\n        }\n\n        var hierarchy = new ScriptHierarchy(null, null, null, new ArrayList<>());\n        while (true) {\n            var changed = false;\n            for (DataStoreCategory cat : categories) {\n                // We don't support the All Scripts root\n                if (cat.getParentCategory() == null) {\n                    continue;\n                }\n\n                var toAdd = new ScriptHierarchy(cat.getName(), cat, null, new ArrayList<>());\n\n                if (cat.getParentCategory().equals(DataStorage.ALL_SCRIPTS_CATEGORY_UUID)) {\n                    if (!hierarchy.getChildren().contains(toAdd)) {\n                        hierarchy.getChildren().add(toAdd);\n                        changed = true;\n                    }\n                    continue;\n                }\n\n                var parentHierarchy = findParent(hierarchy, cat);\n                if (parentHierarchy.isEmpty()) {\n                    continue;\n                }\n\n                var alreadyAdded = parentHierarchy.get().getChildren().contains(toAdd);\n                if (alreadyAdded) {\n                    continue;\n                }\n\n                parentHierarchy.get().getChildren().add(toAdd);\n                changed = true;\n            }\n\n            if (!changed) {\n                break;\n            }\n        }\n\n        for (DataStoreEntryRef<ScriptStore> scriptRef : enabled) {\n            var scriptCategory = DataStorage.get().getStoreCategory(scriptRef.get());\n            var catHierarchy = findParent(hierarchy, scriptCategory);\n            if (catHierarchy.isEmpty()) {\n                continue;\n            }\n\n            var childTarget = catHierarchy.get().getChildren().stream()\n                    .filter(child -> child.getCategory().equals(scriptCategory))\n                    .findFirst();\n            if (childTarget.isEmpty()) {\n                continue;\n            }\n\n            childTarget\n                    .get()\n                    .getChildren()\n                    .add(new ScriptHierarchy(scriptRef.get().getName(), null, scriptRef, List.of()));\n        }\n\n        return condenseHierarchy(hierarchy);\n    }\n\n    private static Optional<ScriptHierarchy> findParent(ScriptHierarchy hierarchy, DataStoreCategory category) {\n        if (category.equals(hierarchy.getCategory())) {\n            return Optional.of(hierarchy);\n        }\n\n        if (hierarchy.getChildren().stream().anyMatch(child -> category.equals(child.getCategory()))) {\n            return Optional.of(hierarchy);\n        }\n\n        var children = hierarchy.getChildren();\n        for (ScriptHierarchy child : children) {\n            var foundInChild = findParent(child, category);\n            if (foundInChild.isPresent()) {\n                return foundInChild;\n            }\n        }\n\n        return Optional.empty();\n    }\n\n    public static ScriptHierarchy condenseHierarchy(ScriptHierarchy hierarchy) {\n        var children =\n                hierarchy.getChildren().stream().map(c -> condenseHierarchy(c)).toList();\n        if (children.size() == 1 && !children.getFirst().isLeaf()) {\n            var nestedChildren = children.getFirst().getChildren();\n            return new ScriptHierarchy(\n                    children.getFirst().getName(), hierarchy.getCategory(), hierarchy.getScript(), nestedChildren);\n        } else {\n            return new ScriptHierarchy(hierarchy.getName(), hierarchy.getCategory(), hierarchy.getScript(), children);\n        }\n    }\n\n    public boolean show() {\n        return isLeaf() || !isEmptyBranch();\n    }\n\n    public boolean isEmptyBranch() {\n        if (category == null) {\n            return false;\n        }\n\n        return children.isEmpty();\n    }\n\n    public boolean isLeaf() {\n        return script != null;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/ScriptQuickEditHubLeafProvider.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.hub.comp.StoreCreationDialog;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.process.OsFileSystem;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.FileOpener;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.Arrays;\n\npublic class ScriptQuickEditHubLeafProvider implements HubLeafProvider<ScriptStore> {\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.CUSTOM;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<ScriptStore> store) {\n        return AppI18n.observable(\"edit\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<ScriptStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdal-edit\");\n    }\n\n    @Override\n    public Class<ScriptStore> getApplicableClass() {\n        return ScriptStore.class;\n    }\n\n    @Override\n    public boolean isDefault() {\n        return true;\n    }\n\n    @Override\n    public AbstractAction createAction(DataStoreEntryRef<ScriptStore> store) {\n        return Action.builder().ref(store).build();\n    }\n\n    @Override\n    public String getId() {\n        return \"editScriptInEditor\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<ScriptStore> {\n\n        @Override\n        public void executeImpl() {\n            var predefined = DataStorage.get()\n                            .getStoreCategoryIfPresent(ref.get().getCategoryUuid())\n                            .map(category -> category.getUuid().equals(DataStorage.PREDEFINED_SCRIPTS_CATEGORY_UUID))\n                            .orElse(false)\n                    && Arrays.stream(PredefinedScriptStore.values())\n                            .anyMatch(predefinedScriptStore -> predefinedScriptStore\n                                    .getName()\n                                    .equals(ref.get().getName()));\n            if (predefined) {\n                StoreCreationDialog.showEdit(ref.get());\n                return;\n            }\n\n            var inPlace = ref.getStore().getTextSource() instanceof ScriptTextSource.InPlace;\n            if (!inPlace) {\n                StoreCreationDialog.showEdit(ref.get());\n                return;\n            }\n\n            var script = ref.getStore();\n            var dialect = script.getShellDialect();\n            var ext = dialect != null ? dialect.getScriptFileEnding() : \"sh\";\n            var name = OsFileSystem.ofLocal().makeFileSystemCompatible(ref.get().getName());\n            FileOpener.openString(\n                    name + \".\" + ext, this, script.getTextSource().getText().getValue(), (s) -> {\n                        DataStorage.get()\n                                .updateEntryStore(\n                                        ref.get(),\n                                        script.toBuilder()\n                                                .textSource(ScriptTextSource.InPlace.builder()\n                                                        .dialect(dialect)\n                                                        .text(ShellScript.of(s))\n                                                        .build())\n                                                .build());\n                    });\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStore.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.process.ScriptHelper;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.process.ShellDialect;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.Validators;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Singular;\nimport lombok.SneakyThrows;\nimport lombok.Value;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.SequencedCollection;\n\n@SuperBuilder(toBuilder = true)\n@Value\n@Jacksonized\n@JsonTypeName(\"script\")\npublic class ScriptStore implements SelfReferentialStore, StatefulDataStore<EnabledStoreState>, ValidatableStore {\n\n    @Singular\n    List<DataStoreEntryRef<ScriptStore>> scripts;\n\n    String description;\n\n    ScriptTextSource textSource;\n    boolean initScript;\n    boolean shellScript;\n    boolean fileScript;\n    boolean runnableScript;\n\n\n\n    @Override\n    public Class<EnabledStoreState> getStateClass() {\n        return EnabledStoreState.class;\n    }\n\n    SequencedCollection<DataStoreEntryRef<ScriptStore>> queryFlattenedScripts() {\n        var seen = new LinkedHashSet<DataStoreEntryRef<ScriptStore>>();\n        queryFlattenedScripts(seen);\n        return seen;\n    }\n\n    public ShellDialect getShellDialect() {\n        return textSource != null ? textSource.getDialect() : null;\n    }\n\n    public boolean isCompatible(ShellControl shellControl) {\n        var targetType = shellControl.getOriginalShellDialect();\n        return getShellDialect() == null || getShellDialect().isCompatibleTo(targetType);\n    }\n\n    public boolean isCompatible(ShellDialect dialect) {\n        return getShellDialect() == null || getShellDialect().isCompatibleTo(dialect);\n    }\n\n    private String assembleScript(ShellControl shellControl, boolean args) {\n        if (isCompatible(shellControl)) {\n            var raw = getTextSource().getText().withoutShebang().getValue();\n            if (raw.isBlank()) {\n                return null;\n            }\n\n            var targetType = shellControl.getOriginalShellDialect();\n            var scriptDialect = getShellDialect() != null ? getShellDialect() : targetType;\n            var script = ScriptHelper.createExecScript(scriptDialect, shellControl, raw);\n            var canSource = targetType.isSourceCompatibleTo(scriptDialect);\n            var base = canSource\n                    ? targetType.sourceScriptCommand(shellControl, script.toString())\n                    : targetType.runScriptCommand(shellControl, script.toString());\n            return base + (args ? \" \" + targetType.getCatchAllVariable() : \"\");\n        }\n\n        return null;\n    }\n\n    @SneakyThrows\n    public ShellScript assembleScriptChain(ShellControl shellControl, boolean args) {\n        var all = queryFlattenedScripts();\n\n        for (DataStoreEntryRef<ScriptStore> ref : all) {\n            ref.getStore().getTextSource().checkAvailable();\n        }\n\n        var r = all.stream()\n                .map(ref -> ref.getStore().assembleScript(shellControl, args))\n                .filter(s -> s != null)\n                .toList();\n        if (r.isEmpty()) {\n            return null;\n        }\n        return ShellScript.lines(r);\n    }\n\n    public ShellScript assembleScriptForFile(ShellControl shellControl) {\n        var raw = getTextSource().getText().withoutShebang().getValue();\n        if (raw.isBlank()) {\n            return null;\n        }\n\n        var targetType = shellControl.getOriginalShellDialect();\n        var scriptDialect = getShellDialect() != null ? getShellDialect() : targetType;\n        var content = scriptDialect.prepareScriptContent(shellControl, raw);\n        return ShellScript.of(content);\n    }\n\n    @Override\n    public void checkComplete() throws Throwable {\n        if (textSource != null) {\n            textSource.checkComplete();\n        }\n        if (!initScript && !shellScript && !fileScript && !runnableScript) {\n            throw new ValidationException(AppI18n.get(\"valueMustNotBeEmpty\"));\n        }\n        if (scripts != null) {\n            Validators.contentNonNull(scripts);\n            for (DataStoreEntryRef<ScriptStore> script : scripts) {\n                Validators.nonNull(script);\n                Validators.isType(script, ScriptStore.class);\n            }\n        }\n    }\n\n    public void queryFlattenedScripts(LinkedHashSet<DataStoreEntryRef<ScriptStore>> all) {\n        DataStoreEntryRef<ScriptStore> ref = getSelfEntry().ref();\n        var added = all.add(ref);\n        // Prevent loop\n        if (added) {\n            getEffectiveScripts().stream()\n                    .filter(scriptStoreDataStoreEntryRef -> !all.contains(scriptStoreDataStoreEntryRef))\n                    .forEach(scriptStoreDataStoreEntryRef -> {\n                        scriptStoreDataStoreEntryRef.getStore().queryFlattenedScripts(all);\n                    });\n            all.remove(ref);\n            all.add(ref);\n        }\n    }\n\n    public List<DataStoreEntryRef<ScriptStore>> getEffectiveScripts() {\n        return scripts != null\n                ? scripts.stream()\n                        .filter(Objects::nonNull)\n                        .filter(ref -> ref.get().getValidity().isUsable())\n                        .toList()\n                : List.of();\n    }\n\n    public ScriptTextSource getTextSource() {\n        return textSource != null\n                ? textSource\n                : ScriptTextSource.InPlace.builder().build();\n    }\n\n    @Override\n    public void validate() throws Exception {\n        getTextSource().validate();\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStoreMigrationDeserializer.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.JsonDeserializer;\nimport com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer;\nimport com.fasterxml.jackson.databind.jsontype.TypeDeserializer;\nimport com.fasterxml.jackson.databind.node.JsonNodeFactory;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.fasterxml.jackson.databind.node.TreeTraversingParser;\n\nimport java.io.IOException;\n\npublic class ScriptStoreMigrationDeserializer extends DelegatingDeserializer {\n\n    public ScriptStoreMigrationDeserializer(JsonDeserializer<?> d) {\n        super(d);\n    }\n\n    @Override\n    protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {\n        return new ScriptStoreMigrationDeserializer(newDelegatee);\n    }\n\n    @Override\n    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {\n        return super.deserialize(restructure(p), ctxt);\n    }\n\n    @Override\n    public Object deserialize(JsonParser p, DeserializationContext ctxt, Object intoValue) throws IOException {\n        return super.deserialize(restructure(p), ctxt, intoValue);\n    }\n\n    public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer typeDeserializer)\n            throws IOException {\n        return super.deserializeWithType(restructure(jp), ctxt, typeDeserializer);\n    }\n\n    public JsonParser restructure(JsonParser p) throws IOException {\n        var node = p.readValueAsTree();\n        if (node == null || !node.isObject()) {\n            return p;\n        }\n\n        // Check if already in the new format\n        if (node.get(\"textSource\") == null) {\n            migrate((ObjectNode) node);\n        }\n\n        var newJsonParser = new TreeTraversingParser(((ObjectNode) node), p.getCodec());\n        newJsonParser.nextToken();\n        return newJsonParser;\n    }\n\n    private void migrate(ObjectNode n) {\n        var commandsNode = n.remove(\"commands\");\n        var dialectNode = n.remove(\"minimumDialect\");\n\n        var obj = JsonNodeFactory.instance.objectNode();\n        obj.put(\"type\", \"inPlace\");\n        obj.put(\"text\", commandsNode.textValue());\n        if (!dialectNode.isNull()) {\n            obj.put(\"dialect\", dialectNode.textValue());\n        } else {\n            obj.putNull(\"dialect\");\n        }\n\n        n.set(\"textSource\", obj);\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStoreProvider.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.comp.base.ListSelectorComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.hub.comp.*;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.platform.OptionsChoiceBuilder;\nimport io.xpipe.app.platform.Validator;\nimport io.xpipe.app.process.OsFileSystem;\nimport io.xpipe.app.process.ShellDialects;\nimport io.xpipe.app.storage.DataStoreCategory;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.util.*;\nimport io.xpipe.core.OsType;\n\nimport javafx.beans.property.*;\nimport javafx.beans.value.ObservableValue;\nimport javafx.collections.FXCollections;\n\nimport lombok.SneakyThrows;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Optional;\nimport java.util.function.Function;\n\npublic class ScriptStoreProvider implements DataStoreProvider {\n\n    @Override\n    public StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) {\n        if (sec.getWrapper().getValidity().getValue() == DataStoreEntry.Validity.LOAD_FAILED) {\n            return StoreEntryComp.create(sec, null, preferLarge);\n        }\n\n        EnabledStoreState initialState = sec.getWrapper().getEntry().getStorePersistentState();\n        var enabled = new SimpleBooleanProperty(initialState.isEnabled());\n        sec.getWrapper().getPersistentState().subscribe((newValue) -> {\n            EnabledStoreState s = sec.getWrapper().getEntry().getStorePersistentState();\n            enabled.set(s.isEnabled());\n        });\n\n        var toggle = StoreToggleComp.<StatefulDataStore<EnabledStoreState>>enableToggle(\n                null, sec, enabled, (s, aBoolean) -> {\n                    var state = s.getState().toBuilder().enabled(aBoolean).build();\n                    s.setState(state);\n                });\n\n        return StoreEntryComp.create(sec, toggle, preferLarge);\n    }\n\n    @Override\n    public DocumentationLink getHelpLink() {\n        return DocumentationLink.SCRIPTING;\n    }\n\n    @Override\n    public boolean canMoveCategories() {\n        return false;\n    }\n\n    @Override\n    public boolean showProviderChoice() {\n        return false;\n    }\n\n    @Override\n    public boolean shouldShowScan() {\n        return false;\n    }\n\n    @Override\n    public BaseRegionBuilder<?, ?> stateDisplay(StoreEntryWrapper w) {\n        return new SystemStateComp(new SimpleObjectProperty<>(SystemStateComp.State.SUCCESS));\n    }\n\n    @Override\n    public DataStoreCreationCategory getCreationCategory() {\n        return DataStoreCreationCategory.SCRIPT;\n    }\n\n    @SneakyThrows\n    @Override\n    public GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {\n        ScriptStore st = store.getValue().asNeeded();\n\n        var textSource = new SimpleObjectProperty<>(\n                st.getTextSource() != null\n                        ? st.getTextSource()\n                        : ScriptTextSource.InPlace.builder().build());\n        var others =\n                new SimpleListProperty<>(FXCollections.observableArrayList(new ArrayList<>(st.getEffectiveScripts())));\n\n        var textSourceChoice = OptionsChoiceBuilder.builder()\n                .property(textSource)\n                .available(ScriptTextSource.getClasses())\n                .allowNull(true)\n                .build();\n\n        var vals = List.of(0, 1, 2, 3);\n        var selectedStart = new ArrayList<Integer>();\n        if (st.isInitScript()) {\n            selectedStart.add(0);\n        }\n        if (st.isRunnableScript()) {\n            selectedStart.add(1);\n        }\n        if (st.isFileScript()) {\n            selectedStart.add(2);\n        }\n        if (st.isShellScript()) {\n            selectedStart.add(3);\n        }\n        var name = new Function<Integer, String>() {\n\n            @Override\n            public String apply(Integer integer) {\n                if (integer == 0) {\n                    return AppI18n.get(\"initScript\");\n                }\n\n                if (integer == 1) {\n                    return AppI18n.get(\"runnableScript\");\n                }\n\n                if (integer == 2) {\n                    return AppI18n.get(\"fileScript\");\n                }\n\n                if (integer == 3) {\n                    return AppI18n.get(\"shellScript\");\n                }\n\n                return \"?\";\n            }\n        };\n        var selectedExecTypes = new SimpleListProperty<>(FXCollections.observableList(selectedStart));\n        var selectorComp = new ListSelectorComp<>(\n                FXCollections.observableList(vals), name, ignored -> null, selectedExecTypes, v -> false, () -> false);\n\n        return new OptionsBuilder()\n                .nameAndDescription(\"scriptSourceType\")\n                .sub(textSourceChoice.build(), textSource)\n                .nameAndDescription(\"executionType\")\n                .documentationLink(DocumentationLink.SCRIPTING_TYPES)\n                .addComp(selectorComp, selectedExecTypes)\n                .check(validator ->\n                        Validator.nonEmpty(validator, AppI18n.observable(\"executionType\"), selectedExecTypes))\n                .name(\"snippets\")\n                .description(\"snippetsDescription\")\n                .documentationLink(DocumentationLink.SCRIPTING_DEPENDENCIES)\n                .addComp(\n                        new StoreListChoiceComp<>(\n                                others,\n                                ScriptStore.class,\n                                scriptStore -> !scriptStore.get().equals(entry) && !others.contains(scriptStore),\n                                StoreViewState.get().getAllScriptsCategory()),\n                        others)\n                .bind(\n                        () -> {\n                            return ScriptStore.builder()\n                                    .textSource(textSource.get())\n                                    .scripts(new ArrayList<>(others.get()))\n                                    .description(st.getDescription())\n                                    .initScript(selectedExecTypes.contains(0))\n                                    .runnableScript(selectedExecTypes.contains(1))\n                                    .fileScript(selectedExecTypes.contains(2))\n                                    .shellScript(selectedExecTypes.contains(3))\n                                    .build();\n                        },\n                        store)\n                .buildDialog();\n    }\n\n    @Override\n    public String summaryString(StoreEntryWrapper wrapper) {\n        ScriptStore st = wrapper.getEntry().getStore().asNeeded();\n        var name = st.getShellDialect() != null ? st.getShellDialect().getExecutableName() : AppI18n.get(\"generic\");\n        return name + \" \" + AppI18n.get(\"script\");\n    }\n\n    @Override\n    public ObservableValue<String> informationString(StoreSection section) {\n        ScriptStore st = section.getWrapper().getEntry().getStore().asNeeded();\n        var init = st.isInitScript() ? AppI18n.get(\"init\") : null;\n        var file = st.isFileScript() ? AppI18n.get(\"fileBrowser\") : null;\n        var shell = st.isShellScript()\n                ? AppI18n.get(\"shell\")\n                : null;\n        var name = st.isShellScript()\n                ? getShellSessionScriptName(section.getWrapper()).orElse(null)\n                : null;\n        var runnable = st.isRunnableScript() ? AppI18n.get(\"hub\") : null;\n        return new ReadOnlyObjectWrapper<>(\n                new StoreStateFormat(List.of(), st.getTextSource().toSummary(), shell, init, file, runnable, name).format());\n    }\n\n    private Optional<String> getShellSessionScriptName(StoreEntryWrapper wrapper) {\n        ScriptStore st = wrapper.getEntry().getStore().asNeeded();\n        if (!st.isShellScript()) {\n            return Optional.empty();\n        }\n\n        var name = wrapper.getName().getValue().toLowerCase(Locale.ROOT).replaceAll(\" \", \"_\");\n        if (st.getShellDialect() == null) {\n            return Optional.of(OsFileSystem.of(OsType.LINUX).makeFileSystemCompatible(name) + \".sh\");\n        }\n\n        var os = st.getShellDialect() == ShellDialects.CMD || ShellDialects.isPowershell(st.getShellDialect())\n                ? OsType.WINDOWS\n                : OsType.LINUX;\n        return Optional.of(OsFileSystem.of(os).makeFileSystemCompatible(name) + \".\"\n                + st.getShellDialect().getScriptFileEnding());\n    }\n\n    @SneakyThrows\n    @Override\n    public String getDisplayIconFileName(DataStore store) {\n        if (store == null) {\n            return \"base:script_icon.svg\";\n        }\n\n        ScriptStore st = store.asNeeded();\n        return ShellDialectIcons.getImageName(st.getShellDialect());\n    }\n\n    @Override\n    public DataStore defaultStore(DataStoreCategory category) {\n        return ScriptStore.builder().scripts(List.of()).build();\n    }\n\n    @Override\n    public String getId() {\n        return \"script\";\n    }\n\n    @Override\n    public List<Class<?>> getStoreClasses() {\n        return List.of(ScriptStore.class);\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStoreSetup.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.ext.StatefulDataStore;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.*;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.core.FilePath;\n\nimport lombok.SneakyThrows;\n\nimport java.util.*;\n\npublic class ScriptStoreSetup {\n\n    public static void controlWithDefaultScripts(ShellControl pc) {\n        controlWithScripts(pc, getEnabledScripts(), false);\n    }\n\n    public static void controlWithScripts(\n            ShellControl pc, Collection<DataStoreEntryRef<ScriptStore>> enabledScripts, boolean append) {\n        try {\n            var dialect = pc.getShellDialect();\n            if (dialect == null) {\n                var source = pc.getSourceStore();\n                if (source.isPresent() && source.get() instanceof StatefulDataStore<?> sds) {\n                    var state = sds.getState();\n                    if (state instanceof SystemState systemState) {\n                        dialect = systemState.getShellDialect();\n                    }\n                }\n            }\n\n            var finalDialect = dialect;\n            var initFlattened = flatten(enabledScripts).stream()\n                    .filter(store -> store.getStore().isInitScript())\n                    .filter(store -> finalDialect == null || store.getStore().isCompatible(finalDialect))\n                    .toList();\n            if (!append) {\n                initFlattened = initFlattened.reversed();\n            }\n            var bringFlattened = flatten(enabledScripts).stream()\n                    .filter(store -> store.getStore().isShellScript())\n                    .filter(store -> finalDialect == null || store.getStore().isCompatible(finalDialect))\n                    .toList();\n            if (!append) {\n                bringFlattened = bringFlattened.reversed();\n            }\n\n            // Optimize if we have nothing to do\n            if (initFlattened.isEmpty() && bringFlattened.isEmpty()) {\n                return;\n            }\n\n            initFlattened.forEach(s -> {\n                pc.withInitSnippet(\n                        new ShellTerminalInitCommand() {\n                            @Override\n                            public Optional<String> terminalContent(ShellControl shellControl) {\n                                var assembled = s.getStore().assembleScriptChain(shellControl, false);\n                                if (assembled == null) {\n                                    return Optional.empty();\n                                }\n\n                                return Optional.ofNullable(assembled.getValue());\n                            }\n\n                            @Override\n                            public boolean canPotentiallyRunInDialect(ShellDialect dialect) {\n                                return s.getStore().isCompatible(dialect);\n                            }\n                        },\n                        append);\n            });\n            if (!bringFlattened.isEmpty()) {\n                var finalBringFlattened = bringFlattened;\n                pc.withInitSnippet(\n                        new ShellTerminalInitCommand() {\n\n                            FilePath dir;\n\n                            @Override\n                            public Optional<String> terminalContent(ShellControl shellControl) throws Exception {\n                                if (dir == null) {\n                                    dir = initScriptsDirectory(shellControl, finalBringFlattened);\n                                }\n\n                                if (dir == null) {\n                                    return Optional.empty();\n                                }\n\n                                return Optional.ofNullable(shellControl\n                                        .getShellDialect()\n                                        .addToPathVariableCommand(List.of(dir.toString()), true));\n                            }\n\n                            @Override\n                            public boolean canPotentiallyRunInDialect(ShellDialect dialect) {\n                                return true;\n                            }\n                        },\n                        append);\n            }\n        } catch (StackOverflowError t) {\n            throw ErrorEventFactory.expected(\n                    new RuntimeException(\"Unable to set up scripts. Is there a circular script dependency?\", t));\n        } catch (Throwable t) {\n            throw new RuntimeException(\"Unable to set up scripts\", t);\n        }\n    }\n\n    @SneakyThrows\n    private static FilePath initScriptsDirectory(ShellControl sc, List<DataStoreEntryRef<ScriptStore>> refs) {\n        if (refs.isEmpty()) {\n            return null;\n        }\n\n        var applicable = refs.stream()\n                .filter(ss -> ss.getStore().isCompatible(sc.getShellDialect()))\n                .toList();\n        if (applicable.isEmpty()) {\n            return null;\n        }\n\n        var hash = refs.stream()\n                .mapToInt(value ->\n                        value.get().getName().hashCode() + value.getStore().hashCode())\n                .sum();\n        var targetDir = ShellTemp.createUserSpecificTempDataDirectory(sc, \"scripts\")\n                .join(sc.getShellDialect().getId());\n        var hashFile = targetDir.join(\"hash\");\n        if (sc.view().fileExists(hashFile)) {\n            var read = sc.view().readTextFile(hashFile);\n            try {\n                var readHash = Integer.parseInt(read.strip());\n                if (hash == readHash) {\n                    return targetDir;\n                }\n            } catch (NumberFormatException e) {\n                ErrorEventFactory.fromThrowable(e).expected().omit().handle();\n            }\n        }\n\n        if (sc.view().directoryExists(targetDir)) {\n            sc.view().deleteDirectory(targetDir);\n        }\n        sc.view().mkdir(targetDir);\n\n        var availableRefs = new ArrayList<>(refs);\n        availableRefs.removeIf(ref -> {\n            try {\n                ref.getStore().getTextSource().checkAvailable();\n                return false;\n            } catch (Exception ex) {\n                ErrorEventFactory.fromThrowable(ex).expected().handle();\n                return true;\n            }\n        });\n\n        var d = sc.getShellDialect();\n        for (DataStoreEntryRef<ScriptStore> scriptStore : availableRefs) {\n            var content = scriptStore.getStore().assembleScriptForFile(sc);\n            if (content != null) {\n                var fileName = OsFileSystem.of(sc.getOsType()).makeFileSystemCompatible(\n                        scriptStore.get().getName().toLowerCase(Locale.ROOT).replaceAll(\" \", \"_\"));\n                var fileType = scriptStore.getStore().getShellDialect() != null ?\n                        scriptStore.getStore().getShellDialect().getScriptFileEnding() :\n                        d.getScriptFileEnding();\n                var scriptFile = targetDir.join(fileName + \".\" + fileType);\n                sc.view().writeScriptFile(scriptFile, content.getValue());\n            }\n        }\n\n        sc.view().writeTextFile(hashFile, String.valueOf(hash));\n        return targetDir;\n    }\n\n    public static List<DataStoreEntryRef<ScriptStore>> getEnabledScripts() {\n        var l = DataStorage.get().getStoreEntries().stream()\n                .filter(dataStoreEntry -> dataStoreEntry.getValidity().isUsable()\n                        && dataStoreEntry.getStore() instanceof ScriptStore ss\n                        && ss.getState().isEnabled())\n                .<DataStoreEntryRef<ScriptStore>>map(DataStoreEntry::ref)\n                .toList();\n        return l;\n    }\n\n    public static List<DataStoreEntryRef<ScriptStore>> flatten(Collection<DataStoreEntryRef<ScriptStore>> scripts) {\n        var seen = new LinkedHashSet<DataStoreEntryRef<ScriptStore>>();\n        scripts.stream()\n                .filter(scriptStoreDataStoreEntryRef ->\n                        scriptStoreDataStoreEntryRef.get().getValidity().isUsable())\n                .forEach(scriptStoreDataStoreEntryRef ->\n                        scriptStoreDataStoreEntryRef.getStore().queryFlattenedScripts(seen));\n\n        var dependencies = new HashMap<DataStoreEntryRef<? extends ScriptStore>, Set<DataStoreEntryRef<ScriptStore>>>();\n        seen.forEach(ref -> {\n            var f = new HashSet<>(ref.getStore().queryFlattenedScripts());\n            f.remove(ref);\n            dependencies.put(ref, f);\n        });\n\n        var sorted = new ArrayList<>(seen);\n        sorted.sort((o1, o2) -> {\n            if (dependencies.get(o1).contains(o2)) {\n                return 1;\n            }\n\n            if (dependencies.get(o2).contains(o1)) {\n                return -1;\n            }\n\n            return 0;\n        });\n        return sorted;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/ScriptTextSource.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.comp.base.ButtonComp;\nimport io.xpipe.app.comp.base.InputGroupComp;\nimport io.xpipe.app.comp.base.IntegratedTextAreaComp;\nimport io.xpipe.app.core.AppCache;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.core.window.AppDialog;\nimport io.xpipe.app.ext.ShellDialectChoiceComp;\nimport io.xpipe.app.ext.ValidationException;\nimport io.xpipe.app.hub.comp.StoreChoiceComp;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.process.ShellDialect;\nimport io.xpipe.app.process.ShellScript;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.app.util.HttpHelper;\nimport io.xpipe.app.util.Validators;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.core.UuidHelper;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.property.SimpleStringProperty;\n\nimport com.fasterxml.jackson.annotation.JsonSubTypes;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.SneakyThrows;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\n@JsonSubTypes({\n    @JsonSubTypes.Type(value = ScriptTextSource.InPlace.class),\n    @JsonSubTypes.Type(value = ScriptTextSource.SourceReference.class),\n    @JsonSubTypes.Type(value = ScriptTextSource.Url.class)\n})\npublic interface ScriptTextSource {\n\n    @JsonTypeName(\"inPlace\")\n    @Value\n    @Jacksonized\n    @Builder\n    class InPlace implements ScriptTextSource {\n\n        ShellDialect dialect;\n        ShellScript text;\n\n        @SuppressWarnings(\"unused\")\n        public static String getOptionsNameKey() {\n            return \"scriptSourceTypeInPlace\";\n        }\n\n        @SuppressWarnings(\"unused\")\n        static OptionsBuilder createOptions(Property<InPlace> property) {\n            var dialect = new SimpleObjectProperty<>(property.getValue().getDialect());\n            var text = new SimpleObjectProperty<>(property.getValue().getText());\n\n            var availableDialects = ScriptDialects.getSupported();\n            var choice = new ShellDialectChoiceComp(\n                    availableDialects, dialect, ShellDialectChoiceComp.NullHandling.NULL_IS_ALL);\n\n            return new OptionsBuilder()\n                    .name(\"minimumShellDialect\")\n                    .description(\"minimumShellDialectDescription\")\n                    .documentationLink(DocumentationLink.SCRIPTING_COMPATIBILITY)\n                    .addComp(choice, dialect)\n                    .name(\"scriptContents\")\n                    .description(\"scriptContentsDescription\")\n                    .documentationLink(DocumentationLink.SCRIPTING_EDITING)\n                    .addComp(\n                            IntegratedTextAreaComp.script(\n                                    text,\n                                    Bindings.createStringBinding(\n                                            () -> {\n                                                return dialect.getValue() != null\n                                                        ? dialect.getValue().getScriptFileEnding()\n                                                        : \"sh\";\n                                            },\n                                            dialect)),\n                            text)\n                    .bind(\n                            () -> InPlace.builder()\n                                    .dialect(dialect.get())\n                                    .text(text.get())\n                                    .build(),\n                            property);\n        }\n\n        @Override\n        public void checkComplete() {}\n\n        @Override\n        public void checkAvailable() {}\n\n        @Override\n        public void validate() {\n            checkAvailable();\n        }\n\n        @Override\n        public String toSummary() {\n            return AppI18n.get(\"inPlaceScript\");\n        }\n\n        @Override\n        public ShellScript getText() {\n            return text != null ? text : ShellScript.empty();\n        }\n    }\n\n    @JsonTypeName(\"url\")\n    @Value\n    @Jacksonized\n    @Builder\n    class Url implements ScriptTextSource {\n\n        ShellDialect dialect;\n        String url;\n\n        @SuppressWarnings(\"unused\")\n        public static String getOptionsNameKey() {\n            return \"scriptSourceTypeUrl\";\n        }\n\n        @SuppressWarnings(\"unused\")\n        static OptionsBuilder createOptions(Property<Url> property) {\n            var dialect = new SimpleObjectProperty<>(property.getValue().getDialect());\n            var url = new SimpleStringProperty(property.getValue().getUrl());\n\n            var availableDialects = ScriptDialects.getSupported();\n            var choice = new ShellDialectChoiceComp(\n                    availableDialects, dialect, ShellDialectChoiceComp.NullHandling.NULL_IS_ALL);\n\n            return new OptionsBuilder()\n                    .name(\"minimumShellDialect\")\n                    .description(\"minimumShellDialectDescription\")\n                    .documentationLink(DocumentationLink.SCRIPTING_COMPATIBILITY)\n                    .addComp(choice, dialect)\n                    .nameAndDescription(\"scriptTextSourceUrl\")\n                    .addString(url)\n                    .nonNull()\n                    .bind(\n                            () -> Url.builder()\n                                    .dialect(dialect.get())\n                                    .url(url.get())\n                                    .build(),\n                            property);\n        }\n\n        public void refresh() throws Exception {\n            var path = getLocalPath();\n            if (Files.exists(path)) {\n                return;\n            }\n\n            var req = HttpRequest.newBuilder().GET().uri(URI.create(url)).build();\n            var r = HttpHelper.client().send(req, HttpResponse.BodyHandlers.ofString());\n            if (r.statusCode() >= 400) {\n                throw ErrorEventFactory.expected(new IOException(r.body()));\n            }\n\n            Files.createDirectories(path.getParent());\n            Files.writeString(path, r.body());\n        }\n\n        private Path getLocalPath() {\n            return AppCache.getBasePath().resolve(\"scripts\").resolve(getName());\n        }\n\n        private String getName() {\n            var name = FilePath.of(url).getFileName();\n            if (!name.isEmpty()) {\n                return name;\n            }\n\n            return UuidHelper.generateFromObject(url).toString();\n        }\n\n        @Override\n        public void checkComplete() throws ValidationException {\n            Validators.nonNull(url);\n        }\n\n        @Override\n        public void checkAvailable() {\n            if (!Files.exists(getLocalPath())) {\n                throw ErrorEventFactory.expected(\n                        new IllegalStateException(\"Script URL \" + url + \" has not been initialized\"));\n            }\n        }\n\n        @Override\n        public void validate() throws Exception {\n            refresh();\n            checkAvailable();\n        }\n\n        @Override\n        public String toSummary() {\n            return AppI18n.get(\n                    \"sourcedFrom\",\n                    url.replace(\"http://\", \"\")\n                            .replace(\"https://\", \"\")\n                            .replace(\"file://\", \"\")\n                            .replace(\"ssh://\", \"\"));\n        }\n\n        @Override\n        @SneakyThrows\n        public ShellScript getText() {\n            var path = getLocalPath();\n\n            if (!Files.exists(path)) {\n                return ShellScript.empty();\n            }\n\n            try {\n                var r = Files.readString(path);\n                return ShellScript.of(r);\n            } catch (IOException e) {\n                ErrorEventFactory.fromThrowable(e).expected().handle();\n                return ShellScript.empty();\n            }\n        }\n    }\n\n    @JsonTypeName(\"source\")\n    @Value\n    @Jacksonized\n    @Builder\n    class SourceReference implements ScriptTextSource {\n\n        DataStoreEntryRef<ScriptCollectionSourceStore> ref;\n        String name;\n\n        @SuppressWarnings(\"unused\")\n        public static String getOptionsNameKey() {\n            return \"scriptSourceTypeSource\";\n        }\n\n        @SuppressWarnings(\"unused\")\n        static OptionsBuilder createOptions(Property<SourceReference> property) {\n            var ref = new SimpleObjectProperty<>(property.getValue().getRef());\n            var name = new SimpleStringProperty(property.getValue().getName());\n\n            var sourceChoice = new StoreChoiceComp<>(\n                    null,\n                    ref,\n                    ScriptCollectionSourceStore.class,\n                    ignored -> true,\n                    StoreViewState.get().getAllScriptsCategory(),\n                    StoreViewState.get().getScriptSourcesCategory(),\n                    true);\n\n            var importButton = new ButtonComp(null, new LabelGraphic.IconGraphic(\"mdi2i-import\"), () -> {\n                var current = AppDialog.getCurrentModalOverlay();\n                current.ifPresent(modalOverlay -> modalOverlay.close());\n\n                var dialog = new ScriptCollectionSourceImportDialog(ref.get());\n                dialog.show();\n            });\n            importButton.disable(ref.isNull());\n\n            return new OptionsBuilder()\n                    .nameAndDescription(\"scriptCollectionSourceEntry\")\n                    .addComp(new InputGroupComp(List.of(sourceChoice, importButton)).setMainReference(0), ref)\n                    .nonNull()\n                    .nameAndDescription(\"scriptSourceName\")\n                    .addString(name)\n                    .nonNull()\n                    .bind(\n                            () -> SourceReference.builder()\n                                    .ref(ref.getValue())\n                                    .name(name.getValue())\n                                    .build(),\n                            property);\n        }\n\n        @Override\n        @SneakyThrows\n        public void checkComplete() {\n            Validators.nonNull(ref);\n            ref.checkComplete();\n        }\n\n        @Override\n        public void checkAvailable() {\n            var cached = ref.getStore().getState().getEntries();\n            if (cached == null) {\n                throw ErrorEventFactory.expected(\n                        new IllegalStateException(\"Source \" + ref.get().getName() + \" has not been initialized\"));\n            }\n\n            var found = cached.stream()\n                    .filter(e -> e.getName().equals(name))\n                    .findFirst()\n                    .orElse(null);\n            if (found == null) {\n                throw ErrorEventFactory.expected(new IllegalStateException(\"Script file \" + name\n                        + \" not found in local cache for source \" + ref.get().getName()));\n            }\n\n            if (!Files.exists(found.getLocalFile())) {\n                throw ErrorEventFactory.expected(\n                        new IllegalStateException(\"Referenced script file \" + found.getLocalFile() + \" does not exist\"));\n            }\n        }\n\n        @Override\n        public void validate() {\n            checkAvailable();\n        }\n\n        @Override\n        public String toSummary() {\n            return AppI18n.get(\"sourcedFrom\", ref.get().getName());\n        }\n\n        @Override\n        public ShellDialect getDialect() {\n            var found = findSourceEntryIfPossible();\n            return found != null ? found.getDialect() : null;\n        }\n\n        @Override\n        @SneakyThrows\n        public ShellScript getText() {\n            var found = findSourceEntryIfPossible();\n            if (found == null) {\n                return ShellScript.empty();\n            }\n\n            if (!Files.exists(found.getLocalFile())) {\n                return ShellScript.empty();\n            }\n\n            try {\n                var r = Files.readString(found.getLocalFile());\n                return ShellScript.of(r);\n            } catch (IOException e) {\n                ErrorEventFactory.fromThrowable(e).expected().handle();\n                return ShellScript.empty();\n            }\n        }\n\n        private ScriptCollectionSourceEntry findSourceEntryIfPossible() {\n            if (ref == null) {\n                return null;\n            }\n\n            var cached = ref.getStore().getState().getEntries();\n            if (cached == null) {\n                return null;\n            }\n\n            var found = cached.stream()\n                    .filter(e -> e.getName().equals(name))\n                    .findFirst()\n                    .orElse(null);\n            if (found == null) {\n                return null;\n            }\n\n            return found;\n        }\n    }\n\n    void checkComplete() throws ValidationException;\n\n    void checkAvailable();\n\n    void validate() throws Exception;\n\n    String toSummary();\n\n    ShellDialect getDialect();\n\n    ShellScript getText();\n\n    static List<Class<?>> getClasses() {\n        var l = new ArrayList<Class<?>>();\n        l.add(InPlace.class);\n        l.add(SourceReference.class);\n        l.add(Url.class);\n        return l;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/script/ScriptUrlSourceRefreshHubProvider.java",
    "content": "package io.xpipe.ext.base.script;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class ScriptUrlSourceRefreshHubProvider implements HubLeafProvider<ScriptStore> {\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.CUSTOM;\n    }\n\n    @Override\n    public boolean isApplicable(DataStoreEntryRef<ScriptStore> o) {\n        return o.getStore().getTextSource() instanceof ScriptTextSource.Url;\n    }\n\n    @Override\n    public boolean isMajor() {\n        return true;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<ScriptStore> store) {\n        return AppI18n.observable(\"refreshSource\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<ScriptStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2r-refresh\");\n    }\n\n    @Override\n    public Class<?> getApplicableClass() {\n        return ScriptStore.class;\n    }\n\n    @Override\n    public String getId() {\n        return \"refreshScriptUrlSource\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<ScriptStore> {\n\n        @Override\n        public void executeImpl() throws Exception {\n            var url = (ScriptTextSource.Url) ref.getStore().getTextSource();\n            url.refresh();\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStore.java",
    "content": "package io.xpipe.ext.base.service;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.GroupStore;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.Validators;\n\nimport lombok.AccessLevel;\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.ToString;\nimport lombok.experimental.FieldDefaults;\nimport lombok.experimental.SuperBuilder;\n\n@Getter\n@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)\n@SuperBuilder\n@EqualsAndHashCode\n@ToString\npublic abstract class AbstractServiceGroupStore<T extends DataStore> implements DataStore, GroupStore<T> {\n\n    DataStoreEntryRef<? extends T> parent;\n\n    @Override\n    public void checkComplete() throws Throwable {\n        Validators.nonNull(parent);\n        // Essentially a null check\n        Validators.isType(parent, DataStore.class);\n        parent.checkComplete();\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStoreProvider.java",
    "content": "package io.xpipe.ext.base.service;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.DataStoreProvider;\nimport io.xpipe.app.ext.DataStoreUsageCategory;\nimport io.xpipe.app.hub.comp.*;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.util.DocumentationLink;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.value.ObservableValue;\n\npublic abstract class AbstractServiceGroupStoreProvider implements DataStoreProvider {\n\n    @Override\n    public DocumentationLink getHelpLink() {\n        return DocumentationLink.SERVICES;\n    }\n\n    @Override\n    public BaseRegionBuilder<?, ?> stateDisplay(StoreEntryWrapper w) {\n        return new SystemStateComp(new SimpleObjectProperty<>(SystemStateComp.State.SUCCESS));\n    }\n\n    @Override\n    public DataStoreUsageCategory getUsageCategory() {\n        return DataStoreUsageCategory.GROUP;\n    }\n\n    @Override\n    public DataStoreEntry getDisplayParent(DataStoreEntry store) {\n        AbstractServiceGroupStore<?> s = store.getStore().asNeeded();\n        return s.getParent().get();\n    }\n\n    @Override\n    public ObservableValue<String> informationString(StoreSection section) {\n        return Bindings.createStringBinding(\n                () -> {\n                    var all = section.getAllChildren().getList();\n                    var shown = section.getShownChildren().getList();\n                    if (shown.size() == 0) {\n                        return null;\n                    }\n\n                    var string = all.size() == shown.size() ? all.size() : shown.size() + \"/\" + all.size();\n                    return all.size() > 0\n                            ? (all.size() == 1 ? AppI18n.get(\"hasService\", string) : AppI18n.get(\"hasServices\", string))\n                            : AppI18n.get(\"noServices\");\n                },\n                section.getShownChildren().getList(),\n                section.getAllChildren().getList(),\n                AppI18n.activeLanguage());\n    }\n\n    @Override\n    public String getDisplayIconFileName(DataStore store) {\n        return \"base:serviceGroup_icon.svg\";\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStore.java",
    "content": "package io.xpipe.ext.base.service;\n\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.HostHelper;\nimport io.xpipe.app.util.LicenseProvider;\nimport io.xpipe.app.util.Validators;\nimport io.xpipe.ext.base.host.HostAddressGatewayStore;\nimport io.xpipe.ext.base.host.HostAddressStore;\n\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.SneakyThrows;\nimport lombok.ToString;\nimport lombok.experimental.SuperBuilder;\n\n@SuperBuilder(toBuilder = true)\n@Getter\n@EqualsAndHashCode\n@ToString\npublic abstract class AbstractServiceStore\n        implements SingletonSessionStore<NetworkTunnelSession>, DataStore, StartOnInitStore {\n\n    private final Integer remotePort;\n    private final Integer localPort;\n    private final ServiceProtocolType serviceProtocolType;\n\n    public abstract boolean shouldTunnel();\n\n    public abstract String getAddress();\n\n    public abstract DataStoreEntryRef<NetworkTunnelStore> getGateway();\n\n    public abstract DataStoreEntryRef<HostAddressStore> getHost();\n\n    public boolean licenseRequired() {\n        return true;\n    }\n\n    @Override\n    public boolean canAutomaticallyStart() {\n        return requiresTunnel();\n    }\n\n    @Override\n    public void checkComplete() throws Throwable {\n        Validators.nonNull(remotePort);\n        Validators.nonNull(serviceProtocolType);\n        if (getHost() == null) {\n            Validators.nonNull(getAddress());\n        }\n    }\n\n    public String getOpenTargetUrl() {\n        var s = getSession();\n        if (s == null) {\n            var address = getAddress();\n\n            if (address == null\n                    && (getHost().getStore() instanceof HostAddressGatewayStore g)\n                    && !(getHost().getStore() instanceof NetworkTunnelStore)) {\n                address = g.getHostAddress().get();\n            }\n\n            if (address == null && (getHost().getStore() instanceof NetworkTunnelStore t)) {\n                var h = t.getTunnelHostName();\n                address = !h.isEmpty() ? h.get() : null;\n            }\n\n            if (address == null) {\n                address = \"localhost\";\n            }\n\n            return ServiceAddressRotation.getRotatedLocalhost(address + \":\" + getRemotePort());\n        }\n\n        return ServiceAddressRotation.getRotatedLocalhost(\"localhost:\" + s.getLocalPort());\n    }\n\n    public boolean requiresTunnel() {\n        if (getAddress() != null) {\n            var gateway = getGateway();\n            if (gateway != null) {\n                return gateway.getStore().requiresTunnel();\n            } else {\n                return false;\n            }\n        }\n\n        if (getHost() == null) {\n            return false;\n        }\n\n        if (getHost().getStore() instanceof HostAddressGatewayStore g\n                && !(getHost().getStore() instanceof NetworkTunnelStore)) {\n            var gw = g.getTunnelGateway();\n            return gw != null && gw.getStore().requiresTunnel();\n        }\n\n        if (!(getHost().getStore() instanceof NetworkTunnelStore t)) {\n            return false;\n        }\n\n        if (!t.isLocallyTunnelable()) {\n            var parent = t.getNetworkParent();\n            if (parent == null || !(parent.getStore() instanceof NetworkTunnelStore nts)) {\n                return false;\n            }\n\n            return shouldTunnel() && nts.requiresTunnel();\n        } else {\n            return shouldTunnel() && t.requiresTunnel();\n        }\n    }\n\n    @Override\n    @SneakyThrows\n    public NetworkTunnelSession newSession() {\n        if (getAddress() != null) {\n            if (getGateway() == null || !getGateway().getStore().isLocallyTunnelable()) {\n                return null;\n            }\n        }\n\n        if (getHost() != null) {\n            if (!(getHost().getStore() instanceof NetworkTunnelStore)\n                    && getHost().getStore() instanceof HostAddressGatewayStore g) {\n                if (g.getTunnelGateway() == null\n                        || !g.getTunnelGateway().getStore().requiresTunnel()\n                        || !g.getTunnelGateway().getStore().isLocallyTunnelable()) {\n                    return null;\n                }\n            } else if (getHost().getStore() instanceof NetworkTunnelStore t) {\n                if (!t.isLocallyTunnelable()) {\n                    var parent = t.getNetworkParent();\n                    if (!(parent.getStore() instanceof NetworkTunnelStore)) {\n                        return null;\n                    }\n                }\n\n                if (!shouldTunnel()) {\n                    return null;\n                }\n            } else {\n                return null;\n            }\n        }\n\n        var f = LicenseProvider.get().getFeature(\"services\");\n        if (licenseRequired() && !f.isSupported()) {\n            var active = DataStorage.get().getStoreEntries().stream()\n                    .filter(dataStoreEntry -> dataStoreEntry.getStore() instanceof AbstractServiceStore a\n                            && a != this\n                            && a.licenseRequired()\n                            && a.isSessionRunning())\n                    .count();\n            if (active > 0) {\n                f.throwIfUnsupported();\n            }\n        }\n\n        var l = localPort != null ? localPort : HostHelper.findRandomOpenPortOnAllLocalInterfaces();\n\n        if (getAddress() != null) {\n            var gateway = getGateway();\n            return gateway.getStore().createTunnelSession(l, remotePort, getAddress());\n        }\n\n        if (getHost().getStore() instanceof NetworkTunnelStore t) {\n            var parent = t.getNetworkParent();\n            if (!t.isLocallyTunnelable() && parent.getStore() instanceof NetworkTunnelStore nts) {\n                getHost().getStore().refreshHostAddressOrThrow();\n                var h = t.getTunnelHostName();\n                return nts.createTunnelSession(l, remotePort, !h.isEmpty() ? h.get() : \"localhost\");\n            }\n\n            return t.createTunnelSession(l, remotePort, \"localhost\");\n        }\n\n        var g = (HostAddressGatewayStore) getHost().getStore();\n        return g.getTunnelGateway()\n                .getStore()\n                .createTunnelSession(l, remotePort, g.getHostAddress().get());\n    }\n\n    @Override\n    public Class<?> getSessionClass() {\n        return NetworkTunnelSession.class;\n    }\n\n    @Override\n    public void startOnInit() throws Exception {\n        startSessionIfNeeded();\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStoreProvider.java",
    "content": "package io.xpipe.ext.base.service;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.hub.comp.*;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.util.DocumentationLink;\nimport io.xpipe.app.util.StoreStateFormat;\nimport io.xpipe.core.FailableRunnable;\nimport io.xpipe.ext.base.host.HostAddressGatewayStore;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic abstract class AbstractServiceStoreProvider implements SingletonSessionStoreProvider, DataStoreProvider {\n\n    @Override\n    public boolean showIncompleteInfo() {\n        return true;\n    }\n\n    @Override\n    public DocumentationLink getHelpLink() {\n        return DocumentationLink.SERVICES;\n    }\n\n    @Override\n    public boolean supportsSession(SingletonSessionStore<?> s) {\n        var abs = (AbstractServiceStore) s;\n        if (abs.getAddress() != null) {\n            return abs.getGateway() != null\n                    && abs.getGateway().getStore().isLocallyTunnelable()\n                    && abs.getGateway().getStore().requiresTunnel();\n        }\n\n        if (abs.getHost() != null) {\n            if (abs.getHost().getStore() instanceof HostAddressGatewayStore a) {\n                if (a.getTunnelGateway() != null\n                        && a.getTunnelGateway().getStore().requiresTunnel()\n                        && a.getTunnelGateway().getStore().isLocallyTunnelable()\n                        && abs.shouldTunnel()) {\n                    return true;\n                }\n            }\n\n            if (abs.getHost().getStore() instanceof NetworkTunnelStore t) {\n                if (!t.requiresTunnel()) {\n                    return false;\n                }\n\n                if (!abs.shouldTunnel()) {\n                    return false;\n                }\n\n                if (t.isLocallyTunnelable()) {\n                    return true;\n                }\n\n                var parent = t.getNetworkParent();\n                if (!t.isLocallyTunnelable() && parent.getStore() instanceof NetworkTunnelStore nts) {\n                    return nts.isLocallyTunnelable();\n                }\n\n                return false;\n            }\n        }\n\n        return false;\n    }\n\n    @Override\n    public FailableRunnable<Exception> launch(DataStoreEntry store) {\n        return () -> {\n            AbstractServiceStore serviceStore = store.getStore().asNeeded();\n            serviceStore.startSessionIfNeeded();\n            var full = serviceStore.getServiceProtocolType().formatAddress(serviceStore.getOpenTargetUrl());\n            serviceStore.getServiceProtocolType().open(full);\n        };\n    }\n\n    public String displayName(DataStoreEntry entry) {\n        AbstractServiceStore s = entry.getStore().asNeeded();\n        return DataStorage.get().getStoreEntryDisplayName(s.getHost().get()) + \" - Port \" + s.getRemotePort();\n    }\n\n    @Override\n    public List<String> getSearchableTerms(DataStore store) {\n        AbstractServiceStore s = store.asNeeded();\n        var l = new ArrayList<String>();\n        l.add(\"\" + s.getRemotePort());\n        if (s.getLocalPort() != null) {\n            l.add(\"\" + s.getLocalPort());\n        }\n        if (s.getAddress() != null) {\n            l.add(s.getAddress());\n        }\n        return l;\n    }\n\n    @Override\n    public DataStoreUsageCategory getUsageCategory() {\n        return DataStoreUsageCategory.TUNNEL;\n    }\n\n    @Override\n    public DataStoreEntry getSyntheticParent(DataStoreEntry store) {\n        AbstractServiceStore s = store.getStore().asNeeded();\n        return DataStorage.get()\n                .getOrCreateNewSyntheticEntry(\n                        s.getHost().get(),\n                        \"Services\",\n                        CustomServiceGroupStore.builder().parent(s.getHost()).build());\n    }\n\n    @Override\n    public ObservableValue<String> informationString(StoreSection section) {\n        return Bindings.createStringBinding(\n                () -> {\n                    AbstractServiceStore s =\n                            section.getWrapper().getEntry().getStore().asNeeded();\n                    var desc = formatService(s);\n                    var type = s.getServiceProtocolType() != null\n                                    && !(s.getServiceProtocolType() instanceof ServiceProtocolType.Undefined)\n                            ? AppI18n.get(s.getServiceProtocolType().getTranslationKey())\n                            : null;\n                    var state = !s.requiresTunnel()\n                            ? null\n                            : s.isSessionRunning()\n                                    ? AppI18n.get(\"active\")\n                                    : s.isSessionEnabled() ? AppI18n.get(\"starting\") : AppI18n.get(\"inactive\");\n                    return new StoreStateFormat(List.of(), desc, type, state).format();\n                },\n                section.getWrapper().getCache(),\n                AppI18n.activeLanguage());\n    }\n\n    @Override\n    public String getDisplayIconFileName(DataStore store) {\n        return \"base:service_icon.svg\";\n    }\n\n    @Override\n    public BaseRegionBuilder<?, ?> stateDisplay(StoreEntryWrapper w) {\n        return new SystemStateComp(Bindings.createObjectBinding(\n                () -> {\n                    if (!w.getEntry().getValidity().isUsable()) {\n                        return SystemStateComp.State.OTHER;\n                    }\n\n                    AbstractServiceStore s = w.getEntry().getStore().asNeeded();\n\n                    if (!s.requiresTunnel()) {\n                        return SystemStateComp.State.SUCCESS;\n                    }\n\n                    if (!s.isSessionEnabled() || (s.isSessionEnabled() && !s.isSessionRunning())) {\n                        return SystemStateComp.State.OTHER;\n                    }\n\n                    return s.isSessionRunning() ? SystemStateComp.State.SUCCESS : SystemStateComp.State.FAILURE;\n                },\n                w.getCache()));\n    }\n\n    protected String formatService(AbstractServiceStore s) {\n        var desc = s.getLocalPort() != null\n                ? \"localhost:\" + s.getLocalPort() + \" <- :\" + s.getRemotePort()\n                : s.isSessionRunning()\n                        ? \"localhost:\" + s.getSession().getLocalPort() + \" <- :\" + s.getRemotePort()\n                        : AppI18n.get(\"servicePort\", s.getRemotePort());\n        return desc;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceGroupStore.java",
    "content": "package io.xpipe.ext.base.service;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.util.Validators;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.*;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\n@SuperBuilder\n@Jacksonized\n@JsonTypeName(\"customServiceGroup\")\n@EqualsAndHashCode(callSuper = true)\n@ToString(callSuper = true)\npublic class CustomServiceGroupStore extends AbstractServiceGroupStore<DataStore> {\n\n    @Override\n    public void checkComplete() throws Throwable {\n        Validators.nonNull(getParent());\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceGroupStoreProvider.java",
    "content": "package io.xpipe.ext.base.service;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.storage.DataStoreCategory;\nimport io.xpipe.app.storage.DataStoreEntry;\n\nimport java.util.List;\n\npublic class CustomServiceGroupStoreProvider extends AbstractServiceGroupStoreProvider {\n\n    @Override\n    public DataStore defaultStore(DataStoreCategory category) {\n        return CustomServiceGroupStore.builder().build();\n    }\n\n    @Override\n    public String getId() {\n        return \"customServiceGroup\";\n    }\n\n    @Override\n    public List<Class<?>> getStoreClasses() {\n        return List.of(CustomServiceGroupStore.class);\n    }\n\n    @Override\n    public DataStoreEntry getDisplayParent(DataStoreEntry store) {\n        CustomServiceGroupStore s = store.getStore().asNeeded();\n        return s.getParent().get();\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStore.java",
    "content": "package io.xpipe.ext.base.service;\n\nimport io.xpipe.app.ext.NetworkTunnelStore;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.ext.base.host.AbstractHostStore;\nimport io.xpipe.ext.base.host.AbstractHostTransformStore;\nimport io.xpipe.ext.base.host.HostAddressStore;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.ToString;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\n@SuperBuilder(toBuilder = true)\n@Getter\n@Jacksonized\n@JsonTypeName(\"customService\")\n@EqualsAndHashCode(callSuper = true)\n@ToString(callSuper = true)\npublic final class CustomServiceStore extends AbstractServiceStore implements AbstractHostTransformStore {\n\n    private final DataStoreEntryRef<HostAddressStore> host;\n    private final String address;\n    private final DataStoreEntryRef<NetworkTunnelStore> gateway;\n    private final Boolean tunnelToLocalhost;\n\n    @Override\n    public void checkComplete() throws Throwable {\n        super.checkComplete();\n        if (gateway != null) {\n            gateway.checkComplete();\n        }\n    }\n\n    @Override\n    public boolean canConvertToAbstractHost() {\n        return host == null;\n    }\n\n    @Override\n    public AbstractHostStore createAbstractHostStore() {\n        return AbstractHostStore.builder().host(address).gateway(gateway).build();\n    }\n\n    @Override\n    public AbstractHostTransformStore withNewParent(DataStoreEntryRef<AbstractHostStore> newParent) {\n        return toBuilder()\n                .address(null)\n                .gateway(null)\n                .host(newParent.asNeeded())\n                .build();\n    }\n\n    @Override\n    public boolean shouldTunnel() {\n        return tunnelToLocalhost == null || tunnelToLocalhost;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStoreProvider.java",
    "content": "package io.xpipe.ext.base.service;\n\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.hub.comp.StoreChoiceComp;\nimport io.xpipe.app.hub.comp.StoreComboChoiceComp;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreCategory;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.ext.base.host.AbstractHostStore;\nimport io.xpipe.ext.base.host.HostAddressGatewayStore;\nimport io.xpipe.ext.base.host.HostAddressStore;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleObjectProperty;\n\nimport java.util.List;\n\npublic class CustomServiceStoreProvider extends AbstractServiceStoreProvider {\n\n    @Override\n    public DataStoreEntry getSyntheticParent(DataStoreEntry store) {\n        var c = (CustomServiceStore) store.getStore();\n        if (c.getHost() == null || c.getHost().getStore() instanceof AbstractHostStore) {\n            return null;\n        }\n\n        return super.getSyntheticParent(store);\n    }\n\n    @Override\n    public DataStoreEntry getDisplayParent(DataStoreEntry store) {\n        var c = (CustomServiceStore) store.getStore();\n        if (c.getHost() != null && c.getHost().getStore() instanceof AbstractHostStore) {\n            return c.getHost().get();\n        }\n\n        return super.getDisplayParent(store);\n    }\n\n    @Override\n    public int getOrderPriority() {\n        return -1;\n    }\n\n    @Override\n    public DataStoreCreationCategory getCreationCategory() {\n        return DataStoreCreationCategory.SERVICE;\n    }\n\n    @Override\n    public GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {\n        CustomServiceStore st = store.getValue().asNeeded();\n\n        var comboHost = new SimpleObjectProperty<>(StoreComboChoiceComp.ComboValue.of(st.getAddress(), st.getHost()));\n        var gateway = new SimpleObjectProperty<>(st.getGateway());\n        var hideGateway = BindingsHelper.map(comboHost, c -> c == null || c.getRef() != null);\n        comboHost.addListener((obs, o, n) -> {\n            if (n != null && n.getRef() != null) {\n                gateway.setValue(null);\n            }\n        });\n\n        var localPort = new SimpleObjectProperty<>(st.getLocalPort());\n        var remotePort = new SimpleObjectProperty<>(st.getRemotePort());\n        var serviceProtocolType = new SimpleObjectProperty<>(st.getServiceProtocolType());\n        var tunnelToLocalhost =\n                new SimpleBooleanProperty(st.getTunnelToLocalhost() != null ? st.getTunnelToLocalhost() : true);\n        var hideTunnelToLocalhost = Bindings.createBooleanBinding(\n                () -> {\n                    return comboHost.get() == null\n                            || gateway.get() != null\n                            || (comboHost.get().getRef().getStore() instanceof HostAddressGatewayStore g\n                                    && g.getTunnelGateway() != null\n                                    && !(g.getTunnelGateway().getStore() instanceof LocalStore));\n                },\n                comboHost,\n                gateway);\n        var hideLocalPort = Bindings.createBooleanBinding(\n                () -> {\n                    return comboHost.get() == null || !tunnelToLocalhost.get();\n                },\n                hideTunnelToLocalhost,\n                tunnelToLocalhost);\n\n        var hostChoice = new StoreComboChoiceComp<>(\n                hostStore -> {\n                    HostAddress addr = hostStore.getHostAddress();\n                    return addr != null && !addr.isEmpty() ? addr.get() : null;\n                },\n                entry,\n                comboHost,\n                HostAddressStore.class,\n                n -> true,\n                StoreViewState.get().getAllConnectionsCategory(),\n                false);\n        var gatewayChoice = new StoreChoiceComp<>(\n                entry,\n                gateway,\n                NetworkTunnelStore.class,\n                ref -> !ref.get().equals(DataStorage.get().local()),\n                StoreViewState.get().getAllConnectionsCategory(),\n                true);\n\n        var q = new OptionsBuilder()\n                .nameAndDescription(\"serviceHost\")\n                .addComp(hostChoice, comboHost)\n                .nonNull()\n                .nameAndDescription(\"gateway\")\n                .addComp(gatewayChoice, gateway)\n                .hide(hideGateway)\n                .nameAndDescription(\"serviceRemotePort\")\n                .addInteger(remotePort)\n                .nonNull()\n                .sub(ServiceProtocolTypeHelper.choice(serviceProtocolType), serviceProtocolType)\n                .nonNull()\n                .nameAndDescription(\"tunnelToLocalhost\")\n                .addToggle(tunnelToLocalhost)\n                .hide(hideTunnelToLocalhost)\n                .nameAndDescription(\"serviceLocalPort\")\n                .addInteger(localPort)\n                .hide(hideLocalPort)\n                .bind(\n                        () -> {\n                            return CustomServiceStore.builder()\n                                    .address(\n                                            comboHost.get() != null\n                                                    ? comboHost.get().getManualHost()\n                                                    : null)\n                                    .host(\n                                            comboHost.get() != null\n                                                    ? comboHost.get().getRef()\n                                                    : null)\n                                    .gateway(gateway.get())\n                                    .localPort(localPort.get())\n                                    .remotePort(remotePort.get())\n                                    .serviceProtocolType(serviceProtocolType.get())\n                                    .tunnelToLocalhost(tunnelToLocalhost.get())\n                                    .build();\n                        },\n                        store);\n        return q.buildDialog();\n    }\n\n    @Override\n    public DataStore defaultStore(DataStoreCategory category) {\n        return CustomServiceStore.builder().build();\n    }\n\n    @Override\n    public String getId() {\n        return \"customService\";\n    }\n\n    @Override\n    public List<Class<?>> getStoreClasses() {\n        return List.of(CustomServiceStore.class);\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceCreatorStore.java",
    "content": "package io.xpipe.ext.base.service;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport java.util.List;\n\npublic interface FixedServiceCreatorStore extends DataStore {\n\n    default boolean allowManualServicesRefresh() {\n        return true;\n    }\n\n    List<? extends DataStoreEntryRef<? extends AbstractServiceStore>> createFixedServices() throws Exception;\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceGroupStore.java",
    "content": "package io.xpipe.ext.base.service;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.FixedChildStore;\nimport io.xpipe.app.ext.FixedHierarchyStore;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.Validators;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.AccessLevel;\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.ToString;\nimport lombok.experimental.FieldDefaults;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\n\n@Getter\n@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)\n@SuperBuilder\n@Jacksonized\n@JsonTypeName(\"fixedServiceGroup\")\n@EqualsAndHashCode(callSuper = true)\n@ToString(callSuper = true)\npublic class FixedServiceGroupStore extends AbstractServiceGroupStore<FixedServiceCreatorStore>\n        implements DataStore, FixedHierarchyStore {\n\n    @Override\n    public void checkComplete() throws Throwable {\n        super.checkComplete();\n        Validators.isType(getParent(), FixedServiceCreatorStore.class);\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public List<? extends DataStoreEntryRef<? extends FixedChildStore>> listChildren() throws Exception {\n        return (List<? extends DataStoreEntryRef<? extends FixedChildStore>>)\n                getParent().getStore().createFixedServices();\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceGroupStoreProvider.java",
    "content": "package io.xpipe.ext.base.service;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.storage.DataStoreCategory;\nimport io.xpipe.app.storage.DataStoreEntry;\n\nimport java.util.List;\n\npublic class FixedServiceGroupStoreProvider extends AbstractServiceGroupStoreProvider {\n\n    @Override\n    public DataStore defaultStore(DataStoreCategory category) {\n        return FixedServiceGroupStore.builder().build();\n    }\n\n    @Override\n    public String getId() {\n        return \"fixedServiceGroup\";\n    }\n\n    @Override\n    public List<Class<?>> getStoreClasses() {\n        return List.of(FixedServiceGroupStore.class);\n    }\n\n    @Override\n    public DataStoreEntry getDisplayParent(DataStoreEntry store) {\n        FixedServiceGroupStore s = store.getStore().asNeeded();\n        return s.getParent().get();\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStore.java",
    "content": "package io.xpipe.ext.base.service;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.FixedChildStore;\nimport io.xpipe.app.ext.NetworkTunnelStore;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.Validators;\nimport io.xpipe.ext.base.host.HostAddressStore;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.ToString;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.OptionalInt;\n\n@SuperBuilder(toBuilder = true)\n@Getter\n@Jacksonized\n@JsonTypeName(\"fixedService\")\n@EqualsAndHashCode(callSuper = true)\n@ToString(callSuper = true)\npublic class FixedServiceStore extends AbstractServiceStore implements FixedChildStore {\n\n    private final DataStoreEntryRef<HostAddressStore> host;\n    private final DataStoreEntryRef<? extends DataStore> displayParent;\n    private final Boolean tunnelToLocalhost;\n\n    @Override\n    public String getAddress() {\n        return null;\n    }\n\n    @Override\n    public DataStoreEntryRef<NetworkTunnelStore> getGateway() {\n        return null;\n    }\n\n    @Override\n    public DataStoreEntryRef<HostAddressStore> getHost() {\n        return host;\n    }\n\n    @Override\n    public boolean licenseRequired() {\n        return false;\n    }\n\n    @Override\n    public FixedChildStore merge(FixedChildStore other) {\n        var o = (FixedServiceStore) other;\n        return toBuilder().tunnelToLocalhost(o.tunnelToLocalhost).build();\n    }\n\n    @Override\n    public void checkComplete() throws Throwable {\n        super.checkComplete();\n        Validators.nonNull(displayParent);\n        Validators.nonNull(displayParent.getStore());\n    }\n\n    @Override\n    public OptionalInt getFixedId() {\n        return OptionalInt.of(getRemotePort());\n    }\n\n    @Override\n    public boolean shouldTunnel() {\n        return tunnelToLocalhost == null || tunnelToLocalhost;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStoreProvider.java",
    "content": "package io.xpipe.ext.base.service;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.GuiDialog;\nimport io.xpipe.app.ext.LocalStore;\nimport io.xpipe.app.hub.comp.StoreChoiceComp;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.ext.base.host.HostAddressGatewayStore;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.ReadOnlyObjectWrapper;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleObjectProperty;\n\nimport java.util.List;\n\npublic class FixedServiceStoreProvider extends AbstractServiceStoreProvider {\n\n    @Override\n    public DataStoreEntry getSyntheticParent(DataStoreEntry store) {\n        FixedServiceStore s = store.getStore().asNeeded();\n        return DataStorage.get()\n                .getOrCreateNewSyntheticEntry(\n                        s.getDisplayParent().get(),\n                        \"Services\",\n                        FixedServiceGroupStore.builder()\n                                .parent(s.getDisplayParent().asNeeded())\n                                .build());\n    }\n\n    @Override\n    public GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {\n        FixedServiceStore st = store.getValue().asNeeded();\n        var host = new ReadOnlyObjectWrapper<>(st.getHost());\n        var localPort = new SimpleObjectProperty<>(st.getLocalPort());\n        var serviceProtocolType = new SimpleObjectProperty<>(st.getServiceProtocolType());\n        var tunnelToLocalhost =\n                new SimpleBooleanProperty(st.getTunnelToLocalhost() != null ? st.getTunnelToLocalhost() : true);\n        var hideTunnelToLocalhost = Bindings.createBooleanBinding(\n                () -> {\n                    return host.get() == null\n                            || (host.get().getStore() instanceof HostAddressGatewayStore g\n                                    && g.getTunnelGateway() != null\n                                    && !(g.getTunnelGateway().getStore() instanceof LocalStore));\n                },\n                host);\n        var hideLocalPort = Bindings.createBooleanBinding(\n                () -> {\n                    return host.get() == null || !tunnelToLocalhost.get();\n                },\n                hideTunnelToLocalhost,\n                tunnelToLocalhost);\n\n        var q = new OptionsBuilder()\n                .nameAndDescription(\"serviceHost\")\n                .addComp(\n                        new StoreChoiceComp<>(\n                                entry,\n                                host,\n                                DataStore.class,\n                                null,\n                                StoreViewState.get().getAllConnectionsCategory()),\n                        host)\n                .nonNull()\n                .disable()\n                .sub(ServiceProtocolTypeHelper.choice(serviceProtocolType), serviceProtocolType)\n                .nonNull()\n                .nameAndDescription(\"serviceRemotePort\")\n                .addStaticString(st.getRemotePort())\n                .nameAndDescription(\"tunnelToLocalhost\")\n                .addToggle(tunnelToLocalhost)\n                .hide(hideTunnelToLocalhost)\n                .nameAndDescription(\"serviceLocalPort\")\n                .addInteger(localPort)\n                .hide(hideLocalPort)\n                .bind(\n                        () -> {\n                            return FixedServiceStore.builder()\n                                    .host(host.get())\n                                    .displayParent(st.getDisplayParent())\n                                    .localPort(localPort.get())\n                                    .remotePort(st.getRemotePort())\n                                    .serviceProtocolType(serviceProtocolType.get())\n                                    .tunnelToLocalhost(tunnelToLocalhost.get())\n                                    .build();\n                        },\n                        store);\n        return q.buildDialog();\n    }\n\n    @Override\n    public String getId() {\n        return \"fixedService\";\n    }\n\n    @Override\n    public List<Class<?>> getStoreClasses() {\n        return List.of(FixedServiceStore.class);\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/service/MappedServiceStore.java",
    "content": "package io.xpipe.ext.base.service;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport lombok.Value;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\n@SuperBuilder\n@Value\n@EqualsAndHashCode(callSuper = true)\n@ToString(callSuper = true)\n@Jacksonized\n@JsonTypeName(\"mappedService\")\npublic class MappedServiceStore extends FixedServiceStore {\n\n    int containerPort;\n\n    @Override\n    public boolean licenseRequired() {\n        return true;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/service/MappedServiceStoreProvider.java",
    "content": "package io.xpipe.ext.base.service;\n\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.GuiDialog;\nimport io.xpipe.app.ext.LocalStore;\nimport io.xpipe.app.hub.comp.StoreChoiceComp;\nimport io.xpipe.app.hub.comp.StoreViewState;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.ext.base.host.HostAddressGatewayStore;\n\nimport javafx.beans.binding.Bindings;\nimport javafx.beans.property.Property;\nimport javafx.beans.property.SimpleBooleanProperty;\nimport javafx.beans.property.SimpleObjectProperty;\n\nimport java.util.List;\n\npublic class MappedServiceStoreProvider extends FixedServiceStoreProvider {\n\n    public String displayName(DataStoreEntry entry) {\n        MappedServiceStore s = entry.getStore().asNeeded();\n        return DataStorage.get().getStoreEntryDisplayName(s.getHost().get()) + \" - Port \" + s.getContainerPort();\n    }\n\n    protected String formatService(AbstractServiceStore s) {\n        var m = (MappedServiceStore) s;\n        var desc = s.getLocalPort() != null\n                ? \"localhost:\" + s.getLocalPort() + \" <- :\" + m.getRemotePort() + \" <- :\" + m.getContainerPort()\n                : s.isSessionRunning()\n                        ? \"localhost:\" + s.getSession().getLocalPort() + \" <- :\" + m.getRemotePort() + \" <- :\"\n                                + m.getContainerPort()\n                        : \":\" + m.getRemotePort() + \" <- :\" + m.getContainerPort();\n        return desc;\n    }\n\n    @Override\n    public GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {\n        MappedServiceStore st = store.getValue().asNeeded();\n\n        var host = new SimpleObjectProperty<>(st.getHost());\n        var localPort = new SimpleObjectProperty<>(st.getLocalPort());\n        var serviceProtocolType = new SimpleObjectProperty<>(st.getServiceProtocolType());\n        var tunnelToLocalhost =\n                new SimpleBooleanProperty(st.getTunnelToLocalhost() != null ? st.getTunnelToLocalhost() : true);\n        var hideTunnelToLocalhost = Bindings.createBooleanBinding(\n                () -> {\n                    return host.get() == null\n                            || (host.get().getStore() instanceof HostAddressGatewayStore g\n                                    && g.getTunnelGateway() != null\n                                    && !(g.getTunnelGateway().getStore() instanceof LocalStore));\n                },\n                host);\n        var hideLocalPort = Bindings.createBooleanBinding(\n                () -> {\n                    return host.get() == null || !tunnelToLocalhost.get();\n                },\n                hideTunnelToLocalhost,\n                tunnelToLocalhost);\n\n        var q = new OptionsBuilder()\n                .nameAndDescription(\"serviceHost\")\n                .addComp(\n                        new StoreChoiceComp<>(\n                                entry,\n                                host,\n                                DataStore.class,\n                                null,\n                                StoreViewState.get().getAllConnectionsCategory()),\n                        host)\n                .nonNull()\n                .disable()\n                .sub(ServiceProtocolTypeHelper.choice(serviceProtocolType), serviceProtocolType)\n                .nonNull()\n                .nameAndDescription(\"serviceRemotePort\")\n                .addStaticString(st.getRemotePort() + \" <- \" + st.getContainerPort())\n                .nameAndDescription(\"tunnelToLocalhost\")\n                .addToggle(tunnelToLocalhost)\n                .hide(hideTunnelToLocalhost)\n                .nameAndDescription(\"serviceLocalPort\")\n                .addInteger(localPort)\n                .hide(hideLocalPort)\n                .bind(\n                        () -> {\n                            return MappedServiceStore.builder()\n                                    .host(host.get())\n                                    .displayParent(st.getDisplayParent())\n                                    .localPort(localPort.get())\n                                    .remotePort(st.getRemotePort())\n                                    .serviceProtocolType(serviceProtocolType.get())\n                                    .containerPort(st.getContainerPort())\n                                    .tunnelToLocalhost(tunnelToLocalhost.get())\n                                    .build();\n                        },\n                        store);\n        return q.buildDialog();\n    }\n\n    @Override\n    public String getId() {\n        return \"mappedService\";\n    }\n\n    @Override\n    public List<Class<?>> getStoreClasses() {\n        return List.of(MappedServiceStore.class);\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/service/ServiceAddressRotation.java",
    "content": "package io.xpipe.ext.base.service;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class ServiceAddressRotation {\n\n    private static final Map<String, String> replacedUrls = new HashMap<>();\n    private static int counter = 0;\n    private static final String[] aliases = new String[] {\"localhost\", \"127.0.0.1\"};\n\n    static synchronized String getRotatedLocalhost(String url) {\n        if (!url.startsWith(\"localhost\")) {\n            return url;\n        }\n\n        if (replacedUrls.containsKey(url)) {\n            return replacedUrls.get(url);\n        }\n\n        var alias = aliases[counter++ % aliases.length];\n        var replaced = url.replaceFirst(\"localhost\", alias);\n        replacedUrls.put(url, replaced);\n        return replaced;\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/service/ServiceCopyAddressHubLeafProvider.java",
    "content": "package io.xpipe.ext.base.service;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.platform.ClipboardHelper;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class ServiceCopyAddressHubLeafProvider implements HubLeafProvider<AbstractServiceStore> {\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.CUSTOM;\n    }\n\n    @Override\n    public boolean isMajor() {\n        return true;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<AbstractServiceStore> store) {\n        return AppI18n.observable(\"copyAddress\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<AbstractServiceStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2c-content-copy\");\n    }\n\n    @Override\n    public Class<AbstractServiceStore> getApplicableClass() {\n        return AbstractServiceStore.class;\n    }\n\n    @Override\n    public AbstractAction createAction(DataStoreEntryRef<AbstractServiceStore> ref) {\n        return Action.builder().ref(ref).build();\n    }\n\n    @Override\n    public String getId() {\n        return \"copyServiceAddress\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<AbstractServiceStore> {\n\n        @Override\n        public void executeImpl() throws Exception {\n            var serviceStore = ref.getStore();\n            serviceStore.startSessionIfNeeded();\n            var full = serviceStore.getServiceProtocolType().formatAddress(serviceStore.getOpenTargetUrl());\n            ClipboardHelper.copyUrl(full);\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/service/ServiceProtocolType.java",
    "content": "package io.xpipe.ext.base.service;\n\nimport io.xpipe.app.prefs.ExternalApplicationHelper;\nimport io.xpipe.app.util.Hyperlinks;\n\nimport com.fasterxml.jackson.annotation.JsonSubTypes;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.Locale;\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\n@JsonSubTypes({\n    @JsonSubTypes.Type(value = ServiceProtocolType.Undefined.class),\n            @JsonSubTypes.Type(value = ServiceProtocolType.Http.class),\n    @JsonSubTypes.Type(value = ServiceProtocolType.Https.class),\n            @JsonSubTypes.Type(value = ServiceProtocolType.Custom.class)\n})\npublic interface ServiceProtocolType {\n\n    String formatAddress(String base);\n\n    void open(String url) throws Exception;\n\n    String getTranslationKey();\n\n    @JsonTypeName(\"none\")\n    @Value\n    @Jacksonized\n    @Builder\n    class Undefined implements ServiceProtocolType {\n\n        @Override\n        public String formatAddress(String base) {\n            return base;\n        }\n\n        @Override\n        public void open(String url) {}\n\n        @Override\n        public String getTranslationKey() {\n            return \"undefined\";\n        }\n    }\n\n    @JsonTypeName(\"http\")\n    @Value\n    @Jacksonized\n    @Builder\n    class Http implements ServiceProtocolType {\n\n        String path;\n\n        @Override\n        public String formatAddress(String base) {\n            var url = \"http://\" + base;\n            if (path != null && !path.isEmpty()) {\n                url += (!path.startsWith(\"/\") ? \"/\" : \"\") + path;\n            }\n            return url;\n        }\n\n        @Override\n        public void open(String url) {\n            Hyperlinks.open(url);\n        }\n\n        @Override\n        public String getTranslationKey() {\n            return \"http\";\n        }\n    }\n\n    @JsonTypeName(\"https\")\n    @Value\n    @Jacksonized\n    @Builder\n    class Https implements ServiceProtocolType {\n\n        String path;\n\n        @Override\n        public String formatAddress(String base) {\n            var url = \"https://\" + base;\n            if (path != null && !path.isEmpty()) {\n                url += (!path.startsWith(\"/\") ? \"/\" : \"\") + path;\n            }\n            return url;\n        }\n\n        @Override\n        public void open(String url) {\n            Hyperlinks.open(url);\n        }\n\n        @Override\n        public String getTranslationKey() {\n            return \"https\";\n        }\n    }\n\n    @JsonTypeName(\"custom\")\n    @Value\n    @Jacksonized\n    @Builder\n    class Custom implements ServiceProtocolType {\n\n        String commandTemplate;\n\n        @Override\n        public String formatAddress(String base) {\n            return base;\n        }\n\n        @Override\n        public void open(String url) throws Exception {\n            if (commandTemplate == null || commandTemplate.isBlank()) {\n                return;\n            }\n\n            var port = url.split(\":\")[1];\n            var format = commandTemplate.toLowerCase(Locale.ROOT).contains(\"$port\")\n                    ? commandTemplate\n                    : commandTemplate + \" localhost:$PORT\";\n            var toExecute = ExternalApplicationHelper.replaceVariableArgument(format, \"PORT\", port);\n            // We can't be sure whether the command is blocking or not, so always make it not blocking\n            ExternalApplicationHelper.startAsync(toExecute);\n        }\n\n        @Override\n        public String getTranslationKey() {\n            return \"custom\";\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/service/ServiceProtocolTypeHelper.java",
    "content": "package io.xpipe.ext.base.service;\n\nimport io.xpipe.app.comp.base.TextFieldComp;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.platform.OptionsBuilder;\n\nimport javafx.beans.property.*;\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.LinkedHashMap;\n\npublic class ServiceProtocolTypeHelper {\n\n    private static OptionsBuilder custom(Property<ServiceProtocolType.Custom> p) {\n        var firstFocus = new SimpleBooleanProperty(false);\n        var path = new SimpleStringProperty(p.getValue() != null ? p.getValue().getCommandTemplate() : null);\n        var comp = new TextFieldComp(path).apply(struc -> {\n            struc.focusedProperty().addListener((observable, oldValue, newValue) -> {\n                if (!firstFocus.get()) {\n                    struc.getParent().requestFocus();\n                    firstFocus.set(true);\n                }\n            });\n            struc.setPromptText(\"mycommand open localhost:$PORT\");\n        });\n        return new OptionsBuilder()\n                .nameAndDescription(\"serviceCommand\")\n                .addComp(comp, path)\n                .bind(\n                        () -> {\n                            return new ServiceProtocolType.Custom(path.get());\n                        },\n                        p);\n    }\n\n    private static OptionsBuilder http(Property<ServiceProtocolType.Http> p) {\n        var path = new SimpleStringProperty(p.getValue() != null ? p.getValue().getPath() : null);\n        return new OptionsBuilder()\n                .nameAndDescription(\"servicePath\")\n                .addComp(\n                        new TextFieldComp(path).apply(struc -> {\n                            struc.setPromptText(\"/sub/path\");\n                        }),\n                        path)\n                .bind(\n                        () -> {\n                            return new ServiceProtocolType.Http(path.get());\n                        },\n                        p);\n    }\n\n    private static OptionsBuilder https(Property<ServiceProtocolType.Https> p) {\n        var path = new SimpleStringProperty(p.getValue() != null ? p.getValue().getPath() : null);\n        return new OptionsBuilder()\n                .nameAndDescription(\"servicePath\")\n                .addComp(\n                        new TextFieldComp(path).apply(struc -> {\n                            struc.setPromptText(\"/sub/path\");\n                        }),\n                        path)\n                .bind(\n                        () -> {\n                            return new ServiceProtocolType.Https(path.get());\n                        },\n                        p);\n    }\n\n    public static OptionsBuilder choice(Property<ServiceProtocolType> serviceProtocolType) {\n        var ex = serviceProtocolType.getValue();\n        var http = new SimpleObjectProperty<>(ex instanceof ServiceProtocolType.Http h ? h : null);\n        var https = new SimpleObjectProperty<>(ex instanceof ServiceProtocolType.Https h ? h : null);\n        var custom = new SimpleObjectProperty<>(ex instanceof ServiceProtocolType.Custom c ? c : null);\n        var selected = new SimpleIntegerProperty(\n                ex instanceof ServiceProtocolType.Undefined\n                        ? 0\n                        : ex instanceof ServiceProtocolType.Http\n                                ? 1\n                                : ex instanceof ServiceProtocolType.Https\n                                        ? 2\n                                        : ex instanceof ServiceProtocolType.Custom ? 3 : -1);\n        var available = new LinkedHashMap<ObservableValue<String>, OptionsBuilder>();\n        available.put(AppI18n.observable(\"undefined\"), new OptionsBuilder());\n        available.put(AppI18n.observable(\"http\"), http(http));\n        available.put(AppI18n.observable(\"https\"), https(https));\n        available.put(AppI18n.observable(\"custom\"), custom(custom));\n        return new OptionsBuilder()\n                .nameAndDescription(\"serviceProtocolType\")\n                .choice(selected, available)\n                .bindChoice(\n                        () -> {\n                            return switch (selected.get()) {\n                                case 0 -> new SimpleObjectProperty<>(new ServiceProtocolType.Undefined());\n                                case 1 -> http;\n                                case 2 -> https;\n                                case 3 -> custom;\n                                default -> new SimpleObjectProperty<>();\n                            };\n                        },\n                        serviceProtocolType);\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/service/ServiceRefreshHubProvider.java",
    "content": "package io.xpipe.ext.base.service;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.BatchHubProvider;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class ServiceRefreshHubProvider\n        implements HubLeafProvider<FixedServiceCreatorStore>, BatchHubProvider<FixedServiceCreatorStore> {\n\n    @Override\n    public boolean requiresValidStore() {\n        return true;\n    }\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.CUSTOM;\n    }\n\n    @Override\n    public boolean isMajor() {\n        return true;\n    }\n\n    @Override\n    public boolean isApplicable(DataStoreEntryRef<FixedServiceCreatorStore> o) {\n        return o.getStore().allowManualServicesRefresh();\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<FixedServiceCreatorStore> store) {\n        return AppI18n.observable(\"refreshServices\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<FixedServiceCreatorStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2w-web\");\n    }\n\n    @Override\n    public Class<?> getApplicableClass() {\n        return FixedServiceCreatorStore.class;\n    }\n\n    @Override\n    public ObservableValue<String> getName() {\n        return AppI18n.observable(\"refreshServices\");\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2w-web\");\n    }\n\n    @Override\n    public Action createBatchAction(DataStoreEntryRef<FixedServiceCreatorStore> ref) {\n        return Action.builder().ref(ref).build();\n    }\n\n    @Override\n    public String getId() {\n        return \"refreshServices\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<FixedServiceCreatorStore> {\n\n        @Override\n        public void executeImpl() {\n            ref.get().setExpanded(true);\n            var e = DataStorage.get()\n                    .addStoreIfNotPresent(\n                            \"Services\",\n                            FixedServiceGroupStore.builder().parent(ref).build());\n            DataStorage.get().refreshChildren(e);\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/store/PauseableStore.java",
    "content": "package io.xpipe.ext.base.store;\n\nimport io.xpipe.app.ext.DataStore;\n\npublic interface PauseableStore extends DataStore {\n\n    void pause() throws Exception;\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/store/ShellStoreProvider.java",
    "content": "package io.xpipe.ext.base.store;\n\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.hub.comp.OsLogoComp;\nimport io.xpipe.app.hub.comp.StoreEntryWrapper;\nimport io.xpipe.app.hub.comp.StoreSection;\nimport io.xpipe.app.hub.comp.SystemStateComp;\nimport io.xpipe.app.process.SystemState;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.terminal.TerminalLaunch;\nimport io.xpipe.app.terminal.TerminalPromptManager;\nimport io.xpipe.app.util.StoreStateFormat;\nimport io.xpipe.core.FailableRunnable;\nimport io.xpipe.ext.base.script.ScriptStoreSetup;\n\nimport javafx.beans.property.BooleanProperty;\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\nimport java.util.UUID;\n\npublic interface ShellStoreProvider extends DataStoreProvider {\n\n    @Override\n    default FailableRunnable<Exception> launch(DataStoreEntry entry) {\n        return () -> {\n            var replacement = ProcessControlProvider.get().replace(entry.ref());\n            ShellStore store = replacement.getStore().asNeeded();\n            var control = store.standaloneControl();\n            // These prepend scripts, not append\n            TerminalPromptManager.configurePromptScript(control);\n            ScriptStoreSetup.controlWithDefaultScripts(control);\n            var request = UUID.randomUUID();\n            TerminalLaunch.builder()\n                    .request(request)\n                    .entry(replacement.get())\n                    .command(control)\n                    .launch();\n        };\n    }\n\n    @Override\n    default FailableRunnable<Exception> launchBrowser(\n            BrowserFullSessionModel sessionModel, DataStoreEntry store, BooleanProperty busy) {\n        return () -> {\n            sessionModel.openFileSystemAsync(store.ref(), null, null, busy);\n        };\n    }\n\n    default BaseRegionBuilder<?, ?> stateDisplay(StoreEntryWrapper w) {\n        return new OsLogoComp(w, SystemStateComp.State.shellState(w));\n    }\n\n    @Override\n    default DataStoreUsageCategory getUsageCategory() {\n        return DataStoreUsageCategory.SHELL;\n    }\n\n    @Override\n    default ObservableValue<String> informationString(StoreSection section) {\n        return StoreStateFormat.shellStore(\n                section, state -> formatAdditionalInformation(section, state).toArray(String[]::new), null);\n    }\n\n    default List<String> formatAdditionalInformation(StoreSection section, SystemState state) {\n        return List.of();\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/store/StartableStore.java",
    "content": "package io.xpipe.ext.base.store;\n\nimport io.xpipe.app.ext.DataStore;\n\npublic interface StartableStore extends DataStore {\n\n    void start() throws Exception;\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/store/StoppableStore.java",
    "content": "package io.xpipe.ext.base.store;\n\nimport io.xpipe.app.ext.DataStore;\n\npublic interface StoppableStore extends DataStore {\n\n    void stop() throws Exception;\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/store/StorePauseActionProvider.java",
    "content": "package io.xpipe.ext.base.store;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.BatchHubProvider;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class StorePauseActionProvider implements HubLeafProvider<PauseableStore>, BatchHubProvider<PauseableStore> {\n\n    @Override\n    public boolean runParallel() {\n        return true;\n    }\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.CUSTOM;\n    }\n\n    @Override\n    public boolean isApplicable(DataStoreEntryRef<PauseableStore> o) {\n        return true;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<PauseableStore> store) {\n        return AppI18n.observable(\"pause\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<PauseableStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2p-pause\");\n    }\n\n    @Override\n    public Class<PauseableStore> getApplicableClass() {\n        return PauseableStore.class;\n    }\n\n    @Override\n    public boolean requiresValidStore() {\n        return false;\n    }\n\n    @Override\n    public ObservableValue<String> getName() {\n        return AppI18n.observable(\"pause\");\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2p-pause\");\n    }\n\n    @Override\n    public String getId() {\n        return \"pauseStore\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<PauseableStore> {\n\n        @Override\n        public void executeImpl() throws Exception {\n            ref.getStore().pause();\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/store/StoreRestartActionProvider.java",
    "content": "package io.xpipe.ext.base.store;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.hub.action.BatchHubProvider;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class StoreRestartActionProvider implements HubLeafProvider<DataStore>, BatchHubProvider<DataStore> {\n\n    @Override\n    public boolean runParallel() {\n        return true;\n    }\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.CUSTOM;\n    }\n\n    @Override\n    public boolean isApplicable(DataStoreEntryRef<DataStore> o) {\n        return o.getStore() instanceof StartableStore && o.getStore() instanceof StoppableStore;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<DataStore> store) {\n        return AppI18n.observable(\"restart\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<DataStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2r-restart\");\n    }\n\n    @Override\n    public Class<?> getApplicableClass() {\n        return DataStore.class;\n    }\n\n    @Override\n    public boolean requiresValidStore() {\n        return false;\n    }\n\n    @Override\n    public ObservableValue<String> getName() {\n        return AppI18n.observable(\"restart\");\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2r-restart\");\n    }\n\n    @Override\n    public String getId() {\n        return \"restartStore\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<DataStore> {\n\n        @Override\n        public void executeImpl() throws Exception {\n            ((StoppableStore) ref.getStore()).stop();\n            ((StartableStore) ref.getStore()).start();\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/store/StoreStartActionProvider.java",
    "content": "package io.xpipe.ext.base.store;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.BatchHubProvider;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class StoreStartActionProvider implements HubLeafProvider<StartableStore>, BatchHubProvider<StartableStore> {\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.CUSTOM;\n    }\n\n    @Override\n    public boolean isApplicable(DataStoreEntryRef<StartableStore> o) {\n        return true;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<StartableStore> store) {\n        return AppI18n.observable(\"start\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<StartableStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2p-play\");\n    }\n\n    @Override\n    public Class<StartableStore> getApplicableClass() {\n        return StartableStore.class;\n    }\n\n    @Override\n    public boolean requiresValidStore() {\n        return false;\n    }\n\n    @Override\n    public ObservableValue<String> getName() {\n        return AppI18n.observable(\"start\");\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2p-play\");\n    }\n\n    @Override\n    public Action createBatchAction(DataStoreEntryRef<StartableStore> ref) {\n        return Action.builder().ref(ref).build();\n    }\n\n    @Override\n    public String getId() {\n        return \"startStore\";\n    }\n\n    @Override\n    public boolean runParallel() {\n        return true;\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<StartableStore> {\n\n        @Override\n        public void executeImpl() throws Exception {\n            ref.getStore().start();\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/io/xpipe/ext/base/store/StoreStopActionProvider.java",
    "content": "package io.xpipe.ext.base.store;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.BatchHubProvider;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.hub.action.StoreActionCategory;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class StoreStopActionProvider implements HubLeafProvider<StoppableStore>, BatchHubProvider<StoppableStore> {\n\n    @Override\n    public boolean runParallel() {\n        return true;\n    }\n\n    @Override\n    public StoreActionCategory getCategory() {\n        return StoreActionCategory.CUSTOM;\n    }\n\n    @Override\n    public boolean isApplicable(DataStoreEntryRef<StoppableStore> o) {\n        return true;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<StoppableStore> store) {\n        return AppI18n.observable(\"stop\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<StoppableStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2s-stop\");\n    }\n\n    @Override\n    public Class<?> getApplicableClass() {\n        return StoppableStore.class;\n    }\n\n    @Override\n    public boolean requiresValidStore() {\n        return false;\n    }\n\n    @Override\n    public ObservableValue<String> getName() {\n        return AppI18n.observable(\"stop\");\n    }\n\n    @Override\n    public LabelGraphic getIcon() {\n        return new LabelGraphic.IconGraphic(\"mdi2s-stop\");\n    }\n\n    @Override\n    public Action createBatchAction(DataStoreEntryRef<StoppableStore> ref) {\n        return Action.builder().ref(ref).build();\n    }\n\n    @Override\n    public String getId() {\n        return \"stopAction\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<StoppableStore> {\n\n        @Override\n        public void executeImpl() throws Exception {\n            ref.getStore().stop();\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "ext/base/src/main/java/module-info.java",
    "content": "import io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.ext.DataStorageExtensionProvider;\nimport io.xpipe.app.ext.DataStoreProvider;\nimport io.xpipe.ext.base.desktop.DesktopApplicationStoreProvider;\nimport io.xpipe.ext.base.host.AbstractHostCreationActionProvider;\nimport io.xpipe.ext.base.host.AbstractHostStoreProvider;\nimport io.xpipe.ext.base.host.HostAddressSwitchBranchProvider;\nimport io.xpipe.ext.base.identity.*;\nimport io.xpipe.ext.base.script.*;\nimport io.xpipe.ext.base.service.*;\nimport io.xpipe.ext.base.store.*;\n\nopen module io.xpipe.ext.base {\n    exports io.xpipe.ext.base.script;\n    exports io.xpipe.ext.base.store;\n    exports io.xpipe.ext.base.desktop;\n    exports io.xpipe.ext.base.service;\n    exports io.xpipe.ext.base.identity;\n    exports io.xpipe.ext.base.identity.ssh;\n    exports io.xpipe.ext.base.host;\n\n    requires java.desktop;\n    requires io.xpipe.core;\n    requires com.fasterxml.jackson.databind;\n    requires com.fasterxml.jackson.annotation;\n    requires java.net.http;\n    requires static lombok;\n    requires static javafx.controls;\n    requires static net.synedra.validatorfx;\n    requires static io.xpipe.app;\n    requires org.kordamp.ikonli.javafx;\n    requires atlantafx.base;\n    requires com.sun.jna.platform;\n    requires com.sun.jna;\n    requires javafx.base;\n    requires javafx.graphics;\n\n    provides ActionProvider with\n            IdentityApplyHubLeafProvider,\n            AbstractHostCreationActionProvider,\n            HostAddressSwitchBranchProvider,\n            LocalIdentityConvertHubLeafProvider,\n            RunBackgroundScriptActionProvider,\n            RunHubBatchScriptActionProvider,\n            RunHubScriptActionProvider,\n            RunTerminalScriptActionProvider,\n            ScriptCollectionSourceImportHubProvider,\n            ScriptUrlSourceRefreshHubProvider,\n            ScriptCollectionSourceRefreshHubProvider,\n            ScriptCollectionSourceBrowseActionProvider,\n            ScriptQuickEditHubLeafProvider,\n            StoreStartActionProvider,\n            StoreStopActionProvider,\n            StorePauseActionProvider,\n            StoreRestartActionProvider,\n            ServiceCopyAddressHubLeafProvider,\n            RunScriptActionProviderMenu,\n            ServiceRefreshHubProvider,\n            RunFileScriptMenuProvider;\n    provides DataStoreProvider with\n            FixedServiceGroupStoreProvider,\n            CustomServiceGroupStoreProvider,\n            CustomServiceStoreProvider,\n            MappedServiceStoreProvider,\n            FixedServiceStoreProvider,\n            ScriptStoreProvider,\n            ScriptCollectionSourceStoreProvider,\n            DesktopApplicationStoreProvider,\n            LocalIdentityStoreProvider,\n            SyncedIdentityStoreProvider,\n            PasswordManagerIdentityStoreProvider,\n            AbstractHostStoreProvider;\n    provides DataStorageExtensionProvider with\n            ScriptDataStorageProvider;\n}\n"
  },
  {
    "path": "ext/base/src/main/resources/io/xpipe/ext/base/resources/extension.properties",
    "content": "name=Base"
  },
  {
    "path": "ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/apt_upgrade.sh",
    "content": "sudo apt update && sudo apt upgrade"
  },
  {
    "path": "ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/clink.bat",
    "content": "WHERE /q clink\r\nIF %ERRORLEVEL%==0 (\r\n    exit /b 0\r\n)\r\n\r\nSET \"PATH=%PATH%;%TEMP%\\xpipe\\scriptdata\\clink\"\r\nWHERE clink >NUL 2>NUL\r\nIF %ERRORLEVEL%==0 (\r\n    exit /b 0\r\n)\r\n\r\necho ^\r\n$downloader = New-Object System.Net.WebClient;^\r\n$defaultCreds = [System.Net.CredentialCache]::DefaultCredentials;^\r\nif ($defaultCreds) {^\r\n    $downloader.Credentials = $defaultCreds^\r\n}^\r\n$downloader.DownloadFile(\"https://github.com/chrisant996/clink/releases/download/v1.7.13/clink.1.7.13.ac5d42.zip\", \"$env:TEMP\\clink.zip\");^\r\nExpand-Archive -Force -LiteralPath \"$env:TEMP\\clink.zip\" -DestinationPath \"$env:TEMP\\xpipe\\scriptdata\\clink\"; | powershell -NoLogo >NUL\r\n\r\nclink set clink.autoupdate off\r\n"
  },
  {
    "path": "ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/crlf_to_lf.sh",
    "content": "for arg in \"$@\"\ndo\n    file=\"$arg\"\n    temp_file=$(mktemp)\n    awk '{ sub(\"\\r$\", \"\"); print }' \"$file\" > \"$temp_file\"\n    cat \"$temp_file\" > \"$file\"\n    rm \"$temp_file\"\ndone\n"
  },
  {
    "path": "ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/diff.sh",
    "content": "diff \"$1\" \"$2\" && echo \"File contents are identical\"\n"
  },
  {
    "path": "ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/git_config.sh",
    "content": "git config --global user.name \"My name\"\ngit config --global user.email \"myemail@myorg.com\"\ngit config --global core.autocrlf true\n"
  },
  {
    "path": "ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/system_health.sh",
    "content": "DELIMITER=\"-------------------------------------\"\n\nhostname -f &> /dev/null && printf \"Hostname : $(hostname -f)\" || printf \"Hostname : $(hostname -s)\"\n\necho -e \"Kernel Version :\" $(uname -r)\nwhich arch && printf \"OS Architecture :\"$(arch | grep x86_64 &> /dev/null) && printf \" 64 Bit OS\\n\"  || printf \" 32 Bit OS\\n\"\n\necho -en \"System Uptime : \" $(uptime -p)\necho -e \"\\nCurrent System Date & Time : \"$(date +%c)\n\necho -e \"Total Swap Memory in MiB : \"$(grep -w SwapTotal /proc/meminfo|awk '{print $2/1024}')\", in GiB : \"\\\n$(grep -w SwapTotal /proc/meminfo|awk '{print $2/1024/1024}')\necho -e \"Swap Free Memory in MiB : \"$(grep -w SwapFree /proc/meminfo|awk '{print $2/1024}')\", in GiB : \"\\\n$(grep -w SwapFree /proc/meminfo|awk '{print $2/1024/1024}')\n\necho -e \"\\n\\nMost Recent 3 Reboots\"\necho -e \"$DELIMITER$DELIMITER\"\nlast -x 2> /dev/null|grep reboot 1> /dev/null && /usr/bin/last -x 2> /dev/null|grep reboot|head -3 || \\\necho -e \"No reboot events are recorded.\"\n\necho -e \"\\n\\nMost Recent 3 Shutdowns\"\necho -e \"$DELIMITER$DELIMITER\"\nlast -x 2> /dev/null|grep shutdown 1> /dev/null && /usr/bin/last -x 2> /dev/null|grep shutdown|head -3 || \\\necho -e \"No shutdown events are recorded.\"\n"
  },
  {
    "path": "ext/proc/build.gradle",
    "content": "plugins { id 'java'\n}\napply from: \"$rootDir/gradle/gradle_scripts/java.gradle\"\napply from: \"$rootDir/gradle/gradle_scripts/extension.gradle\"\n"
  },
  {
    "path": "ext/proc/src/main/java/module-info.java",
    "content": "module io.xpipe.ext.proc {}\n"
  },
  {
    "path": "ext/system/build.gradle",
    "content": "plugins {\n    id 'java'\n}\n\napply from: \"$rootDir/gradle/gradle_scripts/extension.gradle\"\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/incus/IncusCommandView.java",
    "content": "package io.xpipe.ext.system.incus;\n\nimport io.xpipe.app.ext.ContainerStoreState;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.*;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.ext.base.identity.IdentityValue;\n\nimport lombok.NonNull;\n\nimport java.util.*;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\n\npublic class IncusCommandView extends CommandViewBase {\n\n    public IncusCommandView(ShellControl shellControl) {\n        super(shellControl);\n    }\n\n    private static ElevationFunction requiresElevation() {\n        return ElevationFunction.cached(\"incusRequiresElevation\", new ElevationFunction() {\n            @Override\n            public String getPrefix() {\n                return \"Incus\";\n            }\n\n            @Override\n            public boolean isSpecified() {\n                return true;\n            }\n\n            @Override\n            public boolean apply(ShellControl shellControl) throws Exception {\n                // This is not perfect as it does not respect custom locations for the Incus socket\n                // Sadly the socket location changes based on the installation type, and we can't dynamically query the\n                // path\n                return !shellControl\n                        .command(\"test -S /var/lib/incus/unix.socket && test -w /var/lib/incus/unix.socket || \"\n                                + \"test -S /var/snap/incus/common/incus/unix.socket && test -w /var/snap/incus/common/incus/unix.socket || \"\n                                + \"test -S /var/snap/incus/common/incus/unix.socket.user && test -w /var/snap/incus/common/incus/unix.socket.user || \"\n                                + \"test -S /var/lib/incus/unix.socket.user && test -w /var/lib/incus/unix.socket.user\")\n                        .executeAndCheck();\n            }\n        });\n    }\n\n    private static String formatErrorMessage(String s) {\n        return s;\n    }\n\n    private static <T extends Throwable> T convertException(T s) {\n        return ErrorEventFactory.expectedIfContains(s);\n    }\n\n    @Override\n    protected CommandControl build(Consumer<CommandBuilder> builder) {\n        var cmd = CommandBuilder.of().add(\"incus\");\n        builder.accept(cmd);\n        return shellControl\n                .command(cmd)\n                .withErrorFormatter(IncusCommandView::formatErrorMessage)\n                .withExceptionConverter(IncusCommandView::convertException)\n                .elevated(requiresElevation());\n    }\n\n    @Override\n    public IncusCommandView start() throws Exception {\n        shellControl.start();\n        return this;\n    }\n\n    public boolean isSupported() throws Exception {\n        return shellControl\n                .command(\"incus --help\")\n                .withErrorFormatter(IncusCommandView::formatErrorMessage)\n                .withExceptionConverter(IncusCommandView::convertException)\n                .executeAndCheck();\n    }\n\n    public String version() throws Exception {\n        return build(commandBuilder -> commandBuilder.add(\"version\")).readStdoutOrThrow();\n    }\n\n    public void start(String containerName) throws Exception {\n        build(commandBuilder -> commandBuilder.add(\"start\").addQuoted(containerName))\n                .execute();\n    }\n\n    public void stop(String containerName) throws Exception {\n        build(commandBuilder -> commandBuilder.add(\"stop\").addQuoted(containerName))\n                .execute();\n    }\n\n    public void pause(String containerName) throws Exception {\n        build(commandBuilder -> commandBuilder.add(\"pause\").addQuoted(containerName))\n                .execute();\n    }\n\n    public CommandControl console(String containerName) {\n        return build(commandBuilder -> commandBuilder.add(\"console\").addQuoted(containerName));\n    }\n\n    public CommandControl configEdit(String containerName) {\n        return build(commandBuilder -> commandBuilder.add(\"config\", \"edit\").addQuoted(containerName));\n    }\n\n    public List<DataStoreEntryRef<IncusContainerStore>> listContainers(DataStoreEntryRef<IncusInstallStore> store)\n            throws Exception {\n        return listContainersAndStates().entrySet().stream()\n                .map(s -> {\n                    boolean running = s.getValue().toLowerCase(Locale.ROOT).equals(\"running\");\n                    var c = new IncusContainerStore(store, s.getKey(), IdentityValue.ofBreakout(store.get()));\n                    var entry = DataStoreEntry.createNew(c.getContainerName(), c);\n                    entry.setStorePersistentState(ContainerStoreState.builder()\n                            .containerState(s.getValue())\n                            .running(running)\n                            .build());\n                    return Optional.of(entry.<IncusContainerStore>ref());\n                })\n                .flatMap(Optional::stream)\n                .toList();\n    }\n\n    public String queryContainerState(String containerName) throws Exception {\n        var states = listContainersAndStates();\n        return states.getOrDefault(containerName, \"?\");\n    }\n\n    private Map<String, String> listContainersAndStates() throws Exception {\n        try (var c = build(commandBuilder -> commandBuilder.add(\"list\", \"-f\", \"csv\", \"-c\", \"ns\"))\n                .start()) {\n            var output = c.readStdoutOrThrow();\n            return output.lines()\n                    .collect(Collectors.toMap(\n                            s -> s.strip().split(\",\")[0],\n                            s -> s.strip().split(\",\")[1],\n                            (x, y) -> y,\n                            LinkedHashMap::new));\n        }\n    }\n\n    public ShellControl exec(String container, String user, Supplier<Boolean> busybox) {\n        var sub = shellControl.subShell();\n        sub.setDumbOpen(createOpenFunction(container, user, false, busybox));\n        sub.setTerminalOpen(createOpenFunction(container, user, true, busybox));\n        return sub.withExceptionConverter(IncusCommandView::convertException).elevated(requiresElevation());\n    }\n\n    private ShellOpenFunction createOpenFunction(\n            String containerName, String user, boolean terminal, Supplier<Boolean> busybox) {\n        return new ShellOpenFunction() {\n            @Override\n            public CommandBuilder prepareWithoutInitCommand() {\n                var b = execCommand(containerName, terminal).add(\"su\", \"-l\");\n                if (user != null) {\n                    b.addQuoted(user);\n                }\n                return b;\n            }\n\n            @Override\n            public CommandBuilder prepareWithInitCommand(@NonNull String command) {\n                var b = execCommand(containerName, terminal).add(\"su\", \"-l\");\n                if (user != null) {\n                    b.addQuoted(user);\n                }\n                return b.add(sc -> {\n                            var suType = busybox.get();\n                            if (suType) {\n                                return \"-c\";\n                            } else {\n                                return \"--session-command\";\n                            }\n                        })\n                        .addLiteral(command);\n            }\n        };\n    }\n\n    public CommandBuilder execCommand(String containerName, boolean terminal) {\n        var c = CommandBuilder.of().add(\"incus\", \"exec\", terminal ? \"-t\" : \"-T\");\n        return c.addQuoted(containerName).add(\"--\");\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/incus/IncusContainerActionProviderMenu.java",
    "content": "package io.xpipe.ext.system.incus;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.HubBranchProvider;\nimport io.xpipe.app.hub.action.HubMenuItemProvider;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.ext.base.store.StorePauseActionProvider;\nimport io.xpipe.ext.base.store.StoreRestartActionProvider;\nimport io.xpipe.ext.base.store.StoreStartActionProvider;\nimport io.xpipe.ext.base.store.StoreStopActionProvider;\n\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\n\npublic class IncusContainerActionProviderMenu implements HubBranchProvider<IncusContainerStore> {\n\n    @Override\n    public boolean isMajor() {\n        return true;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<IncusContainerStore> store) {\n        return AppI18n.observable(\"containerActions\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<IncusContainerStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2p-package-variant-closed\");\n    }\n\n    @Override\n    public Class<IncusContainerStore> getApplicableClass() {\n        return IncusContainerStore.class;\n    }\n\n    @Override\n    public List<HubMenuItemProvider<?>> getChildren(DataStoreEntryRef<IncusContainerStore> store) {\n        return List.of(\n                new StoreStartActionProvider(),\n                new StoreStopActionProvider(),\n                new StorePauseActionProvider(),\n                new StoreRestartActionProvider(),\n                new IncusContainerConsoleActionProvider(),\n                new IncusContainerEditConfigActionProvider(),\n                new IncusContainerEditRunConfigActionProvider());\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/incus/IncusContainerConsoleActionProvider.java",
    "content": "package io.xpipe.ext.system.incus;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.terminal.TerminalLaunch;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class IncusContainerConsoleActionProvider implements HubLeafProvider<IncusContainerStore> {\n\n    @Override\n    public AbstractAction createAction(DataStoreEntryRef<IncusContainerStore> ref) {\n        return Action.builder().ref(ref).build();\n    }\n\n    @Override\n    public boolean requiresValidStore() {\n        return false;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<IncusContainerStore> store) {\n        return AppI18n.observable(\"serialConsole\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<IncusContainerStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2c-console\");\n    }\n\n    @Override\n    public Class<IncusContainerStore> getApplicableClass() {\n        return IncusContainerStore.class;\n    }\n\n    @Override\n    public String getId() {\n        return \"openIncusContainerConsole\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<IncusContainerStore> {\n\n        @Override\n        public void executeImpl() throws Exception {\n            var d = ref.getStore();\n            var view = new IncusCommandView(\n                    d.getInstall().getStore().getHost().getStore().getOrStartSession());\n            TerminalLaunch.builder()\n                    .entry(ref.get())\n                    .title(\"Console\")\n                    .command(view.console(d.getName()))\n                    .launch();\n        }\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/incus/IncusContainerEditConfigActionProvider.java",
    "content": "package io.xpipe.ext.system.incus;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.terminal.TerminalLaunch;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class IncusContainerEditConfigActionProvider implements HubLeafProvider<IncusContainerStore> {\n\n    @Override\n    public AbstractAction createAction(DataStoreEntryRef<IncusContainerStore> ref) {\n        return Action.builder().ref(ref).build();\n    }\n\n    @Override\n    public boolean requiresValidStore() {\n        return false;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<IncusContainerStore> store) {\n        return AppI18n.observable(\"editConfiguration\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<IncusContainerStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2f-file-document-edit\");\n    }\n\n    @Override\n    public Class<IncusContainerStore> getApplicableClass() {\n        return IncusContainerStore.class;\n    }\n\n    @Override\n    public String getId() {\n        return \"editIncusContainerConfig\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<IncusContainerStore> {\n\n        @Override\n        public void executeImpl() throws Exception {\n            var d = ref.getStore();\n            var view = new IncusCommandView(\n                    d.getInstall().getStore().getHost().getStore().getOrStartSession());\n            TerminalLaunch.builder()\n                    .entry(ref.get())\n                    .title(\"Config\")\n                    .command(view.configEdit(d.getName()))\n                    .launch();\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/incus/IncusContainerEditRunConfigActionProvider.java",
    "content": "package io.xpipe.ext.system.incus;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.browser.file.BrowserFileOpener;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.core.FilePath;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class IncusContainerEditRunConfigActionProvider implements HubLeafProvider<IncusContainerStore> {\n\n    @Override\n    public AbstractAction createAction(DataStoreEntryRef<IncusContainerStore> ref) {\n        return Action.builder().ref(ref).build();\n    }\n\n    @Override\n    public boolean requiresValidStore() {\n        return false;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<IncusContainerStore> store) {\n        return AppI18n.observable(\"editRunConfiguration\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<IncusContainerStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2m-movie-edit\");\n    }\n\n    @Override\n    public Class<IncusContainerStore> getApplicableClass() {\n        return IncusContainerStore.class;\n    }\n\n    @Override\n    public String getId() {\n        return \"editIncusContainerRunConfig\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<IncusContainerStore> {\n\n        @Override\n        public void executeImpl() throws Exception {\n            var d = ref.getStore();\n            var elevatedRef = ProcessControlProvider.get()\n                    .elevated(d.getInstall().getStore().getHost().get().ref());\n            var file = FilePath.of(\"/run/incus/\" + d.getContainerName() + \"/lxc.conf\");\n            var model = BrowserFullSessionModel.DEFAULT.openFileSystemSync(\n                    elevatedRef, null, m -> file.getParent(), null, true);\n            var found = model.findFile(file);\n            if (found.isEmpty()) {\n                return;\n            }\n            BrowserFileOpener.openInTextEditor(model, found.get());\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/incus/IncusContainerStore.java",
    "content": "package io.xpipe.ext.system.incus;\n\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.process.BaseElevationHandler;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.*;\nimport io.xpipe.ext.base.identity.IdentityValue;\nimport io.xpipe.ext.base.store.PauseableStore;\nimport io.xpipe.ext.base.store.StartableStore;\nimport io.xpipe.ext.base.store.StoppableStore;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.AllArgsConstructor;\nimport lombok.Getter;\nimport lombok.Value;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.Objects;\nimport java.util.OptionalInt;\n\n@JsonTypeName(\"incusContainer\")\n@SuperBuilder(toBuilder = true)\n@Jacksonized\n@Getter\n@AllArgsConstructor\n@Value\npublic class IncusContainerStore\n        implements ShellStore,\n                FixedChildStore,\n                StatefulDataStore<ContainerStoreState>,\n                StartableStore,\n                StoppableStore,\n                PauseableStore,\n                NameableStore {\n\n    DataStoreEntryRef<IncusInstallStore> install;\n    String containerName;\n    IdentityValue identity;\n\n    @Override\n    public Class<ContainerStoreState> getStateClass() {\n        return ContainerStoreState.class;\n    }\n\n    @Override\n    public void checkComplete() throws Throwable {\n        Validators.nonNull(install);\n        Validators.isType(install, IncusInstallStore.class);\n        install.checkComplete();\n        Validators.nonNull(containerName);\n        if (identity != null) {\n            identity.checkComplete();\n        }\n    }\n\n    @Override\n    public OptionalInt getFixedId() {\n        return OptionalInt.of(Objects.hash(containerName));\n    }\n\n    @Override\n    public FixedChildStore merge(FixedChildStore other) {\n        var o = (IncusContainerStore) other;\n        return toBuilder().identity(identity != null ? identity : o.identity).build();\n    }\n\n    @Override\n    public ShellControlFunction shellFunction() {\n        return new ShellControlParentStoreFunction() {\n\n            @Override\n            public ShellControl control(ShellControl parent) throws Exception {\n                refreshContainerState(\n                        getInstall().getStore().getHost().getStore().getOrStartSession());\n\n                var user = identity != null ? identity.unwrap().getUsername().retrieveUsername() : null;\n                var sc = new IncusCommandView(parent).exec(containerName, user, () -> {\n                    var state = getState();\n                    var alpine = state.getOsName() != null\n                            && state.getOsName().toLowerCase().contains(\"alpine\");\n                    return alpine;\n                });\n                sc.withSourceStore(IncusContainerStore.this);\n                if (identity != null && identity.unwrap().getPassword() != null) {\n                    sc.setElevationHandler(new BaseElevationHandler(\n                                    IncusContainerStore.this, identity.unwrap().getPassword())\n                            .orElse(sc.getElevationHandler()));\n                }\n                sc.withShellStateInit(IncusContainerStore.this);\n                sc.onStartupFail(throwable -> {\n                    if (throwable instanceof LicenseRequiredException) {\n                        return;\n                    }\n\n                    var s = getState().toBuilder()\n                            .running(false)\n                            .containerState(\"Connection failed\")\n                            .build();\n                    setState(s);\n                });\n\n                return sc;\n            }\n\n            @Override\n            public ShellStore getParentStore() {\n                return getInstall().getStore().getHost().getStore();\n            }\n        };\n    }\n\n    private void refreshContainerState(ShellControl sc) throws Exception {\n        var state = getState();\n        var view = new IncusCommandView(sc);\n        var displayState = view.queryContainerState(containerName);\n        var running = \"RUNNING\".equals(displayState);\n        var newState =\n                state.toBuilder().containerState(displayState).running(running).build();\n        setState(newState);\n    }\n\n    @Override\n    public void start() throws Exception {\n        var sc = getInstall().getStore().getHost().getStore().getOrStartSession();\n        var view = new IncusCommandView(sc);\n        view.start(containerName);\n        refreshContainerState(sc);\n    }\n\n    @Override\n    public void stop() throws Exception {\n        var sc = getInstall().getStore().getHost().getStore().getOrStartSession();\n        var view = new IncusCommandView(sc);\n        view.stop(containerName);\n        refreshContainerState(sc);\n    }\n\n    @Override\n    public void pause() throws Exception {\n        var sc = getInstall().getStore().getHost().getStore().getOrStartSession();\n        var view = new IncusCommandView(sc);\n        view.pause(containerName);\n        refreshContainerState(sc);\n    }\n\n    @Override\n    public String getName() {\n        return containerName;\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/incus/IncusContainerStoreProvider.java",
    "content": "package io.xpipe.ext.system.incus;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.ext.ContainerStoreState;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.GuiDialog;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.hub.comp.*;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.util.*;\nimport io.xpipe.ext.base.identity.IdentityChoiceBuilder;\nimport io.xpipe.ext.base.store.ShellStoreProvider;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.ReadOnlyObjectWrapper;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\n\npublic class IncusContainerStoreProvider implements ShellStoreProvider {\n\n    public BaseRegionBuilder<?, ?> stateDisplay(StoreEntryWrapper w) {\n        return new OsLogoComp(w, BindingsHelper.map(w.getPersistentState(), o -> {\n            var state = (ContainerStoreState) o;\n            var cs = state.getContainerState();\n            if (cs != null && cs.toLowerCase().contains(\"stopped\")) {\n                return SystemStateComp.State.FAILURE;\n            } else if (cs != null && cs.toLowerCase().contains(\"running\")) {\n                return SystemStateComp.State.SUCCESS;\n            } else {\n                return SystemStateComp.State.OTHER;\n            }\n        }));\n    }\n\n    @Override\n    public ObservableValue<String> informationString(StoreSection section) {\n        var c = (ContainerStoreState) section.getWrapper().getPersistentState().getValue();\n        var missing = c.getShellMissing() != null && c.getShellMissing() ? \"No shell available\" : null;\n        return StoreStateFormat.shellStore(\n                section,\n                (ContainerStoreState s) -> new String[] {missing, DataStoreFormatter.capitalize(s.getContainerState())},\n                null);\n    }\n\n    @Override\n    public DocumentationLink getHelpLink() {\n        return DocumentationLink.LXC;\n    }\n\n    @Override\n    public boolean shouldShow(StoreEntryWrapper w) {\n        IncusContainerStore s = w.getEntry().getStore().asNeeded();\n        var state = s.getState();\n        return Boolean.TRUE.equals(state.getRunning())\n                || s.getInstall().getStore().getState().isShowNonRunning();\n    }\n\n    @Override\n    public boolean shouldShowScan() {\n        return false;\n    }\n\n    @Override\n    public DataStoreEntry getDisplayParent(DataStoreEntry store) {\n        IncusContainerStore s = store.getStore().asNeeded();\n        return s.getInstall().get();\n    }\n\n    @Override\n    public GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {\n        IncusContainerStore st = (IncusContainerStore) store.getValue();\n        var identity = new SimpleObjectProperty<>(st.getIdentity());\n\n        var q = new OptionsBuilder()\n                .name(\"host\")\n                .description(\"lxdHostDescription\")\n                .addComp(new StoreChoiceComp<>(\n                        entry,\n                        new ReadOnlyObjectWrapper<>(st.getInstall().getStore().getHost()),\n                        ShellStore.class,\n                        null,\n                        StoreViewState.get().getAllConnectionsCategory()))\n                .disable()\n                .name(\"container\")\n                .description(\"lxdContainerDescription\")\n                .addStaticString(st.getContainerName())\n                .sub(IdentityChoiceBuilder.container(identity), identity)\n                .bind(\n                        () -> {\n                            return IncusContainerStore.builder()\n                                    .containerName(st.getContainerName())\n                                    .install(st.getInstall())\n                                    .identity(identity.getValue())\n                                    .build();\n                        },\n                        store)\n                .buildDialog();\n        return q;\n    }\n\n    @Override\n    public String getDisplayIconFileName(DataStore store) {\n        return \"system:lxd_icon.svg\";\n    }\n\n    @Override\n    public String getId() {\n        return \"incusContainer\";\n    }\n\n    @Override\n    public List<Class<?>> getStoreClasses() {\n        return List.of(IncusContainerStore.class);\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/incus/IncusInstallStore.java",
    "content": "package io.xpipe.ext.system.incus;\n\nimport io.xpipe.app.ext.DataStoreState;\nimport io.xpipe.app.ext.FixedChildStore;\nimport io.xpipe.app.ext.FixedHierarchyStore;\nimport io.xpipe.app.ext.SelfReferentialStore;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.ext.StatefulDataStore;\nimport io.xpipe.app.process.CommandSupport;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.Validators;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.Value;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\nimport java.util.regex.Pattern;\n\n@JsonTypeName(\"incusInstall\")\n@SuperBuilder\n@Jacksonized\n@Getter\n@Value\npublic class IncusInstallStore\n        implements FixedHierarchyStore, StatefulDataStore<IncusInstallStore.State>, SelfReferentialStore {\n\n    DataStoreEntryRef<ShellStore> host;\n\n    public IncusInstallStore(DataStoreEntryRef<ShellStore> host) {\n        this.host = host;\n    }\n\n    @Override\n    public void checkComplete() throws Throwable {\n        Validators.nonNull(host);\n        Validators.isType(host, ShellStore.class);\n        host.checkComplete();\n    }\n\n    private void updateState() throws Exception {\n        var sc = getHost().getStore().getOrStartSession();\n        var view = new IncusCommandView(sc);\n        var out = view.version();\n        var namePattern = Pattern.compile(\"Server version:\\\\s+(.+)\");\n        var nameMatcher = namePattern.matcher(out);\n        var v = nameMatcher.find() ? nameMatcher.group(1) : null;\n        var reachable = v != null && !\"unreachable\".equals(v);\n        setState(getState().toBuilder()\n                .serverVersion(reachable ? v : null)\n                .reachable(reachable)\n                .build());\n    }\n\n    @Override\n    public List<? extends DataStoreEntryRef<? extends FixedChildStore>> listChildren() throws Exception {\n        var sc = getHost().getStore().getOrStartSession();\n        var view = new IncusCommandView(sc);\n        CommandSupport.isSupported(() -> view.isSupported(), \"Incus CLI client (incus)\", host.get());\n        updateState();\n        return view.listContainers(getSelfEntry().ref());\n    }\n\n    @Value\n    @EqualsAndHashCode(callSuper = true)\n    @SuperBuilder(toBuilder = true)\n    @Jacksonized\n    public static class State extends DataStoreState {\n        String serverVersion;\n        boolean reachable;\n        boolean showNonRunning;\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/incus/IncusInstallStoreProvider.java",
    "content": "package io.xpipe.ext.system.incus;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.DataStoreProvider;\nimport io.xpipe.app.ext.DataStoreUsageCategory;\nimport io.xpipe.app.hub.comp.*;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreCategory;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.util.DocumentationLink;\n\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\n\npublic class IncusInstallStoreProvider implements DataStoreProvider {\n\n    @Override\n    public DocumentationLink getHelpLink() {\n        return DocumentationLink.LXC;\n    }\n\n    @Override\n    public StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) {\n        var nonRunning = StoreToggleComp.<IncusInstallStore>childrenToggle(\n                true, sec, s -> s.getState().isShowNonRunning(), (s, aBoolean) -> {\n                    var state =\n                            s.getState().toBuilder().showNonRunning(aBoolean).build();\n                    s.setState(state);\n                });\n        return StoreEntryComp.create(sec, nonRunning, preferLarge);\n    }\n\n    public BaseRegionBuilder<?, ?> stateDisplay(StoreEntryWrapper w) {\n        return new SystemStateComp(BindingsHelper.map(w.getPersistentState(), o -> {\n            var state = (IncusInstallStore.State) o;\n            if (state.isReachable()) {\n                return SystemStateComp.State.SUCCESS;\n            }\n\n            return SystemStateComp.State.FAILURE;\n        }));\n    }\n\n    @Override\n    public DataStoreUsageCategory getUsageCategory() {\n        return DataStoreUsageCategory.GROUP;\n    }\n\n    @Override\n    public DataStoreEntry getDisplayParent(DataStoreEntry store) {\n        IncusInstallStore s = store.getStore().asNeeded();\n        return s.getHost().get();\n    }\n\n    @Override\n    public ObservableValue<String> informationString(StoreSection section) {\n        return BindingsHelper.map(section.getWrapper().getPersistentState(), o -> {\n            var state = (IncusInstallStore.State) o;\n            return state.isReachable() ? \"incus v\" + state.getServerVersion() : \"Connection failed\";\n        });\n    }\n\n    @Override\n    public String getDisplayIconFileName(DataStore store) {\n        return \"system:lxd_icon.svg\";\n    }\n\n    @Override\n    public DataStore defaultStore(DataStoreCategory category) {\n        return new IncusInstallStore(DataStorage.get().local().ref());\n    }\n\n    @Override\n    public String getId() {\n        return \"incusInstall\";\n    }\n\n    @Override\n    public List<Class<?>> getStoreClasses() {\n        return List.of(IncusInstallStore.class);\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/incus/IncusScanProvider.java",
    "content": "package io.xpipe.ext.system.incus;\n\nimport io.xpipe.app.ext.ScanProvider;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.core.OsType;\n\npublic class IncusScanProvider extends ScanProvider {\n\n    @Override\n    public ScanOpportunity create(DataStoreEntry entry, ShellControl sc) throws Exception {\n        if (sc.getOsType() != OsType.LINUX) {\n            return null;\n        }\n\n        return new ScanOpportunity(\"incusContainers\", !new IncusCommandView(sc).isSupported());\n    }\n\n    @Override\n    public void scan(DataStoreEntry entry, ShellControl sc) {\n        var e = DataStorage.get()\n                .addStoreIfNotPresent(\n                        entry,\n                        \"Incus containers\",\n                        IncusInstallStore.builder().host(entry.ref()).build());\n        DataStorage.get().refreshChildren(e);\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/lxd/LxdCmdStore.java",
    "content": "package io.xpipe.ext.system.lxd;\n\nimport io.xpipe.app.ext.DataStoreState;\nimport io.xpipe.app.ext.FixedChildStore;\nimport io.xpipe.app.ext.FixedHierarchyStore;\nimport io.xpipe.app.ext.SelfReferentialStore;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.ext.StatefulDataStore;\nimport io.xpipe.app.process.CommandSupport;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.Validators;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.EqualsAndHashCode;\nimport lombok.Value;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\nimport java.util.regex.Pattern;\n\n@JsonTypeName(\"lxdCmd\")\n@SuperBuilder\n@Jacksonized\n@Value\npublic class LxdCmdStore implements FixedHierarchyStore, StatefulDataStore<LxdCmdStore.State>, SelfReferentialStore {\n\n    DataStoreEntryRef<ShellStore> host;\n\n    public LxdCmdStore(DataStoreEntryRef<ShellStore> host) {\n        this.host = host;\n    }\n\n    @Override\n    public void checkComplete() throws Throwable {\n        Validators.nonNull(host);\n        Validators.isType(host, ShellStore.class);\n        host.checkComplete();\n    }\n\n    private void updateState(LxdCommandView view) throws Exception {\n        var out = view.version();\n        var namePattern = Pattern.compile(\"Server version:\\\\s+(.+)\");\n        var nameMatcher = namePattern.matcher(out);\n        var v = nameMatcher.find() ? nameMatcher.group(1) : null;\n        var reachable = v != null && !\"unreachable\".equals(v);\n        setState(getState().toBuilder()\n                .serverVersion(reachable ? v : null)\n                .reachable(reachable)\n                .build());\n    }\n\n    @Override\n    public List<? extends DataStoreEntryRef<? extends FixedChildStore>> listChildren() throws Exception {\n        var sc = getHost().getStore().getOrStartSession();\n        var view = new LxdCommandView(sc);\n        CommandSupport.isSupported(() -> view.isSupported(), \"LXD CLI client (lxc)\", host.get());\n        updateState(view);\n        return view.listContainers(getSelfEntry().ref());\n    }\n\n    @Value\n    @EqualsAndHashCode(callSuper = true)\n    @SuperBuilder(toBuilder = true)\n    @Jacksonized\n    public static class State extends DataStoreState {\n        String serverVersion;\n        boolean reachable;\n        boolean showNonRunning;\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/lxd/LxdCmdStoreProvider.java",
    "content": "package io.xpipe.ext.system.lxd;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.DataStoreProvider;\nimport io.xpipe.app.ext.DataStoreUsageCategory;\nimport io.xpipe.app.hub.comp.*;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreCategory;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.util.DocumentationLink;\n\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\n\npublic class LxdCmdStoreProvider implements DataStoreProvider {\n\n    @Override\n    public DocumentationLink getHelpLink() {\n        return DocumentationLink.LXC;\n    }\n\n    @Override\n    public StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) {\n        var nonRunning = StoreToggleComp.<LxdCmdStore>childrenToggle(\n                true, sec, s -> s.getState().isShowNonRunning(), (s, aBoolean) -> {\n                    var state =\n                            s.getState().toBuilder().showNonRunning(aBoolean).build();\n                    s.setState(state);\n                });\n        return StoreEntryComp.create(sec, nonRunning, preferLarge);\n    }\n\n    public BaseRegionBuilder<?, ?> stateDisplay(StoreEntryWrapper w) {\n        return new SystemStateComp(BindingsHelper.map(w.getPersistentState(), o -> {\n            var state = (LxdCmdStore.State) o;\n            if (state.isReachable()) {\n                return SystemStateComp.State.SUCCESS;\n            }\n\n            return SystemStateComp.State.FAILURE;\n        }));\n    }\n\n    @Override\n    public DataStoreUsageCategory getUsageCategory() {\n        return DataStoreUsageCategory.GROUP;\n    }\n\n    @Override\n    public DataStoreEntry getDisplayParent(DataStoreEntry store) {\n        LxdCmdStore s = store.getStore().asNeeded();\n        return s.getHost().get();\n    }\n\n    @Override\n    public ObservableValue<String> informationString(StoreSection section) {\n        return BindingsHelper.map(section.getWrapper().getPersistentState(), o -> {\n            var state = (LxdCmdStore.State) o;\n            return state.isReachable() ? \"lxd v\" + state.getServerVersion() : \"Connection failed\";\n        });\n    }\n\n    @Override\n    public String getDisplayIconFileName(DataStore store) {\n        return \"system:lxd_icon.svg\";\n    }\n\n    @Override\n    public DataStore defaultStore(DataStoreCategory category) {\n        return new LxdCmdStore(DataStorage.get().local().ref());\n    }\n\n    @Override\n    public String getId() {\n        return \"lxdCmd\";\n    }\n\n    @Override\n    public List<Class<?>> getStoreClasses() {\n        return List.of(LxdCmdStore.class);\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/lxd/LxdCommandView.java",
    "content": "package io.xpipe.ext.system.lxd;\n\nimport io.xpipe.app.ext.ContainerStoreState;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.*;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.core.FilePath;\nimport io.xpipe.ext.base.identity.IdentityValue;\n\nimport lombok.NonNull;\n\nimport java.util.*;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\n\npublic class LxdCommandView extends CommandViewBase {\n\n    public LxdCommandView(ShellControl shellControl) {\n        super(shellControl);\n    }\n\n    private static ElevationFunction requiresElevation() {\n        return ElevationFunction.cached(\"lxdRequiresElevation\", new ElevationFunction() {\n            @Override\n            public String getPrefix() {\n                return \"LXD\";\n            }\n\n            @Override\n            public boolean isSpecified() {\n                return true;\n            }\n\n            @Override\n            public boolean apply(ShellControl shellControl) throws Exception {\n                // This is not perfect as it does not respect custom locations for the LXD socket\n                // Sadly the socket location changes based on the installation type, and we can't dynamically query the\n                // path\n                return !shellControl\n                        .command(\n                                \"test -S /var/lib/lxd/unix.socket && test -w /var/lib/lxd/unix.socket || test -S /var/snap/lxd/common/lxd/unix\"\n                                        + \".socket && test -w /var/snap/lxd/common/lxd/unix.socket\")\n                        .executeAndCheck();\n            }\n        });\n    }\n\n    private static String formatErrorMessage(String s) {\n        return s;\n    }\n\n    private static <T extends Throwable> T convertException(T s) {\n        return ErrorEventFactory.expectedIfContains(s);\n    }\n\n    @Override\n    protected CommandControl build(Consumer<CommandBuilder> builder) {\n        var cmd = CommandBuilder.of().add(\"lxc\");\n        builder.accept(cmd);\n        return shellControl\n                .command(cmd)\n                .withErrorFormatter(LxdCommandView::formatErrorMessage)\n                .withExceptionConverter(LxdCommandView::convertException)\n                .elevated(requiresElevation());\n    }\n\n    @Override\n    public LxdCommandView start() throws Exception {\n        shellControl.start();\n        return this;\n    }\n\n    public boolean isSupported() throws Exception {\n        // Ubuntu always has the lxc command installed as a stub to install LXD\n        // We don't want to call it as this would automatically install LXD and take a while\n        if (shellControl.getOsName().toLowerCase().contains(\"ubuntu\")) {\n            return shellControl.view().fileExists(FilePath.of(\"/snap/bin/lxc\"));\n        }\n\n        return shellControl\n                .command(\"lxc --help\")\n                .withErrorFormatter(LxdCommandView::formatErrorMessage)\n                .withExceptionConverter(LxdCommandView::convertException)\n                .executeAndCheck();\n    }\n\n    public String version() throws Exception {\n        return shellControl\n                .command(\"lxc version\")\n                .withErrorFormatter(LxdCommandView::formatErrorMessage)\n                .withExceptionConverter(LxdCommandView::convertException)\n                .elevated(requiresElevation())\n                .readStdoutOrThrow();\n    }\n\n    public String queryContainerState(String containerName) throws Exception {\n        var states = listContainersAndStates();\n        return states.getOrDefault(containerName, \"?\");\n    }\n\n    public void start(String containerName) throws Exception {\n        build(commandBuilder -> commandBuilder.add(\"start\").addQuoted(containerName))\n                .execute();\n    }\n\n    public void stop(String containerName) throws Exception {\n        build(commandBuilder -> commandBuilder.add(\"stop\").addQuoted(containerName))\n                .execute();\n    }\n\n    public void pause(String containerName) throws Exception {\n        build(commandBuilder -> commandBuilder.add(\"pause\").addQuoted(containerName))\n                .execute();\n    }\n\n    public CommandControl console(String containerName) {\n        return build(commandBuilder -> commandBuilder.add(\"console\").addQuoted(containerName));\n    }\n\n    public CommandControl configEdit(String containerName) {\n        return build(commandBuilder -> commandBuilder.add(\"config\", \"edit\").addQuoted(containerName));\n    }\n\n    public List<DataStoreEntryRef<LxdContainerStore>> listContainers(DataStoreEntryRef<LxdCmdStore> store)\n            throws Exception {\n        return listContainersAndStates().entrySet().stream()\n                .map(s -> {\n                    boolean running = s.getValue().toLowerCase(Locale.ROOT).equals(\"running\");\n                    var c = LxdContainerStore.builder()\n                            .cmd(store)\n                            .containerName(s.getKey())\n                            .identity(IdentityValue.ofBreakout(store.get()))\n                            .build();\n                    var entry = DataStoreEntry.createNew(c.getContainerName(), c);\n                    entry.setStorePersistentState(ContainerStoreState.builder()\n                            .containerState(s.getValue())\n                            .running(running)\n                            .build());\n                    return Optional.of(entry.<LxdContainerStore>ref());\n                })\n                .flatMap(Optional::stream)\n                .toList();\n    }\n\n    private Map<String, String> listContainersAndStates() throws Exception {\n        try (var c = build(commandBuilder -> commandBuilder.add(\"list\", \"-f\", \"csv\", \"-c\", \"ns\"))\n                .start()) {\n            var output = c.readStdoutOrThrow();\n            return output.lines()\n                    .collect(Collectors.toMap(\n                            s -> s.strip().split(\",\")[0],\n                            s -> s.strip().split(\",\")[1],\n                            (x, y) -> y,\n                            LinkedHashMap::new));\n        } catch (ProcessOutputException ex) {\n            if (ex.getOutput().contains(\"Error: unknown shorthand flag: 'f' in -f\")) {\n                throw ErrorEventFactory.expected(\n                        ProcessOutputException.withPrefix(\"Unsupported legacy LXD version\", ex));\n            } else {\n                throw ex;\n            }\n        }\n    }\n\n    public ShellControl exec(String container, String user, Supplier<Boolean> busybox) {\n        var sub = shellControl.subShell();\n        sub.setDumbOpen(createOpenFunction(container, user, false, busybox));\n        sub.setTerminalOpen(createOpenFunction(container, user, true, busybox));\n        return sub.withExceptionConverter(LxdCommandView::convertException).elevated(requiresElevation());\n    }\n\n    private ShellOpenFunction createOpenFunction(\n            String containerName, String user, boolean terminal, Supplier<Boolean> busybox) {\n        return new ShellOpenFunction() {\n            @Override\n            public CommandBuilder prepareWithoutInitCommand() {\n                var b = execCommand(containerName, terminal).add(\"su\", \"-l\");\n                if (user != null) {\n                    b.addQuoted(user);\n                }\n                return b;\n            }\n\n            @Override\n            public CommandBuilder prepareWithInitCommand(@NonNull String command) {\n                var b = execCommand(containerName, terminal).add(\"su\", \"-l\");\n                if (user != null) {\n                    b.addQuoted(user);\n                }\n                return b.add(sc -> {\n                            var suType = busybox.get();\n                            if (suType) {\n                                return \"-c\";\n                            } else {\n                                return \"--session-command\";\n                            }\n                        })\n                        .addLiteral(command);\n            }\n        };\n    }\n\n    public CommandBuilder execCommand(String containerName, boolean terminal) {\n        var c = CommandBuilder.of().add(\"lxc\", \"exec\", terminal ? \"-t\" : \"-T\");\n        return c.addQuoted(containerName).add(\"--\");\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/lxd/LxdContainerActionProviderMenu.java",
    "content": "package io.xpipe.ext.system.lxd;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.HubBranchProvider;\nimport io.xpipe.app.hub.action.HubMenuItemProvider;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.ext.base.store.StorePauseActionProvider;\nimport io.xpipe.ext.base.store.StoreRestartActionProvider;\nimport io.xpipe.ext.base.store.StoreStartActionProvider;\nimport io.xpipe.ext.base.store.StoreStopActionProvider;\n\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\n\npublic class LxdContainerActionProviderMenu implements HubBranchProvider<LxdContainerStore> {\n\n    @Override\n    public boolean isMajor() {\n        return true;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<LxdContainerStore> store) {\n        return AppI18n.observable(\"containerActions\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<LxdContainerStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2p-package-variant-closed\");\n    }\n\n    @Override\n    public Class<LxdContainerStore> getApplicableClass() {\n        return LxdContainerStore.class;\n    }\n\n    @Override\n    public List<HubMenuItemProvider<?>> getChildren(DataStoreEntryRef<LxdContainerStore> store) {\n        return List.of(\n                new StoreStartActionProvider(),\n                new StoreStopActionProvider(),\n                new StorePauseActionProvider(),\n                new StoreRestartActionProvider(),\n                new LxdContainerConsoleActionProvider(),\n                new LxdContainerEditConfigActionProvider(),\n                new LxdContainerEditRunConfigActionProvider());\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/lxd/LxdContainerConsoleActionProvider.java",
    "content": "package io.xpipe.ext.system.lxd;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.terminal.TerminalLaunch;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class LxdContainerConsoleActionProvider implements HubLeafProvider<LxdContainerStore> {\n\n    @Override\n    public AbstractAction createAction(DataStoreEntryRef<LxdContainerStore> ref) {\n        return Action.builder().ref(ref).build();\n    }\n\n    @Override\n    public boolean requiresValidStore() {\n        return false;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<LxdContainerStore> store) {\n        return AppI18n.observable(\"serialConsole\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<LxdContainerStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2c-console\");\n    }\n\n    @Override\n    public Class<LxdContainerStore> getApplicableClass() {\n        return LxdContainerStore.class;\n    }\n\n    @Override\n    public String getId() {\n        return \"openLxdContainerConsole\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<LxdContainerStore> {\n\n        @Override\n        public void executeImpl() throws Exception {\n            var d = ref.getStore();\n            var view = new LxdCommandView(\n                    d.getCmd().getStore().getHost().getStore().getOrStartSession());\n            TerminalLaunch.builder()\n                    .entry(ref.get())\n                    .title(\"Console\")\n                    .command(view.console(d.getName()))\n                    .launch();\n        }\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/lxd/LxdContainerEditConfigActionProvider.java",
    "content": "package io.xpipe.ext.system.lxd;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.terminal.TerminalLaunch;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class LxdContainerEditConfigActionProvider implements HubLeafProvider<LxdContainerStore> {\n\n    @Override\n    public AbstractAction createAction(DataStoreEntryRef<LxdContainerStore> ref) {\n        return Action.builder().ref(ref).build();\n    }\n\n    @Override\n    public boolean requiresValidStore() {\n        return false;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<LxdContainerStore> store) {\n        return AppI18n.observable(\"editConfiguration\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<LxdContainerStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2f-file-document-edit\");\n    }\n\n    @Override\n    public Class<LxdContainerStore> getApplicableClass() {\n        return LxdContainerStore.class;\n    }\n\n    @Override\n    public String getId() {\n        return \"editLxdContainerConfig\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<LxdContainerStore> {\n\n        @Override\n        public void executeImpl() throws Exception {\n            var d = ref.getStore();\n            var view = new LxdCommandView(\n                    d.getCmd().getStore().getHost().getStore().getOrStartSession());\n            TerminalLaunch.builder()\n                    .entry(ref.get())\n                    .title(\"Config\")\n                    .command(view.configEdit(d.getName()))\n                    .launch();\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/lxd/LxdContainerEditRunConfigActionProvider.java",
    "content": "package io.xpipe.ext.system.lxd;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.browser.BrowserFullSessionModel;\nimport io.xpipe.app.browser.file.BrowserFileOpener;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.ext.ProcessControlProvider;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.core.FilePath;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class LxdContainerEditRunConfigActionProvider implements HubLeafProvider<LxdContainerStore> {\n\n    @Override\n    public AbstractAction createAction(DataStoreEntryRef<LxdContainerStore> ref) {\n        return Action.builder().ref(ref).build();\n    }\n\n    @Override\n    public boolean requiresValidStore() {\n        return false;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<LxdContainerStore> store) {\n        return AppI18n.observable(\"editRunConfiguration\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<LxdContainerStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2m-movie-edit\");\n    }\n\n    @Override\n    public Class<LxdContainerStore> getApplicableClass() {\n        return LxdContainerStore.class;\n    }\n\n    @Override\n    public String getId() {\n        return \"editLxdContainerRunConfig\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<LxdContainerStore> {\n\n        @Override\n        public void executeImpl() throws Exception {\n            var d = ref.getStore();\n            var elevatedRef = ProcessControlProvider.get()\n                    .elevated(d.getCmd().getStore().getHost().get().ref());\n            var file = FilePath.of(\"/run/lxd/\" + d.getContainerName() + \"/lxc.conf\");\n            var model = BrowserFullSessionModel.DEFAULT.openFileSystemSync(\n                    elevatedRef, null, m -> file.getParent(), null, true);\n            var found = model.findFile(file);\n            if (found.isEmpty()) {\n                return;\n            }\n            BrowserFileOpener.openInTextEditor(model, found.get());\n        }\n\n        @Override\n        public boolean isMutation() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/lxd/LxdContainerStore.java",
    "content": "package io.xpipe.ext.system.lxd;\n\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.process.BaseElevationHandler;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.*;\nimport io.xpipe.ext.base.identity.IdentityValue;\nimport io.xpipe.ext.base.store.PauseableStore;\nimport io.xpipe.ext.base.store.StartableStore;\nimport io.xpipe.ext.base.store.StoppableStore;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.AllArgsConstructor;\nimport lombok.Value;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.Objects;\nimport java.util.OptionalInt;\n\n@JsonTypeName(\"lxd\")\n@SuperBuilder(toBuilder = true)\n@Jacksonized\n@Value\n@AllArgsConstructor\npublic class LxdContainerStore\n        implements ShellStore,\n                FixedChildStore,\n                StatefulDataStore<ContainerStoreState>,\n                StartableStore,\n                StoppableStore,\n                PauseableStore,\n                NameableStore {\n\n    DataStoreEntryRef<LxdCmdStore> cmd;\n    String containerName;\n    IdentityValue identity;\n\n    @Override\n    public String getName() {\n        return containerName;\n    }\n\n    @Override\n    public Class<ContainerStoreState> getStateClass() {\n        return ContainerStoreState.class;\n    }\n\n    @Override\n    public void checkComplete() throws Throwable {\n        Validators.nonNull(cmd);\n        Validators.isType(cmd, LxdCmdStore.class);\n        cmd.checkComplete();\n        Validators.nonNull(containerName);\n        if (identity != null) {\n            identity.checkComplete();\n        }\n    }\n\n    @Override\n    public OptionalInt getFixedId() {\n        return OptionalInt.of(Objects.hash(containerName));\n    }\n\n    @Override\n    public FixedChildStore merge(FixedChildStore other) {\n        var o = (LxdContainerStore) other;\n        return toBuilder().identity(identity != null ? identity : o.identity).build();\n    }\n\n    @Override\n    public ShellControlFunction shellFunction() {\n        return new ShellControlParentStoreFunction() {\n\n            @Override\n            public ShellControl control(ShellControl parent) throws Exception {\n                refreshContainerState(getCmd().getStore().getHost().getStore().getOrStartSession());\n\n                var user = identity != null ? identity.unwrap().getUsername().retrieveUsername() : null;\n                var sc = new LxdCommandView(parent).exec(containerName, user, () -> {\n                    var state = getState();\n                    var alpine = state.getOsName() != null\n                            && state.getOsName().toLowerCase().contains(\"alpine\");\n                    return alpine;\n                });\n                if (identity != null && identity.unwrap().getPassword() != null) {\n                    sc.setElevationHandler(new BaseElevationHandler(\n                                    LxdContainerStore.this, identity.unwrap().getPassword())\n                            .orElse(sc.getElevationHandler()));\n                }\n                sc.withSourceStore(LxdContainerStore.this);\n                sc.withShellStateInit(LxdContainerStore.this);\n                sc.onStartupFail(throwable -> {\n                    if (throwable instanceof LicenseRequiredException) {\n                        return;\n                    }\n\n                    var s = getState().toBuilder()\n                            .running(false)\n                            .containerState(\"Connection failed\")\n                            .build();\n                    setState(s);\n                });\n                return sc;\n            }\n\n            @Override\n            public ShellStore getParentStore() {\n                return getCmd().getStore().getHost().getStore();\n            }\n        };\n    }\n\n    private void refreshContainerState(ShellControl sc) throws Exception {\n        var state = getState();\n        var view = new LxdCommandView(sc);\n        var displayState = view.queryContainerState(containerName);\n        var running = \"RUNNING\".equals(displayState);\n        var newState =\n                state.toBuilder().containerState(displayState).running(running).build();\n        setState(newState);\n    }\n\n    @Override\n    public void start() throws Exception {\n        var sc = getCmd().getStore().getHost().getStore().getOrStartSession();\n        var view = new LxdCommandView(sc);\n        view.start(containerName);\n        refreshContainerState(sc);\n    }\n\n    @Override\n    public void stop() throws Exception {\n        var sc = getCmd().getStore().getHost().getStore().getOrStartSession();\n        var view = new LxdCommandView(sc);\n        view.stop(containerName);\n        refreshContainerState(sc);\n    }\n\n    @Override\n    public void pause() throws Exception {\n        var sc = getCmd().getStore().getHost().getStore().getOrStartSession();\n        var view = new LxdCommandView(sc);\n        view.pause(containerName);\n        refreshContainerState(sc);\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/lxd/LxdContainerStoreProvider.java",
    "content": "package io.xpipe.ext.system.lxd;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.ext.ContainerStoreState;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.GuiDialog;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.hub.comp.*;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.util.*;\nimport io.xpipe.ext.base.identity.IdentityChoiceBuilder;\nimport io.xpipe.ext.base.store.ShellStoreProvider;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.ReadOnlyObjectWrapper;\nimport javafx.beans.property.SimpleObjectProperty;\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\n\npublic class LxdContainerStoreProvider implements ShellStoreProvider {\n\n    public BaseRegionBuilder<?, ?> stateDisplay(StoreEntryWrapper w) {\n        return new OsLogoComp(w, BindingsHelper.map(w.getPersistentState(), o -> {\n            var state = (ContainerStoreState) o;\n            var cs = state.getContainerState();\n            if (cs != null && cs.toLowerCase().contains(\"stopped\")) {\n                return SystemStateComp.State.FAILURE;\n            } else if (cs != null && cs.toLowerCase().contains(\"running\")) {\n                return SystemStateComp.State.SUCCESS;\n            } else {\n                return SystemStateComp.State.OTHER;\n            }\n        }));\n    }\n\n    @Override\n    public ObservableValue<String> informationString(StoreSection section) {\n        var c = (ContainerStoreState) section.getWrapper().getPersistentState().getValue();\n        var missing = c.getShellMissing() != null && c.getShellMissing() ? \"No shell available\" : null;\n        return StoreStateFormat.shellStore(\n                section,\n                (ContainerStoreState s) -> new String[] {missing, DataStoreFormatter.capitalize(s.getContainerState())},\n                null);\n    }\n\n    @Override\n    public DocumentationLink getHelpLink() {\n        return DocumentationLink.LXC;\n    }\n\n    @Override\n    public boolean shouldShow(StoreEntryWrapper w) {\n        LxdContainerStore s = w.getEntry().getStore().asNeeded();\n        var state = s.getState();\n        return Boolean.TRUE.equals(state.getRunning())\n                || s.getCmd().getStore().getState().isShowNonRunning();\n    }\n\n    @Override\n    public boolean shouldShowScan() {\n        return false;\n    }\n\n    @Override\n    public DataStoreEntry getDisplayParent(DataStoreEntry store) {\n        LxdContainerStore s = store.getStore().asNeeded();\n        return s.getCmd().get();\n    }\n\n    @Override\n    public GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {\n        LxdContainerStore st = (LxdContainerStore) store.getValue();\n        var identity = new SimpleObjectProperty<>(st.getIdentity());\n\n        var q = new OptionsBuilder()\n                .name(\"host\")\n                .description(\"lxdHostDescription\")\n                .addComp(new StoreChoiceComp<>(\n                        entry,\n                        new ReadOnlyObjectWrapper<>(st.getCmd().getStore().getHost()),\n                        ShellStore.class,\n                        null,\n                        StoreViewState.get().getAllConnectionsCategory()))\n                .disable()\n                .name(\"container\")\n                .description(\"lxdContainerDescription\")\n                .addStaticString(st.getContainerName())\n                .sub(IdentityChoiceBuilder.container(identity), identity)\n                .bind(\n                        () -> {\n                            return LxdContainerStore.builder()\n                                    .containerName(st.getContainerName())\n                                    .cmd(st.getCmd())\n                                    .identity(identity.getValue())\n                                    .build();\n                        },\n                        store)\n                .buildDialog();\n        return q;\n    }\n\n    @Override\n    public String getId() {\n        return \"lxd\";\n    }\n\n    @Override\n    public List<Class<?>> getStoreClasses() {\n        return List.of(LxdContainerStore.class);\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/lxd/LxdScanProvider.java",
    "content": "package io.xpipe.ext.system.lxd;\n\nimport io.xpipe.app.ext.ScanProvider;\nimport io.xpipe.app.process.ProcessOutputException;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.core.OsType;\n\npublic class LxdScanProvider extends ScanProvider {\n\n    @Override\n    public ScanOpportunity create(DataStoreEntry entry, ShellControl sc) throws Exception {\n        if (sc.getOsType() != OsType.LINUX) {\n            return null;\n        }\n\n        if (entry.getStore() instanceof LxdContainerStore) {\n            return null;\n        }\n\n        return new ScanOpportunity(\"lxdContainers\", !new LxdCommandView(sc).isSupported());\n    }\n\n    @Override\n    public void scan(DataStoreEntry entry, ShellControl sc) throws Exception {\n        var e = DataStorage.get()\n                .addStoreIfNotPresent(\n                        entry,\n                        \"LXD containers\",\n                        LxdCmdStore.builder().host(entry.ref()).build());\n        try {\n            DataStorage.get().refreshChildrenOrThrow(e);\n        } catch (ProcessOutputException ex) {\n            if (!ex.getOutput().contains(\"unknown shorthand flag: 'f' in -f\")) {\n                throw ex;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/podman/PodmanCmdStore.java",
    "content": "package io.xpipe.ext.system.podman;\n\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.CommandSupport;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.Validators;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.EqualsAndHashCode;\nimport lombok.Value;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\nimport java.util.regex.Pattern;\n\n@JsonTypeName(\"podmanCmd\")\n@SuperBuilder\n@Jacksonized\n@Value\npublic class PodmanCmdStore\n        implements FixedHierarchyStore, StatefulDataStore<PodmanCmdStore.State>, SelfReferentialStore {\n\n    DataStoreEntryRef<ShellStore> host;\n\n    public PodmanCmdStore(DataStoreEntryRef<ShellStore> host) {\n        this.host = host;\n    }\n\n    @Override\n    public void checkComplete() throws Throwable {\n        Validators.nonNull(host);\n        Validators.isType(host, ShellStore.class);\n        host.checkComplete();\n    }\n\n    private List<DataStoreEntryRef<PodmanContainerStore>> listContainers(ShellControl sc) throws Exception {\n        var view = new PodmanCommandView(sc);\n        var l = view.container().listContainersAndStates();\n        return l.stream()\n                .map(s -> {\n                    boolean running = s.getStatus().startsWith(\"running\")\n                            || s.getStatus().startsWith(\"up\")\n                            || s.getStatus().startsWith(\"Up\");\n                    var c = PodmanContainerStore.builder()\n                            .cmd(getSelfEntry().ref())\n                            .containerName(s.getName())\n                            .build();\n                    var entry = DataStoreEntry.createNew(s.getName(), c);\n                    entry.setStorePersistentState(ContainerStoreState.builder()\n                            .containerState(s.getStatus())\n                            .imageName(s.getImage())\n                            .running(running)\n                            .build());\n                    return entry.<PodmanContainerStore>ref();\n                })\n                .toList();\n    }\n\n    private void updateState(ShellControl host) throws Exception {\n        var out = new PodmanCommandView(host).version();\n\n        var namePattern = Pattern.compile(\"Server:\\\\s+(.+)\");\n        var nameMatcher = namePattern.matcher(out);\n        var name = nameMatcher.find() ? nameMatcher.group(1) : null;\n\n        var versionPattern = Pattern.compile(\"Version:\\\\s+(.+)\");\n        var versionMatcher = versionPattern.matcher(out);\n        var version = versionMatcher.find() ? versionMatcher.group(1) : null;\n\n        setState(getState().toBuilder()\n                .running(true)\n                .serverName(name)\n                .version(version)\n                .build());\n    }\n\n    @Override\n    public List<? extends DataStoreEntryRef<? extends FixedChildStore>> listChildren() throws Exception {\n        var sc = getHost().getStore().getOrStartSession();\n        var view = new PodmanCommandView(sc);\n        CommandSupport.isSupported(() -> view.isSupported(), \"Podman CLI\", host.get());\n        var running = view.isDaemonRunning();\n        if (!running) {\n            setState(getState().toBuilder().running(false).build());\n            throw ErrorEventFactory.expected(new IllegalStateException(\"Podman daemon is not running\"));\n        }\n\n        updateState(sc);\n        return listContainers(sc);\n    }\n\n    @Value\n    @EqualsAndHashCode(callSuper = true)\n    @SuperBuilder(toBuilder = true)\n    @Jacksonized\n    public static class State extends DataStoreState {\n        String serverName;\n        String version;\n        boolean running;\n        boolean showNonRunning;\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/podman/PodmanCmdStoreProvider.java",
    "content": "package io.xpipe.ext.system.podman;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.DataStoreProvider;\nimport io.xpipe.app.ext.DataStoreUsageCategory;\nimport io.xpipe.app.hub.comp.StoreEntryComp;\nimport io.xpipe.app.hub.comp.StoreEntryWrapper;\nimport io.xpipe.app.hub.comp.StoreSection;\nimport io.xpipe.app.hub.comp.StoreToggleComp;\nimport io.xpipe.app.hub.comp.SystemStateComp;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.util.DocumentationLink;\n\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\n\npublic class PodmanCmdStoreProvider implements DataStoreProvider {\n\n    @Override\n    public DocumentationLink getHelpLink() {\n        return DocumentationLink.PODMAN;\n    }\n\n    @Override\n    public StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) {\n        var nonRunning = StoreToggleComp.<PodmanCmdStore>childrenToggle(\n                true, sec, s -> s.getState().isShowNonRunning(), (s, aBoolean) -> {\n                    s.setState(s.getState().toBuilder().showNonRunning(aBoolean).build());\n                });\n        return StoreEntryComp.create(sec, nonRunning, preferLarge);\n    }\n\n    public BaseRegionBuilder<?, ?> stateDisplay(StoreEntryWrapper w) {\n        return new SystemStateComp(BindingsHelper.map(w.getPersistentState(), o -> {\n            var state = (PodmanCmdStore.State) o;\n            if (state.isRunning()) {\n                return SystemStateComp.State.SUCCESS;\n            }\n\n            return SystemStateComp.State.FAILURE;\n        }));\n    }\n\n    @Override\n    public DataStoreUsageCategory getUsageCategory() {\n        return DataStoreUsageCategory.GROUP;\n    }\n\n    @Override\n    public DataStoreEntry getDisplayParent(DataStoreEntry store) {\n        PodmanCmdStore s = store.getStore().asNeeded();\n        return s.getHost().get();\n    }\n\n    @Override\n    public ObservableValue<String> informationString(StoreSection section) {\n        return BindingsHelper.map(section.getWrapper().getPersistentState(), o -> {\n            var state = (PodmanCmdStore.State) o;\n            if (!state.isRunning()) {\n                return \"Connection failed\";\n            }\n\n            return (state.getServerName() != null ? state.getServerName() : \"Podman\") + \" v\" + state.getVersion();\n        });\n    }\n\n    @Override\n    public String getDisplayIconFileName(DataStore store) {\n        return \"system:podman_icon.svg\";\n    }\n\n    @Override\n    public String getId() {\n        return \"podmanCmd\";\n    }\n\n    @Override\n    public List<Class<?>> getStoreClasses() {\n        return List.of(PodmanCmdStore.class);\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/podman/PodmanCommandView.java",
    "content": "package io.xpipe.ext.system.podman;\n\nimport io.xpipe.app.issue.ErrorEventFactory;\nimport io.xpipe.app.process.*;\n\nimport lombok.NonNull;\nimport lombok.Value;\n\nimport java.util.List;\nimport java.util.function.Consumer;\n\npublic class PodmanCommandView extends CommandViewBase {\n\n    public PodmanCommandView(ShellControl shellControl) {\n        super(shellControl);\n    }\n\n    private static String formatErrorMessage(String s) {\n        return s;\n    }\n\n    private static <T extends Throwable> T convertException(T s) {\n        return ErrorEventFactory.expectedIfContains(\n                s,\n                \"Error: unable to connect to Podman.\",\n                \"no connection could be made because the target machine actively refused it.\",\n                \"unable to connect to Podman socket\",\n                \"no such container\",\n                \"OCI runtime attempted to invoke a command that was not found\");\n    }\n\n    @Override\n    public PodmanCommandView start() throws Exception {\n        shellControl.start();\n        return this;\n    }\n\n    @Override\n    protected CommandControl build(Consumer<CommandBuilder> builder) {\n        var cmd = CommandBuilder.of().add(\"podman\");\n        builder.accept(cmd);\n        return shellControl\n                .command(cmd)\n                .withErrorFormatter(PodmanCommandView::formatErrorMessage)\n                .withExceptionConverter(PodmanCommandView::convertException);\n    }\n\n    public boolean isSupported() throws Exception {\n        return shellControl\n                .command(\"podman --help\")\n                .withErrorFormatter(PodmanCommandView::formatErrorMessage)\n                .withExceptionConverter(PodmanCommandView::convertException)\n                .executeAndCheck();\n    }\n\n    public String version() throws Exception {\n        return build(commandBuilder -> commandBuilder.add(\"version\")).readStdoutOrThrow();\n    }\n\n    public boolean isDaemonRunning() throws Exception {\n        return build(commandBuilder -> commandBuilder.add(\"version\")).executeAndCheck();\n    }\n\n    public Container container() {\n        return new Container();\n    }\n\n    public class Container extends CommandView {\n\n        public String queryState(String container) throws Exception {\n            return build(commandBuilder -> commandBuilder.add(\n                            \"ls\", \"-a\", \"-f\", \"name=\\\"^\" + container + \"$\\\"\", \"--format=\\\"{{.Status}}\\\"\"))\n                    .readStdoutOrThrow();\n        }\n\n        @Override\n        protected CommandControl build(Consumer<CommandBuilder> builder) {\n            return PodmanCommandView.this.build((b) -> {\n                b.add(\"container\");\n                builder.accept(b);\n            });\n        }\n\n        @Override\n        protected ShellControl getShellControl() {\n            return PodmanCommandView.this.getShellControl();\n        }\n\n        @Override\n        public Container start() throws Exception {\n            shellControl.start();\n            return this;\n        }\n\n        public List<ContainerEntry> listContainersAndStates() throws Exception {\n            if (!PodmanCommandView.this.isDaemonRunning()) {\n                throw new IllegalStateException(\"Podman daemon is not running\");\n            }\n\n            try (var c = build(commandBuilder ->\n                            commandBuilder.add(\"ls -a --format=\\\"{{.Names}};{{.Image}};{{.Status}}\\\"\"))\n                    .start()) {\n                var output = c.readStdoutOrThrow();\n                return output.lines()\n                        .filter(s -> s.split(\";\").length == 3)\n                        .map(s -> new ContainerEntry(s.split(\";\")[0], s.split(\";\")[1], s.split(\";\")[2]))\n                        .toList();\n            }\n        }\n\n        public ShellControl exec(String container) {\n            var sub = shellControl.subShell();\n            sub.setDumbOpen(createOpenFunction(container, false));\n            sub.setTerminalOpen(createOpenFunction(container, true));\n            return sub.withExceptionConverter(PodmanCommandView::convertException);\n        }\n\n        private ShellOpenFunction createOpenFunction(String containerName, boolean terminal) {\n            return new ShellOpenFunction() {\n                @Override\n                public CommandBuilder prepareWithoutInitCommand() {\n                    return execCommand(terminal)\n                            .addQuoted(containerName)\n                            .add(ShellDialects.SH.getLaunchCommand().loginCommand());\n                }\n\n                @Override\n                public CommandBuilder prepareWithInitCommand(@NonNull String command) {\n                    return execCommand(terminal).addQuoted(containerName).add(command);\n                }\n            };\n        }\n\n        public CommandBuilder execCommand(boolean terminal) {\n            return CommandBuilder.of().add(\"podman\", \"container\", \"exec\", terminal ? \"-it\" : \"-i\");\n        }\n\n        public void start(String container) throws Exception {\n            build(commandBuilder -> commandBuilder.add(\"start\").addQuoted(container))\n                    .execute();\n        }\n\n        public void stop(String container) throws Exception {\n            build(commandBuilder -> commandBuilder.add(\"stop\").addQuoted(container))\n                    .execute();\n        }\n\n        public String port(String container) throws Exception {\n            return build(commandBuilder -> commandBuilder.add(\"port\").addQuoted(container))\n                    .readStdoutOrThrow();\n        }\n\n        public String inspect(String container) throws Exception {\n            return build(commandBuilder -> commandBuilder.add(\"inspect\").addQuoted(container))\n                    .readStdoutOrThrow();\n        }\n\n        public CommandControl attach(String container) {\n            return build(commandBuilder -> commandBuilder.add(\"attach\").addQuoted(container));\n        }\n\n        public CommandControl logs(String container) {\n            return build(commandBuilder -> commandBuilder.add(\"logs\").add(\"-f\").addQuoted(container));\n        }\n\n        @Value\n        public static class ContainerEntry {\n            String name;\n            String image;\n            String status;\n        }\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/podman/PodmanContainerActionProviderMenu.java",
    "content": "package io.xpipe.ext.system.podman;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.HubBranchProvider;\nimport io.xpipe.app.hub.action.HubMenuItemProvider;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.ext.base.store.StoreRestartActionProvider;\nimport io.xpipe.ext.base.store.StoreStartActionProvider;\nimport io.xpipe.ext.base.store.StoreStopActionProvider;\n\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\n\npublic class PodmanContainerActionProviderMenu implements HubBranchProvider<PodmanContainerStore> {\n\n    @Override\n    public boolean isMajor() {\n        return true;\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<PodmanContainerStore> store) {\n        return AppI18n.observable(\"containerActions\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<PodmanContainerStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2p-package-variant-closed\");\n    }\n\n    @Override\n    public Class<PodmanContainerStore> getApplicableClass() {\n        return PodmanContainerStore.class;\n    }\n\n    @Override\n    public List<HubMenuItemProvider<?>> getChildren(DataStoreEntryRef<PodmanContainerStore> store) {\n        return List.of(\n                new StoreStartActionProvider(),\n                new StoreStopActionProvider(),\n                new StoreRestartActionProvider(),\n                new PodmanContainerInspectActionProvider(),\n                new PodmanContainerLogsActionProvider(),\n                new PodmanContainerAttachActionProvider());\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/podman/PodmanContainerAttachActionProvider.java",
    "content": "package io.xpipe.ext.system.podman;\n\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.terminal.TerminalLaunch;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class PodmanContainerAttachActionProvider implements HubLeafProvider<PodmanContainerStore> {\n\n    @Override\n    public Action createAction(DataStoreEntryRef<PodmanContainerStore> ref) {\n        return Action.builder().ref(ref).build();\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<PodmanContainerStore> store) {\n        return AppI18n.observable(\"attachContainer\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<PodmanContainerStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2a-attachment\");\n    }\n\n    @Override\n    public Class<PodmanContainerStore> getApplicableClass() {\n        return PodmanContainerStore.class;\n    }\n\n    @Override\n    public String getId() {\n        return \"attachPodmanContainer\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<PodmanContainerStore> {\n\n        @Override\n        public void executeImpl() throws Exception {\n            var d = ref.getStore();\n            var view = d.commandView(d.getCmd().getStore().getHost().getStore().getOrStartSession());\n            TerminalLaunch.builder()\n                    .entry(ref.get())\n                    .title(\"Attach\")\n                    .command(view.attach(d.getContainerName()))\n                    .launch();\n        }\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/podman/PodmanContainerInspectActionProvider.java",
    "content": "package io.xpipe.ext.system.podman;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.FileOpener;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class PodmanContainerInspectActionProvider implements HubLeafProvider<PodmanContainerStore> {\n\n    @Override\n    public AbstractAction createAction(DataStoreEntryRef<PodmanContainerStore> ref) {\n        return Action.builder().ref(ref).build();\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<PodmanContainerStore> store) {\n        return AppI18n.observable(\"inspectContainer\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<PodmanContainerStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2i-information-outline\");\n    }\n\n    @Override\n    public Class<PodmanContainerStore> getApplicableClass() {\n        return PodmanContainerStore.class;\n    }\n\n    @Override\n    public String getId() {\n        return \"inspectPodmanContainer\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<PodmanContainerStore> {\n\n        @Override\n        public void executeImpl() throws Exception {\n            var d = ref.getStore();\n            var view = d.commandView(d.getCmd().getStore().getHost().getStore().getOrStartSession());\n            var output = view.inspect(d.getContainerName());\n            FileOpener.openReadOnlyString(output);\n        }\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/podman/PodmanContainerLogsActionProvider.java",
    "content": "package io.xpipe.ext.system.podman;\n\nimport io.xpipe.app.action.AbstractAction;\nimport io.xpipe.app.core.AppI18n;\nimport io.xpipe.app.hub.action.HubLeafProvider;\nimport io.xpipe.app.hub.action.StoreAction;\nimport io.xpipe.app.platform.LabelGraphic;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.terminal.TerminalLaunch;\n\nimport javafx.beans.value.ObservableValue;\n\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\npublic class PodmanContainerLogsActionProvider implements HubLeafProvider<PodmanContainerStore> {\n\n    @Override\n    public AbstractAction createAction(DataStoreEntryRef<PodmanContainerStore> ref) {\n        return Action.builder().ref(ref).build();\n    }\n\n    @Override\n    public ObservableValue<String> getName(DataStoreEntryRef<PodmanContainerStore> store) {\n        return AppI18n.observable(\"containerLogs\");\n    }\n\n    @Override\n    public LabelGraphic getIcon(DataStoreEntryRef<PodmanContainerStore> store) {\n        return new LabelGraphic.IconGraphic(\"mdi2v-view-list-outline\");\n    }\n\n    @Override\n    public Class<PodmanContainerStore> getApplicableClass() {\n        return PodmanContainerStore.class;\n    }\n\n    @Override\n    public String getId() {\n        return \"openPodmanContainerLogs\";\n    }\n\n    @Jacksonized\n    @SuperBuilder\n    public static class Action extends StoreAction<PodmanContainerStore> {\n\n        @Override\n        public void executeImpl() throws Exception {\n            var d = ref.getStore();\n            var view = d.commandView(d.getCmd().getStore().getHost().getStore().getOrStartSession());\n            TerminalLaunch.builder()\n                    .pauseOnExit(true)\n                    .entry(ref.get())\n                    .title(\"Logs\")\n                    .command(view.logs(d.getContainerName()))\n                    .launch();\n        }\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/podman/PodmanContainerStore.java",
    "content": "package io.xpipe.ext.system.podman;\n\nimport io.xpipe.app.ext.*;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.storage.DataStoreEntryRef;\nimport io.xpipe.app.util.LicenseRequiredException;\nimport io.xpipe.app.util.Validators;\nimport io.xpipe.ext.base.service.AbstractServiceStore;\nimport io.xpipe.ext.base.service.FixedServiceCreatorStore;\nimport io.xpipe.ext.base.service.MappedServiceStore;\nimport io.xpipe.ext.base.store.StartableStore;\nimport io.xpipe.ext.base.store.StoppableStore;\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport lombok.Value;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.jackson.Jacksonized;\n\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.OptionalInt;\nimport java.util.regex.Pattern;\n\n@JsonTypeName(\"podman\")\n@SuperBuilder\n@Jacksonized\n@Value\npublic class PodmanContainerStore\n        implements StartableStore,\n                StoppableStore,\n                ShellStore,\n                InternalCacheDataStore,\n                FixedChildStore,\n                StatefulDataStore<ContainerStoreState>,\n                FixedServiceCreatorStore,\n                SelfReferentialStore,\n                ContainerImageStore,\n                NameableStore {\n\n    DataStoreEntryRef<PodmanCmdStore> cmd;\n    String containerName;\n\n    @Override\n    public String getName() {\n        return containerName;\n    }\n\n    @Override\n    public String getImageName() {\n        return getState().getImageName();\n    }\n\n    public PodmanCommandView.Container commandView(ShellControl parent) {\n        return new PodmanCommandView(parent).container();\n    }\n\n    @Override\n    public void start() throws Exception {\n        var sc = getCmd().getStore().getHost().getStore().getOrStartSession();\n        var view = commandView(sc);\n        view.start(containerName);\n        refreshContainerState(sc);\n    }\n\n    @Override\n    public void stop() throws Exception {\n        var sc = getCmd().getStore().getHost().getStore().getOrStartSession();\n        var view = commandView(sc);\n        view.stop(containerName);\n        refreshContainerState(sc);\n    }\n\n    @Override\n    public List<? extends DataStoreEntryRef<? extends AbstractServiceStore>> createFixedServices() throws Exception {\n        return findServices().stream()\n                .map(s -> DataStoreEntry.createNew(\"Service\", s).<MappedServiceStore>ref())\n                .toList();\n    }\n\n    private List<MappedServiceStore> findServices() throws Exception {\n        var entry = getSelfEntry();\n        var view = commandView(getCmd().getStore().getHost().getStore().getOrStartSession());\n        var out = view.port(containerName);\n        return out.lines()\n                .map(l -> {\n                    var matcher = Pattern.compile(\"(\\\\d+)/\\\\w+\\\\s*->\\\\s*[^:]+?:(\\\\d+)\")\n                            .matcher(l);\n                    if (!matcher.matches()) {\n                        return (MappedServiceStore) null;\n                    }\n\n                    var containerPort = Integer.parseInt(matcher.group(1));\n                    var remotePort = Integer.parseInt(matcher.group(2));\n                    return MappedServiceStore.builder()\n                            .host(getCmd().getStore().getHost().asNeeded())\n                            .displayParent(entry.ref())\n                            .containerPort(containerPort)\n                            .remotePort(remotePort)\n                            .build();\n                })\n                .filter(dockerServiceStore -> dockerServiceStore != null)\n                .toList();\n    }\n\n    @Override\n    public Class<ContainerStoreState> getStateClass() {\n        return ContainerStoreState.class;\n    }\n\n    @Override\n    public OptionalInt getFixedId() {\n        return OptionalInt.of(Objects.hash(containerName));\n    }\n\n    @Override\n    public void checkComplete() throws Throwable {\n        Validators.nonNull(cmd);\n        Validators.isType(cmd, PodmanCmdStore.class);\n        cmd.checkComplete();\n        Validators.nonNull(containerName);\n    }\n\n    @Override\n    public ShellControlFunction shellFunction() {\n        return new ShellControlParentStoreFunction() {\n\n            @Override\n            public ShellControl control(ShellControl parent) throws Exception {\n                refreshContainerState(getCmd().getStore().getHost().getStore().getOrStartSession());\n                var pc = new PodmanCommandView(parent).container().exec(containerName);\n                pc.withSourceStore(PodmanContainerStore.this);\n                pc.withShellStateInit(PodmanContainerStore.this);\n                pc.onStartupFail(throwable -> {\n                    if (throwable instanceof LicenseRequiredException) {\n                        return;\n                    }\n\n                    var hasShell = throwable.getMessage() == null\n                            || !throwable.getMessage().contains(\"OCI runtime exec failed\");\n                    if (!hasShell) {\n                        var stateBuilder = getState().toBuilder();\n                        stateBuilder.shellMissing(true);\n                        setState(stateBuilder.build());\n                    }\n                });\n                return pc;\n            }\n\n            @Override\n            public ShellStore getParentStore() {\n                return getCmd().getStore().getHost().getStore();\n            }\n        };\n    }\n\n    private void refreshContainerState(ShellControl sc) throws Exception {\n        var state = getState();\n        var view = new PodmanCommandView(sc).container();\n        var displayState = view.queryState(containerName);\n        var running = displayState.startsWith(\"Up\");\n        var newState =\n                state.toBuilder().containerState(displayState).running(running).build();\n        setState(newState);\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/podman/PodmanContainerStoreProvider.java",
    "content": "package io.xpipe.ext.system.podman;\n\nimport io.xpipe.app.comp.BaseRegionBuilder;\nimport io.xpipe.app.ext.ContainerStoreState;\nimport io.xpipe.app.ext.DataStore;\nimport io.xpipe.app.ext.GuiDialog;\nimport io.xpipe.app.ext.ShellStore;\nimport io.xpipe.app.hub.comp.*;\nimport io.xpipe.app.platform.BindingsHelper;\nimport io.xpipe.app.platform.OptionsBuilder;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\nimport io.xpipe.app.util.*;\nimport io.xpipe.ext.base.service.FixedServiceGroupStore;\nimport io.xpipe.ext.base.store.ShellStoreProvider;\n\nimport javafx.beans.property.Property;\nimport javafx.beans.property.ReadOnlyObjectWrapper;\nimport javafx.beans.value.ObservableValue;\n\nimport java.util.List;\n\npublic class PodmanContainerStoreProvider implements ShellStoreProvider {\n\n    @Override\n    public DocumentationLink getHelpLink() {\n        return DocumentationLink.PODMAN;\n    }\n\n    @Override\n    public boolean shouldShow(StoreEntryWrapper w) {\n        PodmanContainerStore s = w.getEntry().getStore().asNeeded();\n        var state = s.getState();\n        return Boolean.TRUE.equals(state.getRunning())\n                || s.getCmd().getStore().getState().isShowNonRunning();\n    }\n\n    public void onParentRefresh(DataStoreEntry entry) {\n        var services = FixedServiceGroupStore.builder().parent(entry.ref()).build();\n        var servicesEntry = DataStorage.get().getStoreEntryIfPresent(services, false);\n        if (servicesEntry.isPresent()) {\n            DataStorage.get().refreshChildren(servicesEntry.get());\n        }\n    }\n\n    @Override\n    public boolean shouldShowScan() {\n        return false;\n    }\n\n    @Override\n    public DataStoreEntry getDisplayParent(DataStoreEntry store) {\n        PodmanContainerStore s = store.getStore().asNeeded();\n        return s.getCmd().get();\n    }\n\n    @Override\n    public GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {\n        PodmanContainerStore st = (PodmanContainerStore) store.getValue();\n\n        return new OptionsBuilder()\n                .name(\"host\")\n                .description(\"podmanHostDescription\")\n                .addComp(new StoreChoiceComp<>(\n                        entry,\n                        new ReadOnlyObjectWrapper<>(\n                                st.getCmd() != null ? st.getCmd().getStore().getHost() : null),\n                        ShellStore.class,\n                        null,\n                        StoreViewState.get().getAllConnectionsCategory()))\n                .disable()\n                .name(\"container\")\n                .description(\"podmanContainerDescription\")\n                .addStaticString(st.getContainerName())\n                .buildDialog();\n    }\n\n    @Override\n    public String getDisplayIconFileName(DataStore store) {\n        return \"system:podman_icon.svg\";\n    }\n\n    @Override\n    public String getId() {\n        return \"podman\";\n    }\n\n    @Override\n    public List<Class<?>> getStoreClasses() {\n        return List.of(PodmanContainerStore.class);\n    }\n\n    public BaseRegionBuilder<?, ?> stateDisplay(StoreEntryWrapper w) {\n        return new OsLogoComp(w, BindingsHelper.map(w.getPersistentState(), o -> {\n            var state = (ContainerStoreState) o;\n            var cs = state.getContainerState();\n            if (cs != null && cs.toLowerCase().contains(\"exited\")) {\n                return SystemStateComp.State.FAILURE;\n            } else if (cs != null && cs.toLowerCase().contains(\"up\")) {\n                return SystemStateComp.State.SUCCESS;\n            } else {\n                return SystemStateComp.State.OTHER;\n            }\n        }));\n    }\n\n    @Override\n    public ObservableValue<String> informationString(StoreSection section) {\n        var c = (ContainerStoreState) section.getWrapper().getPersistentState().getValue();\n        var missing = c.getShellMissing() != null && c.getShellMissing() ? \"No shell available\" : null;\n        return StoreStateFormat.shellStore(\n                section, (ContainerStoreState s) -> new String[] {missing, s.getContainerState()}, null);\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/io/xpipe/ext/system/podman/PodmanScanProvider.java",
    "content": "package io.xpipe.ext.system.podman;\n\nimport io.xpipe.app.ext.ScanProvider;\nimport io.xpipe.app.process.ShellControl;\nimport io.xpipe.app.storage.DataStorage;\nimport io.xpipe.app.storage.DataStoreEntry;\n\npublic class PodmanScanProvider extends ScanProvider {\n\n    @Override\n    public ScanOpportunity create(DataStoreEntry entry, ShellControl sc) throws Exception {\n        var view = new PodmanCommandView(sc);\n        return new ScanOpportunity(\"podmanContainers\", !view.isSupported());\n    }\n\n    @Override\n    public void scan(DataStoreEntry entry, ShellControl sc) throws Throwable {\n        var view = new PodmanCommandView(sc);\n        var e = DataStorage.get()\n                .addStoreIfNotPresent(\n                        entry,\n                        \"Podman containers\",\n                        PodmanCmdStore.builder().host(entry.ref()).build());\n        if (view.isDaemonRunning()) {\n            DataStorage.get().refreshChildren(e);\n        }\n    }\n}\n"
  },
  {
    "path": "ext/system/src/main/java/module-info.java",
    "content": "import io.xpipe.app.action.ActionProvider;\nimport io.xpipe.app.ext.DataStoreProvider;\nimport io.xpipe.app.ext.ScanProvider;\nimport io.xpipe.ext.system.incus.*;\nimport io.xpipe.ext.system.lxd.*;\nimport io.xpipe.ext.system.podman.*;\n\nopen module io.xpipe.ext.system {\n    requires com.fasterxml.jackson.databind;\n    requires com.fasterxml.jackson.annotation;\n    requires java.net.http;\n    requires static lombok;\n    requires static javafx.controls;\n    requires static io.xpipe.app;\n    requires io.xpipe.core;\n    requires io.xpipe.ext.base;\n\n    provides ScanProvider with\n            LxdScanProvider,\n            IncusScanProvider,\n            PodmanScanProvider;\n    provides DataStoreProvider with\n            LxdCmdStoreProvider,\n            LxdContainerStoreProvider,\n            IncusInstallStoreProvider,\n            IncusContainerStoreProvider,\n            PodmanContainerStoreProvider,\n            PodmanCmdStoreProvider;\n    provides ActionProvider with\n            IncusContainerActionProviderMenu,\n            IncusContainerConsoleActionProvider,\n            IncusContainerEditConfigActionProvider,\n            IncusContainerEditRunConfigActionProvider,\n            LxdContainerConsoleActionProvider,\n            LxdContainerEditConfigActionProvider,\n            LxdContainerEditRunConfigActionProvider,\n            LxdContainerActionProviderMenu,\n            PodmanContainerActionProviderMenu,\n            PodmanContainerInspectActionProvider,\n            PodmanContainerAttachActionProvider,\n            PodmanContainerLogsActionProvider;\n}\n"
  },
  {
    "path": "ext/system/src/main/resources/io/xpipe/ext/system/resources/extension.properties",
    "content": "name=System"
  },
  {
    "path": "ext/uacc/build.gradle",
    "content": "plugins { id 'java'\n}\napply from: \"$rootDir/gradle/gradle_scripts/java.gradle\"\napply from: \"$rootDir/gradle/gradle_scripts/extension.gradle\"\n"
  },
  {
    "path": "ext/uacc/src/main/java/module-info.java",
    "content": "module io.xpipe.ext.uacc {}\n"
  },
  {
    "path": "get-xpipe.ps1",
    "content": "<#\n    .SYNOPSIS\n    Downloads and installs XPipe on the local machine.\n\n    .DESCRIPTION\n    Retrieves the XPipe msi for the latest or a specified version, and\n    downloads and installs the application to the local machine.\n#>\n[CmdletBinding(DefaultParameterSetName = 'Default')]\nparam(\n    # Specifies a target version of XPipe to install. By default, the latest\n    # stable version is installed.\n    [Parameter(Mandatory = $false)]\n    [string]\n    $XPipeVersion = $xpipeVersion,\n\n    # If set, will download releases from the staging repository instead.\n    [Parameter(Mandatory = $false)]\n    [switch]\n    $UseStageDownloads = $useStageDownloads\n)\n\n#region Functions\n\nfunction Get-Downloader {\n    <#\n    .SYNOPSIS\n    Gets a System.Net.WebClient that respects relevant proxies to be used for\n    downloading data.\n\n    .DESCRIPTION\n    Retrieves a WebClient object that is pre-configured according to specified\n    environment variables for any proxy and authentication for the proxy.\n    Proxy information may be omitted if the target URL is considered to be\n    bypassed by the proxy (originates from the local network.)\n\n    .PARAMETER Url\n    Target URL that the WebClient will be querying. This URL is not queried by\n    the function, it is only a reference to determine if a proxy is needed.\n\n    .EXAMPLE\n    Get-Downloader -Url $fileUrl\n\n    Verifies whether any proxy configuration is needed, and/or whether $fileUrl\n    is a URL that would need to bypass the proxy, and then outputs the\n    already-configured WebClient object.\n    #>\n    [CmdletBinding()]\n    param(\n        [Parameter(Mandatory = $false)]\n        [string]\n        $Url\n    )\n\n    $downloader = New-Object System.Net.WebClient\n\n    $defaultCreds = [System.Net.CredentialCache]::DefaultCredentials\n    if ($defaultCreds) {\n        $downloader.Credentials = $defaultCreds\n    }\n\n    $downloader\n}\n\nfunction Request-File {\n    <#\n    .SYNOPSIS\n    Downloads a file from a given URL.\n\n    .DESCRIPTION\n    Downloads a target file from a URL to the specified local path.\n    Any existing proxy that may be in use will be utilised.\n\n    .PARAMETER Url\n    URL of the file to download from the remote host.\n\n    .PARAMETER File\n    Local path for the file to be downloaded to.\n\n    Downloads the file to the path specified in $targetFile.\n    #>\n    [CmdletBinding()]\n    param(\n        [Parameter(Mandatory = $false)]\n        [string]\n        $Url,\n\n        [Parameter(Mandatory = $false)]\n        [string]\n        $File\n    )\n\n    Write-Host \"Downloading $url to $file\"\n    (Get-Downloader $url).DownloadFile($url, $file)\n}\n\n\nfunction Uninstall {\n    [CmdletBinding()]\n    param()\n\n    # Quick heuristic to see whether is can be possibly installed\n    if (-not ((Test-Path \"$env:LOCALAPPDATA\\$ProductName\" -PathType Container) -or (Test-Path \"$env:ProgramFiles\\$ProductName\" -PathType Container))) {\n        return\n    }\n\n    Write-Host \"Looking for previous $ProductName installations ...\"\n\n    $cim = Get-CimInstance Win32_Product | Where {$_.Name -eq \"$ProductName\" } | Select-Object -First 1\n    if ($cim) {\n        $message = @(\n            \"Uninstalling existing $ProductName $($cim.Version) installation ...\"\n        ) -join [Environment]::NewLine\n        Write-Host $message\n        $cimResult = Invoke-CimMethod -InputObject $cim -Name Uninstall\n        if ($cimResult.ReturnValue) {\n            Write-Host \"Uninstallation failed: Code $($cimResult.ReturnValue)\"\n            exit\n        }\n    }\n}\n\n#endregion Functions\n\n#region Pre-check\n\nif ($UseStageDownloads) {\n    $XPipeRepoUrl = \"https://github.com/xpipe-io/xpipe-ptb\"\n    $ProductName = \"XPipe PTB\"\n} else {\n    $XPipeRepoUrl = \"https://github.com/xpipe-io/xpipe\"\n    $ProductName = \"XPipe\"\n}\n\nif ($XPipeVersion) {\n    $XPipeDownloadUrl = \"$XPipeRepoUrl/releases/download/$XPipeVersion\"\n} else {\n    $XPipeDownloadUrl = \"$XPipeRepoUrl/releases/latest/download\"\n}\n\nUninstall\n\n#endregion Pre-check\n\n#region Setup\n\n$RawArch = [System.Runtime.InteropServices.RuntimeInformation,mscorlib]::OSArchitecture.ToString().ToLower();\n$Arch = If ($RawArch -eq \"x64\") {\"x86_64\"} Else {\"arm64\"}\n$XPipeDownloadUrl = \"$XPipeDownloadUrl/xpipe-installer-windows-$($Arch).msi\"\n\nif (-not $env:TEMP) {\n    $env:TEMP = Join-Path $env:SystemDrive -ChildPath 'temp'\n}\n\n$tempDir = $env:TEMP\n\nif (-not (Test-Path $tempDir -PathType Container)) {\n    $null = New-Item -Path $tempDir -ItemType Directory\n}\n\n#endregion Setup\n\n#region Download\n\n$file = Join-Path $tempDir \"xpipe-installer.msi\"\nWrite-Host \"Getting $ProductName from $XPipeRepoUrl.\"\nRequest-File -Url $XPipeDownloadUrl -File $file\n\n#endregion Download\n\n#region Install XPipe\n\nWrite-Host \"Installing $ProductName ...\"\n\n# Wait for completion\n# The file variable can contain spaces, so we have to accommodate for that\nStart-Process -FilePath \"msiexec\" -Wait -ArgumentList \"/i\", \"`\"$file`\"\", \"/quiet\"\n\n# Update current process PATH environment variable\n$env:Path=(\n    [System.Environment]::GetEnvironmentVariable(\"Path\", \"Machine\"),\n    [System.Environment]::GetEnvironmentVariable(\"Path\", \"User\")\n) -match '.' -join ';'\n\nWrite-Host\nWrite-Host \"$ProductName has been successfully installed. You should be able to find it in your applications.\"\nWrite-Host\n\n# Use absolute path as we can't assume that the user has selected to put XPipe into the Path\n& \"$env:LOCALAPPDATA\\$ProductName\\bin\\xpipe.exe\" open\n\n#endregion Install XPipe\n"
  },
  {
    "path": "get-xpipe.sh",
    "content": "#!/usr/bin/env bash\n\nrelease_url() {\n  local repo=\"$1\"\n  local version=\"$2\"\n  if [[ -z \"$version\" ]] ; then\n    echo \"$repo/releases/latest/download\"\n  else\n    echo \"$repo/releases/download/$version\"\n  fi\n}\n\nget_file_ending() {\n  local uname_str=\"$(uname -s)\"\n  case \"$uname_str\" in\n  Linux)\n    if [ -f \"/etc/debian_version\" ]; then\n      echo \"deb\"\n    else\n      echo \"rpm\"\n    fi\n    ;;\n  Darwin)\n    echo \"pkg\"\n    ;;\n  *)\n    exit 1\n    ;;\n  esac\n}\n\ndownload_release_from_repo() {\n  local os_info=\"$1\"\n  local tmpdir=\"$2\"\n  local repo=\"$3\"\n  local version=\"$4\"\n  local arch=\"$5\"\n\n  local ending=$(get_file_ending)\n  local release_url=$(release_url \"$repo\" \"$version\")\n\n  local filename=\"xpipe-installer-$os_info-$arch.$ending\"\n  local download_file=\"$tmpdir/$filename\"\n  local archive_url=\"$release_url/$filename\"\n\n  info \"Downloading file $archive_url\"\n  curl --progress-bar --show-error --location --fail \"$archive_url\" --output \"$download_file\" --write-out \"$download_file\"\n}\n\ninfo() {\n  local action=\"$1\"\n  local details=\"$2\"\n  command printf '\\033[1;32m%12s\\033[0m %s\\n' \"$action\" \"$details\" 1>&2\n}\n\nerror() {\n  command printf '\\033[1;31mError\\033[0m: %s\\n\\n' \"$1\" 1>&2\n}\n\nwarning() {\n  command printf '\\033[1;33mWarning\\033[0m: %s\\n\\n' \"$1\" 1>&2\n}\n\nrequest() {\n  command printf '\\033[1m%s\\033[0m\\n' \"$1\" 1>&2\n}\n\neprintf() {\n  command printf '%s\\n' \"$1\" 1>&2\n}\n\nbold() {\n  command printf '\\033[1m%s\\033[0m' \"$1\"\n}\n\n# returns the os name to be used in the packaged release\nparse_os_name() {\n  local uname_str=\"$1\"\n  local arch=\"$(uname -m)\"\n\n  case \"$uname_str\" in\n  Linux)\n    echo \"linux\"\n    ;;\n  FreeBSD)\n    echo \"linux\"\n    ;;\n  Darwin)\n    echo \"macos\"\n    ;;\n  *)\n    return 1\n    ;;\n  esac\n  return 0\n}\n\ninstall() {\n  local uname_str=\"$(uname -s)\"\n  local file=\"$1\"\n\n  case \"$uname_str\" in\n  Linux)\n    if [ -f \"/etc/debian_version\" ]; then\n      info \"Installing file $file with apt\"\n      sudo apt update\n      DEBIAN_FRONTEND=noninteractive sudo apt install \"$file\"\n    elif [ -x \"$(command -v zypper)\" ]; then\n      info \"Installing file $file with zypper\"\n      sudo zypper --no-gpg-checks --gpg-auto-import-keys install \"$file\"\n    elif [ -x \"$(command -v dnf)\" ]; then\n      info \"Installing file $file with dnf\"\n      sudo rpm --import https://xpipe.io/signatures/crschnick.asc\n      sudo dnf install --refresh \"$file\"\n    elif [ -x \"$(command -v yum)\" ]; then\n      info \"Installing file $file with yum\"\n      sudo rpm --import https://xpipe.io/signatures/crschnick.asc\n      sudo yum clean expire-cache\n      sudo yum install \"$file\"\n    else\n      info \"Installing file $file with rpm\"\n      sudo rpm --import https://xpipe.io/signatures/crschnick.asc\n      sudo rpm -U -v --force \"$file\"\n    fi\n    ;;\n  Darwin)\n    sudo installer -verboseR -pkg \"$file\" -target /\n    ;;\n  *)\n    exit 1\n    ;;\n  esac\n}\n\nlaunch() {\n  \"$kebap_product_name\" open\n}\n\ndownload_release() {\n  local uname_str=\"$(uname -s)\"\n  local os_info\n  os_info=\"$(parse_os_name \"$uname_str\")\"\n  if [ \"$?\" != 0 ]; then\n    error \"The current operating system ($uname_str) does not appear to be supported.\"\n    return 1\n  fi\n\n  # store the downloaded archive in a temporary directory\n  local download_dir=\"$(mktemp -d)\"\n  local repo=\"$1\"\n  local version=\"$2\"\n  download_release_from_repo \"$os_info\" \"$download_dir\" \"$repo\" \"$version\" \"$arch\"\n}\n\ncheck_architecture() {\n  local arch=\"$(uname -m)\"\n  case \"$arch\" in\n  x86_64)\n    echo x86_64\n    ;;\n  amd64)\n    echo x86_64\n    ;;\n  arm64)\n    echo arm64\n    ;;\n  aarch64)\n    echo arm64\n    ;;\n  *)\n    exit 1\n    ;;\n  esac\n}\n\n# return if sourced (for testing the functions above)\nreturn 0 2>/dev/null\n\narch=$(check_architecture)\nexit_status=\"$?\"\nif [ \"$exit_status\" != 0 ]; then\n  error \"Sorry! $product_name currently does not support your processor architecture.\"\n  exit \"$exit_status\"\nfi\n\nrepo=\"https://github.com/xpipe-io/xpipe\"\naur=\"https://aur.archlinux.org/xpipe.git\"\nproduct_name=\"XPipe\"\nkebap_product_name=\"xpipe\"\nversion=\nwhile getopts 'sv:' OPTION; do\n  case \"$OPTION\" in\n    s)\n      repo=\"https://github.com/xpipe-io/xpipe-ptb\"\n      aur=\"https://aur.archlinux.org/xpipe-ptb.git\"\n      product_name=\"XPipe PTB\"\n      kebap_product_name=\"xpipe-ptb\"\n      ;;\n\n    v)\n      version=\"$OPTARG\"\n      ;;\n\n    ?)\n      echo \"Usage: $(basename $0) [-s] [-v <version>]\"\n      exit 1\n      ;;\n  esac\ndone\n\nif [ \"$(uname -s)\" = \"Linux\" ]; then\n  if ! [ -x \"$(command -v apt)\" ] && ! [ -x \"$(command -v rpm)\" ] && [ -x \"$(command -v pacman)\" ]; then\n    info \"Installing from AUR at $aur\"\n    rm -rf \"/tmp/xpipe_aur\" || true\n    if [[ -z \"$version\" ]] ; then\n      git clone \"$aur\" /tmp/xpipe_aur\n    else\n      git clone --branch \"$version\" \"$aur\" /tmp/xpipe_aur\n    fi\n    cd \"/tmp/xpipe_aur\"\n    makepkg -si\n    launch\n    exit 0\n  fi\n\n  if ! [ -x \"$(command -v apt)\" ] && ! [ -x \"$(command -v rpm)\" ] && ! [ -x \"$(command -v pacman)\" ]; then\n    info \"Installation is not supported on this system (no apt, rpm, zypper, dnf, yum, pacman). Can you try a portable version of $product_name?\"\n    info \"https://github.com/xpipe-io/xpipe#portable\"\n    exit 1\n  fi\nfi\n\ndownload_archive=\"$(\n  download_release \"$repo\" \"$version\" \"$arch\"\n  exit \"$?\"\n)\"\nexit_status=\"$?\"\nif [ \"$exit_status\" != 0 ]; then\n  error \"Could not download $product_name release.\"\n  exit \"$exit_status\"\nfi\n\ninstall \"$download_archive\"\n\nexit_status=\"$?\"\nif [ \"$exit_status\" != 0 ]; then\n  error \"Installation failed.\"\n  exit \"$exit_status\"\nfi\n\necho \"\"\necho \"$product_name was successfully installed. You should be able to find $product_name in your desktop environment now.\"\n\nlaunch\n"
  },
  {
    "path": "gradle/gradle_scripts/README.md",
    "content": "## Gradle Scripts\n\nThis directory contains helper scripts and Java module generation rules for dependencies used by various XPipe gradle projects.\nIt also contains various other types of shared build script components that are useful.\n\nAs the [jlink](https://docs.oracle.com/en/java/javase/17/docs/specs/man/jlink.html) tool\neffectively requires proper modules as inputs but many established java\nlibraries did not add proper support yet, using an approach like this is required.\nThe modules are generated with the help of [extra-java-module-info](https://github.com/gradlex-org/extra-java-module-info).\nThe generated `module-info.java` file contains the necessary declarations to make a library work.\nWhile gradle already has a [similar system](https://docs.gradle.org/current/userguide/platforms.html)\nto better share dependencies, this system is lacking several features.\nFor one, it can't pass any other customizations to the build that are required by the dependencies,\ne.g. compiler parameters or annotation processors."
  },
  {
    "path": "gradle/gradle_scripts/dev_default.properties",
    "content": "# XPipe will attempt to locate a local XPipe installation with the matching version to fetch production resources from it.\n# If your installation version and development version do not match up, there is the possibility of errors occurring.\n# If you know what you are doing, you can disable this version check.\nio.xpipe.app.locatorDisableInstallationVersionCheck=false\n\n# By default, XPipe will try to locate a normal local installation.\n# If you also have a PTB version installed, you can also choose to use it if the version matches the development version more closely.\nio.xpipe.app.locatorUsePtbInstallation=false\n\n# Start up in fixed 16/9 window size, useful for demos and showcases\nio.xpipe.app.showcase=false\n\n# Location in which your local development connection should be stored. If left empty, it will use your global XPipe storage in ~/.xpipe.\nio.xpipe.app.dataDir=local\n\n# When enabled, all http server input and output is printed. Useful for debugging\nio.xpipe.beacon.printMessages=false\n"
  },
  {
    "path": "gradle/gradle_scripts/extension.gradle",
    "content": "//task copyRuntimeLibs(type: Copy) {\n//    into project.jar.destinationDirectory\n//    from configurations.runtimeClasspath\n//    exclude \"${project.name}.jar\"\n//    duplicatesStrategy(DuplicatesStrategy.EXCLUDE)\n//}\n// Do we need this?\n// jar.dependsOn(copyRuntimeLibs)\n\ntasks.register('createExtOutput', Copy) {\n    mustRunAfter jar\n\n    if (!file(\"${project.jar.destinationDirectory.get()}_prod\").exists()) {\n        copy {\n            from \"${project.jar.destinationDirectory.get()}\"\n            into \"${project.jar.destinationDirectory.get()}_prod\"\n        }\n    }\n\n    def shouldObfuscate = obfuscate && privateExtensions.contains(project.name)\n    var source = shouldObfuscate ? \"${project.jar.destinationDirectory.get()}_prod\" : \"${project.jar.destinationDirectory.get()}\"\n\n    from source\n    into \"${project.jar.destinationDirectory.get()}_ext\"\n}\n\nproject.tasks.withType(org.gradle.jvm.tasks.Jar).configureEach {\n    if (it.name != 'jar') {\n        it.destinationDirectory = project.layout.buildDirectory.dir('libs_test')\n    }\n}\n\napply from: \"$rootDir/gradle/gradle_scripts/java.gradle\"\napply from: \"$rootDir/gradle/gradle_scripts/javafx.gradle\"\napply from: \"$rootDir/gradle/gradle_scripts/lombok.gradle\"\napply from: \"$rootDir/gradle/gradle_scripts/local_junit_suite.gradle\"\n\nlocalTest {\n    dependencies {\n        if (project.name != 'base') {\n            implementation project(':base')\n        }\n\n        testImplementation project(\":$project.name\")\n    }\n}\n\nconfigurations {\n    compileOnly.extendsFrom(javafx)\n}\n\ndependencies {\n    compileOnly \"com.fasterxml.jackson.core:jackson-databind:2.21.0\"\n    compileOnly project(':core')\n    compileOnly project(':beacon')\n    compileOnly project(':app')\n    compileOnly 'net.synedra:validatorfx:0.4.2'\n    compileOnly files(\"$rootDir/gradle/gradle_scripts/atlantafx-base-2.0.2.jar\")\n    compileOnly 'commons-io:commons-io:2.21.0'\n    compileOnly \"org.kordamp.ikonli:ikonli-javafx:12.4.0\"\n\n    if (project != project(':base')) {\n        compileOnly project(':base')\n    }\n}\n\n// To fix https://github.com/gradlex-org/extra-java-module-info/issues/101#issuecomment-1934761334\nconfigurations.javaModulesMergeJars.shouldResolveConsistentlyWith(configurations.compileClasspath)\n\n"
  },
  {
    "path": "gradle/gradle_scripts/java.gradle",
    "content": "tasks.withType(JavaCompile).configureEach {\n    sourceCompatibility = JavaVersion.VERSION_25\n    targetCompatibility = JavaVersion.VERSION_25\n    modularity.inferModulePath = true\n    options.encoding = 'UTF-8'\n    options.compilerArgs << \"-Xlint:unchecked\" << \"-Xdiags:verbose\"\n    // options.compilerArgs << \"--enable-preview\"\n\n    // These settings are explicitly specified as they can cause problems with annotation processors\n    options.compilerArgs << \"-implicit:none\"\n    options.incremental = false\n}\n\ntasks.withType(JavaExec).configureEach {\n    modularity.inferModulePath = true\n}\n\njavadoc{\n    source = sourceSets.main.allJava\n    options {\n        addStringOption('-release', '25')\n        addStringOption('link', 'https://docs.oracle.com/en/java/javase/25/docs/api/')\n        addBooleanOption('html5', true)\n        addStringOption('Xdoclint:none', '-quiet')\n        // addBooleanOption('-enable-preview', true)\n    }\n}\n\nrepositories {\n    mavenCentral()\n    flatDir {\n        dirs \"$rootDir/gradle/gradle_scripts\"\n    }\n}"
  },
  {
    "path": "gradle/gradle_scripts/javafx.gradle",
    "content": "import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform\n\ndef currentOS = DefaultNativePlatform.currentOperatingSystem\ndef platform = null\nif (currentOS.isWindows()) {\n    platform = 'win'\n} else if (currentOS.isMacOsX()) {\n    platform = 'mac'\n} else {\n    platform = 'linux'\n}\n\nif (System.getProperty (\"os.arch\") == 'aarch64') {\n    platform += '-aarch64'\n}\n\nconfigurations {\n    javafx\n}\n\nif (customJavaFxLibsPath != null) {\n    repositories {\n        flatDir {\n            dirs customJavaFxLibsPath\n        }\n    }\n    dependencies {\n        javafx files(\"$customJavaFxLibsPath/javafx.base.jar\")\n        javafx files(\"$customJavaFxLibsPath/javafx.controls.jar\")\n        javafx files(\"$customJavaFxLibsPath/javafx.graphics.jar\")\n        javafx files(\"$customJavaFxLibsPath/javafx.media.jar\")\n        javafx files(\"$customJavaFxLibsPath/javafx.web.jar\")\n    }\n} else if (!bundledJdkJavaFx) {\n    // Always use maven version for development\n    // The jpackage script uses the jmod sdk files for JavaFX\n    dependencies {\n        javafx \"org.openjfx:javafx-base:${devJavafxVersion}:${platform}\"\n        javafx \"org.openjfx:javafx-controls:${devJavafxVersion}:${platform}\"\n        javafx \"org.openjfx:javafx-graphics:${devJavafxVersion}:${platform}\"\n        javafx \"org.openjfx:javafx-media:${devJavafxVersion}:${platform}\"\n        javafx \"org.openjfx:javafx-web:${devJavafxVersion}:${platform}\"\n    }\n}\n"
  },
  {
    "path": "gradle/gradle_scripts/jna.gradle",
    "content": "configurations {\n    jna\n}\n\ndependencies {\n    jna 'net.java.dev.jna:jna-jpms:5.14.0'\n    jna 'net.java.dev.jna:jna-platform-jpms:5.14.0'\n}\n"
  },
  {
    "path": "gradle/gradle_scripts/junit.gradle",
    "content": "import org.gradle.api.tasks.testing.logging.TestLogEvent\n\ndependencies {\n    testImplementation 'org.hamcrest:hamcrest:3.0'\n    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.14.2'\n    testImplementation 'org.junit.jupiter:junit-jupiter-params:5.14.2'\n    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.14.2'\n    testRuntimeOnly \"org.junit.platform:junit-platform-launcher\"\n}\n\ntasks.withType(Test).configureEach {\n    jvmArgs += [\"-Xmx2g\"]\n    useJUnitPlatform()\n    testLogging {\n        events TestLogEvent.FAILED,\n               TestLogEvent.PASSED,\n               TestLogEvent.SKIPPED,\n               TestLogEvent.STANDARD_OUT,\n               TestLogEvent.STANDARD_ERROR,\n               TestLogEvent.STARTED\n\n        exceptionFormat = 'full'\n        showExceptions = true\n        showCauses = true\n        showStandardStreams = true\n    }\n\n    outputs.upToDateWhen { false }\n\n    afterSuite { desc, result ->\n        if (!desc.parent) { // will match the outermost suite\n            def output = \"Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)\"\n            def startItem = '|  ', endItem = '  |'\n            def repeatLength = startItem.length() + output.length() + endItem.length()\n            println('\\n' + ('-' * repeatLength) + '\\n' + startItem + output + endItem + '\\n' + ('-' * repeatLength))\n        }\n    }\n}\n"
  },
  {
    "path": "gradle/gradle_scripts/local_junit_suite.gradle",
    "content": "apply from: \"$rootDir/gradle/gradle_scripts/junit.gradle\"\n\ntesting {\n    suites {\n        localTest(JvmTestSuite) {\n            useJUnitJupiter()\n\n            dependencies {\n                implementation project(':core')\n                implementation project(':beacon')\n                implementation project(':app')\n                implementation project(':base')\n                implementation project()\n            }\n\n            targets {\n                configureEach {\n                    testTask.configure {\n                        workingDir = rootDir\n\n                        jvmArgs += [\"-Xmx2g\"]\n                        jvmArgs += jvmRunArgs\n\n                        def exts = files(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0].outputs.files.singleFile).toList())\n                        classpath += exts\n                        dependsOn(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0]).toList())\n\n                        systemProperty propertyName('fullVersion'), \"true\"\n                        systemProperty propertyName('useVirtualThreads'), \"false\"\n                        systemProperty propertyName(\"beacon\", \"port\"), \"21723\"\n                        systemProperty propertyName(\"dataDir\"), \"$projectDir/local/\"\n                        systemProperty propertyName(\"logLevel\"), \"trace\"\n                        systemProperty propertyName(\"writeSysOut\"), \"true\"\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "gradle/gradle_scripts/lombok.gradle",
    "content": "dependencies {\n    compileOnly 'org.projectlombok:lombok:1.18.40'\n    annotationProcessor 'org.projectlombok:lombok:1.18.40'\n    testCompileOnly 'org.projectlombok:lombok:1.18.40'\n    testAnnotationProcessor 'org.projectlombok:lombok:1.18.40'\n}\n\ntesting {\n    suites {\n        configureEach {\n            dependencies {\n                compileOnly 'org.projectlombok:lombok:1.18.40'\n                annotationProcessor 'org.projectlombok:lombok:1.18.40'\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "gradle/gradle_scripts/modules.gradle",
    "content": "extraJavaModuleInfo {\n    module(\"de.vandermeer:asciitable\", \"de.vandermeer.asciitable\") {\n        exportAllPackages()\n        requires('de.vandermeer.skb_interfaces')\n        requires('de.vandermeer.ascii_utf_themes')\n        requires('org.apache.commons.lang3')\n    }\n\n    module(\"de.vandermeer:skb-interfaces\", \"de.vandermeer.skb_interfaces\") {\n        exportAllPackages()\n        requires('org.apache.commons.lang3')\n    }\n\n    module(\"de.vandermeer:ascii-utf-themes\", \"de.vandermeer.ascii_utf_themes\") {\n        exportAllPackages()\n        requires('org.apache.commons.lang3')\n        requires('de.vandermeer.skb_interfaces')\n    }\n}\n\nextraJavaModuleInfo {\n    module(\"com.vladsch.flexmark:flexmark\", \"com.vladsch.flexmark\") {\n        mergeJar('com.vladsch.flexmark:flexmark-util')\n        mergeJar('com.vladsch.flexmark:flexmark-util-options')\n        mergeJar('com.vladsch.flexmark:flexmark-util-data')\n        mergeJar('com.vladsch.flexmark:flexmark-util-format')\n        mergeJar('com.vladsch.flexmark:flexmark-util-ast')\n        mergeJar('com.vladsch.flexmark:flexmark-util-sequence')\n        mergeJar('com.vladsch.flexmark:flexmark-util-builder')\n        mergeJar('com.vladsch.flexmark:flexmark-util-html')\n        mergeJar('com.vladsch.flexmark:flexmark-util-dependency')\n        mergeJar('com.vladsch.flexmark:flexmark-util-collection')\n        mergeJar('com.vladsch.flexmark:flexmark-util-misc')\n        mergeJar('com.vladsch.flexmark:flexmark-util-visitor')\n        mergeJar('com.vladsch.flexmark:flexmark-ext-tables')\n        mergeJar('com.vladsch.flexmark:flexmark-ext-gfm-strikethrough')\n        mergeJar('com.vladsch.flexmark:flexmark-ext-gfm-tasklist')\n        mergeJar('com.vladsch.flexmark:flexmark-ext-footnotes')\n        mergeJar('com.vladsch.flexmark:flexmark-ext-definition')\n        mergeJar('com.vladsch.flexmark:flexmark-ext-anchorlink')\n        mergeJar('com.vladsch.flexmark:flexmark-ext-yaml-front-matter')\n        mergeJar('com.vladsch.flexmark:flexmark-ext-toc')\n        exportAllPackages()\n    }\n}\n\nextraJavaModuleInfo {\n    module(\"org.kohsuke:github-api\", \"org.kohsuke.github\") {\n        exports('org.kohsuke.github')\n        exports('org.kohsuke.github.function')\n        exports('org.kohsuke.github.authorization')\n        exports('org.kohsuke.github.extras')\n        exports('org.kohsuke.github.connector')\n        requires('java.logging')\n        requires('org.apache.commons.io')\n        requires('org.apache.commons.lang3')\n        requires('com.fasterxml.jackson.databind')\n        overrideModuleName()\n    }\n}\n\nextraJavaModuleInfo {\n    module(\"io.sentry:sentry\", \"io.sentry\") {\n        exportAllPackages()\n    }\n}\n\nextraJavaModuleInfo {\n    module(\"io.modelcontextprotocol.sdk:mcp-core\", \"io.modelcontextprotocol.sdk.mcp\") {\n        mergeJar(\"io.modelcontextprotocol.sdk:mcp-json\")\n        mergeJar(\"io.modelcontextprotocol.sdk:mcp-json-jackson2\")\n        overrideModuleName()\n        exportAllPackages()\n        requires(\"com.fasterxml.jackson.core\")\n        requires(\"com.fasterxml.jackson.databind\")\n        requires(\"com.fasterxml.jackson.annotation\")\n        requires(\"org.slf4j\")\n        requires(\"reactor.core\")\n        requires(\"org.reactivestreams\")\n        requires(\"com.networknt.schema\")\n        uses(\"io.modelcontextprotocol.json.McpJsonMapperSupplier\")\n        uses(\"io.modelcontextprotocol.json.schema.JsonSchemaValidatorSupplier\")\n    }\n}\n\nextraJavaModuleInfo {\n    module(\"io.projectreactor:reactor-core\", \"reactor.core\") {\n        requires(\"org.reactivestreams\")\n        exportAllPackages()\n        ignoreServiceProvider(\"io.micrometer.context.ContextAccessor\", \"reactor.util.context.ReactorContextAccessor\")\n        ignoreServiceProvider(\"reactor.blockhound.integration.BlockHoundIntegration\", \"reactor.core.scheduler.ReactorBlockHoundIntegration\")\n    }\n}\n\nextraJavaModuleInfo {\n    module(\"org.reactivestreams:reactive-streams\", \"org.reactivestreams\") {\n        exportAllPackages()\n    }\n}\n"
  },
  {
    "path": "gradle/gradle_scripts/publish-base.gradle",
    "content": "java {\n    withJavadocJar()\n    withSourcesJar()\n}\n\nsigning {\n    useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword)\n    sign publishing.publications.mavenJava\n}\n"
  },
  {
    "path": "gradle/gradle_scripts/remote_junit_suite.gradle",
    "content": "import java.util.stream.Collectors\n\napply from: \"$rootDir/gradle/gradle_scripts/junit.gradle\"\n\ntesting {\n    suites {\n        remoteTest(JvmTestSuite) {\n            useJUnitJupiter()\n\n            dependencies {\n                implementation project(':core')\n                implementation project(':beacon')\n                implementation project()\n            }\n\n            targets {\n                configureEach {\n                    testTask.configure {\n                        workingDir = projectDir\n\n                        jvmArgs += [\"-Xmx2g\"]\n                        jvmArgs += jvmRunArgs\n\n                        def daemonArgs = Map.of(\n                                propertyName(\"dataDir\"), \"$projectDir/local/\",\n                                propertyName(\"persistData\"), \"false\",\n                                propertyName(\"writeSysOut\"), \"true\",\n                                propertyName(\"writeLogs\"), \"false\",\n                                propertyName(\"logLevel\"), \"trace\",\n                                propertyName(\"beacon\", \"port\"), \"21723\",\n                                propertyName(\"beacon\", \"printMessages\"), \"true\"\n                        )\n                        def daemonArgsString = daemonArgs.entrySet().stream()\n                                .map(e -> e.getKey() + \"=\" + e.getValue())\n                                .collect(Collectors.joining(\" \"))\n\n                        systemProperty propertyName(\"beacon\", \"daemonArgs\"), daemonArgsString\n                        systemProperty propertyName(\"beacon\", \"port\"), \"21723\"\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-9.3.1-all.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# SPDX-License-Identifier: Apache-2.0\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd -P \"${APP_HOME:-./}\" > /dev/null && printf '%s\\n' \"$PWD\" ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\nCLASSPATH=\"\\\\\\\"\\\\\\\"\"\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n    CLASSPATH=$( cygpath --path --mixed \"$CLASSPATH\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -classpath \"$CLASSPATH\" \\\n        -jar \"$APP_HOME/gradle/wrapper/gradle-wrapper.jar\" \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n@rem SPDX-License-Identifier: Apache-2.0\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" -jar \"%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\" %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "lang/README.md",
    "content": "# Translations\n\nThis directory contains the translations for the strings and texts of the XPipe application. The original translations were created in english and initially translated automatically using [DeepL](https://www.deepl.com/en/translator). If you are interested in contributing to the translations, the main task is checking the automatically generated translations, which are pretty accurate most of the time. So you don't have to write long paragraphs, just perform occasional corrections.\n\n## How-to\n\nFirst of all, make sure that the language you want to contribute translations for is already set up here. If you don't find translations for your language in here, please create an issue in this repository and a maintainer will generate the initial automatic translations. This has to be done by a maintainer first as it requires a DeepL API subscription and some scripting.\n\nIf your language exists in the translations, you can simply submit corrections via a pull request if you find strings that are wrong.\n\n### Correcting strings\n\nThe strings in XPipe are located in one of the `.properties` files in the `strings` directories. The set of strings is constantly expanded and some existing strings are refined. Therefore, the translations are frequently regenerated/retranslated. If you want to correct a string, you have to mark it as custom to prevent it being overridden when the translations are updated. So a string in a translation like\n```\nkey=Wrong translation\n```\nhas to be transformed into\n```\n#custom\nkey=Correct translation\n```\nto mark it being a custom string that should be kept. It is important to include the `#custom` annotation.\n\n### Correcting texts\n\nIf you want to correct something in a text in a `.md` file, you don't have to do anything special as these are not being overwritten later on. Just perform and submit your changes.\n\n### Improving automatic translations\n\nIf a string translated from english is wrong in many languages, it might also make sense to adjust the initial translation and context setting to improve the automatic translation. It is possible to augment the original english string and regenerate the translations for that by adding a `#context: ...` annotation to an original english string that is not translated correctly to improve the accuracy. You will already see some english strings that have this information added.\n\n### Trying them out in action\n\nIf you have cloned this repository, you can automatically run a developer build of XPipe by following the instructions in the [contribution guide](/CONTRIBUTING.md). This will use your modified translations, so you can see how they look and behave in practice.\n\n## Status\n\nHere you can see the current status of the translations. Verified means that these were proof-read and corrected by an actual human at a certain version. Later versions might introduce new strings that are not yet proof-read, this is why version information is included.\n\n| Language | Status         |\n|----------|----------------|\n| English  | Reference      |\n| German   | Verified @ 9.0 |\n| Danish   | Verified @ 9.2 |"
  },
  {
    "path": "lang/strings/fixed_en.properties",
    "content": "cmd=cmd.exe\npowershell=Powershell\npwsh=Powershell Core\nwindowsTerminal=Windows Terminal\nwindowsTerminalPreview=Windows Terminal Preview\ngnomeTerminal=Gnome Terminal\ntilix=Tilix\nwezterm=WezTerm\nkonsole=Konsole\nxfce=Xfce 4\nelementaryTerminal=Elementary Terminal\nmacosTerminal=Terminal.app\niterm2=iTerm2\nwarp=Warp\nclaudeCode=Claude Code\nwave=Wave\ntabby=Tabby\nalacritty=Alacritty\nalacrittyMacOs=Alacritty\nkittyMacOs=Kitty\nbbedit=BBEdit\nfleet=Fleet\nfoot=Foot\nintellij=IntelliJ IDEA\npycharm=PyCharm\nwebstorm=WebStorm\nclion=CLion\ntabbyMacOs=Tabby\nnotepad++=Notepad++\nnotepad++Windows=Notepad++\nnotepad++Linux=Notepad++\nnotepad=Notepad\nterminator=Terminator\nkitty=Kitty\nterminology=Terminology\ncoolRetroTerm=Cool Retro Term\nguake=Guake\ntilda=Tilda\nxterm=XTerm\ndeepinTerminal=Deepin Terminal\nqTerminal=QTerminal\nvscode=Visual Studio Code\nvscodium=VSCodium\nvscodeInsiders=Visual Studio Code Insiders\nkate=Kate\ngedit=GEdit\ngnomeTextEditor=Gnome Text Editor\nleafpad=Leafpad\nmousepad=Mousepad\npluma=Pluma\ntextEdit=Text Edit\nsublime=Sublime Text\nnullPointer=Null Pointer\ndiscord=Discord\nslack=Slack\ngithub=GitHub\nmstsc=Microsoft Terminal Services Client (MSTSC)\nremmina=Remmina\nmicrosoftRemoteDesktopApp=Microsoft Remote Desktop.app\nbitwarden=Bitwarden\n1password=1Password\ndashlane=Dashlane\nlastpass=LastPass\nmacosKeychain=macOS keychain\nwindowsTerminalCanary=Windows Terminal Canary\nsecureCrt=SecureCRT\nxShell=Xshell\nmobaXterm=MobaXterm\ntermius=Termius\ndevolutions=Devolutions RDM\ntryPtb=XPipe Public Test Build\nzed=Zed\nwindowsCredentialManager=Windows credential manager\nwebtop=Webtop\nmcp=MCP server\nkeeper=Keeper\nwindowsApp=Windows App.app\nchmod=Chmod\nchgrp=Chgrp\nchown=Chown\nwindowsSubsystem=Windows Subsystem for Linux\ndocker=Docker\nproxmox=Proxmox PVE\nxonXoff=XON/XOFF\nrtsCts=RTS/CTS\ndsrDtr=DSR/DTR\nputty=PuTTY\nscreen=GNU screen\nminicom=Minicom\nodd=Odd\neven=Even\nmark=Mark\nspace=Space\nteleport=Teleport\nhyperV=Hyper-V\ncommunity=Community\nprofessional=Professional\nproPreview=Preview\npreview=Preview\nghostty=Ghostty\nhttp=HTTP\nhttps=HTTPS\nservicePort=Port $PORT$\ngnomeConsole=Gnome Console\nptyxis=Ptyxis\nunknownDist=Unknown\ndevelopmentDist=Development\nportableDist=Portable\ninstallDist=Installer\nhomebrewDist=Homebrew\naptDist=Apt Repository\nrpmDist=Rpm repository\nwebtopDist=Webtop\nchocoDist=Chocolatey\nwingetDist=WinGet\nxfreeRdp=XFreeRDP\ncosmicTerm=Cosmic Terminal\ncursor=Cursor\nwindsurf=Windsurf\nkiro=Kiro\ntrae=Trae\ntheiaide=TheiaIDE\nkeePassXc=KeePassXC\nzellij=zellij\ntmux=tmux\nonePassword=1Password\nstarship=Starship\nohmyposh=Oh My Posh\nohmyzsh=Oh My Zsh\nuxterm=UXTerm\nportainer=Portainer\nwsl=WSL\nenpass=Enpass\naurDist=AUR\nvoid=Void\npsono=Psono\npsonoPlaceholder=Secret ID\npassboltPlaceholder=Resource UUID\nvnc=VNC\ntightVnc=TightVNC\ntigerVnc=TigerVNC\nrealVnc=RealVNC\nscreenSharing=Screen Sharing.app\nlxterminal=LXTerminal\nreddit=Reddit\nhetznerCloud=Hetzner Cloud\nscoop=Scoop\nvsCodium=VSCodium\nfreeRdp=FreeRDP\ntailscale=Tailscale\nnetbird=Netbird\nappImageDist=AppImage\nnixDist=Nix\nantigravity=Antigravity\nrsa=RSA\ned25519=ED25519\ned25519Sk=ED25519 (FIDO2)\nwestonEditor=Weston Editor\npassbolt=Passbolt\nyakuake=Yakuake\nremoteViewer=Virt viewer\ncosmicEdit=Cosmic Editor\nneovim=Neovim\nsms=SMS\n"
  },
  {
    "path": "lang/strings/translations_da.properties",
    "content": "delete=Slet\nproperties=Egenskaber\nusedDate=Brugt $DATE$\nopenDir=Åben mappe\nsortLastUsed=Sorter efter dato for sidste brug\nsortAlphabetical=Sorter alfabetisk efter navn\nsortIndexed=Sorter efter ordreindeks\nrestartDescription=En genstart kan ofte være en hurtig løsning\nreportIssue=Rapportere et problem\nreportIssueDescription=Åbn den integrerede problemrapportør\nusefulActions=Nyttige handlinger\nstored=Gemt\ntroubleshootingOptions=Værktøjer til fejlfinding\ntroubleshoot=Fejlfinding\nremote=Fjernfil\naddShellStore=Tilføj Shell ...\naddShellTitle=Tilføj Shell-forbindelse\nsavedConnections=Gemte forbindelser\nsave=Gem\nclean=Rengør\nmoveTo=Flyt til ...\naddDatabase=Database ...\nbrowseInternalStorage=Gennemse internt lager\naddTunnel=Tunnel ...\naddService=Service ...\naddScript=Script ...\naddHost=Ekstern vært ...\naddShell=Shell-miljø ...\naddCommand=Kommando ...\naddAutomatically=Tilføj automatisk ...\naddOther=Tilføj andre ...\nconnectionAdd=Tilføj forbindelse\nscriptAdd=Tilføj script\nscriptGroupAdd=Tilføj scriptgruppe\nidentityAdd=Tilføj identitet\nnew=Ny\nselectType=Vælg type\nselectTypeDescription=Vælg forbindelsestype\nselectShellType=Shell-type\nselectShellTypeDescription=Vælg typen af shell-forbindelse\nname=Navn\nstoreIntroHeader=Connection Hub\nstoreIntroContent=Her kan du administrere alle dine lokale og eksterne shell-forbindelser på ét sted. Til at begynde med kan du hurtigt registrere tilgængelige forbindelser automatisk og vælge, hvilke du vil tilføje.\nstoreIntroButton=Søg efter forbindelser ...\ndragAndDropFilesHere=Eller bare træk og slip en fil her\nconfirmDsCreationAbortTitle=Bekræft afbrydelse\nconfirmDsCreationAbortHeader=Vil du afbryde oprettelsen af datakilden?\nconfirmDsCreationAbortContent=Alle fremskridt i oprettelsen af datakilder vil gå tabt.\nconfirmInvalidStoreTitle=Spring validering over\nconfirmInvalidStoreContent=Vil du springe valideringen af forbindelsen over? Du kan tilføje denne forbindelse, selv om den ikke kunne valideres, og løse forbindelsesproblemerne senere.\nexpand=Udvid\naccessSubConnections=Adgang til underforbindelser\ncommon=Almindelig\ncolor=Farve\nalwaysConfirmElevation=Bekræft altid forhøjelse af tilladelse\nalwaysConfirmElevationDescription=Styrer, hvordan man håndterer tilfælde, hvor der kræves forhøjede tilladelser for at køre en kommando på et system, f.eks. med sudo.\\n\\nSom standard caches alle sudo-legitimationsoplysninger under en session og leveres automatisk, når det er nødvendigt. Hvis denne indstilling er aktiveret, vil du blive bedt om at bekræfte den udvidede adgang hver gang.\nallow=Tillad\nask=Spørg\ndeny=Afvise\nshare=Tilføj til git repository\nunshare=Fjern fra git-arkivet\nremove=Fjern\ncreateNewCategory=Ny underkategori\nprompt=Spørg\ncustomCommand=Brugerdefineret kommando\nother=En anden\nsetLock=Indstil lås\nselectConnection=Vælg forbindelse\nselectEntry=Vælg indtastning\ncreateLock=Opret adgangssætning\nchangeLock=Skift adgangskode\ntest=Test\n#custom\nfinish=Afslut\nerror=Der opstod en fejl\ndownloadStageDescription=Flytter downloadede filer til dit systems download-bibliotek og åbner det.\nok=Ok\nsearch=Søg\nrepeatPassword=Gentag adgangskode\naskpassAlertTitle=Askpass\nunsupportedOperation=Operation, der ikke understøttes: $MSG$\nfileConflictAlertTitle=Løs en konflikt\nfileConflictAlertContent=Der er opstået en konflikt. Filen $FILE$ findes allerede på målsystemet.\\n\\nHvordan vil du gerne fortsætte?\nfileConflictAlertContentMultiple=Der opstod en konflikt. Filen $FILE$ findes allerede.\\n\\nHvordan vil du gerne fortsætte? Der kan være flere konflikter, som du automatisk kan løse ved at vælge en mulighed, der gælder for alle.\nmoveAlertTitle=Bekræft træk\nmoveAlertHeader=Vil du flytte de ($COUNT$) valgte elementer til $TARGET$?\ndeleteAlertTitle=Bekræft sletning\ndeleteAlertHeader=Vil du slette de ($COUNT$) valgte elementer?\nselectedElements=Udvalgte elementer:\nmustNotBeEmpty=$VALUE$ må ikke være tom\nvalueMustNotBeEmpty=Værdien må ikke være tom\ntransferDescription=Træk filer hertil for at downloade\ndragLocalFiles=Træk downloads herfra\nnull=$VALUE$ skal være ikke-nul\nroots=Rødder\nscripts=Scripts\nsearchFilter=Søg ...\nrecent=Seneste\nshortcut=Genvej\nbrowserWelcomeEmptyHeader=Fil-browser\nbrowserWelcomeEmptyContent=Til venstre kan du vælge, hvilke systemer der skal åbnes i filbrowseren. XPipe vil huske, hvilke systemer og mapper du tidligere har åbnet, og vise dem i en hurtig adgangsmenu her i fremtiden.\nbrowserWelcomeEmptyButton=Åbn lokal filbrowser\nbrowserWelcomeSystems=Du var for nylig forbundet til følgende systemer:\nbrowserWelcomeDocsHeader=Dokumentation\nbrowserWelcomeDocsContent=Hvis du foretrækker en mere guidet tilgang til at gøre dig bekendt med XPipe, kan du tjekke dokumentationswebstedet.\nbrowserWelcomeDocsButton=Åben dokumentation\nhostFeatureUnsupported=$FEATURE$ er ikke installeret på værten\nmissingStore=$NAME$ eksisterer ikke\nconnectionName=Navn på forbindelse\nconnectionNameDescription=Giv denne forbindelse et brugerdefineret navn\nopenFileTitle=Åbn fil\nunknown=Ukendt\nscanAlertTitle=Tilføj forbindelser\nscanAlertChoiceHeader=Mål\nscanAlertChoiceHeaderDescription=Vælg, hvor der skal søges efter forbindelser. Dette vil lede efter alle tilgængelige forbindelser først.\nscanAlertHeader=Forbindelsestyper\nscanAlertHeaderDescription=Vælg de typer af forbindelser, du automatisk vil tilføje til systemet.\nnoInformationAvailable=Ingen information tilgængelig\nyes=Ja\n#custom\nno=Nej\nerrorOccured=Der opstod en fejl\nterminalErrorOccured=Der opstod en terminalfejl\nerrorTypeOccured=En undtagelse af typen $TYPE$ blev kastet\npermissionsAlertTitle=Nødvendige tilladelser\npermissionsAlertHeader=Der kræves yderligere tilladelser for at udføre denne handling.\npermissionsAlertContent=Følg pop op-vinduet for at give XPipe de nødvendige tilladelser i indstillingsmenuen.\nerrorDetails=Detaljer om fejl\nupdateReadyAlertTitle=Opdateringsklar\nupdateReadyAlertHeader=En opdatering til version $VERSION$ er klar til at blive installeret\nupdateReadyAlertContent=Dette vil installere den nye version og genstarte XPipe, når installationen er færdig.\nerrorNoDetail=Ingen detaljer om fejlen er tilgængelige\nerrorNoExceptionMessage=Der opstod en fejl af typen $TYPE$\nupdateAvailableTitle=Opdatering tilgængelig\nupdateAvailableContent=En XPipe-opdatering til version $VERSION$ er tilgængelig til installation. Selv om XPipe ikke kunne startes, kan du forsøge at installere opdateringen for muligvis at løse problemet.\nclipboardActionDetectedTitle=Udklipsholder-handling opdaget\nclipboardActionDetectedContent=XPipe har fundet indhold i dit udklipsholder, som kan åbnes. Vil du åbne det nu? Vil du importere indholdet i din udklipsholder?\ninstall=Installer ...\nignore=Ignorer\npossibleActions=Tilgængelige handlinger\nreportError=Rapporter fejl\nreportOnGithub=Opret en problemrapport på GitHub\nreportOnGithubDescription=Åbn et nyt problem i GitHub-arkivet\n#custom\nreportErrorDescription=Send en fejlrapport med brugerfeedback og diagnostisk info\nignoreError=Ignorer fejl\nignoreErrorDescription=Ignorer denne fejl og fortsæt, som om intet var hændt\nprovideEmail=Hvordan kan vi kontakte dig (valgfrit, kun hvis du ønsker at få svar). Din rapport er som standard anonym, så du kan angive kontaktoplysninger som f.eks. en e-mailadresse her.\nadditionalErrorInfo=Giv yderligere oplysninger (valgfrit)\nadditionalErrorAttachments=Vælg vedhæftede filer (valgfrit)\ndataHandlingPolicies=Politik for beskyttelse af personlige oplysninger\nsendReport=Send rapport\nerrorHandler=Fejlhåndtering\nevents=Begivenheder\nvalidate=Validering\nstackTrace=Staksporing\npreviousStep=< Tidligere\nnextStep=Næste >\n#custom\nfinishStep=Afslut\nselect=Vælg\nbrowseInternal=Gennemse internt\ncheckOutUpdate=Tjek ud-opdatering\nquit=Afslut\nnoTerminalSet=Ingen terminalapplikation er blevet indstillet automatisk. Du kan gøre det manuelt i indstillingsmenuen.\nconnections=Forbindelser\nconnectionHub=Forbindelseshub\nsettings=Indstillinger\nexplorePlans=Licens\nhelp=Hjælp\n#custom\nabout=Om XPipe\ndeveloper=Udvikler\nbrowseFileTitle=Gennemse fil\nbrowser=Fil-browser\nselectFileFromComputer=Vælg en fil fra denne computer\nlinks=Links\nwebsite=Hjemmeside\ndiscordDescription=Deltag i Discord-serveren\nredditDescription=Bliv medlem af XPipe subreddit\nsecurity=Sikkerhed\nsecurityPolicy=Sikkerhedsoplysninger\nsecurityPolicyDescription=Læs den detaljerede sikkerhedspolitik\nprivacy=Politik for beskyttelse af personlige oplysninger\nprivacyDescription=Læs privatlivspolitikken for XPipe-applikationen\nslackDescription=Deltag i Slack-arbejdsområdet\nsupport=Støtte\ngithubDescription=Tjek GitHub-arkivet ud\nopenSourceNotices=Meddelelser om open source\ncheckForUpdates=Tjek for opdateringer\n#custom\ncheckForUpdatesDescription=Download en opdatering, hvis der er en.\nlastChecked=Sidst tjekket\n#custom\nversion=Version\n#custom\nbuild=Build version\nruntimeVersion=Runtime-version\nvirtualMachine=Virtuel maskine\nupdateReady=Installer opdatering\nupdateReadyPortable=Tjek ud-opdatering\nupdateReadyDescription=En opdatering blev downloadet og er klar til at blive installeret\nupdateReadyDescriptionPortable=En opdatering er tilgængelig til download\nupdateRestart=Genstart for at opdatere\nnever=Aldrig\nupdateAvailableTooltip=Opdatering tilgængelig\nptbAvailableTooltip=Offentlig testversion tilgængelig\nvisitGithubRepository=Besøg GitHub-arkivet\nupdateAvailable=Opdatering tilgængelig: $VERSION$\ndownloadUpdate=Download opdatering\nlegalAccept=Jeg accepterer slutbrugerlicensaftalen\n#custom\nconfirm=Bekræft\nprint=Print\nwhatsNew=Hvad er nyt i version $VERSION$ ($DATE$)\nantivirusNoticeTitle=En note om antivirusprogrammer\nupdateChangelogAlertTitle=Changelog\ngreetingsAlertTitle=Velkommen til XPipe\neula=Slutbrugerlicensaftale\nnews=Nyheder\nintroduction=Indledning\nprivacyPolicy=Politik for beskyttelse af personlige oplysninger\nagree=Enig\ndisagree=Uenig\ndirectories=Kataloger\nlogFile=Log-fil\nlogFiles=Logfiler\nlogFilesAttachment=Logfiler\nissueReporter=Rapportør af problemer\nopenCurrentLogFile=Logfiler\nopenCurrentLogFileDescription=Åbner logfilen for den aktuelle session\nopenLogsDirectory=Åbn log-biblioteket\ninstallationFiles=Installationsfiler\nopenInstallationDirectory=Installationsfiler\nopenInstallationDirectoryDescription=Åbn XPipes installationsmappe\nlaunchDebugMode=Fejlfindingstilstand\nlaunchDebugModeDescription=Genstart XPipe i fejlsøgningstilstand\nextensionInstallTitle=Download\nextensionInstallDescription=Denne handling kræver yderligere tredjepartsbiblioteker, som ikke distribueres af XPipe. Du kan automatisk installere dem her. Komponenterne downloades derefter fra leverandørens hjemmeside:\nextensionInstallLicenseNote=Ved at udføre download og automatisk installation accepterer du vilkårene i tredjepartslicenserne:\nlicense=Licens\ninstallRequired=Installation påkrævet\nrestore=Gendannelse\nrestoreAllSessions=Gendan alle sessioner\nlimitedTouchscreenMode=Begrænset touchscreen-tilstand\nlimitedTouchscreenModeDescription=Når du bruger dette program på en mere eksotisk touchscreen-grænseflade som en telefonskærm, fungerer nogle menuer måske ikke korrekt. Når denne indstilling er aktiveret, bruger menuimplementeringen en mere begrænset funktionalitet til at arbejde med sparsomt sendte muse-/berøringshændelser.\nappearance=Udseende\ndisplay=Vis\npersonalization=Personliggørelse\ndisplayOptions=Visningsindstillinger\ntheme=Tema\nrdpConfiguration=Konfiguration af fjernskrivebord\nrdpClient=RDP-klient\nrdpClientDescription=Det RDP-klientprogram, der skal kaldes, når RDP-forbindelser startes.\\n\\nBemærk, at forskellige klienter har forskellige grader af evner og integrationer. Nogle klienter understøtter ikke automatisk overførsel af adgangskoder, så du skal stadig udfylde dem ved opstart.\nlocalShell=Lokal shell\nthemeDescription=Dit foretrukne skærmtema.\ndontAutomaticallyStartVmSshServer=Start ikke automatisk SSH-server for VM'er, når det er nødvendigt\ndontAutomaticallyStartVmSshServerDescription=Enhver shell-forbindelse til en VM, der kører i en hypervisor, sker via SSH. XPipe kan automatisk starte den installerede SSH-server, når det er nødvendigt. Hvis du ikke ønsker dette af sikkerhedsmæssige årsager, kan du bare deaktivere denne adfærd med denne indstilling.\nconfirmGitShareTitle=Git-synkronisering\nconfirmGitShareContent=Vil du tilføje den valgte fil til dit git vault-repository? Dette vil kopiere en krypteret version af filen til din git vault og bekræfte dine ændringer. Du vil derefter have adgang til filen på alle synkroniserede desktops.\ngitShareFileTooltip=Tilføj en fil til git vault-databiblioteket, så den automatisk synkroniseres.\\n\\nDenne handling kan kun bruges, når git vault er aktiveret i indstillingerne.\nperformanceMode=Performance-tilstand\nperformanceModeDescription=Deaktiverer alle visuelle effekter, der ikke er nødvendige for at forbedre programmets ydeevne.\ndontAcceptNewHostKeys=Accepter ikke nye SSH-værtsnøgler automatisk\ndontAcceptNewHostKeysDescription=XPipe accepterer som standard automatisk værtsnøgler fra systemer, hvor din SSH-klient ikke allerede har gemt en kendt værtsnøgle. Men hvis en kendt værtsnøgle er ændret, vil den nægte at oprette forbindelse, medmindre du accepterer den nye.\\n\\nHvis du deaktiverer denne funktion, kan du tjekke alle værtsnøgler, selv om der ikke er nogen konflikt i første omgang.\nuiScale=UI-skala\nuiScaleDescription=En brugerdefineret skaleringsværdi, der kan indstilles uafhængigt af din systemdækkende skærmskala. Værdierne er i procent, så f.eks. vil en værdi på 150 resultere i en UI-skala på 150%.\neditorProgram=Redigeringsprogram\neditorProgramDescription=Standard teksteditor til brug ved redigering af enhver form for tekstdata.\nwindowOpacity=Vinduets opacitet\nwindowOpacityDescription=Ændrer vinduets opacitet for at holde styr på, hvad der sker i baggrunden.\nuseSystemFont=Brug systemskrifttype\nopenDataDir=Vault-datakatalog\nopenDataDirButton=Åben datakatalog\nopenDataDirDescription=Hvis du vil synkronisere yderligere filer, f.eks. SSH-nøgler, på tværs af systemer med dit git-repository, kan du lægge dem i lagerdatabiblioteket. Alle filer, der henvises til der, vil automatisk få tilpasset deres filstier på ethvert synkroniseret system.\nupdates=Opdateringer\nselectAll=Vælg alle\nadvanced=Avanceret\nthirdParty=Open source-meddelelser\neulaDescription=Læs slutbrugerlicensaftalen for XPipe-applikationen\nthirdPartyDescription=Se open source-licenser for tredjepartsbiblioteker\nworkspaceLock=Master-passphrase\nenableGitStorage=Aktiver synkronisering\nsharing=Deling\ngitSync=Git-synkronisering\nenableGitStorageDescription=Når det er aktiveret, vil XPipe initialisere et git-repository for den lokale hvælving og overføre alle ændringer til det. Bemærk, at dette kræver, at git er installeret, og at det kan gøre indlæsning og lagring langsommere.\\n\\nAlle kategorier, der skal synkroniseres, skal udtrykkeligt markeres som synkroniserede.\nstorageGitRemote=URL til fjernsynkronisering\nstorageGitRemoteDescription=Når den er indstillet, vil XPipe automatisk trække alle ændringer, når den indlæses, og skubbe alle ændringer til fjernlageret, når den gemmes.\\n\\nDette giver dig mulighed for at dele din hvælving mellem flere XPipe-installationer. Det understøtter HTTP- og SSH-URL'er samt lokale mapper.\nvault=Vault\nworkspaceLockDescription=Indstiller en brugerdefineret adgangskode til at kryptere alle følsomme oplysninger, der er gemt i XPipe.\\n\\nDette resulterer i øget sikkerhed, da det giver et ekstra lag af kryptering for dine lagrede følsomme oplysninger. Du vil derefter blive bedt om at indtaste adgangskoden, når XPipe starter.\nuseSystemFontDescription=Kontrollerer, om du vil bruge systemets standardskrifttype eller Inter-skrifttypen, som følger med XPipe.\ntooltipDelay=Forsinkelse af værktøjstip\ntooltipDelayDescription=Antallet af millisekunder, der skal gå, før et værktøjstip vises.\nfontSize=Skriftstørrelse\nwindowOptions=Indstillinger for vindue\nsaveWindowLocation=Gemme vinduesplacering\nsaveWindowLocationDescription=Styrer, om vindueskoordinaterne skal gemmes og gendannes ved genstart.\nstartupShutdown=Opstart / nedlukning\nshowChildrenConnectionsInParentCategory=Vis underordnede kategorier i overordnet kategori\nshowChildrenConnectionsInParentCategoryDescription=Om alle forbindelser i underkategorier skal medtages eller ej, når en bestemt overordnet kategori vælges.\\n\\nHvis dette er deaktiveret, opfører kategorierne sig mere som klassiske mapper, der kun viser deres direkte indhold uden at inkludere undermapper.\ncondenseConnectionDisplay=Kondenseret forbindelsesvisning\ncondenseConnectionDisplayDescription=Gør hver forbindelse på øverste niveau mindre lodret for at give mulighed for en mere komprimeret forbindelsesliste.\nopenConnectionSearchWindowOnConnectionCreation=Åbn vindue til forbindelsessøgning ved oprettelse af forbindelse\nopenConnectionSearchWindowOnConnectionCreationDescription=Om der automatisk skal åbnes et vindue for at søge efter tilgængelige underforbindelser, når der tilføjes en ny shell-forbindelse.\nworkflow=Arbejdsgang\nsystem=System\napplication=Applikation\nstorage=Opbevaring\nrunOnStartup=Kører ved opstart\ncloseBehaviour=Afslutningsadfærd\ncloseBehaviourDescription=Styrer, hvordan XPipe skal fortsætte, når hovedvinduet lukkes.\nlanguage=Sprog\nlanguageDescription=Det skærmsprog, der skal bruges. Oversættelserne forbedres gennem bidrag fra fællesskabet. Du kan hjælpe oversættelsesarbejdet ved at indsende oversættelsesrettelser på GitHub.\nlightTheme=Let tema\ndarkTheme=Mørkt tema\nexit=Afslut XPipe\ncontinueInBackground=Fortsæt i baggrunden\nminimizeToTray=Minimér til bakken\ncloseBehaviourAlertTitle=Indstil lukkeadfærd\ncloseBehaviourAlertTitleHeader=Vælg, hvad der skal ske, når vinduet lukkes. Alle aktive forbindelser vil blive lukket, når programmet lukkes ned.\nstartupBehaviour=Opstartsadfærd\nstartupBehaviourDescription=Styrer standardopførslen for skrivebordsprogrammet, når XPipe startes.\nclearCachesAlertTitle=Rens cachen\nclearCachesAlertContent=Vil du rense alle XPipe-cacher? Dette vil slette alle de cachedata, der er gemt for at forbedre brugeroplevelsen.\nstartGui=Start GUI\nstartInTray=Start i bakken\nstartInBackground=Start i baggrunden\nclearCaches=Tøm caches ...\nclearCachesDescription=Slet alle cache-data\ncancel=Annuller\nnotAnAbsolutePath=Ikke en absolut sti\nnotADirectory=Ikke en mappe\nnotAnEmptyDirectory=Ikke en tom mappe\nautomaticallyCheckForUpdates=Tjek for opdateringer\nautomaticallyCheckForUpdatesDescription=Når det er aktiveret, hentes oplysninger om nye udgivelser automatisk, mens XPipe kører efter et stykke tid. Du skal stadig udtrykkeligt bekræfte enhver installation af opdateringer.\nsendAnonymousErrorReports=Send anonyme fejlrapporter\nsendUsageStatistics=Send anonyme brugsstatistikker\nstorageDirectory=Lagerkatalog\nstorageDirectoryDescription=Den placering, hvor XPipe skal gemme alle forbindelsesoplysninger. Når du ændrer dette, kopieres dataene i den gamle mappe ikke til den nye.\nlogLevel=Log-niveau\nappBehaviour=Applikationens adfærd\nlogLevelDescription=Det logniveau, der skal bruges, når man skriver logfiler.\ndeveloperMode=Udvikler-tilstand\ndeveloperModeDescription=Når den er aktiveret, får du adgang til en række ekstra muligheder, som er nyttige for udviklingen.\n#custom\neditor=Editor\ncustom=Brugerdefineret\npasswordManager=Adgangskode-manager\nexternalPasswordManager=Ekstern adgangskodeadministrator\npasswordManagerDescription=Den lokalt installerede adgangskodeadministrator, der skal integreres med.\\n\\nHvis du har installeret en adgangskodeadministrator, kan du konfigurere XPipe til at hente adgangskoder fra den, så XPipe ikke behøver at gemme adgangskoderne selv. Når det er aktiveret, kan alle adgangskodefelter for en forbindelse konfigureres til at bruge adgangskodeadministratoren.\npasswordManagerCommandTest=Test password manager\npasswordManagerCommandTestDescription=Her kan du teste, om output ser korrekt ud, hvis du har sat en password manager op.\npreferTerminalTabs=Foretrækker at åbne nye faner\npreferTerminalTabsDescription=Kontrollerer, om XPipe vil forsøge at åbne nye faner i den valgte terminal i stedet for nye vinduer. Ikke alle terminaler understøtter faner.\ncustomRdpClientCommand=Brugerdefineret kommando\ncustomRdpClientCommandDescription=Den kommando, der skal udføres for at starte den brugerdefinerede RDP-klient.\\n\\nPladsholderstrengen $FILE vil blive erstattet af det citerede absolutte .rdp-filnavn, når den kaldes. Husk at citere stien til den eksekverbare fil, hvis den indeholder mellemrum.\ncustomEditorCommand=Brugerdefineret editor-kommando\ncustomEditorCommandDescription=Den kommando, der skal udføres for at starte den brugerdefinerede editor.\\n\\nPladsholderstrengen $FILE vil blive erstattet af det citerede absolutte filnavn, når den kaldes. Husk at citere stien til den eksekverbare editor, hvis den indeholder mellemrum.\neditorReloadTimeout=Timeout for genindlæsning af editor\neditorReloadTimeoutDescription=Det antal millisekunder, der skal gå, før man læser en fil, efter at den er blevet opdateret. Dette forhindrer problemer i tilfælde, hvor din editor er langsom til at skrive eller frigive fillåse.\nencryptAllVaultData=Krypter alle boksdata\nencryptAllVaultDataDescription=Når det er aktiveret, vil alle dele af vault-forbindelsesdataene blive krypteret med din vault-krypteringsnøgle i modsætning til kun hemmeligheder i disse data. Dette tilføjer endnu et lag af sikkerhed for andre parametre som brugernavne, værtsnavne osv., der ikke er krypteret som standard i vault.\\n\\nDenne indstilling vil gøre din git vault-historik og diffs ubrugelige, da du ikke længere kan se de oprindelige ændringer, kun binære ændringer.\nvaultSecurity=Vault-sikkerhed\ndeveloperDisableUpdateVersionCheck=Deaktiver opdatering af versionskontrol\ndeveloperDisableUpdateVersionCheckDescription=Styrer, om opdateringskontrollen skal ignorere versionsnummeret, når den leder efter en opdatering.\ndeveloperDisableGuiRestrictions=Deaktivering af GUI-begrænsninger\ndeveloperDisableGuiRestrictionsDescription=Kontrollerer, om nogle deaktiverede handlinger stadig kan udføres fra brugergrænsefladen.\ndeveloperShowHiddenEntries=Vis skjulte poster\ndeveloperShowHiddenEntriesDescription=Når den er aktiveret, vises skjulte og interne datakilder.\ndeveloperShowHiddenProviders=Vis skjulte udbydere\ndeveloperShowHiddenProvidersDescription=Styrer, om skjulte og interne forbindelses- og datakildeudbydere skal vises i oprettelsesdialogen.\ndeveloperDisableConnectorInstallationVersionCheck=Deaktivering af Connector Version Check\ndeveloperDisableConnectorInstallationVersionCheckDescription=Kontrollerer, om opdateringskontrollen ignorerer versionsnummeret, når den undersøger versionen af et XPipe-stik, der er installeret på en fjernmaskine.\nshellCommandTest=Shell-kommandotest\nshellCommandTestDescription=Kør en kommando i den shell-session, der bruges internt af XPipe.\nterminal=Terminal\nterminalType=Terminal-emulator\nterminalConfiguration=Konfiguration af terminal\nterminalCustomization=Tilpasning af terminaler\neditorConfiguration=Konfiguration af editor\ndefaultApplication=Standardapplikation\ninitialSetup=Første opsætning\nterminalTypeDescription=Den standardterminal, der skal bruges til at åbne shell-forbindelser.\\n\\nNiveauet for understøttelse af funktioner varierer fra terminal til terminal, og hver enkelt er markeret som enten anbefalet eller ikke anbefalet. Din brugeroplevelse bliver bedst, når du bruger en anbefalet terminal.\nprogram=Program\ncustomTerminalCommand=Brugerdefineret terminalkommando\ncustomTerminalCommandDescription=Den kommando, der skal udføres for at åbne den brugerdefinerede terminal med en given kommando.\\n\\nXPipe opretter et midlertidigt shell-script til din terminal, som den kan udføre. Pladsholderstrengen $CMD i den kommando, du leverer, erstattes af det faktiske startscript, når det kaldes. Husk at citere stien til terminalens eksekverbare fil, hvis den indeholder mellemrum.\nclearTerminalOnInit=Ryd terminal ved start\nclearTerminalOnInitDescription=Når den er aktiveret, vil XPipe køre en clear-kommando, når en ny terminalsession er startet, for at fjerne al unødvendig output, der blev udskrevet, da terminalsessionen blev startet.\ndontCachePasswords=Cache ikke promptede adgangskoder\ndontCachePasswordsDescription=Styrer, om adgangskoder skal cachelagres internt i XPipe, så du ikke behøver at indtaste dem igen i den aktuelle session.\\n\\nHvis denne funktion er deaktiveret, skal du genindtaste eventuelle legitimationsoplysninger, hver gang de kræves af systemet.\ndenyTempScriptCreation=Afvis oprettelse af midlertidigt script\ndenyTempScriptCreationDescription=For at realisere nogle af sine funktioner opretter XPipe nogle gange midlertidige shell-scripts på et målsystem for at muliggøre en nem udførelse af enkle kommandoer. Disse indeholder ingen følsomme oplysninger og er kun oprettet til implementeringsformål.\\n\\nHvis denne adfærd er deaktiveret, vil XPipe ikke oprette midlertidige filer på et fjernsystem. Denne indstilling er nyttig i højsikkerhedssammenhænge, hvor alle ændringer i filsystemet overvåges. Hvis den er deaktiveret, vil nogle funktioner, f.eks. shell-miljøer og scripts, ikke fungere efter hensigten.\ndisableCertutilUse=Deaktiver brug af certutil på Windows\nuseLocalFallbackShell=Brug lokal fallback-shell\nuseLocalFallbackShellDescription=Skift til at bruge en anden lokal shell til at håndtere lokale operationer. Det kan være PowerShell på Windows og bourne shell på andre systemer.\\n\\nDenne indstilling kan bruges, hvis den normale lokale standard-shell er deaktiveret eller ødelagt i en eller anden grad. Nogle funktioner fungerer dog muligvis ikke som forventet, når denne mulighed er aktiveret.\ndisableCertutilUseDescription=På grund af flere mangler og fejl i cmd.exe oprettes midlertidige shell-scripts med certutil ved at bruge det til at afkode base64-input, da cmd.exe går i stykker ved ikke-ASCII-input. XPipe kan også bruge PowerShell til det, men det vil være langsommere.\\n\\nDette deaktiverer enhver brug af certutil på Windows-systemer til at realisere nogle funktioner og falder tilbage på PowerShell i stedet. Det kan glæde nogle antivirusprogrammer, da nogle af dem blokerer for brugen af certutil.\ndisableTerminalRemotePasswordPreparation=Deaktivering af terminalens fjernadgangskode\ndisableTerminalRemotePasswordPreparationDescription=I situationer, hvor en fjern shell-forbindelse, der går gennem flere mellemliggende systemer, skal etableres i terminalen, kan det være nødvendigt at forberede eventuelle nødvendige adgangskoder på et af de mellemliggende systemer for at muliggøre en automatisk udfyldning af eventuelle prompter.\\n\\nHvis du ikke ønsker, at adgangskoderne nogensinde skal overføres til et mellemliggende system, kan du deaktivere denne funktion. Alle nødvendige mellemliggende adgangskoder vil så blive spurgt i selve terminalen, når den åbnes.\nmore=Mere\ntranslate=Oversættelser\nallConnections=Alle forbindelser\nallScripts=Alle scripts\nallIdentities=Alle identiteter\nsynced=Synkroniseret\npredefined=Foruddefineret\nsamples=Prøver\ngoodMorning=Godmorgen\ngoodAfternoon=God eftermiddag\ngoodEvening=God aften\naddVisual=Visuel ...\naddDesktop=Desktop ...\nssh=SSH\nsshConfiguration=SSH-konfiguration\nsize=Størrelse\nattributes=Egenskaber\nmodified=Ændret\nowner=Ejer\nupdateReadyTitle=Opdatering til $VERSION$ klar\ntemplates=Skabeloner\nretry=Forsøg igen\nretryAll=Prøv igen alle\nreplace=Udskift\nreplaceAll=Erstat alle\nhibernateBehaviour=Dvaleadfærd\nhibernateBehaviourDescription=Styrer, hvordan programmet opfører sig, når dit system sættes i dvale.\n#custom\noverview=Oversigt\nhistory=Historie\nskipAll=Spring alle over\nnotes=Bemærkninger\naddNotes=Tilføj noter\norder=Ændre rækkefølge\nkeepFirst=Behold først\nkeepLast=Behold det sidste\npinToTop=Pin til toppen\nunpinFromTop=Fjern nål fra toppen\norderAheadOf=Bestil på forhånd ...\nclearIndex=Nulstil indeks\nhttpServer=HTTP-server\nmcpServer=MCP-server\napiKey=API-nøgle\napiKeyDescription=API-nøglen til godkendelse af XPipe-dæmonens API-anmodninger. Se den generelle API-dokumentation for at få flere oplysninger om, hvordan du godkender.\ndisableApiAuthentication=Deaktiver API-godkendelse\ndisableApiAuthenticationDescription=Deaktiverer alle nødvendige godkendelsesmetoder, så enhver uautoriseret anmodning vil blive håndteret.\\n\\nAutentificering bør kun deaktiveres til udviklingsformål.\napi=API\nstoreIntroImportContent=Bruger du allerede XPipe på et andet system? Synkroniser dine eksisterende forbindelser på tværs af flere systemer via et eksternt git-repository. Du kan også synkronisere senere når som helst, hvis det ikke er sat op endnu.\nstoreIntroImportButton=Synkroniser forbindelser ...\nstoreIntroImportHeader=Importer forbindelser\nshowNonRunningChildren=Vis børn, der ikke kører\nhttpApi=HTTP API\nisOnlySupportedLimit=understøttes kun med en professionel licens, når man har mere end $COUNT$ forbindelser\nareOnlySupportedLimit=understøttes kun med en professionel licens, når man har mere end $COUNT$ forbindelser\nenabled=Aktiveret\nenableGitStoragePtbDisabled=Git-synkronisering er deaktiveret for offentlige test-builds for at forhindre brug med almindelige release-git-repositories og for at modvirke, at man bruger en PTB-build som sin daglige driver.\ncopyId=Kopier API-ID\nrequireDoubleClickForConnections=Kræver dobbeltklik for forbindelser\nrequireDoubleClickForConnectionsDescription=Hvis den er aktiveret, skal du dobbeltklikke på forbindelser for at starte dem. Det er nyttigt, hvis man er vant til at dobbeltklikke på ting.\nclearTransferDescription=Ryd valg\nselectTab=Vælg fane\ncloseTab=Luk fanen\ncloseOtherTabs=Luk andre faner\ncloseAllTabs=Luk alle faner\ncloseLeftTabs=Luk faner til venstre\ncloseRightTabs=Luk faner til højre\naddSerial=Seriel ...\nconnect=Forbind\nworkspaces=Arbejdsområder\nmanageWorkspaces=Administrer arbejdsområder\naddWorkspace=Tilføj arbejdsområde ...\nworkspaceAdd=Tilføj et nyt arbejdsområde\nworkspaceAddDescription=Arbejdsområder er forskellige konfigurationer til at køre XPipe. Hvert arbejdsområde har et datakatalog, hvor alle data gemmes lokalt. Dette omfatter forbindelsesdata, indstillinger og meget mere.\\n\\nHvis du bruger synkroniseringsfunktionen, kan du også vælge at synkronisere hvert arbejdsområde med et forskelligt git-repository.\nworkspaceName=Navn på arbejdsområde\nworkspaceNameDescription=Visningsnavnet på arbejdsområdet\nworkspacePath=Sti til arbejdsområde\nworkspacePathDescription=Placeringen af arbejdsområdets datakatalog\nworkspaceCreationAlertTitle=Oprettelse af arbejdsområde\ndeveloperForceSshTty=Fremtving SSH TTY\ndeveloperForceSshTtyDescription=Få alle SSH-forbindelser til at tildele en pty for at teste understøttelsen af en manglende stderr og en pty.\ndeveloperDisableSshTunnelGateways=Deaktiver SSH-gateway-tunneling\ndeveloperDisableSshTunnelGatewaysDescription=Brug ikke tunnelsessioner til gateways, men forbind i stedet direkte til systemet.\nttyWarning=Forbindelsen har tvangstildelt en pty/tty og giver ikke en separat stderr-strøm.\\n\\nDet kan føre til et par problemer.\\n\\nHvis du kan, så prøv at få forbindelseskommandoen til ikke at tildele en pty.\nxshellSetup=Xshell-opsætning\ntermiusSetup=Termius-opsætning\ntryPtbDescription=Prøv nye funktioner tidligt i XPipe-udviklernes builds\nconfirmVaultUnencryptTitle=Bekræft afkryptering af hvælving\nconfirmVaultUnencryptContent=Vil du virkelig deaktivere avanceret vault-kryptering? Det vil fjerne den ekstra kryptering af lagrede data og overskrive eksisterende data.\nenableHttpApi=Aktiver HTTP API\nenableHttpApiDescription=Aktiverer API'en, så eksterne programmer kan kalde XPipe-dæmonen for at udføre handlinger med dine administrerede forbindelser.\nchooseCustomIcon=Vælg et brugerdefineret ikon\ngitVault=Git-hvælving\nfileBrowser=Fil-browser\nconfirmAllDeletions=Bekræft alle sletninger\nconfirmAllDeletionsDescription=Om der skal vises en bekræftelsesdialog for alle sletteoperationer. Som standard er det kun mapper, der kræver en bekræftelse.\nyesterday=I går\ngreen=Grøn\nyellow=Gul\nblue=Blå\nred=Rød\ncyan=Cyan\npurple=Lilla\nasktextAlertTitle=Spørg\nfileWriteSudoTitle=Sudo filskrivning\nfileWriteSudoContent=Den fil, du forsøger at skrive, giver ikke skriverettigheder til din bruger. Vil du skrive denne fil som root med sudo? Dette vil automatisk hæve niveauet til root med enten de eksisterende legitimationsoplysninger eller via en prompt.\ndontAllowTerminalRestart=Tillad ikke genstart af terminal\ndontAllowTerminalRestartDescription=Som standard kan terminalsessioner genstartes, når de er afsluttet inde fra terminalen. For at tillade dette accepterer XPipe disse eksterne anmodninger fra terminalen om at starte sessionen igen\\n\\nXPipe har ingen kontrol over terminalen, og hvor dette opkald kommer fra, så ondsindede lokale programmer kan også bruge denne funktion til at starte forbindelser gennem XPipe. Ved at deaktivere denne funktion forhindres dette scenarie.\nopenDocumentation=Åben dokumentation\nopenDocumentationDescription=Besøg XPipes dokumentationsside for dette problem\nrenameAll=Omdøb alle\nlogging=Logning\nenableTerminalLogging=Aktiver terminal-logning\nenableTerminalLoggingDescription=Aktiverer logning på klientsiden for alle terminalsessioner. Alle inputs og outputs fra terminalsessionen skrives ind i en sessionslogfil. Bemærk, at følsomme oplysninger som f.eks. adgangskodeprompter ikke registreres.\nterminalLoggingDirectory=Logfiler for terminalsessioner\nterminalLoggingDirectoryDescription=Alle logfiler gemmes i XPipe-databiblioteket på dit lokale system.\nopenSessionLogs=Åbne sessionslogfiler\nsessionLogging=Terminal-logning\nsessionActive=Der kører en baggrundssession for denne forbindelse.\\n\\nKlik på statusindikatoren for at stoppe denne session manuelt.\nskipValidation=Spring validering over\nscriptsIntroHeader=Om scripts\nscriptsIntroContent=Du kan køre scripts ved shell-init, i filbrowseren og efter behov. Du kan selv oprette scripts i XPipe eller importere eksisterende scripts fra dit lokale system eller fra et eksternt git-repository.\nscriptsIntroBottomHeader=Brug af scripts\nscriptsIntroBottomContent=Der er en række eksempler på scripts til at starte med. Du kan klikke på redigeringsknappen for de enkelte scripts for at se, hvordan de er implementeret. Scripts skal først aktiveres for at kunne køre og dukke op i menuer, der er et skifte på hvert script til det.\nscriptsIntroBottomButton=Kom godt i gang\nscriptSourcesIntroHeader=Script-kilder\nscriptSourcesIntroContent=Du kan tilføje brugerdefinerede scriptkilder for at få øjeblikkelig adgang til en hel samling af shell-scripts. Både lokale kilder og eksterne git-repositorier understøttes som kilder. Alle fundne scripts fra kilden bliver automatisk tilgængelige.\nscriptSourcesIntroButton=Tilføj kilde ...\ncheckForSecurityUpdates=Tjek for sikkerhedsopdateringer\ncheckForSecurityUpdatesDescription=XPipe kan tjekke for potentielle sikkerhedsopdateringer separat fra normale funktionsopdateringer. Når dette er aktiveret, vil i det mindste vigtige sikkerhedsopdateringer blive anbefalet til installation, selv om den normale opdateringskontrol er deaktiveret.\\n\\nHvis du deaktiverer denne indstilling, vil der ikke blive udført nogen ekstern versionsanmodning, og du vil ikke få besked om nogen sikkerhedsopdateringer.\nclickToDock=Klik for at docke terminalen\nterminalStarting=Venter på opstart af terminal ...\npinTab=Pin-fane\nunpinTab=Fjern fanebladet\npinned=Fastgjort\nenableConnectionHubTerminalDocking=Aktiver tilslutning af hub terminal docking\nenableConnectionHubTerminalDockingDescription=Du kan docke terminalvinduer til XPipe-applikationsvinduet i forbindelseshubben for at simulere en nogenlunde integreret terminal. Terminalvinduerne styres derefter af XPipe, så de altid passer ind i docken.\nenableFileBrowserTerminalDocking=Aktiver filbrowserens terminal-docking\nenableFileBrowserTerminalDockingDescription=Du kan docke terminalvinduer til XPipe-applikationsvinduet i filbrowseren for at simulere en nogenlunde integreret terminal. Terminalvinduerne styres derefter af XPipe, så de altid passer ind i docken.\ndownloadsDirectory=Brugerdefineret download-mappe\ndownloadsDirectoryDescription=Den brugerdefinerede mappe, som downloadede filer skal placeres i, når man klikker på knappen Flyt til downloads. Som standard vil XPipe bruge din brugers download-bibliotek.\npinLocalMachineOnStartup=Fastgør fane for lokal maskine ved opstart\npinLocalMachineOnStartupDescription=Åbn automatisk en fane på den lokale maskine og fastgør den. Dette er nyttigt, hvis du ofte bruger en delt filbrowser med den lokale maskine og fjernfilsystemet åbent.\nterminalErrorDescription=Denne fejl er terminal, og XPipe kan ikke fortsætte uden at løse den.\ngroupName=Gruppens navn\nchmodPermissions=Nye tilladelser\neditFilesWithDoubleClick=Rediger filer med dobbeltklik\neditFilesWithDoubleClickDescription=Når det er aktiveret, vil dobbeltklik på filer åbne dem direkte i din teksteditor i stedet for at vise kontekstmenuen.\ncensorMode=Censurtilstand\ncensorModeDescription=Udvisker alle oplysninger som værtsnavne, brugernavne, forbindelsesnavne m.m.\\n\\nDet er nyttigt, hvis du vil tage et screenshot eller dele XPipe og ikke ønsker at lække nogen oplysninger.\naddIdentity=Identitet ...\nidentities=Identiteter\naddMacro=Handling ...\nidentitiesIntroHeader=Om identiteter\nidentitiesIntroContent=Hvis du genbruger almindelige kombinationer af brugernavne, adgangskoder og nøgler, kan det give mening at oprette genanvendelige identiteter. På den måde kan du hurtigt henvise til dem, når du tilføjer nye forbindelser.\nidentitiesIntroBottomHeader=Deling af identiteter\nidentitiesIntroBottomContent=Du kan tilføje identiteter lokalt eller også synkronisere dem i git-arkivet, når dette er aktiveret. Det gør det muligt at dele identiteter selektivt på tværs af flere systemer og med andre teammedlemmer.\nidentitiesIntroBottomButton=Opsætning af synkronisering\nidentitiesIntroButton=Opret identitet\nuserName=Brugernavn\nuserAuth=Brugerbaseret password-godkendelse\ngroupAuth=Gruppebaseret hemmelig autentificering\nteam=Team\nteamSettings=Team-indstillinger\nteamVaults=Team-pengeskabe\nvaultTypeNameDefault=Standardboks\nvaultTypeNameLegacy=En ældre personlig boks\nvaultTypeNamePersonal=En personlig boks\nvaultTypeNameTeam=Holdets boks\nteamVaultsDescription=Team vaults giver flere brugere og grupper sikker adgang til en fælles vault. Du kan konfigurere forbindelser og identiteter, så de enten deles med alle brugere eller kun er tilgængelige for individuelle brugere og grupper ved at kryptere dem med deres egen nøgle. Andre vault-brugere kan ikke få adgang til personlige og gruppebaserede forbindelser og identiteter, hvis de ikke har adgang til nøglen.\nvaultTypeContentDefault=Du bruger i øjeblikket en standardboks uden bruger og med en brugerdefineret adgangssætning. Hemmeligheder krypteres med den lokale nøgle til boksen. Du kan opgradere til en personlig vault ved at oprette en vault-brugerkonto. Det giver dig mulighed for at kryptere hvælvingens hemmeligheder med din egen personlige adgangskode, som du skal indtaste ved hvert login for at låse hvælvingen op.\nvaultTypeContentLegacy=Du bruger i øjeblikket et ældre personligt pengeskab til din bruger. Hemmeligheder er krypteret med din personlige adgangskode. Denne ældre kompatibilitet har begrænsede funktioner og kan ikke opgraderes til en team vault på stedet.\nvaultTypeContentPersonal=Du bruger i øjeblikket et personligt pengeskab til din bruger. Hemmelighederne er krypteret med din personlige adgangskode. Du kan opgradere til en holdboks ved at tilføje flere boksbrugere eller tilføje en gruppebaseret adgangskonfiguration.\nvaultTypeContentTeam=Du bruger i øjeblikket et team vault, som giver flere brugere sikker adgang til et delt vault. Du kan konfigurere forbindelser og identiteter, så de enten deles med alle brugere eller kun er tilgængelige for din personlige bruger eller gruppe ved at kryptere dem med din personlige nøgle eller gruppenøgle. Andre vault-brugere kan ikke få adgang til dine personlige og gruppebaserede forbindelser og identiteter, hvis de ikke har adgang til nøglen.\ngroupManagement=Gruppestyring\ngroupManagementEmpty=Gruppestyring\ngroupManagementDescription=Administrer eksisterende vault-grupper eller opret nye. Hver vault-gruppe har sin egen individuelle hemmelige nøgle, som bruges til at kryptere forbindelser og identiteter, der kun skal være tilgængelige for gruppen og ikke for andre.\ngroupManagementEmptyDescription=Administrer eksisterende vault-grupper eller opret nye. Hver vault-gruppe har sin egen individuelle hemmelige nøgle, som bruges til at kryptere forbindelser og identiteter, der kun skal være tilgængelige for gruppen og ikke for andre.\\n\\nGruppebaserede konti for et team understøttes i den professionelle plan.\nuserManagement=Brugeradministration\nuserManagementEmpty=Brugeradministration\nuserManagementDescription=Administrer eksisterende vault-brugere eller opret nye. Hver vault-bruger har sin egen individuelle adgangskode, som bruges til at kryptere forbindelser og identiteter, der kun skal være tilgængelige for brugeren og ikke for andre.\nuserManagementEmptyDescription=Administrer eksisterende vault-brugere eller opret nye. Hver vault-bruger har sin egen individuelle adgangskode, som bruges til at kryptere forbindelser og identiteter, der kun skal være tilgængelige for brugeren og ikke for andre. Opret en bruger til dig selv for at kunne kryptere forbindelser og identiteter med din personlige nøgle.\\n\\nEn enkelt brugerkonto understøttes i community-udgaven. Flere brugerkonti til et team understøttes i den professionelle plan.\nuserIntroHeader=Brugeradministration\nuserIntroContent=Opret den første brugerkonto til dig selv for at komme i gang. Det giver dig mulighed for at låse dette arbejdsområde med en adgangskode.\naddReusableIdentity=Tilføj genanvendelig identitet\nusers=Brugere\nsyncVault=Vault-synkronisering\nsyncVaultDescription=For at synkronisere din hvælving på tværs af flere systemer eller med flere teammedlemmer skal du aktivere git-synkronisering for denne hvælving.\nenableGitSync=Aktiver git-synkronisering\nbrowseVault=Vault-data\nbrowseVaultDescription=Du kan selv tage et kig på vault-biblioteket i din oprindelige filhåndtering. Bemærk, at eksterne redigeringer ikke anbefales og kan forårsage en række problemer.\nbrowseVaultButton=Gennemse hvælving\nvaultUsers=Vault-brugere\ncreateHeapDump=Opret heap-dump\ncreateHeapDumpDescription=Dump hukommelsesindhold til fil for at fejlfinde hukommelsesforbrug\ninitializingApp=Indlæsning af forbindelser\ncheckingLicense=Kontrol af licens\nloadingGit=Synkronisering med git repo\nloadingGpg=Start af GnuPG-dæmon til git\nloadingSettings=Indlæsning af indstillinger\nloadingConnections=Indlæsning af forbindelser\nunlockingVault=Oplåsning af hvælving\nloadingUserInterface=Indlæsning af brugergrænseflade\nptbNotice=Meddelelse om den offentlige testversion\nuserDeletionTitle=Sletning af brugere\nuserDeletionContent=Vil du slette denne vault-bruger? Dette vil genkryptere alle dine personlige identiteter og forbindelseshemmeligheder ved hjælp af den vault-nøgle, der er tilgængelig for alle brugere. Det vil tage et stykke tid, og XPipe vil genstarte for at anvende brugerændringerne.\ngroupDeletionTitle=Sletning af gruppe\ngroupDeletionContent=Vil du slette denne vault-gruppe? Dette vil genkryptere alle gruppebeskyttede identiteter og forbindelseshemmeligheder ved hjælp af den hvælvingsnøgle, der er tilgængelig for alle brugere. Det vil tage et stykke tid, og XPipe vil genstarte for at anvende gruppeændringerne.\nkillTransfer=Kill transfer\ndestination=Destination\nconfiguration=Konfiguration\nnewFile=Ny fil\nnewLink=Nyt link\nlinkName=Navn på link\nscanConnections=Find tilgængelige forbindelser ...\nobserve=Begynd at observere\nstopObserve=Stop med at observere\ncreateShortcut=Opret genvej på skrivebordet\nbrowseFiles=Gennemse filer\nclone=Klon\ntargetPath=Målsti\nnewDirectory=Ny mappe\ncopyShareLink=Kopier link\nselectStore=Vælg butik\nsaveSource=Gem til senere\nexecute=Udfør\n#custom\ndeleteChildren=Fjern alle under-forbindelser\nscriptGroupDescriptionDescription=Giv denne gruppe en valgfri beskrivelse\nabstractHostDescriptionDescription=Giv denne host en valgfri beskrivelse\nselectSource=Vælg kilde\ncommandLineRead=Opdatering\ncommandLineWrite=Skriv\nadditionalOptions=Yderligere muligheder\ninput=Input\nmachine=Maskine\nopen=Åben\nedit=Rediger\nscriptContents=Scriptets indhold\nscriptContentsDescription=De scriptkommandoer, der skal udføres\nsnippets=Afhængighed af script\nsnippetsDescription=Andre scripts, der skal køres først\nsnippetsDependenciesDescription=Alle mulige scripts, der skal køres, hvis det er relevant\nisDefault=Kører på init i alle kompatible shells\n#custom\nbringToShells=Bring til alle kompatible shells\nisDefaultGroup=Kør alle gruppescripts på shell init\nexecutionType=Udførelsestype\nexecutionTypeDescription=I hvilke sammenhænge kan man bruge dette script\nminimumShellDialect=Shell-type\nminimumShellDialectDescription=Shell-typen til at køre dette script i\ndumbOnly=Dum\nterminalOnly=Terminal\nboth=Begge dele\nshouldElevate=Bør hæve\nshouldElevateDescription=Om dette script skal køres med forhøjede rettigheder\nscript.displayName=Shell-script\nscript.displayDescription=Opret et genanvendeligt shell-script\nscriptGroup.displayName=Script-gruppe\nscriptGroup.displayDescription=Gruppér scripts sammen og organisér dem inden for\nscriptGroup=Gruppe\nscriptGroupDescription=Den gruppe, der skal tildeles dette script\nscriptGroupGroupDescription=Den valgfrie overordnede gruppe at tildele denne scriptgruppe til\nopenInNewTab=Åbn i ny fane\nexecuteInBackground=i baggrunden\nexecuteInTerminal=i $TERM$\nback=Gå tilbage\nbrowseInWindowsExplorer=Gennemse i Windows Explorer\nbrowseInDefaultFileManager=Gennemse i standard filhåndtering\nbrowseInFinder=Gennemse i finder\ncopy=Kopier\npaste=Indsæt\ncopyLocation=Kopier placering\nabsolutePaths=Absolutte stier\nabsoluteLinkPaths=Absolutte link-stier\nabsolutePathsQuoted=Absolutte citerede stier\nfileNames=Filnavne\nlinkFileNames=Link til filnavne\nfileNamesQuoted=Filnavne (citeret)\ndeleteFile=Slet $FILE$\neditWithEditor=Rediger med $EDITOR$\nfollowLink=Følg link\ngoForward=Gå fremad\nshowDetails=Vis detaljer\nshowDetailsDescription=Vis stakspor af fejl\nopenFileWith=Åbn med ...\nopenWithDefaultApplication=Åbn med standardprogram\nrename=Omdøb\nrun=Kør\nopenInTerminal=Åbn i terminal\nfile=Fil\n#custom\ndirectory=Mappe\nsymbolicLink=Symbolsk link\n#custom\ndesktopEnvironment.displayName=Skrivebordsmiljø\ndesktopEnvironment.displayDescription=Opret en genanvendelig konfiguration af fjernskrivebordsmiljøet\ndesktopHost=Desktop-vært\ndesktopHostDescription=Den skrivebordsforbindelse, der skal bruges som base\ndesktopShellDialect=Shell-dialekt\ndesktopShellDialectDescription=Den shell-dialekt, der skal bruges til at køre scripts og programmer\ndesktopSnippets=Script-uddrag\ndesktopSnippetsDescription=Liste over genanvendelige scriptstumper, der skal køres først\ndesktopInitScript=Init-script\ndesktopInitScriptDescription=Init-kommandoer, der er specifikke for dette miljø\ndesktopTerminal=Terminal-applikation\ndesktopTerminalDescription=Den terminal, der skal bruges på skrivebordet til at starte scripts i\n#custom\ndesktopApplication.displayName=Skrivebordsprogram\ndesktopApplication.displayDescription=Kør et program på et fjernskrivebord\ndesktopBase=Skrivebord\ndesktopBaseDescription=Skrivebordet til at køre denne applikation på\n#custom\ndesktopEnvironmentBase=Skrivebordsmiljø\ndesktopEnvironmentBaseDescription=Skrivebordsmiljøet til at køre denne applikation på\ndesktopApplicationPath=Applikationssti\ndesktopApplicationPathDescription=Stien til den eksekverbare fil, der skal køres\ndesktopApplicationArguments=Argumenter\n#custom\ndesktopApplicationArgumentsDescription=De argumenter, der skal sendes til programmet#11-bibliotek\ndesktopCommand.displayName=Desktop-kommando\ndesktopCommand.displayDescription=Kør en kommando i et fjernskrivebordsmiljø\ndesktopCommandScript=Kommandoer\ndesktopCommandScriptDescription=De kommandoer, der skal køres i miljøet\nservice.displayName=Service\nservice.displayDescription=Videresend en fjernservice til din lokale maskine\nserviceLocalPort=Eksplicit lokal port\nserviceLocalPortDescription=Den lokale port, der skal videresendes til, ellers bruges en tilfældig port\nserviceRemotePort=Fjernport\nserviceRemotePortDescription=Den port, som tjenesten kører på\nserviceHost=Servicevært\nserviceHostDescription=Den vært, som tjenesten kører på\nopenWebsite=Åben hjemmeside\ncustomServiceGroup.displayName=Service-gruppe\ncustomServiceGroup.displayDescription=Gruppér flere tjenester i én kategori\ninitScript=Init-script - køres ved shell-init\nshellScript=Shell-sessionsscript - Gør et script tilgængeligt til at køre under en shell-session\nrunnableScript=Kørbart script - Tillad, at scriptet køres direkte fra connection hub'en\nfileScript=Filscript - Gør det muligt at kalde et script for udvalgte filer i filbrowseren\nrunScript=Kør script\ncopyUrl=Kopier URL\nfixedServiceGroup.displayName=Service-gruppe\nfixedServiceGroup.displayDescription=Liste over tilgængelige tjenester på et system\nmappedService.displayName=Service\nmappedService.displayDescription=Interagerer med en tjeneste, der er eksponeret af en container\ncustomService.displayName=Service\ncustomService.displayDescription=Åbn eller tunnelér automatisk en ekstern serviceport på din lokale maskine\nfixedService.displayName=Service\nfixedService.displayDescription=Brug en foruddefineret tjeneste\nnoServices=Ingen tilgængelige tjenester\nhasServices=$COUNT$ tilgængelige tjenester\nhasService=$COUNT$ tilgængelig tjeneste\nnoConnections=Ingen tilgængelige forbindelser\nhasConnections=$COUNT$ tilgængelige forbindelser\nhasConnection=$COUNT$ tilgængelig forbindelse\nopenHttp=Åben HTTP-tjeneste\nopenHttps=Åben HTTPS-tjeneste\nnoScriptsAvailable=Ingen aktiverede og kompatible scripts tilgængelige\nscriptsDisabled=Scripts deaktiveret\nchangeIcon=Skift ikon\ninit=Indlæg\nshell=Shell\nhub=Hub\nscript=script\ngenericScript=Generisk\ngradleTasks=Gradle-opgaver\nrunTask=Kør opgave\narchiveName=Arkivets navn\ncompress=Komprimere\ncompressContents=Komprimere indhold\nuntarHere=Untar her\nuntarDirectory=Untar to $DIR$\nunzipDirectory=Pak ud til $DIR$\nunzipHere=Pak ud her\nrequiresRestart=Kræver en genstart for at kunne anvendes.\ndownload=Download\nservicePath=Service-sti\nservicePathDescription=Den valgfri understi, når URL'en åbnes i en browser\nactive=Aktiv\ninactive=Inaktiv\nstarting=Starter\nremotePort=Fjernport\nremotePortNumber=Fjernport $PORT$\nuserIdentity=Personlig identitet\nglobalIdentity=Global identitet\nidentityChoice=Brugeridentitet\nidentityChoiceDescription=Vælg en foruddefineret identitet, eller angiv login-oplysninger kun for denne forbindelse\ndefineNewIdentityOrSelect=Indtast nyt eller vælg eksisterende\nlocalIdentity.displayName=Lokal identitet\nlocalIdentity.displayDescription=Opret en genanvendelig identitet til dette lokale skrivebord\nsyncedIdentity.displayName=Synkroniseret identitet\nsyncedIdentity.displayDescription=Opret en genanvendelig identitet, der synkroniseres på tværs af systemer\nlocalIdentity=Lokal identitet\nkeyNotSynced=Nøglefilen er ikke synkroniseret med git-arkivet endnu. Brug knappen add to git for nøglefilen for at tilføje den.\nusernameDescription=Brugernavnet til at logge ind med\nidentity.displayName=Identitet\nidentity.displayDescription=Opret en genanvendelig identitet til forbindelser\nlocal=Lokalt\nshared=Global\nuserDescription=Brugernavn eller foruddefineret identitet til at logge ind som\nidentityAccessLevel=Adgangsniveau\nidentityPerUser=Adgang til personlig identitet\nidentityPerUserDescription=Begræns adgangen til denne identitet og dens tilknyttede forbindelser til kun din vault-bruger\nidentityPerUserDisabled=Adgang til personlig identitet (deaktiveret)\nidentityPerUserDisabledDescription=Begræns adgangen til denne identitet og dens tilknyttede forbindelser til kun din vault-bruger (Kræver, at teamet er konfigureret)\nidentityPerGroup=Identitetsadgang kun for grupper\nidentityPerGroupDescription=Begræns adgangen til denne identitet og dens tilknyttede forbindelser til kun denne vault-gruppe\nlibrary=Bibliotek\nlocation=Placering\nkeyAuthentication=Nøglebaseret autentificering\nkeyAuthenticationDescription=Den autentificeringsmetode, der skal bruges, hvis nøglebaseret autentificering er påkrævet\nlocationDescription=Filstien til din tilsvarende private nøgle\nkeyFile=Lokal nøglefil\nkeyPassword=Passphrase\nkey=Nøgle\nyubikeyPiv=Yubikey PIV\npageant=Forestilling\ngpgAgent=GPG-agent\ncustomPkcs11Library=Brugerdefineret PKCS#11-bibliotek\nsshAgent=OpenSSH-agent\nnone=Ingen\nindex=Indeks ...\notherExternal=Anden ekstern agent\nsync=Synkronisering\nvaultSync=Vault-synkronisering\ncustomUsername=Brugernavn\ncustomUsernameDescription=Den valgfrie alternative bruger til at logge ind som\ncustomUsernamePassword=Adgangskode\ncustomUsernamePasswordDescription=Brugerens adgangskode, der skal bruges, når sudo-autentificering er påkrævet\nshowInternalPods=Vis interne pods\nshowAllNamespaces=Vis alle navnerum\n#custom\nshowInternalContainers=Vis interne containere\n#custom\nrefresh=Genindlæs\nvmwareGui=Start GUI\nmonitorVm=Overvåg VM\naddCluster=Tilføj klynge ...\nshowNonRunningInstances=Vis instanser, der ikke kører\nvmwareGuiDescription=Om en virtuel maskine skal startes i baggrunden eller i et vindue.\nvmwareEncryptionPassword=Adgangskode til kryptering\n#custom\nvmwareEncryptionPasswordDescription=Den adgangskode, der bruges til at kryptere VM'en.\nvmPasswordDescription=Den nødvendige adgangskode til gæstebrugeren.\nvmPassword=Brugerens adgangskode\nvmUser=Gæstebruger\nrunTempContainer=Kør en midlertidig container\nvmUserDescription=Brugernavnet på din primære gæstebruger\ndockerTempRunAlertTitle=Kør en midlertidig container\ndockerTempRunAlertHeader=Dette vil køre en shell-proces i en midlertidig container, som automatisk fjernes, når den stoppes.\nimageName=Billedets navn\nimageNameDescription=Den containerbillede-identifikator, der skal bruges\n#custom\ncontainerName=Navn på container\n#custom\ncontainerNameDescription=Det brugerdefinerede container navn\nvm=Virtuel maskine\nvmDescription=Den tilknyttede konfigurationsfil.\nvmwareScan=VMware desktop-hypervisorer\nvmwareMachine.displayName=VMware virtuel maskine\nvmwareMachine.displayDescription=Opret forbindelse til en virtuel maskine via SSH\nvmwareInstallation.displayName=Installation af VMware desktop hypervisor\nvmwareInstallation.displayDescription=Interagerer med de installerede VM'er via dens CLI\nstart=Start\nstop=Stop\npause=Pause\nrdpTunnelHost=Målvært\nrdpTunnelHostDescription=SSH-forbindelsen til at tunnelere RDP-forbindelsen til\nrdpTunnelUsername=Brugernavn\nrdpTunnelUsernameDescription=Den brugerdefinerede bruger at logge ind som, bruger SSH-brugeren, hvis den er tom\nrdpFileLocation=Filens placering\nrdpFileLocationDescription=Filstien til .rdp-filen\nrdpPasswordAuthentication=Godkendelse af adgangskode\nrdpFiles=RDP-filer\nrdpPasswordAuthenticationDescription=Adgangskoden, der skal udfyldes eller kopieres til udklipsholderen, afhængigt af klientunderstøttelsen\nrdpFile.displayName=RDP-fil\nrdpFile.displayDescription=Opret forbindelse til et system via en eksisterende .rdp-fil\nrequiredSshServerAlertTitle=Opsætning af SSH-server\nrequiredSshServerAlertHeader=Kan ikke finde en installeret SSH-server i VM'en.\nrequiredSshServerAlertContent=For at oprette forbindelse til VM'en leder XPipe efter en kørende SSH-server, men der blev ikke fundet nogen tilgængelig SSH-server til VM'en.\ncomputerName=Computerens navn\npssComputerNameDescription=Navnet på den computer, der skal oprettes forbindelse til\ncredentialUser=Legitimationsbruger\ncredentialUserDescription=Den bruger, man skal logge ind som.\ncredentialPassword=Adgangskode til legitimation\ncredentialPasswordDescription=Brugerens adgangskode.\nsshConfig=SSH-konfigurationsfiler\nautostart=Opret automatisk forbindelse ved opstart af XPipe\nacceptHostKey=Accepter værtsnøgle\nmodifyHostKeyPermissions=Ændre tilladelser til værtsnøgler\n#custom\nattachContainer=Fastgør til container\ncontainerLogs=Vis logfiler\nopenSftpClient=Åbn i en ekstern SFTP-klient\nopenTermius=Åbn i Termius\nshowInternalInstances=Vis interne forekomster\neditPod=Rediger pod\nacceptHostKeyDescription=Stol på den nye værtsnøgle, og fortsæt\nmodifyHostKeyPermissionsDescription=Forsøg på at fjerne tilladelser fra den oprindelige fil, så OpenSSH er tilfreds\npsSession.displayName=PowerShell-fjernsession\npsSession.displayDescription=Opret forbindelse via New-PSSession og Enter-PSSession\nsshLocalTunnel.displayName=Lokal SSH-tunnel\nsshLocalTunnel.displayDescription=Etablering af en SSH-tunnel til en ekstern vært\nsshRemoteTunnel.displayName=Ekstern SSH-tunnel\nsshRemoteTunnel.displayDescription=Etablering af en omvendt SSH-tunnel fra en ekstern vært\nsshDynamicTunnel.displayName=Dynamisk SSH-tunnel\nsshDynamicTunnel.displayDescription=Etablering af en SOCKS-proxy via en SSH-forbindelse\nshellEnvironmentGroup.displayName=Shell-miljøer\nshellEnvironmentGroup.displayDescription=Shell-miljøer\nshellEnvironment.displayName=Shell-miljø\nshellEnvironment.displayDescription=Opret et tilpasset shell-startmiljø\nshellEnvironment.informationFormat=$TYPE$ miljø\nshellEnvironment.elevatedInformationFormat=$ELEVATION$ $TYPE$ miljø\nenvironmentConnectionDescription=Basisforbindelsen til at skabe et miljø for\n#custom\nenvironmentScriptDescription=Det brugerdefinerede init-script, der skal køres i shellen\nenvironmentSnippets=Shell-scripts\n#custom\ncommandSnippetsDescription=De foruddefinerede scriptuddrag, der skal køres først\n#custom\nenvironmentSnippetsDescription=De foruddefinerede scriptuddrag, der skal køres ved initialisering\nshellTypeDescription=Den eksplicitte shell-type, der skal startes\noriginPort=Oprindelsesport\noriginAddress=Oprindelsesadresse\nremoteAddress=Ekstern adresse\nremoteSourceAddress=Ekstern kildeadresse\nremoteSourcePort=Ekstern kildeport\noriginDestinationPort=Oprindelses- og destinationsport\noriginDestinationAddress=Oprindelses- og destinationsadresse\norigin=Oprindelse\nremoteHost=Ekstern vært\naddress=Adresse\nproxmox.displayName=Proxmox\nproxmox.displayDescription=Opret forbindelse til systemer i et virtuelt Proxmox-miljø\nproxmoxVm.displayName=Proxmox VM\nproxmoxVm.displayDescription=Opret forbindelse til en virtuel maskine i en Proxmox VE via SSH\nproxmoxContainer.displayName=Proxmox-container\nproxmoxContainer.displayDescription=Opret forbindelse til en container i en Proxmox VE\nsshDynamicTunnel.hostDescription=Det system, der skal bruges som SOCKS-proxy\nsshDynamicTunnel.bindingDescription=Hvilke adresser tunnelen skal bindes til\nsshRemoteTunnel.hostDescription=Det system, hvorfra fjerntunnelen til oprindelsen skal startes\nsshRemoteTunnel.bindingDescription=Hvilke adresser tunnelen skal bindes til\nsshLocalTunnel.hostDescription=Systemet til at åbne tunnelen til\nsshLocalTunnel.bindingDescription=Hvilke adresser tunnelen skal bindes til\nsshLocalTunnel.localAddressDescription=Den lokale adresse, der skal bindes\nsshLocalTunnel.remoteAddressDescription=Den eksterne adresse, der skal bindes\ncmd.displayName=Kommando\ncmd.displayDescription=Udføre en vilkårlig kommando på et system\nk8sPod.displayName=Kubernetes Pod\nk8sPod.displayDescription=Opret forbindelse til en pod og dens containere via kubectl\nk8sContainer.displayName=Kubernetes-container\nk8sContainer.displayDescription=Åbn en shell til en container\nk8sCluster.displayName=Kubernetes-klynge\nk8sCluster.displayDescription=Opret forbindelse til en klynge og dens pods via kubectl\nsshTunnelGroup.displayName=SSH-tunneler\nsshTunnelGroup.displayCategory=Alle typer af SSH-tunneler\nlocal.displayName=Lokal maskine\nlocal.displayDescription=Skallen på den lokale maskine\ncygwin=Cygwin\nmsys2=MSYS2\ngitWindows=Git til Windows\ngitForWindows.displayName=Git til Windows\ngitForWindows.displayDescription=Få adgang til dit lokale Git for Windows-miljø\nmsys2.displayName=MSYS2\nmsys2.displayDescription=Adgang til skaller i dit MSYS2-miljø\ncygwin.displayName=Cygwin\ncygwin.displayDescription=Adgang til skaller i dit Cygwin-miljø\nnamespace=Navnerum\ngitVaultIdentityStrategy=Git SSH-identitet\ngitVaultIdentityStrategyDescription=Hvis du har valgt at bruge en SSH git-URL som fjernlager, og dit fjernlager kræver en SSH-identitet, skal du indstille denne mulighed.\\n\\nHvis du har angivet en HTTP-URL, kan du ignorere denne indstilling.\ndockerContainers=Docker-containere\ndockerCmd.displayName=docker CLI-klient\ndockerCmd.displayDescription=Få adgang til Docker-containere via docker CLI-klienten\nwslCmd.displayName=WSL-installation\nwslCmd.displayDescription=Få adgang til WSL-instanser via wsl CLI-klienten\nk8sCmd.displayName=kubectl-klient\nk8sCmd.displayDescription=Få adgang til Kubernetes-klynger via kubectl\n#custom\nk8sClusters=Kubernetes clusters\n#custom\nshells=Tilgængelige shells\n#custom\ninspectContainer=Inspicér container\ninspectContext=Undersøg\nk8sClusterNameDescription=Navnet på den kontekst, klyngen befinder sig i.\n#custom\npod=Pod\npodName=Pod-navn\nk8sClusterContext=Sammenhæng\nk8sClusterContextDescription=Navnet på den kontekst, klyngen befinder sig i\nk8sClusterNamespace=Navnerum\nk8sClusterNamespaceDescription=Det brugerdefinerede namespace eller standard namespace, hvis det er tomt\nk8sConfigLocation=Konfig-fil\nk8sConfigLocationDescription=Den brugerdefinerede kubeconfig-fil eller standardfilen, hvis den er tom\ninspectPod=Undersøg\nshowAllContainers=Vis containere, der ikke kører\nshowAllPods=Vis pods, der ikke kører\nk8sPodHostDescription=Den vært, som pod'en er placeret på\nk8sContainerDescription=Navnet på Kubernetes-containeren\nk8sPodDescription=Navnet på Kubernetes-poden\n#custom\npodDescription=Den pod, som containeren er placeret på\nk8sClusterHostDescription=Den vært, hvorigennem klyngen skal tilgås. Skal have kubectl installeret og konfigureret for at kunne få adgang til klyngen.\nconnection=Forbindelse\nshellCommand.displayName=Brugerdefineret shell-kommando\nshellCommand.displayDescription=Åbn en standard shell gennem en brugerdefineret kommando\nssh.displayName=SSH-forbindelse\nssh.displayDescription=Opret forbindelse til et eksternt system via SSH-kommandolinjeklienten\nsshConfig.displayName=SSH-konfigurationsfil\nsshConfig.displayDescription=Forbind til værter defineret i en SSH-konfigurationsfil\nsshConfigHost.displayName=SSH-konfigurationsfil host\nsshConfigHost.displayDescription=Forbind til en vært, der er defineret i en SSH-konfigurationsfil\nsshConfigHost.password=Adgangskode\n#custom\nsshConfigHost.passwordDescription=Angiv adgangskoden til brugerlogin.\nsshConfigHost.identityPassphrase=Nøgle-passphrase\n#custom\nsshConfigHost.identityPassphraseDescription=Angiv adgangsætningen for din identitetsnøgle.\nshellCommand.hostDescription=Værten, som kommandoen skal udføres på\nshellCommand.commandDescription=Den kommando, der åbner en shell\ncommandType=Kommandotype\ncommandTypeDescription=Sådan udføres kommandoen\ncommandDescription=De brugerdefinerede kommandoer, der skal udføres på værten\ncommandHostDescription=Værten, som kommandoen skal køres på\ncommandDataFlowDescription=Hvordan denne kommando håndterer input og output\ncommandElevationDescription=Kør denne kommando med forhøjede rettigheder\ncommandShellTypeDescription=Den shell, der skal bruges til denne kommando\nlimitedSystem=Dette er et begrænset eller indlejret system\nlimitedSystemDescription=Forsøg ikke at identificere shell-typen, hvilket er nødvendigt for begrænsede indlejrede systemer eller IOT-enheder\nsshForwardX11=Forward X11\nsshForwardX11Description=Aktiverer X11-videresendelse for forbindelsen\ncustomAgent=Brugerdefineret agent\nidentityAgent=Identitetsagent\n#custom\nssh.proxyDescription=Den proxyvært, der skal bruges, når SSH-forbindelsen oprettes. Skal have en ssh-klient installeret.\nusage=Anvendelse\nwslHostDescription=Den vært, som WSL-instansen er placeret på. Skal have wsl installeret.\nwslDistributionDescription=Navnet på WSL-instansen\nwslUsernameDescription=Det eksplicitte brugernavn, der skal logges ind med. Hvis det ikke er angivet, bruges standardbrugernavnet.\nwslPasswordDescription=Brugerens adgangskode, som kan bruges til sudo-kommandoer.\ndockerHostDescription=Den vært, som docker-containeren er placeret på. Skal have docker installeret.\ndockerContainerDescription=Navnet på docker-containeren\nlocalMachine=Lokal maskine\nrootScan=Sudo shell-miljø\nloginEnvironmentScan=Brugerdefineret login-miljø\nk8sScan=Kubernetes-klynge\noptions=Valgmuligheder\ndockerRunningScan=Kører docker-containere\ndockerAllScan=Alle docker-containere\nwslScan=WSL-instanser\nsshScan=SSH-konfigurationsforbindelser\nrunAsUser=Kør som bruger\nrunAsUserDescription=Start dette shell-miljø som en anden bruger\ndefault=Standard\nadministrator=Administrator\nwslHost=WSL-vært\ntimeout=Timeout\ninstallLocation=Installer placering\ninstallLocationDescription=Den placering, hvor dit $NAME$ -miljø er installeret\nwsl.displayName=Windows Subsystem til Linux\nwsl.displayDescription=Opret forbindelse til en WSL-instans, der kører på Windows\ndocker.displayName=Docker-container\ndocker.displayDescription=Opret forbindelse til en docker-container\nport=Port\nuser=Bruger\npassword=Adgangskode\nmethod=Metode\nuri=URL\nproxy=Proxy\n#custom\ndistribution=Styresystem\nusername=Brugernavn\nshellType=Shell-type\nbrowseFile=Gennemse fil\nopenShell=Åbn shell i terminal\nopenCommand=Udfør en kommando i en terminal\neditFile=Rediger fil\ndescription=Beskrivelse\nfurtherCustomization=Yderligere tilpasning\nfurtherCustomizationDescription=Brug ssh-konfigurationsfilerne for at få flere konfigurationsmuligheder\nbrowse=Gennemse\nconfigHost=Vært\nconfigHostDescription=Den vært, som konfigurationen er placeret på\nconfigLocation=Konfig-placering\nconfigLocationDescription=Filstien til konfigurationsfilen\ngateway=Gateway\n#custom\ngatewayDescription=Den gateway, der skal bruges når der oprettes forbindelse.\nconnectionInformation=Information om forbindelse\nconnectionInformationDescription=Hvilket system der skal oprettes forbindelse til\npasswordAuthentication=Godkendelse af adgangskode\npasswordAuthenticationDescription=Den valgfri adgangskode, der skal bruges til at godkende\nsshConfigString.displayName=Konfig-baseret SSH-forbindelse\nsshConfigString.displayDescription=Opret en fuldt tilpasset SSH-forbindelse i SSH config-formatet\nsshConfigStringContent=Konfiguration\nsshConfigStringContentDescription=SSH-indstillinger for forbindelsen i OpenSSH-konfigurationsformatet\nvnc.displayName=VNC-forbindelse over SSH\nvnc.displayDescription=Åbn en VNC-session over en tunnelforbindelse\nbinding=Binding\nvncPortDescription=Den port, VNC-serveren lytter på\nrdpPortDescription=Den port, RDP-serveren lytter på\nvncUsername=Brugernavn\n#custom\nvncUsernameDescription=VNC-brugernavnet\nvncPassword=Adgangskode\nvncPasswordDescription=VNC-adgangskoden\nx11WslInstance=X11 Forward WSL-instans\nx11WslInstanceDescription=Den lokale Windows Subsystem for Linux-distribution, der skal bruges som X11-server ved brug af X11-forwarding i en SSH-forbindelse. Denne distribution skal være en WSL2-distribution.\nopenAsRoot=Åbn som root\nopenInWSL=Åbn i WSL\nlaunch=Start\nsshTrustKeyContent=Værtsnøglen er ikke kendt, og du har aktiveret manuel bekræftelse af værtsnøglen. $CONTENT$\nsshTrustKeyTitle=Ukendt værtsnøgle\nrdpTunnel.displayName=RDP-forbindelse over SSH\nrdpTunnel.displayDescription=Opret forbindelse via RDP over en tunnelforbindelse\nrdpEnableDesktopIntegration=Aktiver desktop-integration\nrdpEnableDesktopIntegrationDescription=Køre fjernprogrammer under forudsætning af, at RDP-tilladelseslisten tillader det\nrdpSetupAdminTitle=RDP-opsætning påkrævet\nrdpSetupAllowTitle=RDP-fjernbetjeningsapplikation\nrdpSetupAllowContent=Det er i øjeblikket ikke tilladt at starte fjernprogrammer direkte på dette system. Ønsker du at aktivere det? Dette giver dig mulighed for at køre dine fjernprogrammer direkte fra XPipe ved at deaktivere tilladelseslisten for RDP-fjernprogrammer.\nrdpServerEnableTitle=RDP-server\nrdpServerEnableContent=RDP-serveren er deaktiveret på målsystemet. Vil du aktivere den i registreringsdatabasen for at tillade RDP-fjernforbindelser?\nrdp=RDP\nrdpScan=RDP-tunnel over SSH\nwslX11SetupTitle=WSL X11-opsætning\nwslX11SetupContent=XPipe kan bruge din lokale WSL-distribution til at fungere som en X11-skærmserver. Vil du gerne sætte X11 op på $DIST$? Dette vil installere de grundlæggende X11-pakker på WSL-distributionen og kan tage et stykke tid. Du kan også ændre, hvilken distribution der bruges i indstillingsmenuen.\ncommand=Kommando\ncommandGroup=Kommandogruppe\nvncSystem=VNC-målsystem\nvncSystemDescription=Det faktiske system, der skal interageres med. Dette er normalt det samme som tunnelværten\nvncHost=Mål VNC-vært\nvncHostDescription=Det system, som VNC-serveren kører på\nvncDirectHost=Vært\nvncDirectHostDescription=Værtsposten eller den manuelle adresse på den server, som VNC-serveren kører på\nrdpDirectHost=Vært\nrdpDirectHostDescription=Værtsposten eller den manuelle adresse på den server, som RDP-serveren kører på\n#custom\ngitVaultTitle=Git-Vault\ngitVaultForcePushContent=Vil du gennemtvinge push til fjernarkivet? Dette vil fuldstændig erstatte alt indhold i fjernarkivet med dit lokale, inklusive historikken.\ngitVaultOverwriteLocalContent=Vil du tilsidesætte dine lokale vault-ændringer? Dette vil anvende alle fjernændringer i dit lokale repository.\nrdpSimple.displayName=Direkte RDP-forbindelse\nrdpSimple.displayDescription=Opret forbindelse til en vært via RDP\nrdpUsername=Brugernavn\nrdpUsernameDescription=Den bruger, der skal logges ind som. Kan indeholde et domænepræfiks\naddressDescription=Hvor skal man oprette forbindelse til\nrdpAdditionalOptions=Yderligere RDP-muligheder\nrdpAdditionalOptionsDescription=Rå RDP-muligheder, der skal inkluderes, formateret på samme måde som i .rdp-filer\nproxmoxVncConfirmTitle=VNC adgang\nproxmoxVncConfirmContent=Vil du aktivere VNC-adgang for VM'en? Dette vil aktivere direkte VNC-klientadgang i VM-konfigurationsfilen og genstarte den virtuelle maskine.\ndockerContext.displayName=Docker-kontekst\ndockerContext.displayDescription=Interagerer med containere placeret i en specifik kontekst\nvmActions=VM-handlinger\ndockerContextActions=Kontekst-handlinger\nk8sPodActions=Pod-handlinger\nopenVnc=Aktiver VNC-adgang\naddVnc=Tilføj VNC-forbindelse\ncommandGroup.displayName=Kommandogruppe\ncommandGroup.displayDescription=Grupper tilgængelige kommandoer for et system\nserial.displayName=Seriel forbindelse\nserial.displayDescription=Åbn en seriel forbindelse i en terminal\nserialPort=Seriel port\nserialPortDescription=Den serielle port/enhed, der skal oprettes forbindelse til\nbaudRate=Baud-hastighed\ndataBits=Data-bits\nstopBits=Stop-bits\nparity=Paritet\nflowControlWindow=Flow-kontrol\nserialImplementation=Seriel implementering\nserialImplementationDescription=Det værktøj, der skal bruges til at oprette forbindelse til den serielle port\nserialHost=Vært\nserialHostDescription=Systemet til at få adgang til den serielle port på\nserialPortConfiguration=Konfiguration af seriel port\nserialPortConfigurationDescription=Konfigurationsparametre for den tilsluttede serielle enhed\nserialInformation=Seriel information\nopenXShell=Åbn i XShell\ntsh.displayName=Teleport\ntsh.displayDescription=Opret forbindelse til dine teleport-noder via tsh\ntshNode.displayName=Teleport-knudepunkt\ntshNode.displayDescription=Opret forbindelse til en teleport-node i en klynge\nteleportCluster=Klynge\nteleportClusterDescription=Den klynge, som noden befinder sig i\nteleportProxy=Proxy\nteleportProxyDescription=Den proxyserver, der bruges til at oprette forbindelse til noden\nteleportHost=Vært\nteleportHostDescription=Værtsnavnet på noden\nteleportUser=Bruger\nteleportUserDescription=Den bruger, du skal logge ind som\nlogin=Login\nhyperVInstall.displayName=Hyper-V\nhyperVInstall.displayDescription=Opret forbindelse til VM'er, der administreres af Hyper-V\nhyperVVm.displayName=Hyper-V VM\nhyperVVm.displayDescription=Opret forbindelse til en Hyper-V VM via SSH eller PSSession\ntrustHost=Tillidsvært\ntrustHostDescription=Tilføj ComputerName til listen over betroede værter\ncopyIp=Kopier IP\nvncDirect.displayName=Direkte VNC-forbindelse\nvncDirect.displayDescription=Opret direkte forbindelse til et system via VNC\neditConfiguration=Rediger konfiguration\nviewInDashboard=Visning i dashboard\nsetDefault=Indstil standard\nremoveDefault=Fjern standard\nconnectAsOtherUser=Opret forbindelse som anden bruger\nprovideUsername=Giv et alternativt brugernavn til at logge ind med\nvmIdentity=Gæsteidentitet\nvmIdentityDescription=Den SSH-identitetsgodkendelsesmetode, der skal bruges til at oprette forbindelse, hvis det er nødvendigt\nvmPort=Port\nvmPortDescription=Den port, der skal oprettes forbindelse til via SSH\nforwardAgent=Fremadrettet agent\nforwardAgentDescription=Gør SSH-agent-identiteter tilgængelige på fjernsystemet\nvirshUri=URI\nvirshUriDescription=Hypervisor-URI'en, aliaser understøttes også\nvirshDomain.displayName=libvirt-domæne\nvirshDomain.displayDescription=Opret forbindelse til et libvirt-domæne\nvirshHypervisor.displayName=libvirt-hypervisor\nvirshHypervisor.displayDescription=Opret forbindelse til en libvirt-understøttet hypervisor-driver\nvirshInstall.displayName=libvirt-kommandolinjeklient\nvirshInstall.displayDescription=Opret forbindelse til alle tilgængelige libvirt-hypervisorer via virsh\naddHypervisor=Tilføj hypervisor\ninteractiveTerminal=Interaktiv terminal\neditDomain=Rediger domæne\nlibvirt=libvirt-domæner\ncustomIp=Brugerdefineret IP\ncustomIpDescription=Tilsidesæt den lokale VM-IP-standardregistrering, hvis du bruger avanceret netværk\nautomaticallyDetect=Registrerer automatisk\nuserAddDialogTitle=Oprettelse af bruger\ngroupAddDialogTitle=Oprettelse af gruppe\npassphrase=Passphrase\nrepeatPassphrase=Gentag passphrase\ngroupSecret=Gruppe-hemmelighed\nrepeatGroupSecret=Gentag gruppens hemmelighed\nvaultGroup=Vault-gruppe\nloginAlertTitle=Login påkrævet\nloginAlertHeader=Lås boksen op for at få adgang til dine personlige forbindelser\nvaultUser=Vault-bruger\nme=Mig\naddGroup=Tilføj gruppe ...\naddGroupDescription=Opret en ny gruppe til dette pengeskab\naddUser=Tilføj bruger ...\naddUserDescription=Opret en ny bruger til dette pengeskab\nskip=Spring over\nuserChangePasswordAlertTitle=Ændring af adgangskode\ngroupChangeSecretAlertTitle=Hemmelig ændring\ndocs=Dokumentation\nlxd.displayName=LXD-container\nlxd.displayDescription=Opret forbindelse til en LXD-container via lxc\nlxdCmd.displayName=LXD CLI-klient\nlxdCmd.displayDescription=Få adgang til LXD-containere via lxc CLI-klienten\npodman.displayName=Podman-container\npodman.displayDescription=Opret forbindelse til en Podman-container\nincusInstall.displayName=Incus maskine manager\nincusInstall.displayDescription=Adgang til incus-containere via incus CLI-klienten\nincusContainer.displayName=Incus-beholder\nincusContainer.displayDescription=Forbind til en incus-container\npodmanCmd.displayName=Podman CLI\npodmanCmd.displayDescription=Få adgang til Podman-containere via CLI-klienten\nlxdHostDescription=Den vært, som LXD-containeren er placeret på. Skal have lxc installeret.\nlxdContainerDescription=Navnet på LXD-containeren\npodmanContainers=Podman-containere\nlxdContainers=LXD-containere\nincusContainers=Incus-beholdere\n#custom\ncontainer=Container\nhost=Vært\ncontainerActions=Container-handlinger\nserialConsole=Seriel konsol\neditRunConfiguration=Rediger kørekonfiguration\ncommunityDescription=Et forbindelsesværktøj, der er perfekt til din personlige brug.\nupgradeDescription=Professionel forbindelsesstyring til hele din serverinfrastruktur.\ndiscoverPlans=Opdag opgraderingsmuligheder\nextendProfessional=Opgrader til de nyeste professionelle funktioner\ncommunityItem1=Ubegrænsede forbindelser til ikke-kommercielle systemer og værktøjer\ncommunityItem2=Sømløs integration med dine installerede terminaler og editorer\ncommunityItem3=Fuldt udstyret ekstern filbrowser\ncommunityItem4=Kraftfuldt scripting-system til alle shells\ncommunityItem5=Git-integration til synkronisering og deling af forbindelsesoplysninger\nupgradeItem1=Inkluderer alle funktioner i community-udgaven\nupgradeItem2=Homelab-planen understøtter ubegrænsede hypervisorer og avancerede SSH-funktioner\nupgradeItem3=Professional-planen understøtter desuden virksomhedsoperativsystemer og -værktøjer\nupgradeItem4=Enterprise-planen kommer med fuld fleksibilitet til din individuelle brugssag\nupgrade=Opgradering\nupgradeTitle=Tilgængelige planer\nstatus=Status\ntype=Type\nlicenseAlertTitle=Licens påkrævet\nuseCommunity=Fortsæt med fællesskab\npreviewDescription=Prøv nye funktioner i et par uger efter udgivelsen.\ntryPreview=Aktiver forhåndsvisning\npreviewItem1=Fuld adgang til nyudgivne professionelle funktioner i 2 uger efter udgivelsen\npreviewItem2=Prøv nye funktioner uden nogen forpligtelse\nlicensedTo=Licenseret til\n#custom\nemail=E-mailadresse\napply=Anvend\nclear=Slet\nactivate=Aktiver\nvalidUntil=Gyldig indtil\nlicenseActivated=Licens aktiveret\nrestart=Genstart\nlockVault=Låseboks\nrestartApp=Genstart XPipe\nfree=Gratis\nupgradeInfo=Du kan finde oplysninger om opgradering til en licens nedenfor.\nupgradeInfoPreview=Du kan finde oplysninger om opgradering til en licens nedenfor eller prøve forhåndsvisningen.\nenterLicenseKey=Indtast licensnøgle for at opgradere\nisOnlySupported=understøttes kun med mindst en $TYPE$ -licens\nareOnlySupported=understøttes kun med mindst en $TYPE$ -licens\nlegacyLicense=Denne licens omfatter kun nye Professional-funktioner, der udgives inden for et år efter købet.\npreviewExpiredLicense=Denne funktion var for nylig gratis tilgængelig i en forhåndsvisning, men denne periode er nu udløbet.\nopenApiDocs=API-dokumentation\nopenApiDocsDescription=HTTP API-dokumentationen er tilgængelig online, inklusive en OpenAPI .yaml-specifikation. Du kan åbne den i din webbrowser eller din foretrukne HTTP-klient.\nopenApiDocsButton=Åbn dokumenter\npythonApi=Python API\npersonalConnection=Denne forbindelse og alle dens børn er kun tilgængelige for din bruger, da de afhænger af en personlig identitet.\ndeveloperPrintInitFiles=Udskrivning af init-fil\ndeveloperPrintInitFilesDescription=Udskriv alle shell-init-scripts, der køres, når en terminal startes.\ndeveloperShowSensitiveCommands=Log følsomme kommandoer\ndeveloperShowSensitiveCommandsDescription=Inkluder følsomme kommandoer i logoutput til fejlfinding.\ncheckingForUpdates=Tjekker for opdateringer\ncheckingForUpdatesDescription=Henter oplysninger om seneste udgivelse\ndownloadingUpdate=Hentning af udgivelse (Version $VERSION$)\ndownloadingUpdateDescription=Download af udgivelsespakke\nupdateNag=Du har ikke opdateret XPipe i et stykke tid. Du går måske glip af nye funktioner og rettelser i nyere udgivelser.\nupdateNagTitle=Påmindelse om opdatering\nupdateNagButton=Se udgivelser\nrefreshServices=Opdater tjenester\nserviceProtocolType=Type serviceprotokol\nserviceProtocolTypeDescription=Kontrollerer, hvordan man åbner tjenesten\nserviceCommand=Den kommando, der skal køres, når tjenesten er aktiv\nserviceCommandDescription=Pladsholderen $PORT erstattes med den faktiske tunnelerede lokale port\nvalue=Værdi\nshowAdvancedOptions=Vis avancerede indstillinger\nsshAdditionalConfigOptions=Yderligere konfigurationsmuligheder\nremoteFileManager=Ekstern filhåndtering\nclearUserData=Sletning af brugerdata\nclearUserDataDescription=Slet alle brugerkonfigurationsdata, herunder forbindelser\nclearUserDataTitle=Sletning af brugerdata\nclearUserDataContent=Dette vil slette alle lokale brugerdata for xpipe og genstarte. Hvis du er interesseret i dine forbindelser, skal du sørge for at synkronisere dem først med et git-repository.\nundefined=Udefineret\ncopyAddress=Kopier adresse\nnetbirdDeviceScan=Netbird-forbindelser\nnetbirdId=Peer offentlig nøgle\nnetbirdIdDescription=Den interne netbird offentlige nøgle-id for peer'en\ntailscaleDeviceScan=Tailscale-forbindelser\ntailscaleInstall.displayName=Tailscale-installation\ntailscaleInstall.displayDescription=Opret forbindelse til enheder i dit tailnet via SSH\ntailscaleDevice.displayName=Tailscale-enhed\ntailscaleDevice.displayDescription=Opret forbindelse til en enhed i dit tailnet via SSH\ntailscaleId=Enheds-id\ntailscaleIdDescription=Det interne tailscale-enheds-ID\ntailscaleHostName=Værtsnavn\ntailscaleHostNameDescription=Værtsnavnet på enheden i tailnet\ntailscaleUsername=Brugernavn\ntailscaleUsernameDescription=Den bruger, du skal logge ind som\ntailscalePassword=Adgangskode\ntailscalePasswordDescription=Den valgfri brugeradgangskode, der kan bruges til sudo\nscriptName=Navn på script\nscriptNameDescription=Giv dette script et brugerdefineret navn\nscriptGroupName=Navn på scriptgruppe\nscriptGroupNameDescription=Giv denne scriptgruppe et brugerdefineret navn\nidentityName=Identitetsnavn\nidentityNameDescription=Giv denne identitet et brugerdefineret navn\ntailscaleTailnet.displayName=Tailnet\ntailscaleTailnet.displayDescription=Opret forbindelse til et specifikt tailnet med din konto\nputtyConnections=PuTTY-forbindelser\nkittyConnections=KiTTY-forbindelser\nicons=Ikoner\ncustomIcons=Brugerdefinerede ikoner\niconSources=Ikon-kilder\niconSourcesDescription=Du kan tilføje dine egne kilder til ikoner her. XPipe henter alle .svg-filer på den tilføjede placering og føjer dem til det tilgængelige sæt af ikoner.\\n\\nBåde lokale mapper og eksterne git-repositories understøttes som ikonplaceringer.\nrefreshSources=Opdater ikoner\nrefreshSourcesDescription=Opdater alle ikoner fra de tilgængelige kilder\naddDirectoryIconSource=Tilføj bibliotekskilde ...\naddDirectoryIconSourceDescription=Tilføj ikoner fra en lokal mappe\naddGitIconSource=Tilføj git-kilde ...\naddGitIconSourceDescription=Tilføj ikoner placeret i et eksternt git-arkiv\nrepositoryUrl=URL til Git-arkiv\niconDirectory=Ikon-katalog\naddUnsupportedKexMethod=Tilføj ikke-understøttet nøgleudvekslingsmetode\naddUnsupportedKexMethodDescription=Tillad, at nøgleudvekslingsmetoden $VAL$ bruges til denne forbindelse\naddUnsupportedHostKeyType=Tilføj ikke-understøttet værtsnøgletype\naddUnsupportedHostKeyTypeDescription=Tillad, at værtsnøgletypen $VAL$ bruges til denne forbindelse\naddUnsupportedMacType=Tilføj ikke-understøttet MAC-type\naddUnsupportedMacTypeDescription=Tillad, at MAC-typen $VAL$ bruges til denne forbindelse\nrunSilent=lydløst i baggrunden\nrunInFileBrowser=i en filbrowser\nrunInConnectionHub=i forbindelseshub\ncommandOutput=Kommando-output\niconSourceDeletionTitle=Kilde til slette-ikon\niconSourceDeletionContent=Vil du slette denne ikonkilde og alle tilknyttede ikoner?\nrefreshIcons=Opdater ikoner\nrefreshIconsDescription=Hentning, rendering og caching af alle tilgængelige 1000+ ikoner fra eksterne kilder til .png-filer. Det kan tage et stykke tid ...\nvaultUserLegacy=Vault-bruger (begrænset legacy-kompatibilitetstilstand)\nupgradeInstructions=Instruktioner til opgradering\nexternalActionTitle=Anmodning om ekstern handling\nexternalActionContent=Der blev anmodet om en ekstern handling. Vil du tillade start af handlinger uden for XPipe?\nnoScriptStateAvailable=Opdater for at bestemme scriptkompatibilitet ...\ndocumentationDescription=Tjek dokumentationen ud\ncustomEditorCommandInTerminal=Kør en brugerdefineret kommando i en terminal\ncustomEditorCommandInTerminalDescription=Hvis din editor er terminalbaseret, kan du aktivere denne mulighed for automatisk at åbne en terminal og køre kommandoen i terminalsessionen i stedet.\\n\\nDu kan bruge denne indstilling til editorer som vi, vim, nvim og andre.\ndisableHttpsTlsCheck=Deaktiver verifikation af HTTPS-anmodningscertifikater\ndisableHttpsTlsCheckDescription=Hvis din organisation dekrypterer din HTTPS-trafik i firewalls ved hjælp af SSL-aflytning, vil enhver opdateringskontrol eller licenskontrol mislykkes, fordi certifikaterne ikke stemmer overens. Du kan løse dette ved at aktivere denne indstilling og deaktivere TLS-certifikatvalidering.\nconnectionsSelected=$NUMBER$ forbindelser valgt\naddConnections=Tilføj forbindelser\nbrowseDirectory=Gennemse bibliotek\nopenTerminal=Åben terminal\ndocumentation=Dokumentation\nreport=Rapporter fejl\nkeePassXcNotAssociated=KeePassXC-link\nkeePassXcNotAssociatedDescription=XPipe er ikke forbundet med din lokale KeePassXC-database. Klik nedenfor for at udføre det engangstrin, hvor XPipe tilknyttes KeePassXC-databasen, så XPipe kan forespørge på adgangskoder.\nkeePassXcAssociateMore=Forbind flere databaser\nkeePassXcAssociateMoreDescription=Du kan være forbundet til flere KeePassXC-databaser på samme tid\nkeePassXcAssociated=KeePassXC-links\nkeePassXcAssociatedDescription=XPipe er forbundet med følgende lokale KeePassXC-databaser:\nkeePassXcNotAssociatedButton=Link-database\nidentifier=Identifikator\npasswordManagerCommand=Brugerdefineret kommando\npasswordManagerCommandDescription=Den brugerdefinerede kommando, der skal udføres for at hente adgangskoder. Pladsholderstrengen $KEY vil blive erstattet af den citerede adgangskode, når den kaldes. Dette bør kalde din password manager CLI for at udskrive adgangskoden til stdout, f.eks. mypassmgr get $KEY.\nchooseTemplate=Vælg skabelon\nkeePassXcPlaceholder=URL til KeePassXC-indgang\nterminalEnvironment=Terminal-miljø\nterminalEnvironmentDescription=Hvis du vil bruge funktioner i et lokalt Linux-baseret WSL-miljø til din terminaltilpasning, kan du bruge dem som terminalmiljø.\\n\\nAlle brugerdefinerede terminal-init-kommandoer og terminal-multiplexer-konfigurationer vil så blive kørt i denne WSL-distribution.\nterminalInitScript=Terminal init-script\nterminalInitScriptDescription=Kommandoer, der skal køres i terminalmiljøet, før forbindelsen startes. Du kan bruge dette til at konfigurere terminalmiljøet ved opstart.\nterminalMultiplexer=Terminal-multiplexer\nterminalMultiplexerDescription=Terminalmultiplexer til brug som et alternativ til faneblade i en terminal. Dette vil erstatte visse terminalhåndteringsegenskaber, f.eks. fanebladshåndtering, med multiplexerfunktionaliteten.\\n\\nKræver, at den respektive eksekverbare multiplexer er installeret på systemet.\nterminalMultiplexerWindowsDescription=Terminalmultiplexer til brug som et alternativ til faneblade i en terminal. Dette vil erstatte visse terminalhåndteringsegenskaber, f.eks. fanebladshåndtering, med multiplexerfunktionaliteten.\\n\\nKræver brug af et WSL-terminalmiljø på Windows, og at den eksekverbare multiplexer-fil er installeret på WSL-systemet.\nterminalAlwaysPauseOnExit=Hold altid pause ved afslutning\nterminalAlwaysPauseOnExitDescription=Når den er aktiveret, vil du altid blive bedt om enten at genstarte eller lukke sessionen, når du afslutter en terminalsession. Hvis det er deaktiveret, vil XPipe kun gøre det for mislykkede forbindelser, der afsluttes med en fejl.\nquerying=Forespørgsel ...\nretrievedPassword=Opnået: $PASSWORD$\nrefreshOpenpubkey=Opdater openpubkey-identitet\nrefreshOpenpubkeyDescription=Kør opkssh refresh for at gøre openpubkey-identiteten gyldig igen\nall=Alle\nterminalPrompt=Terminal-prompt\nterminalPromptDescription=Det terminalprompt-værktøj, der skal bruges i dine fjernterminaler. Hvis du aktiverer en terminalprompt, opsættes og konfigureres prompt-værktøjet automatisk på målsystemet, når du åbner en terminalsession.\\n\\nDette ændrer ikke eksisterende prompt-konfigurationer eller profilfiler på et system. Det vil øge terminalens indlæsningstid den første tid, mens prompten er ved at blive sat op på fjernsystemet. Din terminal har muligvis brug for ekstra skrifttyper for at vise prompten korrekt.\nterminalPromptConfiguration=Konfiguration af terminalprompt\nterminalPromptConfig=Konfig-fil\nterminalPromptConfigDescription=Den brugerdefinerede konfigurationsfil, der skal anvendes på prompten. Denne konfiguration bliver automatisk sat op på målsystemet, når terminalen initialiseres, og bruges som standardkonfiguration for prompten.\\n\\nHvis du vil bruge den eksisterende standardkonfigurationsfil på hvert system, kan du lade dette felt være tomt.\npasswordManagerKey=Nøgle til adgangskodehåndtering\npasswordManagerKeyDescription=Adgangskodeadministratorens identifikation af hemmeligheden\npasswordManagerAgent=Password manager-agent\ndockerComposeProject.displayName=Docker compose-projektet\ndockerComposeProject.displayDescription=Grupper containere i et compose-projekt sammen\nsshVerboseOutput=Aktiver verbose SSH-output\nsshVerboseOutputDescription=Dette udskriver en masse fejlfindingsoplysninger, når der oprettes forbindelse via SSH. Nyttig til fejlfinding af problemer med SSH-forbindelser.\ndontUseGateway=Brug ikke gateway\ndontUseGatewayDescription=Brug ikke hypervisor-værten som gateway, og opret forbindelse direkte til IP'en\ncategoryColor=Kategori farve\ncategoryColorDescription=Den standardfarve, der skal bruges til forbindelser inden for denne kategori\ncategorySync=Synkroniser med git-arkiv\ncategorySyncDescription=Synkroniser automatisk alle forbindelser med git-repository. Alle lokale ændringer af forbindelser vil blive skubbet til fjernlageret.\ncategorySyncSpecial=Synkroniser med git-arkiv\\n(Kan ikke konfigureres for specialkategorien \"$NAME$\")\ncategoryDontAllowScripts=Deaktiver alle ændringer\ncategoryDontAllowScriptsDescription=Deaktiver enhver kommandoafvikling og andre operationer på systemer i denne kategori for at forhindre ændringer. Dette vil deaktivere al scripting-funktionalitet, shell-miljøkommandoer, prompter og meget mere.\ncategoryConfirmAllModifications=Bekræft alle ændringer\ncategoryConfirmAllModificationsDescription=Bekræft først enhver form for ændring af en forbindelse eller et filsystem. Det kan forhindre utilsigtede handlinger på vigtige systemer.\ncategoryDefaultIdentity=Standard-identitet\ncategoryDefaultIdentityDescription=Hvis du ofte bruger en bestemt identitet på mange af systemerne i denne kategori, kan du indstille en standardidentitet, så du kan vælge den på forhånd, når du opretter nye forbindelser.\ncategoryConfigTitle=$NAME$ konfiguration\nconfigure=Konfigurer\naddConnection=Tilføj forbindelse\nnoCompatibleConnection=Ingen kompatibel forbindelse fundet\nnoCompatibleIdentity=Ingen kompatibel identitet fundet\nnewCategory=Ny kategori\ndockerComposeRestricted=Compose-projektet er begrænset af $NAME$ og kan ikke ændres eksternt. Brug venligst $NAME$ til at administrere dette compose-projekt.\nrestricted=Begrænset\ndisableSshPinCaching=Deaktiver SSH PIN caching\ndisableSshPinCachingDescription=XPipe gemmer automatisk alle PIN-koder, der er indtastet for en nøgle, når der bruges en form for hardwarebaseret godkendelse.\\n\\nHvis du deaktiverer dette, skal du indtaste PIN-koden igen ved hvert forbindelsesforsøg.\ngitSyncPull=Pull til synkronisering af eksterne git-ændringer\nenpassVaultFile=Vault-fil\nenpassVaultFileDescription=Den lokale Enpass vault-fil.\nflat=Flad\nrecursive=Rekursiv\nrdpAllowListBlocked=Den valgte RemoteApp ser ikke ud til at være inkluderet i listen over tilladte RDP'er for serveren.\npsonoServerUrl=Server-URL\npsonoServerUrlDescription=URL til psono-backend-serveren\npsonoApiKey=API-nøgle\npsonoApiKeyDescription=Den API-nøgle, der skal bruges, formateret som en uuid\npsonoApiSecretKey=Hemmelig API-nøgle\npsonoApiSecretKeyDescription=Den hemmelige API-nøgle som 64 byte hex-streng\npassboltServerUrl=Server-URL\npassboltServerUrlDescription=URL til passbolt-backend-serveren\npassboltPassphrase=Passphrase\npassboltPassphraseDescription=Passphrase til den private nøgle til boksen\npassboltPrivateKey=Privat nøgle\npassboltPrivateKeyDescription=Den private gpg-nøglefil til boksen\nfocusWindowOnNotifications=Fokus-vindue på notifikationer\nfocusWindowOnNotificationsDescription=Bring XPipe i forgrunden, når der vises en meddelelse eller fejlmeddelelse, f.eks. når en forbindelse eller tunnel uventet afbrydes.\ngitUsername=Brugerdefineret git-brugernavn\ngitUsernameDescription=Den brugerdefinerede bruger, der skal godkendes til git-fjernlageret. Som standard vil XPipe bruge de aktuelt konfigurerede legitimationsoplysninger for git CLI.\\n\\nDenne indstilling tilsidesætter alle standardlegitimationsoplysninger, der allerede er konfigureret til din lokale git CLI-klient.\ngitPassword=Brugerdefineret git-adgangskode/personligt adgangstoken\ngitPasswordDescription=Adgangskoden eller det personlige adgangstoken, der skal bruges til at godkende. Om du har brug for en adgangskode eller et personligt adgangstoken afhænger af git-fjernudbyderen. Denne indstilling tilsidesætter alle standardoplysninger, der allerede er konfigureret til din lokale git CLI-klient.\nsetReadOnly=Sæt skrivebeskyttet\nunsetReadOnly=Uindstillet skrivebeskyttet\nreadOnlyStoreError=Denne indgangs konfiguration er frosset. Vælg et andet navn for at gemme dine ændringer i en ny kopi.\ncategoryFreeze=Frys forbindelseskonfigurationer\ncategoryFreezeDescription=Markerer forbindelseskonfigurationer som skrivebeskyttede. Det betyder, at ingen eksisterende forbindelsesindgangskonfiguration i denne kategori kan ændres. Nye forbindelser kan dog tilføjes.\nupdateFail=Installation af opdatering lykkedes ikke\nupdateFailAction=Installer opdatering manuelt\nupdateFailActionDescription=Tjek de seneste udgivelser på GitHub\nonePasswordPlaceholder=Elementets navn eller op:// URL\ncomputeDirectorySizes=Beregning af mappestørrelser\ncomputeSize=Beregn størrelse\ncustomSpiceCommand=Brugerdefineret kommando\ncustomSpiceCommandDescription=Den brugerdefinerede kommando, der skal udføres for at starte SPICE-sessioner. Pladsholderstrengen $FILE vil blive erstattet af den citerede filsti til .vv-filen, når den kaldes.\nvncClient=VNC-klient\nvncClientDescription=Den VNC-klient, der skal startes, når der åbnes VNC-forbindelser i XPipe.\\n\\nDu har mulighed for enten at bruge den integrerede VNC-klient i XPipe eller alternativt starte en ekstern lokalt installeret VNC-klient, hvis du er på udkig efter mere tilpasning.\nintegratedXPipeVncClient=Integreret XPipe VNC-klient\ncustomVncCommand=Brugerdefineret kommando\ncustomVncCommandDescription=Den brugerdefinerede kommando, der skal udføres for at starte VNC-sessioner. Pladsholderstrengen $ADDRESS vil blive erstattet af den citerede adresse, når den kaldes.\nvncConnections=VNC-forbindelser\npasswordManagerIdentity=Password manager-identitet\npasswordManagerIdentity.displayName=Password manager-identitet\npasswordManagerIdentity.displayDescription=Hent brugernavn og adgangskode til en identitet fra din adgangskodeadministrator\npasswordCopied=Forbindelsesadgangskode kopieret til udklipsholder\nerrorOccurred=Der opstod en fejl\nactionMacro.displayName=Handlingsmakro\nactionMacro.displayDescription=Kør i aktion ved hjælp af tilpassede triggere\nmacroAdd=Tilføj makro\nmacroName=Makro-navn\nmacroNameDescription=Giv denne makro et brugerdefineret navn\nactionId=Action ID\nactionIdDescription=Den handling, der skal køres med denne makro\nmacroRefs=Tilknyttede forbindelser\nmacroRefsDescription=De forbindelser, der skal bruges til at køre handlingen\nconnectionCopy=En kopi\nactionPickerTitle=Vælg handling\nactionPickerDescription=Klik på noget for at udføre en handling. I stedet for at udføre handlingen kan du oprette og redigere genveje til handlingen i tilstanden for valg af genvej til handling.\ncancelActionPicker=Annuller valg af handling\nactionShortcut=Genvej til handling\nactionShortcuts=Action-genveje\nactionStore=Action store\nactionStoreDescription=Den butikspost, som handlingen skal køres på\nactionStores=Handling gemmer\nactionStoresDescription=Butiksposterne til at køre handlingen på\nactionDesktopShortcut=Genvej på skrivebordet\nactionDesktopShortcutDescription=Opret en genvej til denne handling på dit skrivebord\nactionUrlShortcut=URL-genvej\nactionUrlShortcutDescription=Kopier en URL, der kan udløse disse handlinger, når den åbnes\nactionUrlShortcutDisabled=URL-genvej (Ikke tilgængelig)\nactionUrlShortcutDisabledDescription=Installationstypen $TYPE$ understøtter ikke åbning af URL'er\nactionApiCall=API-anmodning\nactionApiCallDescription=Kald denne handling fra HTTP API'en\nactionMacro=Handlingsmakro\nactionMacroDescription=Opret en makro med avanceret funktionalitet til denne handling\ncreateMacro=Opret makro\nactionConfiguration=Parametre\nactionConfigurationDescription=De parametre, der skal sendes til den udførte handling\nconfirmAction=Bekræft handling\nactionConnections=Action-forbindelser\nactionConnectionsDescription=De forbindelser, som handlingen skal køre på\nactionConnection=Action-forbindelse\nactionConnectionDescription=Forbindelsen til at køre handlingen på\nappleContainerInstall.displayName=Apple-containere\nappleContainerInstall.displayDescription=Få adgang til apple-container-instanser via container CLI\nappleContainer.displayName=Apple-container\nappleContainer.displayDescription=Få adgang til apple container-instanser via container CLI\nappleContainerHostDescription=Den host, som apple-containeren er placeret på\nappleContainerDescription=Navnet på apple-containeren\nappleContainers=Apple-containere\nchangeOrderIndexTitle=Ændre rækkefølge\norderIndex=Indeks\norderIndexDescription=Eksplicit indeks for at rangordne denne post i forhold til andre. De laveste indekser vises øverst, de højeste nederst\nmoveToFirst=Flyt til den første\nmoveToLast=Flyt til sidst\ncategory=Kategori\nincludeRoot=Inkluderer rod\nexcludeRoot=Ekskluder rod\nfreezeConfiguration=Frys konfiguration\nunfreezeConfiguration=Frigør konfiguration\nwaylandScalingTitle=Wayland-skalering\nactionApiUrl=$URL$ (Kopier json-krop)\ncopyBody=Kopi af anmodningens brødtekst\ngitRepoTerminalOpen=Åbn depotet i terminalen\ngitRepoTerminalOpenDescription=Tag selv et kig på repositoriet med kommandolinjen\ngitRepoOverwriteLocal=Overskriv lokalt depot\ngitRepoOverwriteLocalDescription=Erstat alle lokale ændringer med ændringer fra fjernbetjeningen\ngitRepoForcePush=Overskriv eksternt depot\ngitRepoForcePushDescription=Brug git push --force til at anvende dine lokale ændringer på fjerncomputeren\ngitRepoDontWarn=Advar ikke længere\ngitRepoDontWarnDescription=Hvis dette er forventet, skal du få XPipe til at ignorere denne fejl i fremtiden\ngitRepoTryAgain=Prøv igen\ngitRepoTryAgainDescription=Forsøg den samme operation igen\ngitRepoEnablePlain=Brug almindelig mappesynkronisering\ngitRepoEnablePlainDescription=Initialiser ikke et git-repository for at synkronisere ændringer til biblioteket\ngitRepoCreateBare=Brug git sync\ngitRepoCreateBareDescription=Initialiser et nyt tomt git-arkiv i synkroniseringsmappen\ngitRepoDisable=Deaktiver git vault indtil videre\ngitRepoDisableDescription=Foretag ingen ændringer under denne session\ngitRepoPullRefresh=Træk ændringer og opdater\ngitRepoPullRefreshDescription=Flet fjernændringer og genindlæs data\nbreakOutCategory=Break out-kategori\nmergeCategory=Flet kategori\nopenWinScp=Åbn i WinSCP\nuninstallApplication=Afinstallation\nuninstallApplicationDescription=Kører .pkg et installationsscript for at afinstallere XPipe fuldstændigt\nk8sEditPodTitle=Anvend ændringer\nk8sEditPodContent=Vil du anvende de ændringer, der er foretaget via kommandoen kubectl apply? Det kræver sandsynligvis en genstart, før ændringerne kan anvendes.\nvirshEditDomainTitle=Anvend ændringer\nvirshEditDomainContent=Vil du anvende ændringerne på domænet? En genstart er sandsynligvis nødvendig for at ændringerne kan anvendes.\npkcs11Library=PKCS#11-bibliotek\npkcs11LibraryDescription=Stien til den dynamisk linkede biblioteksfil\nsshAgentSocket=Brugerdefineret SSH-agent-socket\nsshAgentSocketDescription=Den brugerdefinerede socket, der skal bruges til at kommunikere med SSH-agenten. Denne brugerdefinerede agent kan bruges til en forbindelse ved at vælge indstillingen brugerdefineret agent for den.\npublicKey=Offentlig nøgle-identifikator\npublicKeyDescription=Den valgfri offentlige nøgle for at tvinge agenten til kun at tilbyde den matchende private nøgle\nactions=Handlinger\nhcloudServer.displayName=Hetzner cloud server\nhcloudServer.displayDescription=Få adgang til en server i Hetzner-skyen via SSH\nhcloudInstall.displayName=Hetzner Cloud CLI\nhcloudInstall.displayDescription=Få adgang til servere i Hetzner-skyen via hcloud\nhcloudContext.displayName=hcloud-kontekst\nhcloudContext.displayDescription=Adgangsservere i en hcloud-kontekst\nmetrics=Metrik\nopenInVsCode=Åbn i VsCode\naddCloud=Cloud ...\nhcloudToken=hcloud-token\nhcloudTokenDescription=Det Hetzner-sky-token, der skal bruges. For mere information, se dokumentationen\nhcloudLogin=Hetzner cloud-login\nclearHcloudToken=Ryd hcloud-token\nclearHcloudTokenDescription=Slet eksisterende token, så du kan logge ind igen\nselectIdentity=Vælg identitet\nenableMcpServer=Aktiver MCP-server\nenableMcpServerDescription=Aktiverer XPipe MCP-serveren, så eksterne MCP-klienter kan sende anmodninger til MCP-serveren. Se nedenfor for konfigurationsdetaljer.\\n\\nBemærk, at HTTP-API'en ikke behøver at være aktiveret for MCP-funktionaliteten.\nenableMcpMutationTools=Aktiver MCP-mutationsværktøjer\nenableMcpMutationToolsDescription=Som standard er kun skrivebeskyttede værktøjer aktiveret på MCP-serveren. Dette er for at sikre, at der ikke kan foretages utilsigtede handlinger, som potentielt kan ændre et system.\\n\\nHvis du planlægger at foretage ændringer i systemer via MCP-klienter, skal du sørge for at kontrollere, at din MCP-klient er konfigureret til at bekræfte eventuelle potentielt destruktive handlinger, før du aktiverer denne indstilling. Kræver en genforbindelse af alle MCP-klienter for at gælde.\nmcpClientConfigurationDetails=Konfiguration af MCP-klient\nmcpClientConfigurationDetailsDescription=Brug disse konfigurationsdata til at oprette forbindelse til XPipe MCP-serveren fra din valgte MCP-klient.\nswitchHostAddress=Skift værtsadresse\naddAnotherHostName=Tilføj et andet værtsnavn\naddNetwork=Netværksscanning ...\nnetworkScan=Netværksscanning\nnetworkScanStore=Målvært\nnetworkScanStoreDescription=Den vært, der skal scannes efter på det lokale netværk\nuseAsGateway=Brug host som gateway\nuseAsGatewayDescription=Om målværten skal bruges som gateway for de oprettede forbindelser\nnetworkScanPorts=Porte, der skal scannes\nnetworkScanPortsDescription=Den kommaseparerede liste over porte, der skal medtages i scanningen\nnetworkScanType=Forbindelsestype\nnetworkScanTypeDescription=Den type servere, man skal kigge efter\nemptyDirectory=Denne mappe ser ud til at være tom\nhcloudConfigFile=hcloud-konfigurationsfil\nhcloudConfigFileDescription=Placeringen af hcloud CLI .toml-konfigurationsfilen\npreferMonochromeIcons=Foretrækker monokrome ikoner\npreferMonochromeIconsDescription=Når det er aktiveret, vælges monokrome ikonvariabler frem for de farvede standardversioner af et ikon, forudsat at der findes en separat ikonvariant i lys eller mørk tilstand for et ikon fra en kilde.\\n\\nKræver en opdatering af de ikoner, der skal anvendes.\nalwaysShowSshMotd=Vis altid MOTD\nalwaysShowSshMotdDescription=Om dagens besked, der er konfigureret på et fjernsystem, skal vises eller ej ved login i en ny terminalsession. Bemærk, at ændring af dette kan ændre SSH-forbindelsers initialiseringsadfærd.\nmanageSubscription=Administrer abonnement\nnoListeningServer=Ingen lytteserver\nnetworkScanResults=Scanningsresultater\nnetworkScanResultsDescription=Listen over fundne systemer i netværket\nlocalShellDialect=Lokal shell\nlocalShellDialectDescription=Den shell, der bruges til lokale operationer. Hvis den normale lokale standard-shell er deaktiveret eller ødelagt i en eller anden grad, kan denne mulighed bruges til at falde tilbage på et andet alternativ.\\n\\nNogle konfigurationer som f.eks. brugerdefinerede PATH-poster gælder muligvis ikke for fallback-shellen, hvis de endnu ikke er konfigureret i de respektive shell-profilfiler.\nagentSocketNotFound=Der blev ikke fundet nogen aktiv agent-socket\nagentSocket=Socket-placering\nagentSocketDescription=Stien til agentens socket-fil\nagentSocketNotConfigured=Der er ikke konfigureret nogen brugerdefineret socket endnu\ndownloadInProgress=$NAME$ download i gang\nenableTerminalStartupBell=Aktiver terminalens startklokke\nenableTerminalStartupBellDescription=Afspil en bip/klokke-kommando i en ny terminalsession. Hvis din terminalemulator understøtter klokker, kan dette bruges til at gøre det lettere at identificere nystartede terminalinstanser.\ninvalidSshGatewayChain=Ugyldig blandet gateway-kædekonfiguration med jump-gateways og non-jump-gateways.\nsyncFileExists=Den synkroniserede fil $FILE$ findes allerede\nreplaceFile=Erstat fil\nreplaceFileDescription=Erstattet den eksisterende fil med denne\nrenameFile=Omdøb fil\nrenameFileDescription=Giv denne fil et andet navn for at synkronisere\nnewFileName=Nyt filnavn\nparentHostDoesNotSupportTunneling=Parent host $NAME$ understøtter ikke tunneling\nconnectionNotesTemplate=Skabelon til noter\nconnectionNotesTemplateDescription=Den markdown-skabelon, der skal bruges, når man tilføjer en ny notepost til en forbindelse.\nconnectionNotesButton=Rediger noter\nrdpSmartSizing=Aktiver smart dimensionering\nrdpSmartSizingDescription=Når det er aktiveret, vil mstsc nedskalere skrivebordsstørrelsen, hvis vinduet er for lille til at vise det i fuld opløsning. Skrivebordets størrelsesforhold bevares, når det skaleres ned.\ndisableStartOnInit=Deaktiver automatisk opstart\nenableStartOnInit=Aktiver automatisk opstart\nfileReadSudoTitle=Sudo-fil læst\nfileReadSudoContent=Den fil, du forsøger at læse, giver dig ikke læserettigheder som nuværende bruger. Vil du læse denne fil som root-bruger med sudo? Dette vil automatisk hæve niveauet til root med enten de eksisterende legitimationsoplysninger eller via en prompt.\nnetbirdInstall.displayName=Installation af Netbird\nnetbirdInstall.displayDescription=Opret forbindelse til jævnaldrende i dit Netbird-netværk\nnetbirdProfile.displayName=Netbird-profil\nnetbirdProfile.displayDescription=Liste over peers i en bestemt profil\nnetbirdPeer.displayName=Netbird-peer\nnetbirdPeer.displayDescription=Opret forbindelse til en peer via SSH\nnetbirdPublicKey=Offentlig nøgle\nnetbirdPublicKeyDescription=Den interne offentlige nøgle for peer'en\nnetbirdHostName=Værtsnavn\nnetbirdHostNameDescription=Værtsnavnet på peer'en i netværket\nvncRefSystem=Tilknyttet system\nvncRefSystemDescription=Den forbindelsespost, som denne VNC-forbindelse skal knyttes til. Lad den være tom, hvis der ikke er nogen\nabstractHost.displayName=Abstrakt vært\nabstractHost.displayDescription=Opret en post for en host, der ikke understøtter shell-forbindelser\nabstractHostAddress=Værtsadresse\nabstractHostAddressDescription=Adressen på værten\nabstractHostGateway=Gateway\nabstractHostGatewayDescription=Det valgfrie gateway-system, som man kan nå denne vært igennem\nabstractHostConvert=Konverter til abstrakt værtsindgang\nhostNoConnections=Ingen tilgængelige forbindelser\nhostHasConnections=$COUNT$ tilgængelige forbindelser\nhostHasConnection=$COUNT$ tilgængelig forbindelse\nlargeFileWarningTitle=Redigering af stor fil\nlargeFileWarningContent=Den fil, du vil redigere, er ret stor med $SIZE$. Vil du virkelig åbne denne fil i din teksteditor?\nrdpAskpassUser=RDP-brugernavn til vært $HOST$\nrdpAskpassPassword=Adgangskode til bruger $USER$\ninPlaceKey=Nøgle\ninPlaceKeyText=Indhold af privat nøgle\ninPlaceKeyTextDescription=Den private nøgles indhold\nnetbirdSelfhosted=Selv-hostet netbird-instans\nnetbirdSelfhostedDescription=Giv en brugerdefineret URL i stedet for at bruge den cloud-hostede version\nnetbirdManagementUrl=URL til Netbird-administration\nnetbirdManagementUrlDescription=Administrations-URL'en til din selvhostede instans\nnetbirdSetupKey=Opsætningstast\nnetbirdSetupKeyDescription=Hvis du bruger opsætningsnøgler, kan du bruge en til login\nnetbirdLogin=Netbird-login\naddProfile=Tilføj profil\nnetbirdProfileNameAsktext=Navn på ny netbird-profil\nopenSftp=Åbn i SFTP-session\ncapslockWarning=Du har aktiveret capslock\ninherit=Arve\nsshConfigStringSelected=Målvært\nsshConfigStringSelectedDescription=For flere hosts bruges den første som mål. Omorganiser dine værter for at ændre målet\ntunnelToLocalhost=Tunnel til localhost\ntunnelToLocalhostDescription=Tunneler automatisk fjernporten til localhost\ntags=Mærker\ntag=Tag\naddNewTag=Opret nyt tag\ncreateTag=Opret tag ...\ninPlacePublicKey=Offentlig nøgle\ninPlacePublicKeyDescription=Den tilknyttede offentlige nøgle til den angivne private nøgle\nsshKeygenTitle=Generer ny SSH-nøgle\nsshKeygenAlgorithm=Algoritme\nsshKeygenAlgorithmDescription=Den asymmetriske keygen-algoritme, der skal bruges til nøglen\nrsaBits=Bits\nrsaBitsDescription=Antal bits i den genererede nøgle\nsshKeygenComment=Kommentar\nsshKeygenCommentDescription=Den valgfrie kommentar til denne nøgle\nsshKeygenPassphrase=Passphrase\nsshKeygenPassphraseDescription=Den valgfri passphrase for denne nøgle\ned25519SkResident=Lav en beboernøgle\ned25519SkResidentDescription=Gemmer privat nøgle på hardware-sikkerhedsnøglen\ned25519SkResidentKeyName=Etiket til resident nøgle\ned25519SkResidentKeyNameDescription=Giv nøglen en etiket. Nødvendigt ved lagring af flere nøgler på sikkerhedsnøglen\ned25519SkPinRequired=Kræver PIN-kode\ned25519SkPinRequiredDescription=Kræver indtastning af PIN-kode ved brug\ned25519SkUserPresenceRequired=Kræver brugerens tilstedeværelse\ned25519SkUserPresenceRequiredDescription=Kræver berøring eller lignende ved brug. Nogle sikkerhedsnøgler kræver, at dette er aktiveret\ncopyPublicKey=Kopier offentlig nøgle\ngeneratePublicKey=Generer offentlig nøgle\npublicKeyGenerateNotice=Kan genereres ud fra en privat nøgle\nidentityApplyTargetHost=Mål\nidentityApplyTargetHostDescription=Systemet til at anvende identiteten på\nidentityApplyAuthorizedHost=SSH-nøgle autoriseret\nidentityApplyAuthorizedHostDescription=SSH-nøglen føjes til den autoriserede hosts-fil\nidentityApplyAuthorizedHostButton=Tilføj nøgle til fil\napplyIdentityToHost=Anvend identitet på værten ...\nidentityApplyMissingPublicKeyTitle=Manglende offentlig nøgle\nidentityApplyMissingPublicKeyContent=Identitetens SSH-nøgle har ikke en offentlig nøgle tilknyttet. Tjek konfigurationen for detaljer.\nvalid=Gyldig\nnotValid=Ikke gyldig\nwarning=Advarsel\nidentityApplyTitle=Anvend identitet\nidentityApplyConfigPasswordEnabled=Adgangskode-auth aktiveret\nidentityApplyConfigPasswordEnabledDescription=Password-godkendelse er stadig aktiveret i sshd-konfigurationen\nidentityApplyConfigPasswordDisabled=Password auth deaktiveret\nidentityApplyConfigPasswordDisabledDescription=Password-godkendelse er stadig deaktiveret i sshd-konfigurationen\nidentityApplyConfigKeyEnabled=Key auth aktiveret\nidentityApplyConfigKeyEnabledDescription=Nøglebaseret godkendelse er stadig aktiveret i sshd-konfigurationen\nidentityApplyConfigKeyDisabled=Key auth deaktiveret\nidentityApplyConfigKeyDisabledDescription=Nøglebaseret godkendelse er stadig deaktiveret i sshd-konfigurationen\nidentityApplyConfigRootDisabledWarning=Root-login deaktiveret\nidentityApplyConfigRootDisabledWarningDescription=Root-bruger-login er ikke aktiveret i sshd-konfigurationen\nidentityApplyConfigAdminWarning=Konfigurering af administratortaster\nidentityApplyConfigAdminWarningDescription=Nøglen skal måske tilføjes til administrators_authorized_keys i stedet for admin-brugere\nidentityApplyEditConfig=Rediger konfiguration\nidentityApplyEditConfigDescription=Åbn sshd-konfigurationen i editoren for at løse eventuelle problemer\nidentityApplyEditAuthorizedKeys=Rediger autoriserede nøgler\nidentityApplyEditAuthorizedKeysDescription=Åbn filen authorized_keys i editoren for at redigere eller fjerne andre nøgler\nidentityApplyEditConfigButton=Åbn sshd_config\nidentityApplyEditAuthorizedKeysButton=Åbn autoriserede_nøgler\nidentityApplySetStoreIdentity=Forbindelsesidentitetssæt\nidentityApplySetStoreIdentityDescription=Identiteten er konfigureret til at blive brugt af forbindelsen\nidentityApplySetStoreIdentityButton=Anvend identitet\ngenerateKey=Genererer nøgle\ngroupSecretStrategy=Gruppebaseret adgangskontrol\ngroupSecretStrategyDescription=Hvordan man henter den gruppehemmelighed, der bruges til kryptering og dekryptering for gruppen. Den hentningsmetode, du vælger, vil blive kørt, når en bruger logger ind i boksen ved opstart.\\n\\nDenne indstilling konfigureres pr. gruppe. Hvis du vil ændre denne indstilling for en anden gruppe end den, der er aktiv i øjeblikket, skal du logge ind i boksen som medlem af den pågældende gruppe.\nfileSecret=Filbaseret hemmelighed\ncommandSecret=Kommando\nhttpRequestSecret=HTTP-svar\nfileSecretChoice=Filens placering\nfileSecretChoiceDescription=Stien til den fil, der indeholder gruppekrypteringshemmeligheden. Da denne fil kan forespørges på alle platforme, kan du bruge ~ i stien til at henvise til hjemmebiblioteket. Filen skal være tilgængelig på alle de systemer, du låser boksen op fra, ellers vil login mislykkes.\ncommandSecretField=Hentning af script\ncommandSecretFieldDescription=Kommandoen, der returnerer den hemmelige krypteringsnøgle for den aktuelle gruppe. Kommandoen køres i det lokale systems standard-shell, og nøglen skal udskrives til stdout.\nhttpRequestSecretField=Anmodning om URI\nhttpRequestSecretFieldDescription=Den URI, der skal sendes en HTTP-anmodning til. Gruppehemmeligheden tages fra HTTP-svaret.\nvaultAuthentication=Vault-godkendelse\nvaultAuthenticationDescription=Sådan autentificeres/oplåses vault-data. Der er flere forskellige måder at kryptere og låse vault-data op på, afhængigt af hvem du vil dele vault-dataene med.\ngroupAuthFailed=Hemmelig autentificering mislykkedes\nuserAuthFailed=Godkendelse af adgangskode mislykkedes\nsavingChanges=Gemmer ændringer\nawsDeviceScan=AWS\naws=AWS\nawsCliInstallTitle=AWS CLI påkrævet\nawsCliInstallContent=AWS-integrationen kræver, at AWS CLI er installeret på dit lokale system\nawsProfileCreateTitle=Ny AWS-profil\nawsProfileAccessKey=Adgangsnøgle\nawsProfileName=Navn på profil\nawsProfileNameDescription=Visningsnavnet på den nye profil\nawsProfileRegion=Region\nawsProfileRegionDescription=AWS-regionen, der er knyttet til profilen\nawsProfileAccessKeyId=Adgangsnøgle-ID\nawsProfileAccessKeyIdDescription=IAM-brugerens adgangsnøgle-ID\nawsProfileSecretAccessKey=Hemmelig adgangsnøgle\nawsProfileSecretAccessKeyDescription=Den tilhørende hemmelige adgangsnøgle\nawsInstall.displayName=Installation af AWS CLI\nawsInstall.displayDescription=Opret forbindelse til dine AWS-systemer via AWS CLI\nawsProfile.displayName=AWS CLI-profil\nawsProfile.displayDescription=Adgang til AWS gennem en specifik profil\nawsInstanceId=Forekomst-ID\nawsInstanceIdDescription=Det interne ID for denne instans\nawsInstanceUseSsm=Opret forbindelse via SSM\nawsInstanceUseSsmDescription=Brug SSM-værktøjet til at oprette forbindelse til instansen via SSH\nawsEc2Instance.displayName=AWS EC2-instans\nawsEc2Instance.displayDescription=Opret forbindelse til en EC2-instans via SSH\nawsS3Group.displayName=S3-spande\nawsS3Group.displayDescription=Få adgang til S3-buckets i en AWS-profil\nawsS3Bucket.displayName=S3-spand\nawsS3Bucket.displayDescription=Få adgang til en S3-bucket i en AWS-profil\nawsEc2Group.displayName=EC2-instanser\nawsEc2Group.displayDescription=Adgang til EC2-instanser i en AWS-profil\nawsEc2InstanceSsmTerminal=Åbn SSM-terminalen\ngenericS3Bucket.displayName=Generisk S3-spand\ngenericS3Bucket.displayDescription=Få adgang til en generisk S3-bucket via AWS CLI\naddFileSystem=Filsystem ...\ngenericS3BucketHost=Vært\ngenericS3BucketHostDescription=Værtsposten eller den manuelle adresse på S3-serveren\ngenericS3BucketPortDescription=Den port, S3-serveren lytter på\ngenericS3BucketAccessKeyId=Adgangsnøgle-ID\ngenericS3BucketAccessKeyIdDescription=IAM-brugerens adgangsnøgle-ID\ngenericS3BucketSecretAccessKey=Hemmelig adgangsnøgle\ngenericS3BucketSecretAccessKeyDescription=Den tilhørende hemmelige adgangsnøgle\ngenericS3BucketHttps=Aktiver HTTPS\ngenericS3BucketHttpsDescription=Brug HTTPS til at oprette forbindelse til serveren. Nogle udbydere kan kræve HTTPS\ntunnelled=Tunnel\nawsInstallSync=Konfigurationssynkronisering\nawsInstallSyncDescription=Synkroniser AWS CLI-konfigurationsfilerne til git vault\nawsInstallLocation=Placering af brugerdata\nawsInstallLocationDescription=Stien, hvor AWS CLI-konfigurationsfilerne kommer fra\ninstanceActions=Forekomst af handlinger\nopenSplit=Åbn i delt terminal\nterminalSplitStrategy=Retning for delt visning\nterminalSplitStrategyDescription=Styrer, hvordan terminalfaner opdeles, når man bruger split view-funktionen i batch-tilstand til at åbne flere terminalsessioner ved siden af hinanden.\nterminalSplitStrategyDisabledDescription=Styrer, hvordan terminalfaner opdeles, når man bruger split view-funktionen i batch-tilstand til at åbne flere terminalsessioner ved siden af hinanden.\\n\\nDin nuværende terminalkonfiguration understøtter ikke opdelte visninger.\nhorizontal=Vandret\nvertical=Lodret\nbalanced=Afbalanceret\nclose=Luk\nhelpButton=$TOPIC$ dokumentationslink\nquickAccess=Hurtig adgang\ntoggleEnabled=Toggle-tilstand\ncurrentPath=Nuværende sti\ndirectoryContents=Indholdet i en mappe\ndirectoryOptions=Indstillinger for mapper\nchooseConnectionType=Vælg forbindelsestype\nbatchMode=Batch-tilstand\ntoggleButton=Toggle-knap\ntailscaleUseSsh=Brug tailscale SSH auth\ntailscaleUseSshDescription=Log ind via selve tailscale SSH-serveren uden nogen SSH-auth\nportDescription=Den port, SSH-serveren kører på\nloginAs=Log ind som\nsshGatewayType=Gateway-type\nsshGatewayTypeDescription=Om der skal oprettes forbindelse til målet via en tunnel eller med ProxyJump-indstillingen\ngatewayTunnel=Gateway-tunnel\nproxyJump=Proxy-spring\ncommandTypeAsyncBackground=Kør løs i baggrunden\ncommandTypeSyncBackground=Kører i baggrunden og venter på at blive færdig\ncommandTypeTerminalBackground=Åbn i terminal\nasyncBackgroundCommand=Baggrundskommando\nsyncBackgroundCommand=Blokering af baggrundskommando\nterminalBackgroundCommand=Terminal-kommando\ntestingConnection=Test af forbindelse ...\nopenManagementConsole=Åben administrationskonsol\nopenLxcTerminal=Åbn LXC-terminalen\nopenContainerConsole=Åbn seriel konsol\nkeeper2fa=2FA-metode\nkeeper2faDescription=Den primære to-faktor-godkendelsesmetode, der er konfigureret til din konto. Aktivér denne, hvis din Keeper-konto kræver to-faktor-autentificering for at få adgang til adgangskoder.\nkeeperTotpDuration=Varighed af brugerdefineret 2FA-kode\nkeeperTotpDurationDescription=Tilsidesæt standardvarigheden for, hvor længe en 2FA-kode er gyldig. Gælder kun, hvis din organisations politik tillader ændring af varigheden.\\n\\nMulige værdier er: $VALUES$\nkeeperOtherAuth=Andet (RSA SecurID, Duo Security, Keeper DNA osv.)\nextractReusableIdentities=Udtræk af genanvendelige identiteter\nidentitiesAdded=Identiteter tilføjet\nsyncMode=Synkroniseringstilstand\nsyncModeDescription=Styrer, hvordan ændringer skal synkroniseres.\\n\\nØjeblikkelig tilstand skubber og trækker ændringer så hurtigt som muligt, opstarts- og afslutningsmodus synkroniserer alle ændringer, der er foretaget i løbet af en session, på én gang, og manuel tilstand synkroniserer kun, når du igangsætter det.\ntoggleTerminalDock=Toggle terminal dock\nscriptDirectory=Placering af mappe\nscriptDirectoryDescription=Den lokale mappe, der indeholder shell-script-filer\nscriptSourceUrl=URL til depot\nscriptSourceUrlDescription=URL'en til et eksternt git-repository, der indeholder shell-script-filer\nscriptCollectionSourceType=Kildetype\nscriptCollectionSourceTypeDescription=Den type kilde, hvorfra shell-scripts skal indlæses\nscriptCollectionSourceEntry=Kildeindtastning\nscriptCollectionSourceEntryDescription=Kilden, hvorfra shell-scripts skal indlæses\ngitRepository=Git-arkiv\nscriptCollectionSource.displayName=Script-kilde\nscriptCollectionSource.displayDescription=Importer automatisk shell-scripts fra en eksisterende kilde\ndirectorySource=Kilde til mappe\ngitRepositorySource=Git-arkivets kilde\nrefreshSource=Opdater kilde\nscriptTextSourceUrl=Script-URL\nscriptTextSourceUrlDescription=URL'en til at hente scriptfilen fra\nscriptSourceType=Script-kilde\nscriptSourceTypeDescription=Hvor skal man hente scriptet fra?\nscriptSourceTypeInPlace=Script på stedet\nscriptSourceTypeUrl=Ekstern URL\nscriptSourceTypeSource=Eksisterende kilde\nimportScripts=Importere scripts\nscriptsContained=$NUMBER$ scripts\nscriptSourceCollectionImportTitle=Import af scripts fra kilde ($SELECTED$/$COUNT$)\nnoScriptsFound=Ingen scripts fundet\ntunnel=Tunnel\nnotInitialized=Ikke initialiseret\nselectCategory=Vælg kategori ...\nscriptSourceName=Navn på script\nscriptSourceNameDescription=Filnavnet på scriptet i kilden\nworkspaceRestartTitle=Arbejdsområde klar\nworkspaceRestartContent=Der blev oprettet en genvej til det nye arbejdsområde på $PATH$. Du kan navigere til genvejen eller genstarte XPipe nu for at åbne det nye arbejdsområde automatisk.\nbrowseShortcut=Gennemse fil\nsyncModeInstant=Synkroniser med det samme\nsyncModeSession=Synkronisering ved opstart og afslutning\nsyncModeManual=Synkroniser manuelt\npushChanges=Tryk på ændringer\npullChanges=Træk ændringer\nsourcedFrom=Hentet fra $SOURCE$\ninPlaceScript=Script på stedet\ngeneric=Generisk\nsyncToPlainDirectory=Synkroniser til almindelig mappe\nsyncToPlainDirectoryDescription=Når du synkroniserer til en lokal mappe, kan du enten behandle denne mappe som et andet git-repository eller bare som en almindelig mappe. Hvis indstillingen almindelig mappe er aktiveret, initialiseres mappen ikke som et git-repository.\nopenSpiceSession=Åbn SPICE-session\nterminalBehaviour=Terminalens adfærd\nnoScanPossible=Der blev ikke fundet nogen understøttede forbindelser\nnetworkSwitchPorts=Netværksporte\nnswitchGroup.displayName=Netværksporte\nnswitchGroup.displayDescription=Liste over tilgængelige porte på en netværksenhed\nnswitchPort.displayName=Netværksport\nnswitchPort.displayDescription=Styr en individuel port på en netværksswitch-enhed\nenablePort=Aktiver port\nshutdownPort=Lukker porten ned\nresetPort=Nulstil port\nuseSystemDefault=Brug systemets standardindstillinger\nportStatus=Status for port\nclearCounters=Rydde tællere\nshowStatus=Vis status\nshowAllPorts=Vis alle porte\nactiveLicense=Licens\nactiveLicenseDescription=Aktiver en XPipe-licensnøgle\nauthenticatorApp=Autentificerings-app\nsecurityKey=Sikkerhedsnøgle\nmcpAdditionalContext=Yderligere MCP-kontekst\nmcpAdditionalContextDescription=Yderligere instruktioner, der skal sendes til MCP-klienten. Brug dette til at styre agentens adfærd og give yderligere kontekst til din individuelle opsætning.\nmcpAdditionalContextSample=- Genstart ikke tjenester og dæmoner automatisk uden at bekræfte det først\\n- Når du konfigurerer en netværksgrænseflade, skal du altid bruge 192.168.1.1/24 som gateway\nprefsRestartTitle=Genstart påkrævet\nprefsRestartContent=Nogle af de indstillinger, du har ændret, kræver en genstart af programmet for at blive anvendt. Vil du genstarte XPipe nu?\nbashShell=Bash-skal\n"
  },
  {
    "path": "lang/strings/translations_de.properties",
    "content": "delete=Löschen\nproperties=Eigenschaften\nusedDate=Verwendet $DATE$\nopenDir=Verzeichnis öffnen\nsortLastUsed=Nach dem Datum der letzten Verwendung sortieren\nsortAlphabetical=Alphabetisch nach Namen sortieren\nsortIndexed=Nach Bestellindex sortieren\nrestartDescription=Ein Neustart kann oft eine schnelle Lösung sein\nreportIssue=Ein Problem melden\nreportIssueDescription=Öffnen Sie den integrierten Issue Reporter\nusefulActions=Nützliche Aktionen\nstored=Gespeicherte\ntroubleshootingOptions=Tools zur Fehlersuche\ntroubleshoot=Fehlerbehebung\nremote=Entfernte Datei\naddShellStore=Shell hinzufügen ...\naddShellTitle=Shell-Verbindung hinzufügen\nsavedConnections=Gespeicherte Verbindungen\nsave=Speichern\nclean=Reinigen\n#custom\nmoveTo=Kategorie ändern ...\naddDatabase=Datenbank ...\nbrowseInternalStorage=Internen Speicher durchsuchen\naddTunnel=Tunnel ...\naddService=Service ...\naddScript=Skript ...\n#custom\naddHost=Remote Host ...\naddShell=Shell-Umgebung ...\naddCommand=Befehl ...\naddAutomatically=Automatisch hinzufügen ...\naddOther=Andere hinzufügen ...\nconnectionAdd=Verbindung hinzufügen\nscriptAdd=Skript hinzufügen\nscriptGroupAdd=Skriptgruppe hinzufügen\nidentityAdd=Identität hinzufügen\nnew=Neu\nselectType=Typ auswählen\nselectTypeDescription=Verbindungstyp auswählen\nselectShellType=Shell-Typ\nselectShellTypeDescription=Wähle den Typ der Shell-Verbindung\nname=Name\nstoreIntroHeader=Verbindungs-Hub\nstoreIntroContent=Hier kannst du alle deine lokalen und entfernten Shell-Verbindungen an einem Ort verwalten. Zu Beginn kannst du verfügbare Verbindungen schnell und automatisch erkennen und auswählen, welche du hinzufügen möchtest.\nstoreIntroButton=Suche nach Verbindungen ...\ndragAndDropFilesHere=Oder ziehe eine Datei einfach per Drag & Drop hierher\nconfirmDsCreationAbortTitle=Abbruch bestätigen\nconfirmDsCreationAbortHeader=Willst du die Erstellung der Datenquelle abbrechen?\nconfirmDsCreationAbortContent=Alle Fortschritte bei der Erstellung von Datenquellen gehen verloren.\nconfirmInvalidStoreTitle=Validierung überspringen\nconfirmInvalidStoreContent=Willst du die Überprüfung der Verbindung überspringen? Du kannst diese Verbindung hinzufügen, auch wenn sie nicht validiert werden konnte, und die Verbindungsprobleme später beheben.\n#custom\nexpand=Erweitern\naccessSubConnections=Zugang zu Unterverbindungen\ncommon=Allgemein\ncolor=Farbe\nalwaysConfirmElevation=Erlaubniserhöhung immer bestätigen\nalwaysConfirmElevationDescription=Legt fest, wie mit Fällen umgegangen werden soll, in denen erhöhte Berechtigungen erforderlich sind, um einen Befehl auf einem System auszuführen, z. B. mit sudo.\\n\\nStandardmäßig werden alle sudo-Anmeldedaten während einer Sitzung zwischengespeichert und bei Bedarf automatisch bereitgestellt. Wenn diese Option aktiviert ist, wirst du jedes Mal aufgefordert, den erweiterten Zugriff zu bestätigen.\nallow=Erlaube\nask=Frag\ndeny=Verweigern\nshare=Zum Git-Repository hinzufügen\nunshare=Aus Git-Repository entfernen\nremove=Entfernen\ncreateNewCategory=Neue Unterkategorie\nprompt=Eingabeaufforderung\ncustomCommand=Benutzerdefinierter Befehl\nother=Andere\nsetLock=Sperre setzen\nselectConnection=Verbindung auswählen\nselectEntry=Eintrag auswählen\ncreateLock=Passphrase erstellen\nchangeLock=Passphrase ändern\ntest=Test\n#custom\nfinish=Fertigstellen\nerror=Ein Fehler ist aufgetreten\ndownloadStageDescription=Verschiebt heruntergeladene Dateien in das Download-Verzeichnis deines Systems und öffnet sie.\nok=Ok\nsearch=Suche\nrepeatPassword=Passwort wiederholen\naskpassAlertTitle=Askpass\nunsupportedOperation=Nicht unterstützte Operation: $MSG$\nfileConflictAlertTitle=Konflikt auflösen\nfileConflictAlertContent=Es wurde ein Konflikt festgestellt. Die Datei $FILE$ existiert bereits auf dem Zielsystem.\\n\\nWie möchtest du fortfahren?\nfileConflictAlertContentMultiple=Es wurde ein Konflikt festgestellt. Die Datei $FILE$ existiert bereits.\\n\\nWie möchtest du fortfahren? Möglicherweise gibt es weitere Konflikte, die du automatisch lösen kannst, indem du eine Option wählst, die für alle gilt.\nmoveAlertTitle=Umzug bestätigen\nmoveAlertHeader=Möchtest du die ($COUNT$) ausgewählten Elemente in $TARGET$ verschieben?\ndeleteAlertTitle=Bestätigung der Löschung\ndeleteAlertHeader=Willst du die ($COUNT$) ausgewählten Elemente löschen?\nselectedElements=Ausgewählte Elemente:\nmustNotBeEmpty=$VALUE$ darf nicht leer sein\nvalueMustNotBeEmpty=Der Wert darf nicht leer sein\ntransferDescription=Dateien zum Herunterladen hierher ziehen\ndragLocalFiles=Downloads von hier ziehen\nnull=$VALUE$ muss nicht null sein\nroots=Wurzeln\nscripts=Skripte\nsearchFilter=Suche ...\nrecent=Neueste\nshortcut=Shortcut\nbrowserWelcomeEmptyHeader=Dateibrowser\nbrowserWelcomeEmptyContent=Du kannst auf der linken Seite auswählen, welche Systeme im Dateibrowser geöffnet werden sollen. XPipe merkt sich, auf welche Systeme und Verzeichnisse du zuvor zugegriffen hast und zeigt sie in Zukunft in einem Schnellzugriffsmenü an.\nbrowserWelcomeEmptyButton=Lokalen Dateibrowser öffnen\nbrowserWelcomeSystems=Du warst vor kurzem mit den folgenden Systemen verbunden:\nbrowserWelcomeDocsHeader=Dokumentation\nbrowserWelcomeDocsContent=Wenn du dich lieber mit einem Leitfaden mit XPipe vertraut machen willst, schau dir die Dokumentations-Website an.\nbrowserWelcomeDocsButton=Offene Dokumentation\nhostFeatureUnsupported=$FEATURE$ ist nicht auf dem Host installiert\nmissingStore=$NAME$ gibt es nicht\nconnectionName=Verbindungsname\nconnectionNameDescription=Gib dieser Verbindung einen eigenen Namen\nopenFileTitle=Datei öffnen\nunknown=Unbekannt\nscanAlertTitle=Verbindungen hinzufügen\nscanAlertChoiceHeader=Ziel\nscanAlertChoiceHeaderDescription=Wähle aus, wo du nach Verbindungen suchen willst. Es wird zuerst nach allen verfügbaren Verbindungen gesucht.\nscanAlertHeader=Verbindungsarten\nscanAlertHeaderDescription=Wähle die Arten von Verbindungen aus, die du automatisch für das System hinzufügen möchtest.\nnoInformationAvailable=Keine Informationen verfügbar\nyes=Ja\nno=Nein\nerrorOccured=Ein Fehler ist aufgetreten\n#custom\nterminalErrorOccured=Ein terminaler Fehler ist aufgetreten\nerrorTypeOccured=Eine Ausnahme des Typs $TYPE$ wurde ausgelöst\npermissionsAlertTitle=Erforderliche Berechtigungen\npermissionsAlertHeader=Für die Durchführung dieses Vorgangs sind zusätzliche Berechtigungen erforderlich.\npermissionsAlertContent=Bitte folge dem Pop-up, um XPipe im Einstellungsmenü die erforderlichen Berechtigungen zu erteilen.\nerrorDetails=Fehlerdetails\nupdateReadyAlertTitle=Update bereit\nupdateReadyAlertHeader=Ein Update auf die Version $VERSION$ ist bereit zur Installation\nupdateReadyAlertContent=Dadurch wird die neue Version installiert und XPipe neu gestartet, sobald die Installation abgeschlossen ist.\nerrorNoDetail=Es sind keine Fehlerdetails verfügbar\nerrorNoExceptionMessage=Ein Fehler des Typs $TYPE$ wurde ausgelöst\nupdateAvailableTitle=Update verfügbar\nupdateAvailableContent=Ein XPipe-Update auf Version $VERSION$ steht zur Installation bereit. Auch wenn XPipe nicht gestartet werden konnte, kannst du versuchen, das Update zu installieren, um das Problem möglicherweise zu beheben.\nclipboardActionDetectedTitle=Zwischenablage Aktion erkannt\nclipboardActionDetectedContent=XPipe hat einen Inhalt in deiner Zwischenablage entdeckt, der geöffnet werden kann. Willst du ihn jetzt öffnen? Willst du den Inhalt deiner Zwischenablage importieren?\ninstall=Installieren ...\nignore=Ignorieren\npossibleActions=Verfügbare Aktionen\nreportError=Fehler melden\nreportOnGithub=Einen Fehlerbericht auf GitHub erstellen\nreportOnGithubDescription=Eröffne ein neues Thema im GitHub-Repository\nreportErrorDescription=Senden eines Fehlerberichts mit optionalem Benutzerfeedback und Diagnoseinformationen\nignoreError=Fehler ignorieren\nignoreErrorDescription=Ignoriere diesen Fehler und mach weiter, als wäre nichts passiert\nprovideEmail=Wie können wir dich kontaktieren (optional, nur wenn du eine Antwort erhalten möchtest). Dein Bericht ist standardmäßig anonym. Du kannst hier Kontaktinformationen wie eine E-Mail-Adresse angeben.\nadditionalErrorInfo=Zusätzliche Informationen bereitstellen (optional)\nadditionalErrorAttachments=Anhänge auswählen (optional)\ndataHandlingPolicies=Datenschutzrichtlinie\nsendReport=Bericht senden\nerrorHandler=Fehlerhandler\nevents=Ereignisse\nvalidate=Validiere\nstackTrace=Stack-Trace\npreviousStep=< Vorherige\nnextStep=Weiter >\n#custom\nfinishStep=Fertigstellen\n#custom\nselect=Auswählen\nbrowseInternal=Intern durchsuchen\ncheckOutUpdate=Update auschecken\nquit=Beenden\nnoTerminalSet=Es wurde keine Terminalanwendung automatisch eingestellt. Du kannst dies manuell im Einstellungsmenü tun.\nconnections=Verbindungen\nconnectionHub=Verbindungs-Hub\nsettings=Einstellungen\nexplorePlans=Lizenz\nhelp=Hilfe\n#custom\nabout=Informationen\ndeveloper=Entwickler\nbrowseFileTitle=Datei durchsuchen\nbrowser=Dateibrowser\nselectFileFromComputer=Eine Datei von diesem Computer auswählen\nlinks=Links\nwebsite=Website\ndiscordDescription=Dem Discord-Server beitreten\nredditDescription=Tritt dem XPipe Subreddit bei\nsecurity=Sicherheit\nsecurityPolicy=Sicherheitsinformationen\nsecurityPolicyDescription=Lies die detaillierte Sicherheitsrichtlinie\nprivacy=Datenschutzrichtlinie\nprivacyDescription=Lies die Datenschutzbestimmungen für die XPipe-Anwendung\nslackDescription=Dem Slack-Arbeitsbereich beitreten\nsupport=Unterstützung\ngithubDescription=Schau dir das GitHub-Repository an\nopenSourceNotices=Open-Source-Hinweise\ncheckForUpdates=Nach Updates suchen\ncheckForUpdatesDescription=Ein Update herunterladen, wenn es eins gibt\nlastChecked=Zuletzt geprüft\nversion=Version\n#custom\nbuild=Build\nruntimeVersion=Laufzeitversion\nvirtualMachine=Virtuelle Maschine\nupdateReady=Update installieren\nupdateReadyPortable=Update auschecken\nupdateReadyDescription=Ein Update wurde heruntergeladen und ist bereit zur Installation\nupdateReadyDescriptionPortable=Ein Update ist zum Download verfügbar\nupdateRestart=Neustart zur Aktualisierung\nnever=Niemals\nupdateAvailableTooltip=Update verfügbar\nptbAvailableTooltip=Öffentliche Testversion verfügbar\nvisitGithubRepository=GitHub-Repository besuchen\nupdateAvailable=Update verfügbar: $VERSION$\ndownloadUpdate=Update herunterladen\nlegalAccept=Ich akzeptiere die Endbenutzer-Lizenzvereinbarung\nconfirm=Bestätige\nprint=Drucken\nwhatsNew=Was ist neu in der Version $VERSION$ ($DATE$)\nantivirusNoticeTitle=Ein Hinweis auf Antivirenprogramme\nupdateChangelogAlertTitle=Changelog\ngreetingsAlertTitle=Willkommen bei XPipe\neula=Endbenutzer-Lizenzvertrag\nnews=Nachrichten\nintroduction=Einführung\nprivacyPolicy=Datenschutzrichtlinie\nagree=Zustimmen\ndisagree=Widerspreche\ndirectories=Verzeichnisse\nlogFile=Log-Datei\nlogFiles=Log-Dateien\nlogFilesAttachment=Log-Dateien\nissueReporter=Fehlerberichterstatter\nopenCurrentLogFile=Log-Dateien\nopenCurrentLogFileDescription=Die Protokolldatei der aktuellen Sitzung öffnen\nopenLogsDirectory=Logs-Verzeichnis öffnen\ninstallationFiles=Installationsdateien\nopenInstallationDirectory=Installationsdateien\nopenInstallationDirectoryDescription=XPipe-Installationsverzeichnis öffnen\nlaunchDebugMode=Debug-Modus\nlaunchDebugModeDescription=XPipe im Debug-Modus neu starten\nextensionInstallTitle=Herunterladen\nextensionInstallDescription=Diese Aktion erfordert zusätzliche Bibliotheken von Drittanbietern, die nicht von XPipe vertrieben werden. Du kannst sie hier automatisch installieren. Die Komponenten werden dann von der Website des Anbieters heruntergeladen:\nextensionInstallLicenseNote=Mit dem Download und der automatischen Installation erklärst du dich mit den Bedingungen der Drittanbieterlizenzen einverstanden:\nlicense=Lizenz\ninstallRequired=Installation erforderlich\nrestore=Wiederherstellen\nrestoreAllSessions=Alle Sitzungen wiederherstellen\nlimitedTouchscreenMode=Eingeschränkter Touchscreen-Modus\nlimitedTouchscreenModeDescription=Wenn du diese Anwendung auf einer eher exotischen Touchscreen-Oberfläche wie einem Telefonbildschirm verwendest, funktionieren einige Menüs möglicherweise nicht richtig. Wenn diese Option aktiviert ist, verwendet die Menüimplementierung eine eingeschränktere Funktionalität, um mit spärlich gesendeten Maus-/Touch-Ereignissen zu arbeiten.\nappearance=Erscheinungsbild\ndisplay=Anzeige\npersonalization=Personalisierung\ndisplayOptions=Optionen anzeigen\ntheme=Thema\nrdpConfiguration=Konfiguration des Remote-Desktops\nrdpClient=RDP-Client\nrdpClientDescription=Das RDP-Client-Programm, das beim Starten von RDP-Verbindungen aufgerufen wird.\\n\\nBeachte, dass die verschiedenen Clients einen unterschiedlichen Grad an Fähigkeiten und Integrationen haben. Einige Clients unterstützen die automatische Übergabe von Passwörtern nicht, so dass du sie beim Start immer noch eingeben musst.\nlocalShell=Lokale Shell\nthemeDescription=Dein bevorzugtes Anzeigethema.\ndontAutomaticallyStartVmSshServer=SSH-Server für VMs bei Bedarf nicht automatisch starten\ndontAutomaticallyStartVmSshServerDescription=Jede Shell-Verbindung zu einer VM, die in einem Hypervisor läuft, wird über SSH hergestellt. XPipe kann bei Bedarf automatisch den installierten SSH-Server starten. Wenn du das aus Sicherheitsgründen nicht möchtest, kannst du dieses Verhalten mit dieser Option einfach deaktivieren.\nconfirmGitShareTitle=Git-Synchronisation\nconfirmGitShareContent=Willst du die ausgewählte Datei zu deinem Git Repository hinzufügen? Dadurch wird eine verschlüsselte Version der Datei in dein Git-Repository kopiert und deine Änderungen werden übertragen. Du hast dann auf allen synchronisierten Desktops Zugriff auf die Datei.\ngitShareFileTooltip=Füge die Datei zum Git Vault-Datenverzeichnis hinzu, damit sie automatisch synchronisiert wird.\\n\\nDiese Aktion kann nur verwendet werden, wenn der Git Tresor in den Einstellungen aktiviert ist.\nperformanceMode=Leistungsmodus\nperformanceModeDescription=Deaktiviert alle visuellen Effekte, die nicht benötigt werden, um die Leistung der Anwendung zu verbessern.\ndontAcceptNewHostKeys=Neue SSH-Hostschlüssel nicht automatisch akzeptieren\ndontAcceptNewHostKeysDescription=XPipe akzeptiert standardmäßig automatisch Host-Schlüssel von Systemen, auf denen dein SSH-Client noch keinen bekannten Host-Schlüssel gespeichert hat. Wenn sich jedoch ein bekannter Host-Schlüssel geändert hat, wird die Verbindung verweigert, bis du den neuen Schlüssel akzeptierst.\\n\\nWenn du dieses Verhalten deaktivierst, kannst du alle Host-Schlüssel überprüfen, auch wenn es zunächst keinen Konflikt gibt.\nuiScale=UI-Skala\nuiScaleDescription=Ein benutzerdefinierter Skalierungswert, der unabhängig von der systemweiten Anzeigeskala eingestellt werden kann. Die Werte sind in Prozent angegeben, d.h. ein Wert von 150 ergibt eine UI-Skalierung von 150%.\neditorProgram=Editor-Programm\neditorProgramDescription=Der Standard-Texteditor, der beim Bearbeiten von Textdaten aller Art verwendet wird.\nwindowOpacity=Fenster-Opazität\nwindowOpacityDescription=Ändert die Deckkraft des Fensters, um zu verfolgen, was im Hintergrund passiert.\nuseSystemFont=Systemschriftart verwenden\nopenDataDir=Tresor-Datenverzeichnis\nopenDataDirButton=Offenes Datenverzeichnis\nopenDataDirDescription=Wenn du zusätzliche Dateien, wie z.B. SSH-Schlüssel, systemübergreifend mit deinem Git-Repository synchronisieren möchtest, kannst du sie in das Verzeichnis Speicherdaten legen. Bei allen Dateien, die dort referenziert werden, werden die Dateipfade automatisch auf allen synchronisierten Systemen angepasst.\n#custom\nupdates=Updates\nselectAll=Alles auswählen\nadvanced=Fortgeschrittene\nthirdParty=Open-Source-Hinweise\neulaDescription=Lies die Endbenutzer-Lizenzvereinbarung für die XPipe-Anwendung\nthirdPartyDescription=Die Open-Source-Lizenzen von Bibliotheken Dritter anzeigen\nworkspaceLock=Master-Passphrase\nenableGitStorage=Synchronisierung aktivieren\nsharing=Freigabe\ngitSync=Git-Synchronisation\nenableGitStorageDescription=Wenn diese Option aktiviert ist, initialisiert XPipe ein Git-Repository für den lokalen Tresor und überträgt alle Änderungen dorthin. Beachte, dass dafür Git installiert sein muss und das Laden und Speichern verlangsamen kann.\\n\\nAlle Kategorien, die synchronisiert werden sollen, müssen explizit als synchronisiert markiert werden.\nstorageGitRemote=URL der Fernsynchronisation\nstorageGitRemoteDescription=Wenn diese Option gesetzt ist, zieht XPipe beim Laden automatisch alle Änderungen ein und überträgt sie beim Speichern an das entfernte Repository.\\n\\nSo kannst du deinen Tresor für mehrere XPipe-Installationen freigeben. Es werden HTTP- und SSH-URLs sowie lokale Verzeichnisse unterstützt.\nvault=Tresor\nworkspaceLockDescription=Legt ein benutzerdefiniertes Passwort fest, um alle in XPipe gespeicherten vertraulichen Informationen zu verschlüsseln.\\n\\nDies erhöht die Sicherheit, da es eine zusätzliche Verschlüsselungsebene für deine gespeicherten sensiblen Daten bietet. Du wirst dann beim Start von XPipe aufgefordert, das Passwort einzugeben.\nuseSystemFontDescription=Legt fest, ob die Standard-Systemschriftart oder die Inter-Schriftart verwendet werden soll, die in XPipe enthalten ist.\ntooltipDelay=Tooltip-Verzögerung\ntooltipDelayDescription=Die Anzahl der Millisekunden, die gewartet wird, bis ein Tooltip angezeigt wird.\nfontSize=Schriftgröße\nwindowOptions=Fensteroptionen\nsaveWindowLocation=Speicherort des Fensters\nsaveWindowLocationDescription=Legt fest, ob die Fensterkoordinaten gespeichert und bei Neustarts wiederhergestellt werden sollen.\nstartupShutdown=Starten / Herunterfahren\nshowChildrenConnectionsInParentCategory=Unterkategorien in der übergeordneten Kategorie anzeigen\nshowChildrenConnectionsInParentCategoryDescription=Ob alle Verbindungen, die sich in Unterkategorien befinden, einbezogen werden sollen oder nicht, wenn eine bestimmte übergeordnete Kategorie ausgewählt wird.\\n\\nWenn diese Option deaktiviert ist, verhalten sich die Kategorien eher wie klassische Ordner, die nur ihren direkten Inhalt anzeigen, ohne Unterordner einzubeziehen.\ncondenseConnectionDisplay=Verbindungsanzeige verdichten\ncondenseConnectionDisplayDescription=Jede Verbindung auf der obersten Ebene sollte weniger Platz in der Vertikalen einnehmen, damit die Verbindungsliste übersichtlicher wird.\nopenConnectionSearchWindowOnConnectionCreation=Fenster für die Verbindungssuche bei der Verbindungsherstellung öffnen\nopenConnectionSearchWindowOnConnectionCreationDescription=Ob beim Hinzufügen einer neuen Shell-Verbindung automatisch das Fenster zur Suche nach verfügbaren Unterverbindungen geöffnet werden soll oder nicht.\nworkflow=Workflow\nsystem=System\napplication=Anwendung\nstorage=Speicher\nrunOnStartup=Beim Starten ausführen\ncloseBehaviour=Exit-Verhalten\ncloseBehaviourDescription=Legt fest, wie XPipe beim Schließen des Hauptfensters vorgehen soll.\nlanguage=Sprache\nlanguageDescription=Die zu verwendende Anzeigesprache. Die Übersetzungen werden durch Beiträge der Community verbessert. Du kannst die Übersetzungsarbeit unterstützen, indem du auf GitHub Korrekturen an den Übersetzungen einreichst.\nlightTheme=Licht-Thema\ndarkTheme=Dunkles Thema\nexit=XPipe beenden\ncontinueInBackground=Im Hintergrund fortfahren\nminimizeToTray=In die Taskleiste minimieren\ncloseBehaviourAlertTitle=Schließverhalten einstellen\ncloseBehaviourAlertTitleHeader=Wähle aus, was beim Schließen des Fensters passieren soll. Alle aktiven Verbindungen werden geschlossen, wenn die Anwendung heruntergefahren wird.\nstartupBehaviour=Startverhalten\nstartupBehaviourDescription=Steuert das Standardverhalten der Desktop-Anwendung, wenn XPipe gestartet wird.\nclearCachesAlertTitle=Cache säubern\nclearCachesAlertContent=Willst du alle XPipe-Caches löschen? Damit werden alle Cache-Daten gelöscht, die zur Verbesserung der Benutzerfreundlichkeit gespeichert werden.\nstartGui=GUI starten\nstartInTray=Start im Tray\nstartInBackground=Im Hintergrund starten\nclearCaches=Caches löschen ...\nclearCachesDescription=Alle Cache-Daten löschen\ncancel=Abbrechen\nnotAnAbsolutePath=Kein absoluter Pfad\nnotADirectory=Nicht ein Verzeichnis\nnotAnEmptyDirectory=Kein leeres Verzeichnis\nautomaticallyCheckForUpdates=Nach Updates suchen\nautomaticallyCheckForUpdatesDescription=Wenn diese Funktion aktiviert ist, werden die Informationen zu neuen Versionen automatisch abgerufen, während XPipe nach einer Weile läuft. Du musst die Installation von Updates immer noch explizit bestätigen.\nsendAnonymousErrorReports=Anonyme Fehlerberichte senden\nsendUsageStatistics=Anonyme Nutzungsstatistiken senden\nstorageDirectory=Speicherverzeichnis\nstorageDirectoryDescription=Der Ort, an dem XPipe alle Verbindungsinformationen speichern soll. Wenn du diesen Ort änderst, werden die Daten aus dem alten Verzeichnis nicht in das neue kopiert.\nlogLevel=Log-Level\nappBehaviour=Verhalten der Anwendung\nlogLevelDescription=Die Protokollstufe, die beim Schreiben von Protokolldateien verwendet werden soll.\ndeveloperMode=Entwickler-Modus\ndeveloperModeDescription=Wenn du diese Option aktivierst, hast du Zugang zu einer Reihe von zusätzlichen Optionen, die für die Entwicklung nützlich sind.\neditor=Editor\ncustom=Benutzerdefiniert\npasswordManager=Passwort-Manager\nexternalPasswordManager=Externer Passwort-Manager\npasswordManagerDescription=Der lokal installierte Passwortmanager, in den du dich integrieren kannst.\\n\\nWenn du einen Passwortmanager installiert hast, kannst du XPipe so konfigurieren, dass es Passwörter von diesem abruft, damit XPipe die Passwörter nicht selbst speichern muss. Wenn diese Funktion aktiviert ist, kann jedes Passwortfeld für eine Verbindung so konfiguriert werden, dass der Passwortmanager verwendet wird.\npasswordManagerCommandTest=Passwort-Manager testen\npasswordManagerCommandTestDescription=Du kannst hier testen, ob die Ausgabe korrekt aussieht, wenn du einen Passwortmanager eingerichtet hast.\npreferTerminalTabs=Lieber neue Tabs öffnen\npreferTerminalTabsDescription=Legt fest, ob XPipe versuchen soll, neue Tabs in dem von dir gewählten Terminal zu öffnen, anstatt neue Fenster zu öffnen. Nicht jedes Terminal unterstützt Tabs.\ncustomRdpClientCommand=Benutzerdefinierter Befehl\ncustomRdpClientCommandDescription=Der Befehl, der ausgeführt werden soll, um den benutzerdefinierten RDP-Client zu starten.\\n\\nDer Platzhalter-String $FILE wird beim Aufruf durch den absoluten .rdp-Dateinamen in Anführungszeichen ersetzt. Vergiss nicht, den ausführbaren Pfad in Anführungszeichen zu setzen, wenn er Leerzeichen enthält.\ncustomEditorCommand=Benutzerdefinierter Editor-Befehl\ncustomEditorCommandDescription=Der Befehl, der ausgeführt werden muss, um den benutzerdefinierten Editor zu starten.\\n\\nDer Platzhalter-String $FILE wird beim Aufruf durch den absoluten Dateinamen in Anführungszeichen ersetzt. Denke daran, den ausführbaren Pfad deines Editors in Anführungszeichen zu setzen, wenn er Leerzeichen enthält.\neditorReloadTimeout=Zeitüberschreitung beim Neuladen des Editors\neditorReloadTimeoutDescription=Die Anzahl der Millisekunden, die gewartet wird, bevor eine Datei nach einer Aktualisierung gelesen wird. Dadurch werden Probleme vermieden, wenn dein Editor beim Schreiben oder Freigeben von Dateisperren langsam ist.\nencryptAllVaultData=Alle Tresordaten verschlüsseln\nencryptAllVaultDataDescription=Wenn diese Funktion aktiviert ist, werden alle Verbindungsdaten im Tresor mit dem Verschlüsselungscode deines Benutzertresors verschlüsselt, nicht nur die geheimen Daten. Dies bietet eine zusätzliche Sicherheitsebene für andere Parameter wie Benutzernamen, Hostnamen usw., die im Tresor standardmäßig nicht verschlüsselt sind.\\n\\nDiese Option macht den Verlauf und die Diffs deines Git-Datenspeichers unbrauchbar, da du die ursprünglichen Änderungen nicht mehr sehen kannst, sondern nur noch die binären Änderungen.\nvaultSecurity=Tresor Sicherheit\ndeveloperDisableUpdateVersionCheck=Update-Versionsprüfung deaktivieren\ndeveloperDisableUpdateVersionCheckDescription=Legt fest, ob der Update-Checker die Versionsnummer bei der Suche nach einem Update ignorieren soll.\ndeveloperDisableGuiRestrictions=GUI-Einschränkungen deaktivieren\ndeveloperDisableGuiRestrictionsDescription=Steuert, ob einige deaktivierte Aktionen noch über die Benutzeroberfläche ausgeführt werden können.\ndeveloperShowHiddenEntries=Versteckte Einträge anzeigen\ndeveloperShowHiddenEntriesDescription=Wenn aktiviert, werden versteckte und interne Datenquellen angezeigt.\ndeveloperShowHiddenProviders=Versteckte Anbieter anzeigen\ndeveloperShowHiddenProvidersDescription=Steuert, ob versteckte und interne Verbindungs- und Datenquellenanbieter im Erstellungsdialog angezeigt werden.\ndeveloperDisableConnectorInstallationVersionCheck=Connector-Versionsprüfung deaktivieren\ndeveloperDisableConnectorInstallationVersionCheckDescription=Legt fest, ob der Update-Checker die Versionsnummer ignoriert, wenn er die Version eines XPipe-Anschlusses prüft, der auf einem entfernten Computer installiert ist.\nshellCommandTest=Shell-Befehlstest\nshellCommandTestDescription=Führe einen Befehl in der Shell-Sitzung aus, die intern von XPipe verwendet wird.\nterminal=Terminal\nterminalType=Terminal-Emulator\nterminalConfiguration=Terminal-Konfiguration\nterminalCustomization=Terminal-Anpassung\neditorConfiguration=Editor-Konfiguration\ndefaultApplication=Standardanwendung\ninitialSetup=Ersteinrichtung\nterminalTypeDescription=Das Standardterminal, das zum Öffnen von Shell-Verbindungen verwendet wird.\\n\\nDie Unterstützung der Funktionen ist je nach Terminal unterschiedlich und jedes Terminal ist entweder als empfohlen oder nicht empfohlen gekennzeichnet. Das empfohlene Terminal bietet dir die beste Benutzererfahrung.\nprogram=Programm\ncustomTerminalCommand=Benutzerdefinierter Terminalbefehl\ncustomTerminalCommandDescription=Der Befehl, der ausgeführt werden soll, um das benutzerdefinierte Terminal mit einem bestimmten Befehl zu öffnen.\\n\\nXPipe erstellt ein temporäres Launcher-Shell-Skript für dein Terminal, das ausgeführt wird. Die Platzhalterzeichenfolge $CMD in dem von dir angegebenen Befehl wird beim Aufruf durch das eigentliche Launcher-Skript ersetzt. Vergiss nicht, den ausführbaren Pfad deines Terminals in Anführungszeichen zu setzen, wenn er Leerzeichen enthält.\nclearTerminalOnInit=Terminal bei Init löschen\nclearTerminalOnInitDescription=Wenn diese Funktion aktiviert ist, führt XPipe nach dem Start einer neuen Terminalsitzung einen Löschbefehl aus, um alle unnötigen Ausgaben zu entfernen, die beim Start der Terminalsitzung ausgegeben wurden.\ndontCachePasswords=Aufgeforderte Passwörter nicht zwischenspeichern\ndontCachePasswordsDescription=Steuert, ob abgefragte Passwörter von XPipe intern zwischengespeichert werden sollen, damit du sie in der aktuellen Sitzung nicht erneut eingeben musst.\\n\\nWenn dieses Verhalten deaktiviert ist, musst du die abgefragten Anmeldedaten jedes Mal neu eingeben, wenn sie vom System verlangt werden.\ndenyTempScriptCreation=Temporäre Skripterstellung verweigern\ndenyTempScriptCreationDescription=Um einige seiner Funktionen zu realisieren, erstellt XPipe manchmal temporäre Shell-Skripte auf einem Zielsystem, um die einfache Ausführung von Befehlen zu ermöglichen. Diese enthalten keine sensiblen Informationen und werden nur zu Implementierungszwecken erstellt.\\n\\nWenn dieses Verhalten deaktiviert ist, erstellt XPipe keine temporären Dateien auf einem entfernten System. Diese Option ist in hochsicheren Kontexten nützlich, in denen jede Dateisystemänderung überwacht wird. Wenn diese Option deaktiviert ist, funktionieren einige Funktionen, z. B. Shell-Umgebungen und Skripte, nicht wie vorgesehen.\ndisableCertutilUse=Deaktiviere die Verwendung von certutil unter Windows\nuseLocalFallbackShell=Lokale Fallback-Shell verwenden\nuseLocalFallbackShellDescription=Wechsle zur Verwendung einer anderen lokalen Shell, um lokale Operationen durchzuführen. Das wäre die PowerShell unter Windows und die Bourne Shell auf anderen Systemen.\\n\\nDiese Option kann verwendet werden, wenn die normale lokale Standardshell deaktiviert oder in gewissem Maße beschädigt ist. Einige Funktionen funktionieren möglicherweise nicht wie erwartet, wenn diese Option aktiviert ist.\ndisableCertutilUseDescription=Aufgrund verschiedener Unzulänglichkeiten und Bugs in cmd.exe werden temporäre Shell-Skripte mit certutil erstellt, indem es zur Dekodierung von base64-Eingaben verwendet wird, da cmd.exe bei Nicht-ASCII-Eingaben versagt. XPipe kann auch die PowerShell dafür verwenden, aber das ist langsamer.\\n\\nDadurch wird die Verwendung von certutil auf Windows-Systemen deaktiviert, um einige Funktionen zu realisieren und stattdessen auf die PowerShell zurückzugreifen. Das könnte einige AVs freuen, da einige von ihnen die Verwendung von certutil blockieren.\ndisableTerminalRemotePasswordPreparation=Terminal-Fernpasswortvorbereitung deaktivieren\ndisableTerminalRemotePasswordPreparationDescription=In Situationen, in denen eine Remote-Shell-Verbindung über mehrere Zwischensysteme im Terminal hergestellt werden soll, kann es erforderlich sein, alle erforderlichen Passwörter auf einem der Zwischensysteme vorzubereiten, um ein automatisches Ausfüllen der Eingabeaufforderungen zu ermöglichen.\\n\\nWenn du nicht möchtest, dass die Passwörter jemals an ein Zwischensystem übertragen werden, kannst du dieses Verhalten deaktivieren. Jedes erforderliche Zwischen-System-Passwort wird dann beim Öffnen des Terminals selbst abgefragt.\nmore=Mehr\ntranslate=Übersetzungen\nallConnections=Alle Verbindungen\nallScripts=Alle Skripte\nallIdentities=Alle Identitäten\nsynced=Synchronisiert\npredefined=Vordefinierte\nsamples=Beispiele\ngoodMorning=Guten Morgen\ngoodAfternoon=Guten Tag\ngoodEvening=Guten Abend\n#custom\naddVisual=Visuell ...\naddDesktop=Desktop ...\nssh=SSH\nsshConfiguration=SSH-Konfiguration\nsize=Größe\nattributes=Attribute\nmodified=Geändert\nowner=Eigentümer\nupdateReadyTitle=Update auf $VERSION$ bereit\n#custom\ntemplates=Vorlagen\nretry=Wiederholung\nretryAll=Alle Versuche wiederholen\nreplace=Ersetze\nreplaceAll=Ersetze alle\n#custom\nhibernateBehaviour=Verhalten im Ruhezustand\nhibernateBehaviourDescription=Steuert, wie sich die Anwendung verhält, wenn dein System in den Ruhezustand versetzt wird.\noverview=Übersicht\n#custom\nhistory=Verlauf\nskipAll=Alles überspringen\nnotes=Anmerkungen\naddNotes=Notizen hinzufügen\norder=Neu ordnen\nkeepFirst=Zuerst behalten\nkeepLast=Zuletzt behalten\npinToTop=Pin nach oben\nunpinFromTop=Unpin von oben\norderAheadOf=Vorbestellen ...\nclearIndex=Index zurücksetzen\nhttpServer=HTTP-Server\nmcpServer=MCP-Server\napiKey=API-Schlüssel\napiKeyDescription=Der API-Schlüssel zur Authentifizierung von XPipe Daemon API-Anfragen. Weitere Informationen zur Authentifizierung findest du in der allgemeinen API-Dokumentation.\ndisableApiAuthentication=API-Authentifizierung deaktivieren\ndisableApiAuthenticationDescription=Deaktiviert alle erforderlichen Authentifizierungsmethoden, so dass jede nicht authentifizierte Anfrage bearbeitet wird.\\n\\nDie Authentifizierung sollte nur zu Entwicklungszwecken deaktiviert werden.\napi=API\nstoreIntroImportContent=Du nutzt XPipe bereits auf einem anderen System? Synchronisiere deine bestehenden Verbindungen über mehrere Systeme hinweg über ein entferntes Git-Repository. Du kannst auch später jederzeit synchronisieren, wenn es noch nicht eingerichtet ist.\nstoreIntroImportButton=Verbindungen synchronisieren ...\nstoreIntroImportHeader=Verbindungen importieren\nshowNonRunningChildren=Nicht laufende Kinder anzeigen\nhttpApi=HTTP-API\nisOnlySupportedLimit=wird nur mit einer professionellen Lizenz unterstützt, wenn mehr als $COUNT$ Verbindungen bestehen\nareOnlySupportedLimit=werden nur mit einer professionellen Lizenz unterstützt, wenn mehr als $COUNT$ Verbindungen bestehen\nenabled=Aktiviert\nenableGitStoragePtbDisabled=Die Git-Synchronisierung ist für öffentliche Test-Builds deaktiviert, um die Verwendung mit regulären Git-Repositories zu verhindern und um davon abzuraten, einen PTB-Build als täglichen Treiber zu verwenden.\ncopyId=API-ID kopieren\nrequireDoubleClickForConnections=Doppelklick für Verbindungen erforderlich\nrequireDoubleClickForConnectionsDescription=Wenn diese Funktion aktiviert ist, musst du auf die Verbindungen doppelklicken, um sie zu starten. Das ist nützlich, wenn du es gewohnt bist, auf Dinge doppelt zu klicken.\nclearTransferDescription=Auswahl löschen\n#custom\nselectTab=Tab auswählen\n#custom\ncloseTab=Tab schließen\n#custom\ncloseOtherTabs=Andere Tabs schließen\n#custom\ncloseAllTabs=Alle Tabs schließen\ncloseLeftTabs=Tabs nach links schließen\ncloseRightTabs=Tabs nach rechts schließen\naddSerial=Serielle ...\nconnect=Verbinden\nworkspaces=Arbeitsbereiche\nmanageWorkspaces=Arbeitsbereiche verwalten\naddWorkspace=Arbeitsbereich hinzufügen ...\nworkspaceAdd=Einen neuen Arbeitsbereich hinzufügen\nworkspaceAddDescription=Arbeitsbereiche sind unterschiedliche Konfigurationen für die Ausführung von XPipe. Jeder Arbeitsbereich hat ein Datenverzeichnis, in dem alle Daten lokal gespeichert werden. Dazu gehören Verbindungsdaten, Einstellungen und mehr.\\n\\nWenn du die Synchronisierungsfunktion nutzt, kannst du jeden Arbeitsbereich auch mit einem anderen Git-Repository synchronisieren.\nworkspaceName=Name des Arbeitsbereichs\nworkspaceNameDescription=Der Anzeigename des Arbeitsbereichs\nworkspacePath=Pfad zum Arbeitsbereich\nworkspacePathDescription=Der Ort des Datenverzeichnisses des Arbeitsbereichs\nworkspaceCreationAlertTitle=Arbeitsbereich erstellen\ndeveloperForceSshTty=SSH TTY erzwingen\ndeveloperForceSshTtyDescription=Lass alle SSH-Verbindungen ein pty zuweisen, um die Unterstützung für einen fehlenden stderr und ein pty zu testen.\ndeveloperDisableSshTunnelGateways=SSH-Gateway-Tunneling deaktivieren\ndeveloperDisableSshTunnelGatewaysDescription=Verwende keine Tunnelsitzungen für Gateways und verbinde dich stattdessen direkt mit dem System.\nttyWarning=Die Verbindung hat zwangsweise ein pty/tty zugewiesen und stellt keinen separaten stderr-Stream zur Verfügung.\\n\\nDas kann zu einigen Problemen führen.\\n\\nWenn du kannst, solltest du dafür sorgen, dass der Verbindungsbefehl kein pty zuweist.\nxshellSetup=Xshell-Einrichtung\ntermiusSetup=Termius Einrichtung\ntryPtbDescription=Probiere neue Funktionen in XPipe-Entwickler-Builds frühzeitig aus\nconfirmVaultUnencryptTitle=Entschlüsselung des Tresors bestätigen\nconfirmVaultUnencryptContent=Willst du die erweiterte Tresorverschlüsselung wirklich deaktivieren? Dadurch wird die zusätzliche Verschlüsselung für gespeicherte Daten entfernt und die vorhandenen Daten werden überschrieben.\nenableHttpApi=HTTP-API aktivieren\nenableHttpApiDescription=Aktiviert die API, damit externe Programme den XPipe-Daemon aufrufen können, um Aktionen mit deinen verwalteten Verbindungen durchzuführen.\nchooseCustomIcon=Benutzerdefiniertes Symbol auswählen\ngitVault=Git-Tresor\nfileBrowser=Dateibrowser\nconfirmAllDeletions=Bestätige alle Löschungen\nconfirmAllDeletionsDescription=Ob ein Bestätigungsdialog für alle Löschvorgänge angezeigt werden soll. Standardmäßig benötigen nur Verzeichnisse eine Bestätigung.\nyesterday=Gestern\ngreen=Grün\nyellow=Gelb\nblue=Blau\nred=Rot\ncyan=Cyan\npurple=Lila\nasktextAlertTitle=Eingabeaufforderung\nfileWriteSudoTitle=Sudo-Datei schreiben\nfileWriteSudoContent=Die Datei, die du zu schreiben versuchst, gewährt deinem Benutzer keine Schreibrechte. Willst du diese Datei als root mit sudo schreiben? Dadurch wirst du entweder mit den vorhandenen Anmeldeinformationen oder über eine Eingabeaufforderung automatisch zu root befördert.\ndontAllowTerminalRestart=Terminal-Neustart nicht zulassen\ndontAllowTerminalRestartDescription=Standardmäßig können Terminalsitzungen neu gestartet werden, nachdem sie vom Terminal aus beendet wurden. Um dies zu ermöglichen, akzeptiert XPipe diese externen Anfragen vom Terminal, um die Sitzung erneut zu starten\\n\\nXPipe hat keine Kontrolle über das Terminal und darüber, woher dieser Aufruf kommt. Daher können böswillige lokale Anwendungen diese Funktion ebenfalls nutzen, um Verbindungen über XPipe zu starten. Die Deaktivierung dieser Funktion verhindert dieses Szenario.\nopenDocumentation=Dokumentation öffnen\nopenDocumentationDescription=Besuche die XPipe-Dokumentationsseite zu diesem Thema\nrenameAll=Alle umbenennen\nlogging=Protokollierung\nenableTerminalLogging=Terminalprotokollierung einschalten\nenableTerminalLoggingDescription=Aktiviert die clientseitige Protokollierung für alle Terminalsitzungen. Alle Eingaben und Ausgaben der Terminalsitzung werden in eine Sitzungsprotokolldatei geschrieben. Beachte, dass sensible Informationen wie Passwortabfragen nicht aufgezeichnet werden.\nterminalLoggingDirectory=Terminal-Sitzungsprotokolle\nterminalLoggingDirectoryDescription=Alle Protokolle werden in dem XPipe-Datenverzeichnis auf deinem lokalen System gespeichert.\nopenSessionLogs=Sitzungsprotokolle öffnen\nsessionLogging=Terminal-Protokollierung\nsessionActive=Für diese Verbindung wird eine Hintergrundsitzung durchgeführt.\\n\\nUm diese Sitzung manuell zu beenden, klicke auf die Statusanzeige.\nskipValidation=Validierung überspringen\nscriptsIntroHeader=Über Skripte\nscriptsIntroContent=Du kannst Skripte auf der Shell init, im Dateibrowser und bei Bedarf ausführen. Du kannst Skripte in XPipe selbst erstellen oder bestehende Skripte von deinem lokalen System oder aus einem entfernten Git-Repository importieren.\nscriptsIntroBottomHeader=Skripte verwenden\nscriptsIntroBottomContent=Für den Anfang gibt es eine Reihe von Beispielskripten. Du kannst auf die Bearbeitungsschaltfläche der einzelnen Skripte klicken, um zu sehen, wie sie implementiert sind. Die Skripte müssen zuerst aktiviert werden, damit sie ausgeführt werden und in den Menüs erscheinen.\nscriptsIntroBottomButton=Anfangen\nscriptSourcesIntroHeader=Skript-Quellen\nscriptSourcesIntroContent=Du kannst eigene Skriptquellen hinzufügen, um sofortigen Zugriff auf eine ganze Sammlung von Shell-Skripten zu haben. Als Quellen werden sowohl lokale Quellen als auch entfernte Git-Repositories unterstützt. Alle erkannten Skripte aus der Quelle werden automatisch verfügbar.\nscriptSourcesIntroButton=Quelle hinzufügen ...\ncheckForSecurityUpdates=Nach Sicherheitsupdates suchen\ncheckForSecurityUpdatesDescription=XPipe kann getrennt von den normalen Funktionsupdates auf mögliche Sicherheitsupdates prüfen. Wenn dies aktiviert ist, werden zumindest wichtige Sicherheitsupdates zur Installation empfohlen, auch wenn die normale Updateprüfung deaktiviert ist.\\n\\nWenn du diese Einstellung deaktivierst, wird keine externe Versionsabfrage durchgeführt und du wirst nicht über Sicherheitsaktualisierungen benachrichtigt.\nclickToDock=Zum Andocken des Terminals klicken\nterminalStarting=Warten auf den Start des Terminals ...\n#custom\npinTab=Tab anheften\n#custom\nunpinTab=Tab abheften\npinned=Angepinnt\nenableConnectionHubTerminalDocking=Aktiviere das Andocken des Terminals an den Hub\nenableConnectionHubTerminalDockingDescription=Du kannst Terminalfenster an das XPipe-Anwendungsfenster im Verbindungs-Hub andocken, um eine Art integriertes Terminal zu simulieren. Die Terminalfenster werden dann von XPipe so verwaltet, dass sie immer in das Dock passen.\nenableFileBrowserTerminalDocking=Andocken des Dateibrowsers an das Terminal aktivieren\nenableFileBrowserTerminalDockingDescription=Du kannst Terminalfenster an das XPipe-Anwendungsfenster im Dateibrowser andocken, um eine Art integriertes Terminal zu simulieren. Die Terminalfenster werden dann von XPipe so verwaltet, dass sie immer in das Dock passen.\ndownloadsDirectory=Benutzerdefiniertes Download-Verzeichnis\ndownloadsDirectoryDescription=Das benutzerdefinierte Verzeichnis, in das heruntergeladene Dateien verschoben werden sollen, wenn du auf die Schaltfläche In Downloads verschieben klickst. Standardmäßig verwendet XPipe dein Benutzer-Download-Verzeichnis.\n#custom\npinLocalMachineOnStartup=Tab \"Lokaler Rechner\" beim Starten anheften\npinLocalMachineOnStartupDescription=Öffne automatisch eine Registerkarte für den lokalen Rechner und pinne sie an. Dies ist nützlich, wenn du häufig einen geteilten Dateibrowser verwendest, bei dem der lokale Rechner und das entfernte Dateisystem geöffnet sind.\nterminalErrorDescription=Dieser Fehler ist terminal und XPipe kann nicht fortfahren, ohne ihn zu beheben.\ngroupName=Gruppenname\nchmodPermissions=Neue Berechtigungen\neditFilesWithDoubleClick=Dateien mit Doppelklick bearbeiten\neditFilesWithDoubleClickDescription=Wenn diese Funktion aktiviert ist, werden Dateien durch einen Doppelklick direkt in deinem Texteditor geöffnet, anstatt das Kontextmenü anzuzeigen.\ncensorMode=Zensurmodus\ncensorModeDescription=Blendet alle Informationen wie Hostnamen, Benutzernamen, Verbindungsnamen und mehr aus.\\n\\nDas ist nützlich, wenn du einen Screenshot oder einen Screenshare von XPipe machen willst und keine Informationen preisgeben möchtest.\naddIdentity=Identität ...\nidentities=Identitäten\naddMacro=Aktion ...\nidentitiesIntroHeader=Über Identitäten\nidentitiesIntroContent=Wenn du häufige Kombinationen von Benutzernamen, Passwörtern und Schlüsseln verwendest, kann es sinnvoll sein, wiederverwendbare Identitäten zu erstellen. So kannst du sie schnell referenzieren, wenn du neue Verbindungen hinzufügst.\nidentitiesIntroBottomHeader=Identitäten teilen\nidentitiesIntroBottomContent=Du kannst Identitäten lokal hinzufügen oder sie auch im Git-Repository synchronisieren, wenn dies aktiviert ist. So kannst du Identitäten selektiv über mehrere Systeme hinweg und mit anderen Teammitgliedern teilen.\nidentitiesIntroBottomButton=Sync einrichten\nidentitiesIntroButton=Identität erstellen\nuserName=Benutzername\nuserAuth=Benutzerbasierte Passwortauthentifizierung\ngroupAuth=Gruppenbasierte geheime Authentifizierung\nteam=Team\nteamSettings=Team-Einstellungen\nteamVaults=Team-Tresore\nvaultTypeNameDefault=Standard-Tresor\nvaultTypeNameLegacy=Alter persönlicher Tresor\nvaultTypeNamePersonal=Persönlicher Tresor\nvaultTypeNameTeam=Team-Tresor\nteamVaultsDescription=Team-Tresore ermöglichen mehreren Benutzern und Gruppen den sicheren Zugriff auf einen gemeinsamen Tresor. Du kannst Verbindungen und Identitäten so konfigurieren, dass sie entweder für alle Benutzer/innen freigegeben werden oder nur für einzelne Benutzer/innen und Gruppen verfügbar sind, indem du sie mit ihrem eigenen Schlüssel verschlüsselst. Andere Tresorbenutzer können nicht auf persönliche und gruppenbasierte Verbindungen und Identitäten zugreifen, wenn sie keinen Zugriff auf den Schlüssel haben.\nvaultTypeContentDefault=Du verwendest derzeit einen Standard-Tresor ohne Benutzer und mit einer benutzerdefinierten Passphrase. Geheimnisse werden mit dem lokalen Tresorschlüssel verschlüsselt. Du kannst auf einen persönlichen Tresor upgraden, indem du ein Tresor-Benutzerkonto erstellst. Damit kannst du die Geheimnisse des Tresors mit deiner eigenen persönlichen Passphrase verschlüsseln, die du bei jeder Anmeldung eingeben musst, um den Tresor zu öffnen.\nvaultTypeContentLegacy=Du verwendest derzeit einen persönlichen Tresor für deinen Benutzer. Geheimnisse werden mit deiner persönlichen Passphrase verschlüsselt. Diese Legacy-Kompatibilität hat nur begrenzte Funktionen und kann nicht direkt zu einem Team-Tresor aufgerüstet werden.\nvaultTypeContentPersonal=Du verwendest derzeit einen persönlichen Tresor für deinen Benutzer. Geheimnisse werden mit deiner persönlichen Passphrase verschlüsselt. Du kannst auf einen Team-Tresor aufrüsten, indem du zusätzliche Tresor-Benutzer hinzufügst oder eine gruppenbasierte Zugriffskonfiguration hinzufügst.\nvaultTypeContentTeam=Du verwendest derzeit einen Teamtresor, der mehreren Benutzern einen sicheren Zugriff auf einen gemeinsamen Tresor ermöglicht. Du kannst Verbindungen und Identitäten so konfigurieren, dass sie entweder für alle Benutzer/innen freigegeben werden oder nur für deinen persönlichen Benutzer oder deine Gruppe verfügbar sind, indem du sie mit deinem persönlichen oder Gruppenschlüssel verschlüsselst. Andere Tresorbenutzer können nicht auf deine persönlichen und gruppenbasierten Verbindungen und Identitäten zugreifen, wenn sie keinen Zugriff auf den Schlüssel haben.\ngroupManagement=Gruppenmanagement\ngroupManagementEmpty=Gruppenmanagement\ngroupManagementDescription=Verwalte bestehende Tresorgruppen oder erstelle neue. Jede Tresorgruppe hat ihren eigenen geheimen Schlüssel, der zur Verschlüsselung von Verbindungen und Identitäten verwendet wird, die nur für die Gruppe und nicht für andere zugänglich sein sollen.\ngroupManagementEmptyDescription=Verwalte bestehende Tresorgruppen oder erstelle neue. Jede Tresorgruppe hat ihren eigenen geheimen Schlüssel, der zur Verschlüsselung von Verbindungen und Identitäten verwendet wird, die nur für die Gruppe und nicht für andere zugänglich sein sollen.\\n\\nGruppenbasierte Konten für ein Team werden im Professional-Plan unterstützt.\nuserManagement=Benutzerverwaltung\nuserManagementEmpty=Benutzerverwaltung\nuserManagementDescription=Verwalte bestehende Tresor-Benutzer oder erstelle neue. Jeder Tresor-Benutzer hat sein eigenes, individuelles Passwort, das zur Verschlüsselung von Verbindungen und Identitäten verwendet wird, die nur für den Benutzer und nicht für andere zugänglich sein sollen.\nuserManagementEmptyDescription=Verwalte bestehende Tresor-Benutzer oder erstelle neue. Jeder Tresor-Benutzer hat sein eigenes Passwort, mit dem er Verbindungen und Identitäten verschlüsselt, die nur für ihn selbst und nicht für andere zugänglich sein sollen. Erstelle einen Benutzer für dich selbst, um Verbindungen und Identitäten mit deinem persönlichen Schlüssel verschlüsseln zu können.\\n\\nIn der Community-Edition wird ein einzelnes Benutzerkonto unterstützt. Mehrere Benutzerkonten für ein Team werden im Professional-Plan unterstützt.\nuserIntroHeader=Benutzerverwaltung\nuserIntroContent=Erstelle das erste Benutzerkonto für dich, um loszulegen. Damit kannst du diesen Arbeitsbereich mit einem Passwort sperren.\naddReusableIdentity=Wiederverwendbare Identität hinzufügen\nusers=Benutzer\nsyncVault=Tresor-Synchronisation\nsyncVaultDescription=Um deinen Tresor über mehrere Systeme oder mit mehreren Teammitgliedern zu synchronisieren, aktiviere die Git-Synchronisierung für diesen Tresor.\nenableGitSync=Git Sync aktivieren\nbrowseVault=Tresordaten\nbrowseVaultDescription=Du kannst dir das Tresorverzeichnis selbst in deinem nativen Dateimanager ansehen. Beachte, dass externe Bearbeitungen nicht empfohlen werden und eine Reihe von Problemen verursachen können.\nbrowseVaultButton=Tresor durchsuchen\nvaultUsers=Tresor-Benutzer\ncreateHeapDump=Heap-Dump erstellen\ncreateHeapDumpDescription=Speicherinhalte in eine Datei ausgeben, um Fehler bei der Speichernutzung zu beheben\ninitializingApp=Verbindungen laden\ncheckingLicense=Prüfen der Lizenz\nloadingGit=Synchronisierung mit git repo\nloadingGpg=GnuPG-Daemon für Git starten\nloadingSettings=Einstellungen laden\nloadingConnections=Verbindungen laden\nunlockingVault=Tresor entriegeln\nloadingUserInterface=Benutzeroberfläche laden\nptbNotice=Hinweis für die öffentliche Testversion\nuserDeletionTitle=Benutzerlöschung\nuserDeletionContent=Willst du diesen Tresorbenutzer löschen? Dadurch werden alle deine persönlichen Identitäten und Verbindungsgeheimnisse mit dem Tresorschlüssel, der allen Benutzern zur Verfügung steht, neu verschlüsselt. Dies wird eine Weile dauern und XPipe muss neu gestartet werden, um die Benutzeränderungen zu übernehmen.\ngroupDeletionTitle=Gruppenlöschung\ngroupDeletionContent=Willst du diese Tresorgruppe löschen? Dadurch werden alle gruppeneigenen Identitäten und Verbindungsgeheimnisse mit dem Tresorschlüssel, der allen Benutzern zur Verfügung steht, neu verschlüsselt. Dies wird eine Weile dauern und XPipe muss neu gestartet werden, um die Gruppenänderungen zu übernehmen.\nkillTransfer=Übertragung abbrechen\ndestination=Ziel\nconfiguration=Konfiguration\nnewFile=Neue Datei\nnewLink=Neuer Link\nlinkName=Link-Name\nscanConnections=Verfügbare Verbindungen finden ...\nobserve=Beobachten beginnen\nstopObserve=Beobachten stoppen\ncreateShortcut=Desktop-Verknüpfung erstellen\nbrowseFiles=Dateien durchsuchen\nclone=Klonen\ntargetPath=Zielpfad\nnewDirectory=Neues Verzeichnis\ncopyShareLink=Link kopieren\nselectStore=Store auswählen\nsaveSource=Für später speichern\n#custom\nexecute=Ausführen\ndeleteChildren=Alle Kinder entfernen\nscriptGroupDescriptionDescription=Gib dieser Gruppe eine optionale Beschreibung\nabstractHostDescriptionDescription=Gib diesem Host eine optionale Beschreibung\nselectSource=Quelle auswählen\ncommandLineRead=Aktualisieren\ncommandLineWrite=Schreibe\nadditionalOptions=Zusätzliche Optionen\ninput=Eingabe\nmachine=Maschine\nopen=Öffnen\nedit=Bearbeiten\nscriptContents=Skript-Inhalte\nscriptContentsDescription=Die auszuführenden Skriptbefehle\nsnippets=Skript-Abhängigkeiten\nsnippetsDescription=Andere Skripte, die zuerst ausgeführt werden sollen\nsnippetsDependenciesDescription=Alle möglichen Skripte, die ggf. ausgeführt werden sollten\nisDefault=Wird in allen kompatiblen Shells auf init ausgeführt\nbringToShells=Zu allen kompatiblen Shells bringen\nisDefaultGroup=Alle Gruppenskripte auf der Shell init ausführen\nexecutionType=Ausführungsart\nexecutionTypeDescription=In welchen Kontexten ist dieses Skript zu verwenden?\nminimumShellDialect=Shell-Typ\nminimumShellDialectDescription=Der Shell-Typ, in dem das Skript ausgeführt werden soll\ndumbOnly=Dumm\nterminalOnly=Terminal\nboth=Beide\nshouldElevate=Sollte erheben\nshouldElevateDescription=Ob dieses Skript mit erhöhten Rechten ausgeführt werden soll\nscript.displayName=Shell-Skript\nscript.displayDescription=Ein wiederverwendbares Shell-Skript erstellen\nscriptGroup.displayName=Skript-Gruppe\nscriptGroup.displayDescription=Gruppieren Sie Skripte und organisieren Sie sie innerhalb\nscriptGroup=Gruppe\nscriptGroupDescription=Die Gruppe, der dieses Skript zugewiesen werden soll\nscriptGroupGroupDescription=Die optionale übergeordnete Gruppe, der diese Skriptgruppe zugewiesen wird\nopenInNewTab=In neuem Tab öffnen\nexecuteInBackground=im Hintergrund\nexecuteInTerminal=in $TERM$\nback=Zurückgehen\nbrowseInWindowsExplorer=Blättern im Windows-Explorer\nbrowseInDefaultFileManager=Blättern im Standard-Dateimanager\nbrowseInFinder=Im Finder suchen\n#custom\ncopy=Kopieren\npaste=Einfügen\ncopyLocation=Ort kopieren\nabsolutePaths=Absolute Pfade\nabsoluteLinkPaths=Absolute Linkpfade\nabsolutePathsQuoted=Absolute Pfade in Anführungszeichen\nfileNames=Dateinamen\nlinkFileNames=Link Dateinamen\nfileNamesQuoted=Dateinamen (in Anführungszeichen)\ndeleteFile=Löschen $FILE$\neditWithEditor=Bearbeiten mit $EDITOR$\nfollowLink=Link folgen\ngoForward=Vorwärts gehen\nshowDetails=Details anzeigen\nshowDetailsDescription=Stack Trace eines Fehlers anzeigen\nopenFileWith=Öffnen mit ...\nopenWithDefaultApplication=Mit Standardanwendung öffnen\nrename=Umbenennen\nrun=Ausführen\nopenInTerminal=Im Terminal öffnen\nfile=Datei\ndirectory=Verzeichnis\nsymbolicLink=Symbolischer Link\ndesktopEnvironment.displayName=Desktop-Umgebung\ndesktopEnvironment.displayDescription=Eine wiederverwendbare Konfiguration der Remotedesktopumgebung erstellen\ndesktopHost=Desktop-Host\ndesktopHostDescription=Die Desktop-Verbindung, die als Basis verwendet wird\ndesktopShellDialect=Shell-Dialekt\ndesktopShellDialectDescription=Der Shell-Dialekt, der zum Ausführen von Skripten und Anwendungen verwendet wird\ndesktopSnippets=Skript-Schnipsel\ndesktopSnippetsDescription=Liste der wiederverwendbaren Skriptschnipsel, die zuerst ausgeführt werden sollen\ndesktopInitScript=Init-Skript\ndesktopInitScriptDescription=Spezifische Init-Befehle für diese Umgebung\ndesktopTerminal=Terminal-Anwendung\ndesktopTerminalDescription=Das Terminal, das auf dem Desktop zum Starten von Skripten verwendet wird\ndesktopApplication.displayName=Desktop-Anwendung\ndesktopApplication.displayDescription=Eine Anwendung auf einem entfernten Desktop ausführen\ndesktopBase=Desktop\ndesktopBaseDescription=Der Desktop, auf dem diese Anwendung ausgeführt wird\ndesktopEnvironmentBase=Desktop-Umgebung\ndesktopEnvironmentBaseDescription=Die Desktop-Umgebung, auf der diese Anwendung ausgeführt werden soll\ndesktopApplicationPath=Anwendungspfad\ndesktopApplicationPathDescription=Der Pfad der ausführbaren Datei, die ausgeführt werden soll\ndesktopApplicationArguments=Argumente\ndesktopApplicationArgumentsDescription=Die optionalen Argumente, die an die Anwendung übergeben werden\ndesktopCommand.displayName=Desktop-Befehl\ndesktopCommand.displayDescription=Einen Befehl in einer Remote-Desktop-Umgebung ausführen\ndesktopCommandScript=Befehle\ndesktopCommandScriptDescription=Die Befehle, die in der Umgebung ausgeführt werden sollen\nservice.displayName=Dienst\nservice.displayDescription=Einen Ferndienst an deinen lokalen Rechner weiterleiten\nserviceLocalPort=Expliziter lokaler Port\nserviceLocalPortDescription=Der lokale Port, an den weitergeleitet werden soll, andernfalls wird ein zufälliger Port verwendet\nserviceRemotePort=Entfernter Anschluss\nserviceRemotePortDescription=Der Port, auf dem der Dienst läuft\nserviceHost=Diensthost\nserviceHostDescription=Der Host, auf dem der Dienst läuft\nopenWebsite=Website öffnen\ncustomServiceGroup.displayName=Dienstgruppe\ncustomServiceGroup.displayDescription=Mehrere Dienste in einer Kategorie zusammenfassen\ninitScript=Init-Skript - Wird beim Shell-Init ausgeführt\nshellScript=Shell-Sitzungsskript - Skript für die Ausführung während einer Shell-Sitzung verfügbar machen\nrunnableScript=Ausführbares Skript - Erlaubt die direkte Ausführung eines Skripts über den Verbindungs-Hub\nfileScript=Dateiskript - Erlaubt den Aufruf eines Skripts für ausgewählte Dateien im Dateibrowser\nrunScript=Skript ausführen\ncopyUrl=URL kopieren\nfixedServiceGroup.displayName=Dienstgruppe\nfixedServiceGroup.displayDescription=Liste der verfügbaren Dienste auf einem System\nmappedService.displayName=Dienst\nmappedService.displayDescription=Interaktion mit einem Dienst, der von einem Container angeboten wird\ncustomService.displayName=Dienst\ncustomService.displayDescription=Automatisches Öffnen oder Tunneln eines Ferndienstports auf deinem lokalen Rechner\nfixedService.displayName=Dienst\nfixedService.displayDescription=Einen vordefinierten Dienst verwenden\nnoServices=Keine verfügbaren Dienste\nhasServices=$COUNT$ verfügbare Dienste\nhasService=$COUNT$ verfügbarer Dienst\nnoConnections=Keine verfügbaren Verbindungen\nhasConnections=$COUNT$ verfügbare Verbindungen\nhasConnection=$COUNT$ verfügbare Verbindung\n#custom\nopenHttp=HTTP-Dienst öffnen#11-Bibliothek\nopenHttps=HTTPS-Dienst öffnen\nnoScriptsAvailable=Keine aktivierten und kompatiblen Skripte verfügbar\nscriptsDisabled=Skripte deaktiviert\nchangeIcon=Symbol ändern\ninit=Init\nshell=Shell\nhub=Hub\nscript=skript\ngenericScript=Allgemein\ngradleTasks=Gradle Aufgaben\nrunTask=Aufgabe ausführen\narchiveName=Name des Archivs\ncompress=Komprimieren\ncompressContents=Inhalte komprimieren\nuntarHere=Untar hier\nuntarDirectory=Untar zu $DIR$\nunzipDirectory=Entpacken nach $DIR$\nunzipHere=Hier entpacken\nrequiresRestart=Erfordert einen Neustart zur Anwendung.\ndownload=Herunterladen\nservicePath=Dienstpfad\nservicePathDescription=Der optionale Unterpfad beim Öffnen der URL in einem Browser\nactive=Aktiv\ninactive=Inaktiv\n#custom\nstarting=Startend\n#custom\nremotePort=Entfernter Port\nremotePortNumber=Entfernter Anschluss $PORT$\nuserIdentity=Persönliche Identität\nglobalIdentity=Globale Identität\nidentityChoice=Benutzeridentität\nidentityChoiceDescription=Wähle eine vordefinierte Identität oder gib Anmeldedaten nur für diese Verbindung an\ndefineNewIdentityOrSelect=Neu eingeben oder vorhandene auswählen\nlocalIdentity.displayName=Lokale Identität\nlocalIdentity.displayDescription=Eine wiederverwendbare Identität für diesen lokalen Desktop erstellen\nsyncedIdentity.displayName=Synchrone Identität\nsyncedIdentity.displayDescription=Eine wiederverwendbare Identität erstellen, die systemübergreifend synchronisiert wird\nlocalIdentity=Lokale Identität\nkeyNotSynced=Die Schlüsseldatei ist noch nicht mit dem Git-Repository synchronisiert. Verwende die Schaltfläche Zu Git hinzufügen für die Schlüsseldatei, um sie hinzuzufügen.\nusernameDescription=Der Benutzername, mit dem du dich anmeldest\nidentity.displayName=Identität\nidentity.displayDescription=Eine wiederverwendbare Identität für Verbindungen erstellen\nlocal=Lokale\nshared=Global\nuserDescription=Der Benutzername oder die vordefinierte Identität, mit der du dich anmeldest\nidentityAccessLevel=Zugriffsebene\nidentityPerUser=Persönlicher Identitätszugang\nidentityPerUserDescription=Beschränke den Zugriff auf diese Identität und die damit verbundenen Verbindungen nur auf deinen Tresorbenutzer\nidentityPerUserDisabled=Persönlicher Identitätszugang (deaktiviert)\nidentityPerUserDisabledDescription=Beschränke den Zugriff auf diese Identität und die damit verbundenen Verbindungen nur auf deinen Tresor-Benutzer (Konfiguration des Teams erforderlich)\nidentityPerGroup=Gruppenbezogener Identitätszugang\nidentityPerGroupDescription=Beschränke den Zugriff auf diese Identität und die damit verbundenen Verbindungen nur auf diese Tresorgruppe\nlibrary=Bibliothek\nlocation=Standort\nkeyAuthentication=Schlüsselbasierte Authentifizierung\nkeyAuthenticationDescription=Die zu verwendende Authentifizierungsmethode, wenn eine schlüsselbasierte Authentifizierung erforderlich ist\nlocationDescription=Der Dateipfad deines entsprechenden privaten Schlüssels\nkeyFile=Lokale Schlüsseldatei\nkeyPassword=Passphrase\nkey=Schlüssel\nyubikeyPiv=Yubikey PIV\npageant=Pageant\ngpgAgent=GPG-Agent\ncustomPkcs11Library=Benutzerdefinierte PKCS#11-Bibliothek\nsshAgent=OpenSSH-Agent\n#custom\nnone=Nichts\nindex=Index ...\notherExternal=Ein anderer externer Agent\nsync=Sync\nvaultSync=Tresor-Synchronisation\ncustomUsername=Benutzername\ncustomUsernameDescription=Der optionale alternative Benutzer, als der man sich anmeldet\ncustomUsernamePassword=Passwort\ncustomUsernamePasswordDescription=Das Passwort des Benutzers, das verwendet wird, wenn eine sudo-Authentifizierung erforderlich ist\nshowInternalPods=Interne Pods anzeigen\nshowAllNamespaces=Alle Namespaces anzeigen\nshowInternalContainers=Interne Container anzeigen\nrefresh=Aktualisieren\nvmwareGui=GUI starten\nmonitorVm=VM überwachen\naddCluster=Cluster hinzufügen ...\nshowNonRunningInstances=Nicht laufende Instanzen anzeigen\nvmwareGuiDescription=Ob eine virtuelle Maschine im Hintergrund oder in einem Fenster gestartet werden soll.\nvmwareEncryptionPassword=Verschlüsselungspasswort\nvmwareEncryptionPasswordDescription=Das optionale Passwort, das zur Verschlüsselung der VM verwendet wird.\nvmPasswordDescription=Das erforderliche Passwort für den Gastbenutzer.\nvmPassword=Benutzer-Passwort\nvmUser=Gast-Benutzer\nrunTempContainer=Temporärer Container ausführen\nvmUserDescription=Der Benutzername deines primären Gastbenutzers\ndockerTempRunAlertTitle=Temporärer Container ausführen\ndockerTempRunAlertHeader=Damit wird ein Shell-Prozess in einem temporären Container ausgeführt, der automatisch entfernt wird, sobald er gestoppt wird.\n#custom\nimageName=Imagename\nimageNameDescription=Die zu verwendende Container-Image-Kennung\ncontainerName=Name des Containers\ncontainerNameDescription=Der optionale benutzerdefinierte Containername\nvm=Virtuelle Maschine\nvmDescription=Die zugehörige Konfigurationsdatei.\nvmwareScan=VMware Desktop-Hypervisoren\nvmwareMachine.displayName=VMware Virtuelle Maschine\nvmwareMachine.displayDescription=Verbindung zu einer virtuellen Maschine über SSH\nvmwareInstallation.displayName=VMware Desktop Hypervisor Installation\nvmwareInstallation.displayDescription=Interaktion mit den installierten VMs über deren CLI\nstart=Start\nstop=Stopp\npause=Pause\nrdpTunnelHost=Ziel-Host\nrdpTunnelHostDescription=Die SSH-Verbindung, über die die RDP-Verbindung getunnelt wird\nrdpTunnelUsername=Benutzername\nrdpTunnelUsernameDescription=Der benutzerdefinierte Benutzer, mit dem man sich anmeldet, verwendet den SSH-Benutzer, wenn er leer bleibt\nrdpFileLocation=Dateispeicherort\nrdpFileLocationDescription=Der Dateipfad der .rdp-Datei\nrdpPasswordAuthentication=Passwort-Authentifizierung\nrdpFiles=RDP-Dateien\nrdpPasswordAuthenticationDescription=Das Passwort zum Ausfüllen oder Kopieren in die Zwischenablage, je nach Client-Unterstützung\nrdpFile.displayName=RDP-Datei\nrdpFile.displayDescription=Verbindung zu einem System über eine bestehende .rdp-Datei\nrequiredSshServerAlertTitle=SSH-Server einrichten\nrequiredSshServerAlertHeader=Es kann kein installierter SSH-Server in der VM gefunden werden.\nrequiredSshServerAlertContent=Um sich mit der VM zu verbinden, sucht XPipe nach einem laufenden SSH-Server, aber es wurde kein verfügbarer SSH-Server für die VM gefunden.\ncomputerName=Computer Name\npssComputerNameDescription=Der Computername, mit dem eine Verbindung hergestellt werden soll\ncredentialUser=Berechtigungsnachweis Benutzer\ncredentialUserDescription=Der Benutzer, als der du dich anmeldest.\ncredentialPassword=Berechtigungsnachweis Passwort\ncredentialPasswordDescription=Das Passwort des Benutzers.\nsshConfig=SSH-Konfigurationsdateien\nautostart=Automatisches Verbinden beim Start von XPipe\nacceptHostKey=Host-Schlüssel akzeptieren\nmodifyHostKeyPermissions=Host Key Berechtigungen ändern\nattachContainer=Anhängen\ncontainerLogs=Protokolle anzeigen\nopenSftpClient=In einem externen SFTP-Client öffnen\nopenTermius=In Termius öffnen\nshowInternalInstances=Interne Instanzen anzeigen\neditPod=Pod bearbeiten\nacceptHostKeyDescription=Vertraue dem neuen Host-Schlüssel und fahre fort\nmodifyHostKeyPermissionsDescription=Versuchen Sie, die Berechtigungen der Originaldatei zu entfernen, damit OpenSSH zufrieden ist\npsSession.displayName=PowerShell Remote-Sitzung\npsSession.displayDescription=Verbinden über New-PSSession und Enter-PSSession\nsshLocalTunnel.displayName=Lokaler SSH-Tunnel\nsshLocalTunnel.displayDescription=Einen SSH-Tunnel zu einem entfernten Host einrichten\nsshRemoteTunnel.displayName=Entfernter SSH-Tunnel\nsshRemoteTunnel.displayDescription=Einen umgekehrten SSH-Tunnel von einem entfernten Host aus aufbauen\nsshDynamicTunnel.displayName=Dynamischer SSH-Tunnel\nsshDynamicTunnel.displayDescription=Einen SOCKS-Proxy über eine SSH-Verbindung einrichten\nshellEnvironmentGroup.displayName=Shell-Umgebungen\nshellEnvironmentGroup.displayDescription=Shell-Umgebungen\nshellEnvironment.displayName=Shell-Umgebung\nshellEnvironment.displayDescription=Eine angepasste Shell-Startumgebung erstellen\nshellEnvironment.informationFormat=$TYPE$ umgebung\nshellEnvironment.elevatedInformationFormat=$ELEVATION$ $TYPE$ umgebung\nenvironmentConnectionDescription=Die Basisverbindung, um eine Umgebung zu schaffen für\nenvironmentScriptDescription=Das optionale benutzerdefinierte Init-Skript, das in der Shell ausgeführt wird\nenvironmentSnippets=Shell-Skripte\ncommandSnippetsDescription=Die optionalen vordefinierten Shell-Skripte, die zuerst ausgeführt werden\nenvironmentSnippetsDescription=Die optionalen vordefinierten Shell-Skripte, die bei der Initialisierung ausgeführt werden\nshellTypeDescription=Der explizite Shell-Typ zum Starten\noriginPort=Ursprungsport\noriginAddress=Herkunftsadresse\nremoteAddress=Entfernte Adresse\nremoteSourceAddress=Entfernte Quelladresse\nremoteSourcePort=Entfernter Quellport\noriginDestinationPort=Ursprung-Ziel-Port\noriginDestinationAddress=Ursprüngliche Zieladresse\norigin=Herkunft\nremoteHost=Entfernter Host\naddress=Adresse\nproxmox.displayName=Proxmox\nproxmox.displayDescription=Verbindung zu Systemen in einer virtuellen Umgebung von Proxmox\nproxmoxVm.displayName=Proxmox VM\nproxmoxVm.displayDescription=Verbindung zu einer virtuellen Maschine in einer Proxmox VE über SSH\nproxmoxContainer.displayName=Proxmox Container\nproxmoxContainer.displayDescription=Verbindung zu einem Container in einer Proxmox VE\nsshDynamicTunnel.hostDescription=Das System, das als SOCKS-Proxy verwendet werden soll\nsshDynamicTunnel.bindingDescription=An welche Adressen der Tunnel gebunden werden soll\nsshRemoteTunnel.hostDescription=Das System, von dem aus der Ferntunnel zum Ursprung gestartet werden soll\nsshRemoteTunnel.bindingDescription=An welche Adressen der Tunnel gebunden werden soll\nsshLocalTunnel.hostDescription=Das System, zu dem der Tunnel geöffnet werden soll\nsshLocalTunnel.bindingDescription=An welche Adressen der Tunnel gebunden werden soll\nsshLocalTunnel.localAddressDescription=Die lokale Adresse zum Binden\nsshLocalTunnel.remoteAddressDescription=Die zu bindende Remote-Adresse\ncmd.displayName=Befehl\ncmd.displayDescription=Ein beliebiges Kommando auf einem System ausführen\nk8sPod.displayName=Kubernetes Pod\nk8sPod.displayDescription=Verbindung zu einem Pod und seinen Containern über kubectl\nk8sContainer.displayName=Kubernetes Container\nk8sContainer.displayDescription=Eine Shell für einen Container öffnen\nk8sCluster.displayName=Kubernetes Cluster\nk8sCluster.displayDescription=Verbinden mit einem Cluster und seinen Pods über kubectl\nsshTunnelGroup.displayName=SSH-Tunnel\nsshTunnelGroup.displayCategory=Alle Arten von SSH-Tunneln\nlocal.displayName=Lokale Maschine\nlocal.displayDescription=Die Shell des lokalen Rechners\ncygwin=Cygwin\nmsys2=MSYS2\ngitWindows=Git für Windows\ngitForWindows.displayName=Git für Windows\ngitForWindows.displayDescription=Zugriff auf deine lokale Git For Windows-Umgebung\nmsys2.displayName=MSYS2\nmsys2.displayDescription=Zugriff auf die Shells deiner MSYS2 Umgebung\ncygwin.displayName=Cygwin\ncygwin.displayDescription=Zugriff auf die Shells deiner Cygwin-Umgebung\nnamespace=Namespace\ngitVaultIdentityStrategy=Git SSH Identität\ngitVaultIdentityStrategyDescription=Wenn du dich entschieden hast, eine SSH-Git-URL als Remote zu verwenden und dein Remote-Repository eine SSH-Identität erfordert, dann setze diese Option.\\n\\nWenn du eine HTTP-URL angegeben hast, kannst du diese Option ignorieren.\ndockerContainers=Docker-Container\ndockerCmd.displayName=docker CLI-Client\ndockerCmd.displayDescription=Zugriff auf Docker-Container über den Docker CLI-Client\nwslCmd.displayName=WSL installieren\nwslCmd.displayDescription=Zugriff auf WSL-Instanzen über den wsl CLI-Client\nk8sCmd.displayName=kubectl-Client\nk8sCmd.displayDescription=Zugriff auf Kubernetes-Cluster über kubectl\nk8sClusters=Kubernetes-Cluster\n#custom\nshells=Verfügbare Shells\ninspectContainer=Untersuche\ninspectContext=Untersuche\nk8sClusterNameDescription=Der Name des Kontexts, in dem sich der Cluster befindet.\npod=Pod\npodName=Pod-Name\nk8sClusterContext=Kontext\nk8sClusterContextDescription=Der Name des Kontexts, in dem sich der Cluster befindet\nk8sClusterNamespace=Namespace\nk8sClusterNamespaceDescription=Der benutzerdefinierte Namespace oder der Standard-Namespace, falls leer\nk8sConfigLocation=Config-Datei\nk8sConfigLocationDescription=Die benutzerdefinierte kubeconfig-Datei oder die Standarddatei, wenn sie leer ist\ninspectPod=Untersuchen Sie\nshowAllContainers=Nicht laufende Container anzeigen\nshowAllPods=Nicht laufende Pods anzeigen\nk8sPodHostDescription=Der Host, auf dem sich der Pod befindet\nk8sContainerDescription=Der Name des Kubernetes-Containers\nk8sPodDescription=Der Name des Kubernetes-Pods\npodDescription=Der Pod, auf dem sich der Container befindet\nk8sClusterHostDescription=Der Host, über den auf den Cluster zugegriffen werden soll. Muss kubectl installiert und konfiguriert haben, um auf den Cluster zugreifen zu können.\nconnection=Verbindung\nshellCommand.displayName=Benutzerdefinierter Shell-Befehl\nshellCommand.displayDescription=Öffnen einer Standard-Shell durch einen benutzerdefinierten Befehl\nssh.displayName=SSH-Verbindung\nssh.displayDescription=Verbindung zu einem entfernten System über den SSH-Befehlszeilen-Client\nsshConfig.displayName=SSH-Konfigurationsdatei\nsshConfig.displayDescription=Verbindung zu Hosts, die in einer SSH-Konfigurationsdatei definiert sind\nsshConfigHost.displayName=SSH-Konfigurationsdatei Host\nsshConfigHost.displayDescription=Verbindung zu einem in einer SSH-Konfigurationsdatei definierten Host\nsshConfigHost.password=Passwort\nsshConfigHost.passwordDescription=Geben Sie das optionale Passwort für die Benutzeranmeldung an.\nsshConfigHost.identityPassphrase=Schlüssel-Passphrase\nsshConfigHost.identityPassphraseDescription=Gib die optionale Passphrase für deinen Schlüssel an.\nshellCommand.hostDescription=Der Host, auf dem der Befehl ausgeführt werden soll\nshellCommand.commandDescription=Der Befehl, der eine Shell öffnet\ncommandType=Befehlstyp\ncommandTypeDescription=Wie man den Befehl ausführt\ncommandDescription=Die benutzerdefinierten Befehle, die auf dem Host ausgeführt werden sollen\ncommandHostDescription=Der Host, auf dem der Befehl ausgeführt werden soll\ncommandDataFlowDescription=Wie dieser Befehl Ein- und Ausgaben behandelt\ncommandElevationDescription=Führe diesen Befehl mit erweiterten Rechten aus\ncommandShellTypeDescription=Die Shell, die für diesen Befehl verwendet werden soll\nlimitedSystem=Dies ist ein begrenztes oder eingebettetes System\nlimitedSystemDescription=Versuche nicht, den Shell-Typ zu identifizieren, was für begrenzte eingebettete Systeme oder IOT-Geräte notwendig ist\nsshForwardX11=X11 weiterleiten\nsshForwardX11Description=Aktiviert die X11-Weiterleitung für die Verbindung\ncustomAgent=Benutzerdefinierter Agent\nidentityAgent=Identitätsagent\nssh.proxyDescription=Der optionale Proxy-Host, der beim Aufbau der SSH-Verbindung verwendet wird. Es muss ein SSH-Client installiert sein.\nusage=Verwendung\nwslHostDescription=Der Host, auf dem sich die WSL-Instanz befindet. Muss wsl installiert haben.\nwslDistributionDescription=Der Name der WSL-Instanz\nwslUsernameDescription=Der explizite Benutzername, mit dem du dich anmeldest. Wenn er nicht angegeben wird, wird der Standardbenutzername verwendet.\nwslPasswordDescription=Das Passwort des Benutzers, das für sudo-Befehle verwendet werden kann.\ndockerHostDescription=Der Host, auf dem sich der Docker-Container befindet. Muss Docker installiert haben.\ndockerContainerDescription=Der Name des Docker-Containers\nlocalMachine=Lokale Maschine\nrootScan=Sudo-Shell-Umgebung\nloginEnvironmentScan=Benutzerdefinierte Anmeldeumgebung\nk8sScan=Kubernetes-Cluster\noptions=Optionen\ndockerRunningScan=Laufende Docker-Container\ndockerAllScan=Alle Docker-Container\nwslScan=WSL-Instanzen\nsshScan=SSH-Konfigurationsverbindungen\nrunAsUser=Als Benutzer ausführen\nrunAsUserDescription=Starten Sie diese Shell-Umgebung als ein anderer Benutzer\ndefault=Standard\nadministrator=Administrator\nwslHost=WSL-Host\ntimeout=Timeout\ninstallLocation=Installationsort\ninstallLocationDescription=Der Ort, an dem deine $NAME$ Umgebung installiert ist\nwsl.displayName=Windows-Subsystem für Linux\nwsl.displayDescription=Verbindung zu einer WSL-Instanz, die unter Windows läuft\ndocker.displayName=Docker-Container\ndocker.displayDescription=Verbindung zu einem Docker-Container\nport=Port\nuser=Benutzer\npassword=Passwort\nmethod=Methode\nuri=URL\nproxy=Proxy\n#custom\ndistribution=Installation\nusername=Benutzername\nshellType=Shell-Typ\nbrowseFile=Datei durchsuchen\nopenShell=Shell im Terminal öffnen\nopenCommand=Befehl im Terminal ausführen\neditFile=Datei bearbeiten\ndescription=Beschreibung\nfurtherCustomization=Weitere Anpassungen\nfurtherCustomizationDescription=Weitere Konfigurationsoptionen findest du in den ssh-Konfigurationsdateien\nbrowse=Durchsuchen\nconfigHost=Host\nconfigHostDescription=Der Host, auf dem sich die Konfiguration befindet\nconfigLocation=Config-Speicherort\nconfigLocationDescription=Der Dateipfad der Konfigurationsdatei\ngateway=Gateway\ngatewayDescription=Das optionale Gateway, das bei der Verbindung verwendet wird\nconnectionInformation=Verbindungsinformationen\nconnectionInformationDescription=Mit welchem System soll eine Verbindung hergestellt werden?\npasswordAuthentication=Passwort-Authentifizierung\npasswordAuthenticationDescription=Das optionale Passwort für die Authentifizierung\nsshConfigString.displayName=Konfig-basierte SSH-Verbindung\nsshConfigString.displayDescription=Eine vollständig angepasste SSH-Verbindung im SSH-Config-Format erstellen\nsshConfigStringContent=Konfiguration\nsshConfigStringContentDescription=SSH-Optionen für die Verbindung im OpenSSH-Config-Format\nvnc.displayName=VNC-Verbindung über SSH\nvnc.displayDescription=Eine VNC-Sitzung über eine getunnelte Verbindung öffnen\n#custom\nbinding=Bindings\nvncPortDescription=Der Port, an dem der VNC-Server lauscht\nrdpPortDescription=Der Port, an dem der RDP-Server lauscht\nvncUsername=Benutzername\nvncUsernameDescription=Der optionale VNC-Benutzername\nvncPassword=Passwort\nvncPasswordDescription=Das VNC-Passwort\nx11WslInstance=X11 Forward WSL-Instanz\nx11WslInstanceDescription=Die lokale Windows Subsystem für Linux-Distribution, die als X11-Server verwendet werden soll, wenn die X11-Weiterleitung in einer SSH-Verbindung genutzt wird. Diese Distribution muss eine WSL2-Distribution sein.\nopenAsRoot=Als root öffnen\nopenInWSL=In WSL öffnen\nlaunch=Starten\nsshTrustKeyContent=Der Host-Schlüssel ist nicht bekannt, und du hast die manuelle Überprüfung des Host-Schlüssels aktiviert. $CONTENT$\nsshTrustKeyTitle=Unbekannter Host-Schlüssel\nrdpTunnel.displayName=RDP-Verbindung über SSH\nrdpTunnel.displayDescription=Verbinden über RDP über eine getunnelte Verbindung\nrdpEnableDesktopIntegration=Aktiviere die Desktop-Integration\nrdpEnableDesktopIntegrationDescription=Remote-Anwendungen ausführen, wenn die RDP-Zulassungsliste dies zulässt\nrdpSetupAdminTitle=RDP-Einrichtung erforderlich\nrdpSetupAllowTitle=RDP-Fernanwendung\nrdpSetupAllowContent=Das direkte Starten von Remote-Anwendungen ist auf diesem System derzeit nicht erlaubt. Willst du es aktivieren? Dann kannst du deine Fernanwendungen direkt von XPipe aus starten, indem du die Zulassungsliste für RDP-Fernanwendungen deaktivierst.\nrdpServerEnableTitle=RDP-Server\nrdpServerEnableContent=Der RDP-Server ist auf dem Zielsystem deaktiviert. Willst du ihn in der Registry aktivieren, um RDP-Verbindungen zu ermöglichen?\nrdp=RDP\nrdpScan=RDP-Tunnel über SSH\nwslX11SetupTitle=WSL X11-Einrichtung\nwslX11SetupContent=XPipe kann deine lokale WSL-Distribution nutzen, um als X11-Anzeigeserver zu fungieren. Möchtest du X11 auf $DIST$ einrichten? Dabei werden die grundlegenden X11-Pakete auf der WSL-Distribution installiert, was eine Weile dauern kann. Du kannst auch im Einstellungsmenü ändern, welche Distribution verwendet wird.\ncommand=Befehl\ncommandGroup=Befehlsgruppe\nvncSystem=VNC-Zielsystem\nvncSystemDescription=Das eigentliche System, mit dem interagiert werden soll. Dies ist normalerweise dasselbe wie der Tunnel-Host\nvncHost=Ziel-VNC-Host\nvncHostDescription=Das System, auf dem der VNC-Server läuft\nvncDirectHost=Host\nvncDirectHostDescription=Der Host-Eintrag oder die manuelle Adresse des Servers, auf dem der VNC-Server läuft\nrdpDirectHost=Host\nrdpDirectHostDescription=Der Host-Eintrag oder die manuelle Adresse des Servers, auf dem der RDP-Server läuft\ngitVaultTitle=Git-Tresor\ngitVaultForcePushContent=Willst du einen Push in das entfernte Repository erzwingen? Dadurch werden alle Inhalte des entfernten Repositorys vollständig durch dein lokales Repository ersetzt, einschließlich des Verlaufs.\ngitVaultOverwriteLocalContent=Willst du die Änderungen in deinem lokalen Tresor außer Kraft setzen? Damit werden alle entfernten Änderungen auf dein lokales Repository angewendet.\nrdpSimple.displayName=Direkte RDP-Verbindung\nrdpSimple.displayDescription=Verbindung zu einem Host über RDP\nrdpUsername=Benutzername\nrdpUsernameDescription=Der Benutzer, mit dem du dich anmeldest. Kann ein Domänenpräfix enthalten\naddressDescription=Wohin soll die Verbindung gehen?\nrdpAdditionalOptions=Zusätzliche RDP-Optionen\nrdpAdditionalOptionsDescription=Rohe RDP-Optionen, die genauso formatiert sind wie in .rdp-Dateien\nproxmoxVncConfirmTitle=VNC-Zugang\nproxmoxVncConfirmContent=Willst du den VNC-Zugriff für die VM aktivieren? Dadurch wird der direkte VNC-Client-Zugriff in der VM-Konfigurationsdatei aktiviert und die virtuelle Maschine neu gestartet.\ndockerContext.displayName=Docker-Kontext\ndockerContext.displayDescription=Interaktion mit Containern, die sich in einem bestimmten Kontext befinden\nvmActions=VM-Aktionen\ndockerContextActions=Kontextbezogene Aktionen\nk8sPodActions=Pod-Aktionen\nopenVnc=VNC-Zugang aktivieren\naddVnc=VNC-Verbindung hinzufügen\ncommandGroup.displayName=Befehlsgruppe\ncommandGroup.displayDescription=Verfügbare Befehle für ein System gruppieren\nserial.displayName=Serielle Verbindung\nserial.displayDescription=Eine serielle Verbindung in einem Terminal öffnen\nserialPort=Serieller Anschluss\nserialPortDescription=Der serielle Anschluss/das Gerät, mit dem eine Verbindung hergestellt werden soll\nbaudRate=Baudrate\ndataBits=Datenbits\nstopBits=Stoppbits\nparity=Parität\nflowControlWindow=Flusskontrolle\nserialImplementation=Serielle Implementierung\nserialImplementationDescription=Das Tool für die Verbindung mit der seriellen Schnittstelle\nserialHost=Host\nserialHostDescription=Das System für den Zugriff auf die serielle Schnittstelle auf\nserialPortConfiguration=Konfiguration der seriellen Schnittstelle\nserialPortConfigurationDescription=Konfigurationsparameter des angeschlossenen seriellen Geräts\nserialInformation=Serielle Informationen\nopenXShell=In XShell öffnen\ntsh.displayName=Teleport\ntsh.displayDescription=Verbinde dich mit deinen Teleportknoten über tsh\ntshNode.displayName=Teleport-Knoten\ntshNode.displayDescription=Verbindung zu einem Teleport-Knoten in einem Cluster\nteleportCluster=Cluster\nteleportClusterDescription=Der Cluster, in dem sich der Knoten befindet\nteleportProxy=Proxy\nteleportProxyDescription=Der Proxy-Server, der für die Verbindung mit dem Knoten verwendet wird\nteleportHost=Host\nteleportHostDescription=Der Hostname des Knotens\nteleportUser=Benutzer\nteleportUserDescription=Der Benutzer, als der er sich anmelden soll\nlogin=Anmeldung\nhyperVInstall.displayName=Hyper-V\nhyperVInstall.displayDescription=Verbindung zu VMs, die von Hyper-V verwaltet werden\nhyperVVm.displayName=Hyper-V VM\nhyperVVm.displayDescription=Verbindung zu einer Hyper-V VM über SSH oder PSSession\ntrustHost=Vertrauenswürdiger Host\ntrustHostDescription=Computername zur Liste der vertrauenswürdigen Hosts hinzufügen\ncopyIp=IP kopieren\nvncDirect.displayName=Direkte VNC-Verbindung\nvncDirect.displayDescription=Über VNC direkt mit einem System verbinden\neditConfiguration=Konfiguration bearbeiten\nviewInDashboard=Ansicht im Dashboard\nsetDefault=Standard einstellen\nremoveDefault=Standard entfernen\nconnectAsOtherUser=Als anderer Benutzer verbinden\nprovideUsername=Einen alternativen Benutzernamen zum Einloggen angeben\nvmIdentity=Gast-Identität\nvmIdentityDescription=Die SSH-Identitätsauthentifizierungsmethode, die bei Bedarf für die Verbindung verwendet wird\nvmPort=Port\nvmPortDescription=Der Port, mit dem du dich über SSH verbinden kannst\nforwardAgent=Weiterleitungsagent\nforwardAgentDescription=SSH-Agenten-Identitäten auf dem entfernten System verfügbar machen\nvirshUri=URI\nvirshUriDescription=Der Hypervisor-URI, Aliasnamen werden ebenfalls unterstützt\nvirshDomain.displayName=libvirt-Domäne\nvirshDomain.displayDescription=Mit einer libvirt-Domäne verbinden\nvirshHypervisor.displayName=libvirt Hypervisor\nvirshHypervisor.displayDescription=Verbindung zu einem von libvirt unterstützten Hypervisor-Treiber\nvirshInstall.displayName=libvirt Kommandozeilen-Client\nvirshInstall.displayDescription=Verbindung zu allen verfügbaren libvirt-Hypervisoren über virsh\naddHypervisor=Hypervisor hinzufügen\ninteractiveTerminal=Interaktives Terminal\neditDomain=Domäne bearbeiten\nlibvirt=libvirt-Domänen\ncustomIp=Benutzerdefinierte IP\ncustomIpDescription=Überschreibe die Standard-IP-Erkennung der lokalen VM, wenn du ein erweitertes Netzwerk verwendest\nautomaticallyDetect=Automatisch erkennen\nuserAddDialogTitle=Benutzer erstellen\ngroupAddDialogTitle=Gruppenbildung\npassphrase=Passphrase\nrepeatPassphrase=Passphrase wiederholen\ngroupSecret=Gruppengeheimnis\nrepeatGroupSecret=Gruppengeheimnis wiederholen\nvaultGroup=Tresorgruppe\nloginAlertTitle=Anmeldung erforderlich\nloginAlertHeader=Entsperre den Tresor, um auf deine persönlichen Verbindungen zuzugreifen\nvaultUser=Tresor-Benutzer\nme=Ich\naddGroup=Gruppe hinzufügen ...\naddGroupDescription=Erstelle eine neue Gruppe für diesen Tresor\naddUser=Benutzer hinzufügen ...\naddUserDescription=Einen neuen Benutzer für diesen Tresor erstellen\nskip=Überspringen\nuserChangePasswordAlertTitle=Passwort ändern\ngroupChangeSecretAlertTitle=Geheime Änderung\ndocs=Dokumentation\nlxd.displayName=LXD-Container\nlxd.displayDescription=Verbindung zu einem LXD-Container über lxc\nlxdCmd.displayName=LXD CLI-Client\nlxdCmd.displayDescription=Zugriff auf LXD-Container über den lxc CLI-Client\npodman.displayName=Podman Container\npodman.displayDescription=Verbindung zu einem Podman-Container\nincusInstall.displayName=Incus Maschinenmanager\nincusInstall.displayDescription=Zugriff auf incus Container über den incus CLI-Client\nincusContainer.displayName=Incus-Container\nincusContainer.displayDescription=Verbindung zu einem Incus-Container\npodmanCmd.displayName=Podman CLI\npodmanCmd.displayDescription=Zugriff auf Podman-Container über den CLI-Client\nlxdHostDescription=Der Rechner, auf dem sich der LXD-Container befindet. Muss lxc installiert haben.\nlxdContainerDescription=Der Name des LXD-Containers\npodmanContainers=Podman-Container\nlxdContainers=LXD-Container\nincusContainers=Incus-Container\ncontainer=Container\nhost=Host\ncontainerActions=Container-Aktionen\nserialConsole=Serielle Konsole\neditRunConfiguration=Laufkonfiguration bearbeiten\ncommunityDescription=Ein Power-Tool für Verbindungen, das perfekt für deine persönlichen Anwendungsfälle ist.\nupgradeDescription=Professionelles Verbindungsmanagement für deine gesamte Serverinfrastruktur.\ndiscoverPlans=Upgrade-Optionen entdecken\nextendProfessional=Upgrade auf die neuesten professionellen Funktionen\ncommunityItem1=Unbegrenzte Verbindungen zu nicht-kommerziellen Systemen und Tools\ncommunityItem2=Nahtlose Integration mit deinen installierten Terminals und Editoren\ncommunityItem3=Voll funktionsfähiger Remote-Dateibrowser\ncommunityItem4=Leistungsstarkes Skripting-System für alle Shells\ncommunityItem5=Git-Integration zur Synchronisierung und zum Austausch von Verbindungsinformationen\nupgradeItem1=Enthält alle Funktionen der Community Edition\nupgradeItem2=Der Homelab-Tarif unterstützt unbegrenzt Hypervisoren und erweiterte SSH-Funktionen\nupgradeItem3=Der Professional-Plan unterstützt zusätzlich Betriebssysteme und Tools für Unternehmen\nupgradeItem4=Der Enterprise Plan bietet volle Flexibilität für deinen individuellen Anwendungsfall\nupgrade=Upgrade\nupgradeTitle=Verfügbare Pläne\nstatus=Status\ntype=Typ\nlicenseAlertTitle=Erforderliche Lizenz\nuseCommunity=Weiter mit Community\npreviewDescription=Teste die neuen Funktionen ein paar Wochen lang nach der Veröffentlichung.\ntryPreview=Vorschau aktivieren\npreviewItem1=Voller Zugang zu neu veröffentlichten professionellen Funktionen für 2 Wochen nach Veröffentlichung\npreviewItem2=Probiere neue Funktionen unverbindlich aus\nlicensedTo=Lizensiert für\nemail=E-Mail Adresse\napply=Anwenden\nclear=Löschen\n#custom\nactivate=Aktivieren\nvalidUntil=Gültig bis\nlicenseActivated=Lizenz aktiviert\nrestart=Neustart\nlockVault=Tresor schließen\nrestartApp=XPipe neu starten\nfree=Kostenlos\nupgradeInfo=Informationen zum Upgrade auf eine Lizenz findest du weiter unten.\nupgradeInfoPreview=Informationen zum Upgrade auf eine Lizenz findest du unten oder du kannst die Vorschau ausprobieren.\nenterLicenseKey=Lizenzschlüssel für das Upgrade eingeben\nisOnlySupported=wird nur mit mindestens einer $TYPE$ Lizenz unterstützt\nareOnlySupported=werden nur mit mindestens einer $TYPE$ Lizenz unterstützt\nlegacyLicense=Diese Lizenz umfasst nur neue Professional-Funktionen, die innerhalb eines Jahres nach dem Kauf veröffentlicht werden.\npreviewExpiredLicense=Diese Funktion war vor kurzem in der Vorschau kostenlos verfügbar, aber dieser Zeitraum ist jetzt abgelaufen.\nopenApiDocs=API-Dokumentation\nopenApiDocsDescription=Die Dokumentation der HTTP-API ist online verfügbar, einschließlich einer OpenAPI .yaml-Spezifikation. Du kannst sie in deinem Webbrowser oder deinem bevorzugten HTTP-Client öffnen.\nopenApiDocsButton=Docs öffnen\npythonApi=Python-API\npersonalConnection=Diese Verbindung und alle ihre Kinder sind nur für deinen Nutzer verfügbar, da sie von einer persönlichen Identität abhängen.\ndeveloperPrintInitFiles=Ausführung der Init-Datei drucken\ndeveloperPrintInitFilesDescription=Alle Shell-Init-Skripte ausgeben, die beim Starten eines Terminals ausgeführt werden.\ndeveloperShowSensitiveCommands=Sensible Befehle protokollieren\ndeveloperShowSensitiveCommandsDescription=Sensible Befehle in die Protokollausgabe zur Fehlersuche aufnehmen.\ncheckingForUpdates=Prüfen auf Updates\ncheckingForUpdatesDescription=Informationen über die neueste Version abrufen\ndownloadingUpdate=Freigabe abrufen (Version $VERSION$)\ndownloadingUpdateDescription=Herunterladen des Release-Pakets\nupdateNag=Du hast XPipe schon eine Weile nicht mehr aktualisiert. Möglicherweise verpasst du neue Funktionen und Fehlerbehebungen in neueren Versionen.\nupdateNagTitle=Update-Erinnerung\nupdateNagButton=Siehe Veröffentlichungen\nrefreshServices=Dienste aktualisieren\nserviceProtocolType=Dienstprotokolltyp\nserviceProtocolTypeDescription=Steuern, wie der Dienst geöffnet werden soll\nserviceCommand=Der Befehl, der ausgeführt wird, sobald der Dienst aktiv ist\nserviceCommandDescription=Der Platzhalter $PORT wird durch den tatsächlichen getunnelten lokalen Port ersetzt\nvalue=Wert\nshowAdvancedOptions=Erweiterte Optionen anzeigen\nsshAdditionalConfigOptions=Zusätzliche Konfigurationsoptionen\nremoteFileManager=Remote-Dateimanager\nclearUserData=Benutzerdaten löschen\nclearUserDataDescription=Löschen aller Benutzerkonfigurationsdaten, einschließlich der Verbindungen\nclearUserDataTitle=Löschung von Benutzerdaten\nclearUserDataContent=Dadurch werden alle lokalen Benutzerdaten für xpipe gelöscht und neu gestartet. Wenn du dich um deine Verbindungen sorgst, solltest du sie vorher mit einem Git-Repository synchronisieren.\nundefined=Undefiniert\ncopyAddress=Adresse kopieren\nnetbirdDeviceScan=Netbird Verbindungen\nnetbirdId=Öffentlicher Schlüssel eines Peers\nnetbirdIdDescription=Die interne Netbird Public Key ID des Peers\ntailscaleDeviceScan=Tailscale Verbindungen\ntailscaleInstall.displayName=Tailscale Installation\ntailscaleInstall.displayDescription=Verbinde dich mit Geräten in deinem Tailnet über SSH\ntailscaleDevice.displayName=Tailscale Gerät\ntailscaleDevice.displayDescription=Verbinde dich über SSH mit einem Gerät in deinem Tailnet\ntailscaleId=Geräte-ID\ntailscaleIdDescription=Die interne Tailscale-Geräte-ID\ntailscaleHostName=Hostname\ntailscaleHostNameDescription=Der Hostname des Geräts im Tailnet\ntailscaleUsername=Benutzername\ntailscaleUsernameDescription=Der Benutzer, als der er sich anmelden soll\ntailscalePassword=Passwort\ntailscalePasswordDescription=Das optionale Benutzerpasswort, das für sudo verwendet werden kann\nscriptName=Skriptname\nscriptNameDescription=Diesem Skript einen eigenen Namen geben\nscriptGroupName=Name der Skriptgruppe\nscriptGroupNameDescription=Gib dieser Skriptgruppe einen eigenen Namen\nidentityName=Name der Identität\nidentityNameDescription=Gib dieser Identität einen eigenen Namen\ntailscaleTailnet.displayName=Tailnet\ntailscaleTailnet.displayDescription=Verbinde dich mit einem bestimmten Tailnet mit deinem Account\nputtyConnections=PuTTY-Verbindungen\nkittyConnections=KiTTY-Verbindungen\nicons=Icons\ncustomIcons=Benutzerdefinierte Icons\niconSources=Icon-Quellen\niconSourcesDescription=Du kannst hier deine eigenen Quellen für Icons hinzufügen. XPipe übernimmt alle .svg-Dateien an dem hinzugefügten Ort und fügt sie dem verfügbaren Icon-Set hinzu.\\n\\nAls Speicherorte für Symbole werden sowohl lokale Verzeichnisse als auch entfernte Git-Repositories unterstützt.\nrefreshSources=Aktualisieren von Symbolen\nrefreshSourcesDescription=Aktualisiere alle Icons aus den verfügbaren Quellen\naddDirectoryIconSource=Verzeichnisquelle hinzufügen ...\naddDirectoryIconSourceDescription=Hinzufügen von Symbolen aus einem lokalen Verzeichnis\naddGitIconSource=Git-Quelle hinzufügen ...\naddGitIconSourceDescription=Hinzufügen von Symbolen, die sich in einem entfernten Git-Repository befinden\nrepositoryUrl=Git Repository URL\niconDirectory=Icon-Verzeichnis\naddUnsupportedKexMethod=Nicht unterstützte Schlüsselaustauschmethode hinzufügen\naddUnsupportedKexMethodDescription=Erlaube die Verwendung der Schlüsselaustauschmethode $VAL$ für diese Verbindung\naddUnsupportedHostKeyType=Nicht unterstützten Host-Schlüsseltyp hinzufügen\naddUnsupportedHostKeyTypeDescription=Erlaube die Verwendung des Host-Schlüsseltyps $VAL$ für diese Verbindung\naddUnsupportedMacType=Nicht unterstützten MAC-Typ hinzufügen\naddUnsupportedMacTypeDescription=Erlaube die Verwendung des MAC-Typs $VAL$ für diese Verbindung\nrunSilent=leise im Hintergrund\nrunInFileBrowser=im Dateibrowser\nrunInConnectionHub=im Verbindungs-Hub\ncommandOutput=Befehlsausgabe\niconSourceDeletionTitle=Symbolquelle löschen\niconSourceDeletionContent=Möchtest du diese Icon-Quelle und alle damit verbundenen Icons löschen?\nrefreshIcons=Aktualisieren von Symbolen\nrefreshIconsDescription=Abrufen, Rendern und Zwischenspeichern aller über 1000 verfügbaren Icons aus externen Quellen in .png-Dateien. Das kann eine Weile dauern ...\nvaultUserLegacy=Tresor-Benutzer (eingeschränkter Legacy-Kompatibilitätsmodus)\nupgradeInstructions=Upgrade-Anweisungen\nexternalActionTitle=Externe Aktionsanfrage\nexternalActionContent=Eine externe Aktion wurde angefordert. Willst du das Starten von Aktionen von außerhalb von XPipe erlauben?\nnoScriptStateAvailable=Aktualisieren, um die Skriptkompatibilität zu bestimmen ...\ndocumentationDescription=Schau dir die Dokumentation an\ncustomEditorCommandInTerminal=Benutzerdefinierten Befehl in einem Terminal ausführen\ncustomEditorCommandInTerminalDescription=Wenn dein Editor terminalbasiert ist, kannst du diese Option aktivieren, um automatisch ein Terminal zu öffnen und den Befehl stattdessen in der Terminalsitzung auszuführen.\\n\\nDu kannst diese Option für Editoren wie vi, vim, nvim und andere verwenden.\ndisableHttpsTlsCheck=Zertifikatsprüfung für HTTPS-Anfragen deaktivieren\ndisableHttpsTlsCheckDescription=Wenn dein Unternehmen deinen HTTPS-Verkehr in Firewalls mit SSL-Interception entschlüsselt, schlagen alle Update- oder Lizenzprüfungen fehl, weil die Zertifikate nicht übereinstimmen. Du kannst dies beheben, indem du diese Option aktivierst und die TLS-Zertifikatsüberprüfung deaktivierst.\nconnectionsSelected=$NUMBER$ ausgewählte Verbindungen\naddConnections=Verbindungen hinzufügen\nbrowseDirectory=Verzeichnis durchsuchen\nopenTerminal=Terminal öffnen\ndocumentation=Dokumentation\nreport=Fehler melden\nkeePassXcNotAssociated=KeePassXC-Link\nkeePassXcNotAssociatedDescription=XPipe ist nicht mit deiner lokalen KeePassXC-Datenbank verbunden. Klicke unten, um XPipe einmalig mit der KeePassXC-Datenbank zu verknüpfen, damit XPipe Passwörter abfragen kann.\nkeePassXcAssociateMore=Mehrere Datenbanken verbinden\nkeePassXcAssociateMoreDescription=Du kannst mit mehreren KeePassXC-Datenbanken gleichzeitig verbunden sein\nkeePassXcAssociated=KeePassXC Links\nkeePassXcAssociatedDescription=XPipe ist mit den folgenden lokalen KeePassXC-Datenbanken verbunden:\nkeePassXcNotAssociatedButton=Datenbank verknüpfen\nidentifier=Kennung\npasswordManagerCommand=Benutzerdefinierter Befehl\n#custom\npasswordManagerCommandDescription=Der Befehl, der ausgeführt werden soll, um Passwörter abzurufen. Der Platzhalterstring $KEY wird beim Aufruf durch den Passwortschlüssel mit Anführungszeichen ersetzt. Dies sollte deinen Passwortmanager CLI aufrufen, um das Passwort auf stdout auszugeben, z. B. mypassmgr get $KEY.\\n\\nDu kannst den Schlüssel dann so einstellen, dass er immer dann abgefragt wird, wenn du eine Verbindung aufbaust, die ein Passwort erfordert.\nchooseTemplate=Vorlage wählen\nkeePassXcPlaceholder=KeePassXC Eintrag URL\nterminalEnvironment=Terminal-Umgebung\nterminalEnvironmentDescription=Falls du die Funktionen einer lokalen Linux-basierten WSL-Umgebung für deine Terminalanpassung nutzen möchtest, kannst du sie als Terminalumgebung verwenden.\\n\\nAlle benutzerdefinierten Terminal-Init-Befehle und die Konfiguration des Terminal-Multiplexers werden dann in dieser WSL-Distribution ausgeführt.\nterminalInitScript=Terminal-Init-Skript\nterminalInitScriptDescription=Befehle, die in der Terminalumgebung ausgeführt werden, bevor die Verbindung gestartet wird. Damit kannst du die Terminalumgebung beim Starten konfigurieren.\nterminalMultiplexer=Terminal-Multiplexer\nterminalMultiplexerDescription=Der Terminal-Multiplexer, der als Alternative zu Tabulatoren in einem Terminal verwendet wird. Dadurch werden bestimmte Eigenschaften des Terminals, z. B. die Handhabung von Tabs, durch die Multiplexer-Funktionalität ersetzt.\\n\\nErfordert, dass die entsprechende Multiplexer-Datei auf dem System installiert ist.\nterminalMultiplexerWindowsDescription=Der Terminal-Multiplexer, der als Alternative zu Tabulatoren in einem Terminal verwendet wird. Dadurch werden bestimmte Eigenschaften des Terminals, z. B. die Handhabung von Tabs, durch die Multiplexer-Funktionalität ersetzt.\\n\\nErfordert die Verwendung einer WSL-Terminalumgebung unter Windows und die Installation des Multiplexers auf dem WSL-System.\nterminalAlwaysPauseOnExit=Beim Beenden immer pausieren\nterminalAlwaysPauseOnExitDescription=Wenn diese Funktion aktiviert ist, wirst du beim Beenden einer Terminalsitzung immer aufgefordert, die Sitzung entweder neu zu starten oder zu schließen. Wenn sie deaktiviert ist, tut XPipe dies nur bei fehlgeschlagenen Verbindungen, die mit einem Fehler beendet werden.\nquerying=Abfragen ...\nretrievedPassword=Erhalten: $PASSWORD$\nrefreshOpenpubkey=Openpubkey-Identität aktualisieren\nrefreshOpenpubkeyDescription=Führe opkssh refresh aus, um die openpubkey-Identität wieder gültig zu machen\nall=Alle\nterminalPrompt=Terminal-Eingabeaufforderung\nterminalPromptDescription=Das Terminal-Prompt-Tool, das in deinen Remote-Terminals verwendet werden soll. Wenn du einen Terminalprompt aktivierst, wird das Prompt-Tool automatisch auf dem Zielsystem eingerichtet und konfiguriert, wenn du eine Terminalsitzung öffnest.\\n\\nBestehende Prompt-Konfigurationen oder Profildateien auf einem System werden dabei nicht verändert. Dadurch verlängert sich die Ladezeit des Terminals beim ersten Mal, während der Prompt auf dem entfernten System eingerichtet wird. Dein Terminal benötigt möglicherweise zusätzliche Schriftarten, um die Eingabeaufforderung korrekt anzuzeigen.\nterminalPromptConfiguration=Konfiguration der Terminal-Eingabeaufforderung\nterminalPromptConfig=Config-Datei\nterminalPromptConfigDescription=Die benutzerdefinierte Konfigurationsdatei, die auf den Prompt angewendet werden soll. Diese Konfiguration wird automatisch auf dem Zielsystem eingerichtet, wenn das Terminal initialisiert wird, und als Standardkonfiguration für den Prompt verwendet.\\n\\nWenn du die vorhandene Standardkonfigurationsdatei auf jedem System verwenden willst, kannst du dieses Feld leer lassen.\npasswordManagerKey=Passwortmanager-Schlüssel\npasswordManagerKeyDescription=Die Kennung des Passwortmanagers für das Geheimnis\npasswordManagerAgent=Passwortmanager-Agent\ndockerComposeProject.displayName=Docker compose Projekt\ndockerComposeProject.displayDescription=Container eines Compose-Projekts zusammenfassen\nsshVerboseOutput=Ausführliche SSH-Ausgabe aktivieren\nsshVerboseOutputDescription=Damit werden bei einer Verbindung über SSH viele Debug-Informationen ausgegeben. Nützlich für die Fehlersuche bei Problemen mit SSH-Verbindungen.\ndontUseGateway=Verwende kein Gateway\ndontUseGatewayDescription=Verwende den Hypervisor-Host nicht als Gateway und verbinde dich direkt mit der IP\ncategoryColor=Kategorie Farbe\ncategoryColorDescription=Die Standardfarbe, die für Verbindungen innerhalb dieser Kategorie verwendet wird\ncategorySync=Mit Git-Repository synchronisieren\ncategorySyncDescription=Synchronisiere alle Verbindungen automatisch mit dem git repository. Alle lokalen Änderungen an den Verbindungen werden in das Remote-Repository übertragen.\ncategorySyncSpecial=Mit Git-Repository synchronisieren\\n(Nicht konfigurierbar für die spezielle Kategorie \"$NAME$\")\ncategoryDontAllowScripts=Alle Änderungen deaktivieren\ncategoryDontAllowScriptsDescription=Deaktiviere die Ausführung von Befehlen und anderen Operationen auf Systemen in dieser Kategorie, um Änderungen zu verhindern. Dadurch werden alle Skriptfunktionen, Shell-Umgebungsbefehle, Eingabeaufforderungen und mehr deaktiviert.\ncategoryConfirmAllModifications=Bestätige alle Änderungen\ncategoryConfirmAllModificationsDescription=Bestätige jede Art von Änderung an einer Verbindung oder einem Dateisystem zuerst. Dies kann versehentliche Eingriffe in wichtige Systeme verhindern.\ncategoryDefaultIdentity=Standard-Identität\ncategoryDefaultIdentityDescription=Wenn du häufig eine bestimmte Identität auf vielen der Systeme in dieser Kategorie verwendest, kannst du eine Standardidentität festlegen, die du beim Erstellen neuer Verbindungen vorauswählen kannst.\ncategoryConfigTitle=$NAME$ konfiguration\n#custom\nconfigure=Konfigurieren\naddConnection=Verbindung hinzufügen\nnoCompatibleConnection=Keine kompatible Verbindung gefunden\nnoCompatibleIdentity=Keine kompatible Identität gefunden\nnewCategory=Neue Kategorie\ndockerComposeRestricted=Das Kompositionsprojekt ist durch $NAME$ eingeschränkt und kann nicht von außen geändert werden. Bitte verwende $NAME$, um dieses Compose-Projekt zu verwalten.\nrestricted=Eingeschränkt\ndisableSshPinCaching=SSH-PIN-Caching deaktivieren\ndisableSshPinCachingDescription=XPipe speichert automatisch alle PINs, die für einen Schlüssel eingegeben wurden, wenn eine Form der hardwarebasierten Authentifizierung verwendet wird.\\n\\nWenn du dies deaktivierst, musst du die PIN bei jedem Verbindungsversuch erneut eingeben.\ngitSyncPull=Pull, um entfernte Git-Änderungen zu synchronisieren\nenpassVaultFile=Tresor-Datei\nenpassVaultFileDescription=Die lokale Enpass-Tresordatei.\nflat=Flach\nrecursive=Rekursiv\nrdpAllowListBlocked=Die ausgewählte RemoteApp scheint nicht in der RDP-Zulassungsliste für den Server enthalten zu sein.\npsonoServerUrl=Server-URL\npsonoServerUrlDescription=URL des Psono-Backend-Servers\npsonoApiKey=API-Schlüssel\npsonoApiKeyDescription=Der zu verwendende API-Schlüssel, formatiert als uuid\npsonoApiSecretKey=API-Geheimschlüssel\npsonoApiSecretKeyDescription=Der geheime API-Schlüssel als 64-Byte-Hex-String\npassboltServerUrl=Server-URL\npassboltServerUrlDescription=URL des Passbolt-Backend-Servers\npassboltPassphrase=Passphrase\npassboltPassphraseDescription=Die Passphrase für den privaten Schlüssel des Tresors\npassboltPrivateKey=Privater Schlüssel\npassboltPrivateKeyDescription=Die private gpg-Schlüsseldatei für den Tresor\nfocusWindowOnNotifications=Fokusfenster auf Benachrichtigungen\nfocusWindowOnNotificationsDescription=Bringe XPipe in den Vordergrund, wenn eine Benachrichtigung oder Fehlermeldung angezeigt wird, z.B. wenn eine Verbindung oder ein Tunnel unerwartet beendet wird.\ngitUsername=Benutzerdefinierter Git-Benutzername\ngitUsernameDescription=Der benutzerdefinierte Benutzer, der sich beim Git-Remote-Repository authentifiziert. Standardmäßig verwendet XPipe die aktuell konfigurierten Anmeldedaten des git CLI.\\n\\nDiese Einstellung setzt alle Standard-Anmeldedaten außer Kraft, die bereits für deinen lokalen Git-CLI-Client konfiguriert sind.\ngitPassword=Benutzerdefiniertes Git-Passwort / persönliches Zugangs-Token\ngitPasswordDescription=Das Passwort oder das persönliche Zugangs-Token, das zur Authentifizierung verwendet wird. Ob du ein Passwort oder ein persönliches Access Token brauchst, hängt vom Git Remote Provider ab. Diese Einstellung setzt alle Standard-Anmeldedaten außer Kraft, die bereits für deinen lokalen Git CLI-Client konfiguriert sind.\nsetReadOnly=Schreibgeschützt einstellen\nunsetReadOnly=Nicht gesetzt schreibgeschützt\nreadOnlyStoreError=Die Konfiguration dieses Eintrags ist eingefroren. Wähle einen anderen Namen, um deine Änderungen in einer neuen Kopie zu speichern.\ncategoryFreeze=Verbindungskonfigurationen einfrieren\ncategoryFreezeDescription=Markiert Verbindungskonfigurationen als schreibgeschützt. Das bedeutet, dass keine bestehende Verbindungseintragskonfiguration in dieser Kategorie geändert werden kann. Neue Verbindungen können jedoch hinzugefügt werden.\nupdateFail=Update-Installation war nicht erfolgreich\nupdateFailAction=Update manuell installieren\nupdateFailActionDescription=Schau dir die neuesten Versionen auf GitHub an\nonePasswordPlaceholder=Objektname oder op:// URL\ncomputeDirectorySizes=Verzeichnisgrößen berechnen\ncomputeSize=Größe berechnen\ncustomSpiceCommand=Benutzerdefinierter Befehl\ncustomSpiceCommandDescription=Der benutzerdefinierte Befehl, der ausgeführt wird, um SPICE-Sitzungen zu starten. Der Platzhalter-String $FILE wird beim Aufruf durch den zitierten Dateipfad zur .vv-Datei ersetzt.\nvncClient=VNC-Client\nvncClientDescription=Der VNC-Client, der beim Öffnen von VNC-Verbindungen in XPipe gestartet wird.\\n\\nDu hast die Möglichkeit, entweder den integrierten VNC-Client in XPipe zu verwenden oder alternativ einen externen, lokal installierten VNC-Client zu starten, wenn du mehr Anpassungen wünschst.\nintegratedXPipeVncClient=Integrierter XPipe VNC-Client\ncustomVncCommand=Benutzerdefinierter Befehl\ncustomVncCommandDescription=Der benutzerdefinierte Befehl, der ausgeführt wird, um VNC-Sitzungen zu starten. Der Platzhalter-String $ADDRESS wird beim Aufruf durch die angegebene Adresse ersetzt.\nvncConnections=VNC-Verbindungen\npasswordManagerIdentity=Passwort Manager Identität\npasswordManagerIdentity.displayName=Passwort Manager Identität\npasswordManagerIdentity.displayDescription=Den Benutzernamen und das Passwort einer Identität aus dem Passwortmanager abrufen\npasswordCopied=Verbindungskennwort in die Zwischenablage kopiert\nerrorOccurred=Fehler aufgetreten\nactionMacro.displayName=Aktionsmakro\nactionMacro.displayDescription=Mit benutzerdefinierten Auslösern in Aktion treten\nmacroAdd=Makro hinzufügen\nmacroName=Makroname\nmacroNameDescription=Diesem Makro einen eigenen Namen geben\nactionId=Aktions-ID\nactionIdDescription=Die Aktion, die mit diesem Makro ausgeführt werden soll\nmacroRefs=Assoziierte Verbindungen\nmacroRefsDescription=Die Verbindungen, mit denen die Aktion ausgeführt werden soll\n#custom\nconnectionCopy=Kopie\nactionPickerTitle=Aktion auswählen\nactionPickerDescription=Klicke auf etwas, um eine Aktion auszuführen. Anstatt die Aktion auszuführen, kannst du im Aktionskürzel-Auswahlmodus Verknüpfungen für die Aktion erstellen und bearbeiten.\ncancelActionPicker=Aktionsauswahl abbrechen\nactionShortcut=Aktionskürzel\nactionShortcuts=Aktionskürzel\nactionStore=Aktionsspeicher\nactionStoreDescription=Der Speichereintrag, auf dem die Aktion ausgeführt werden soll\nactionStores=Aktion speichert\nactionStoresDescription=Die Speichereinträge, auf denen die Aktion ausgeführt werden soll\nactionDesktopShortcut=Desktop-Verknüpfung\nactionDesktopShortcutDescription=Erstelle eine Verknüpfung für diese Aktion auf deinem Desktop\nactionUrlShortcut=URL-Verknüpfung\nactionUrlShortcutDescription=Kopiere eine URL, die beim Öffnen diese Aktion auslösen kann\nactionUrlShortcutDisabled=URL-Verknüpfung (nicht verfügbar)\nactionUrlShortcutDisabledDescription=Der Installationstyp $TYPE$ unterstützt das Öffnen von URLs nicht\nactionApiCall=API-Anforderung\nactionApiCallDescription=Diese Aktion über die HTTP-API aufrufen\nactionMacro=Aktionsmakro\nactionMacroDescription=Erstelle ein Makro mit erweiterten Funktionen für diese Aktion\ncreateMacro=Makro erstellen\nactionConfiguration=Parameter\nactionConfigurationDescription=Die Parameter, die an die ausgeführte Aktion übergeben werden\nconfirmAction=Aktion bestätigen\nactionConnections=Aktion Verbindungen\nactionConnectionsDescription=Die Verbindungen, auf denen die Aktion ausgeführt werden soll\nactionConnection=Aktion Verbindung\nactionConnectionDescription=Die Verbindung, über die die Aktion ausgeführt werden soll\nappleContainerInstall.displayName=Apple-Container\nappleContainerInstall.displayDescription=Zugriff auf Apple-Container-Instanzen über das Container-CLI\nappleContainer.displayName=Apple-Container\nappleContainer.displayDescription=Zugriff auf Apple-Container-Instanzen über das Container-CLI\nappleContainerHostDescription=Der Host, auf dem sich der Apfel-Container befindet\nappleContainerDescription=Der Name des Apfel-Containers\nappleContainers=Apple-Container\nchangeOrderIndexTitle=Reihenfolge ändern\norderIndex=Index\norderIndexDescription=Expliziter Index, um diesen Eintrag im Verhältnis zu anderen zu ordnen. Die niedrigsten Indizes werden oben angezeigt, die höchsten unten\nmoveToFirst=An den Anfang verschieben\nmoveToLast=Auf den letzten Platz verschieben\ncategory=Kategorie\nincludeRoot=Wurzel einbeziehen\nexcludeRoot=Wurzel ausschließen\nfreezeConfiguration=Konfiguration einfrieren\n#custom\nunfreezeConfiguration=Konfiguration auftauen\nwaylandScalingTitle=Wayland-Skalierung\nactionApiUrl=$URL$ (Json-Body kopieren)\ncopyBody=Anfragetext kopieren\ngitRepoTerminalOpen=Öffne das Repository im Terminal\ngitRepoTerminalOpenDescription=Sieh dir das Repository selbst mit der Kommandozeile an\ngitRepoOverwriteLocal=Lokales Repository überschreiben\ngitRepoOverwriteLocalDescription=Ersetze alle lokalen Änderungen durch Änderungen aus der Ferne\ngitRepoForcePush=Überschreiben eines entfernten Repositorys\ngitRepoForcePushDescription=Verwende git push --force, um deine lokalen Änderungen auf den entfernten Server zu übertragen\ngitRepoDontWarn=Nicht mehr warnen\ngitRepoDontWarnDescription=Wenn dies erwartet wird, sorge dafür, dass XPipe diesen Fehler in Zukunft ignoriert\ngitRepoTryAgain=Erneut versuchen\ngitRepoTryAgainDescription=Den gleichen Vorgang noch einmal versuchen\ngitRepoEnablePlain=Einfache Verzeichnissynchronisation verwenden\ngitRepoEnablePlainDescription=Ein Git-Repository nicht initialisieren, um Änderungen mit dem Verzeichnis zu synchronisieren\ngitRepoCreateBare=Git Sync verwenden\ngitRepoCreateBareDescription=Initialisiere ein neues Git-Repository im Sync-Verzeichnis\ngitRepoDisable=Git Tresor vorerst deaktivieren\ngitRepoDisableDescription=Keine Änderungen während dieser Sitzung vornehmen\ngitRepoPullRefresh=Änderungen ziehen und aktualisieren\ngitRepoPullRefreshDescription=Fernänderungen zusammenführen und Daten neu laden\nbreakOutCategory=Kategorie ausbrechen\nmergeCategory=Kategorie zusammenführen\nopenWinScp=In WinSCP öffnen\nuninstallApplication=Deinstallieren\nuninstallApplicationDescription=Führt das .pkg-Installationsskript aus, um XPipe vollständig zu deinstallieren\nk8sEditPodTitle=Änderungen anwenden\nk8sEditPodContent=Willst du die Änderungen, die du mit dem Befehl kubectl apply vorgenommen hast, übernehmen? Wahrscheinlich ist ein Neustart erforderlich, damit die Änderungen übernommen werden.\nvirshEditDomainTitle=Änderungen anwenden\nvirshEditDomainContent=Willst du die Änderungen in der Domäne übernehmen? Wahrscheinlich ist ein Neustart erforderlich, damit die Änderungen übernommen werden.\npkcs11Library=PKCS#11-Bibliothek\npkcs11LibraryDescription=Der Pfad der dynamisch verlinkten Bibliotheksdatei\nsshAgentSocket=Benutzerdefinierter SSH-Agent-Socket\nsshAgentSocketDescription=Der benutzerdefinierte Socket, der für die Kommunikation mit dem SSH-Agenten verwendet wird. Dieser benutzerdefinierte Agent kann für eine Verbindung verwendet werden, indem du die Option benutzerdefinierter Agent für ihn auswählst.\npublicKey=Kennung des öffentlichen Schlüssels\npublicKeyDescription=Der optionale öffentliche Schlüssel, um den Agenten zu zwingen, nur den passenden privaten Schlüssel anzubieten\nactions=Aktionen\nhcloudServer.displayName=Hetzner Cloud Server\nhcloudServer.displayDescription=Zugriff auf einen in der Hetzner-Cloud gehosteten Server über SSH\nhcloudInstall.displayName=Hetzner Cloud CLI\nhcloudInstall.displayDescription=Zugang zu Servern, die in der Hetzner-Cloud gehostet werden, über hcloud\nhcloudContext.displayName=hcloud-Kontext\nhcloudContext.displayDescription=Zugangsserver eines hcloud-Kontextes\nmetrics=Metriken\nopenInVsCode=In VsCode öffnen\naddCloud=Cloud ...\nhcloudToken=hcloud-Token\nhcloudTokenDescription=Der zu verwendende Hetzner-Cloud-Token. Weitere Informationen findest du in der Dokumentation\nhcloudLogin=Hetzner Cloud Anmeldung\nclearHcloudToken=Hcloud-Token löschen\nclearHcloudTokenDescription=Vorhandenes Token löschen, damit du dich erneut anmelden kannst\nselectIdentity=Identität auswählen\nenableMcpServer=MCP-Server aktivieren\nenableMcpServerDescription=Aktiviert den XPipe MCP-Server, damit externe MCP-Clients Anfragen an den MCP-Server senden können. Siehe unten für die Konfigurationsdetails.\\n\\nBeachte, dass die HTTP-API für die MCP-Funktionalität nicht aktiviert sein muss.\nenableMcpMutationTools=MCP-Mutationswerkzeuge einschalten\nenableMcpMutationToolsDescription=Standardmäßig sind auf dem MCP-Server nur schreibgeschützte Tools aktiviert. Damit soll sichergestellt werden, dass keine versehentlichen Eingriffe vorgenommen werden können, die das System verändern könnten.\\n\\nWenn du planst, Änderungen an Systemen über MCP-Clients vorzunehmen, solltest du sicherstellen, dass dein MCP-Client so konfiguriert ist, dass er alle potenziell zerstörerischen Aktionen bestätigt, bevor du diese Option aktivierst. Erfordert eine erneute Verbindung aller MCP-Clients zur Anwendung.\nmcpClientConfigurationDetails=MCP-Client-Konfiguration\nmcpClientConfigurationDetailsDescription=Verwende diese Konfigurationsdaten, um dich mit dem XPipe MCP-Server von einem MCP-Client deiner Wahl aus zu verbinden.\nswitchHostAddress=Host-Adresse ändern\naddAnotherHostName=Einen weiteren Hostnamen hinzufügen\naddNetwork=Netzwerk-Scan ...\nnetworkScan=Netzwerk-Scan\nnetworkScanStore=Ziel-Host\nnetworkScanStoreDescription=Der Host, nach dem das lokale Netzwerk durchsucht werden soll\nuseAsGateway=Host als Gateway verwenden\nuseAsGatewayDescription=Ob der Zielhost als Gateway für die erstellten Verbindungen verwendet werden soll\nnetworkScanPorts=Zu scannende Ports\nnetworkScanPortsDescription=Die kommagetrennte Liste der Ports, die in den Scan einbezogen werden sollen\nnetworkScanType=Verbindungstyp\nnetworkScanTypeDescription=Die Art von Servern, nach denen man suchen muss\nemptyDirectory=Dieses Verzeichnis scheint leer zu sein\nhcloudConfigFile=hcloud-Konfigurationsdatei\nhcloudConfigFileDescription=Der Speicherort der hcloud CLI .toml Konfigurationsdatei\npreferMonochromeIcons=Bevorzuge monochrome Symbole\npreferMonochromeIconsDescription=Wenn diese Option aktiviert ist, werden einfarbige Icon-Variablen den standardmäßigen farbigen Versionen eines Icons vorgezogen, vorausgesetzt, dass für ein Icon aus einer Quelle eine separate Icon-Variante im hellen oder dunklen Modus verfügbar ist.\\n\\nErfordert eine Aktualisierung der Icons zur Anwendung.\nalwaysShowSshMotd=MOTD immer anzeigen\nalwaysShowSshMotdDescription=Ob die Nachricht des Tages, die auf einem entfernten System konfiguriert wurde, bei der Anmeldung in einer neuen Terminalsitzung angezeigt werden soll oder nicht. Beachte, dass eine Änderung dieser Einstellung das Initialisierungsverhalten von SSH-Verbindungen verändern kann.\nmanageSubscription=Abonnement verwalten\nnoListeningServer=Kein abhörender Server\nnetworkScanResults=Scan-Ergebnisse\nnetworkScanResultsDescription=Die Liste der gefundenen Systeme im Netzwerk\nlocalShellDialect=Lokale Shell\nlocalShellDialectDescription=Die Shell, die für lokale Operationen verwendet wird. Falls die normale lokale Standard-Shell deaktiviert oder in gewissem Maße beschädigt ist, kann mit dieser Option auf eine andere Alternative zurückgegriffen werden.\\n\\nEinige Konfigurationen, wie z. B. benutzerdefinierte PATH-Einträge, gelten möglicherweise nicht für die Fallback-Shell, wenn sie noch nicht in den entsprechenden Shell-Profildateien konfiguriert sind.\nagentSocketNotFound=Es wurde kein aktiver Agentensocket gefunden\nagentSocket=Standort der Steckdose\nagentSocketDescription=Der Pfad der Socket-Datei des Agenten\nagentSocketNotConfigured=Es wurde noch kein benutzerdefinierter Socket konfiguriert\ndownloadInProgress=$NAME$ download in Bearbeitung\nenableTerminalStartupBell=Terminal-Startglocke einschalten\nenableTerminalStartupBellDescription=Einen Piep-/Glockenbefehl in einer neuen Terminalsitzung abspielen. Wenn dein Terminalemulator Glocken unterstützt, kannst du damit neu gestartete Terminalinstanzen leichter identifizieren.\ninvalidSshGatewayChain=Ungültige gemischte Gateway-Kettenkonfiguration mit Jump Gateways und Nicht-Jump Gateways.\nsyncFileExists=Die synchronisierte Datei $FILE$ existiert bereits\nreplaceFile=Datei austauschen\nreplaceFileDescription=Ersetze die bestehende Datei durch diese\nrenameFile=Datei umbenennen\nrenameFileDescription=Gib dieser Datei einen anderen Namen, um sie zu synchronisieren\nnewFileName=Neuer Dateiname\nparentHostDoesNotSupportTunneling=Der übergeordnete Host $NAME$ unterstützt kein Tunneling\nconnectionNotesTemplate=Vorlage für Notizen\nconnectionNotesTemplateDescription=Die Markdown-Vorlage, die beim Hinzufügen eines neuen Notizeintrags zu einer Verbindung verwendet werden soll.\nconnectionNotesButton=Notizen bearbeiten\nrdpSmartSizing=Smart Sizing einschalten\nrdpSmartSizingDescription=Wenn diese Funktion aktiviert ist, verkleinert mstsc den Desktop, wenn das Fenster zu klein ist, um es in seiner vollen Auflösung anzuzeigen. Das Seitenverhältnis des Desktops bleibt beim Verkleinern erhalten.\ndisableStartOnInit=Automatisches Starten deaktivieren\nenableStartOnInit=Automatisches Starten aktivieren\nfileReadSudoTitle=Sudo-Datei lesen\nfileReadSudoContent=Die Datei, die du zu lesen versuchst, gewährt dir als aktuellem Benutzer keine Leseberechtigung. Willst du diese Datei als root-Benutzer mit sudo lesen? Dadurch wirst du entweder mit den vorhandenen Anmeldedaten oder über eine Eingabeaufforderung automatisch zum Root-Benutzer ernannt.\nnetbirdInstall.displayName=Netbird Installation\nnetbirdInstall.displayDescription=Verbinde dich mit Peers in deinem Netbird Netzwerk\nnetbirdProfile.displayName=Netbird Profil\nnetbirdProfile.displayDescription=Peers in einem bestimmten Profil auflisten\nnetbirdPeer.displayName=Netbird Peer\nnetbirdPeer.displayDescription=Mit einer Gegenstelle über SSH verbinden\nnetbirdPublicKey=Öffentlicher Schlüssel\nnetbirdPublicKeyDescription=Der interne öffentliche Schlüssel der Gegenstelle\nnetbirdHostName=Hostname\nnetbirdHostNameDescription=Der Hostname der Gegenstelle im Netzwerk\nvncRefSystem=Assoziiertes System\nvncRefSystemDescription=Der Verbindungseintrag, mit dem diese VNC-Verbindung verknüpft werden soll. Leer lassen, wenn es keinen gibt\nabstractHost.displayName=Abstrakter Gastgeber\nabstractHost.displayDescription=Einen Eintrag für einen Host erstellen, der keine Shell-Verbindungen unterstützt\nabstractHostAddress=Host-Adresse\nabstractHostAddressDescription=Die Adresse des Hosts\nabstractHostGateway=Gateway\nabstractHostGatewayDescription=Das optionale Gatewaysystem, über das dieser Host zu erreichen ist\nabstractHostConvert=In einen abstrakten Host-Eintrag umwandeln\nhostNoConnections=Keine verfügbaren Verbindungen\nhostHasConnections=$COUNT$ verfügbare Verbindungen\nhostHasConnection=$COUNT$ verfügbare Verbindung\nlargeFileWarningTitle=Große Datei bearbeiten\nlargeFileWarningContent=Die Datei, die du bearbeiten möchtest, ist mit $SIZE$ ziemlich groß. Willst du diese Datei wirklich in deinem Texteditor öffnen?\nrdpAskpassUser=RDP-Benutzername für Host $HOST$\nrdpAskpassPassword=Passwort für Benutzer $USER$\ninPlaceKey=Schlüssel\ninPlaceKeyText=Inhalt des privaten Schlüssels\ninPlaceKeyTextDescription=Der Inhalt des privaten Schlüssels\nnetbirdSelfhosted=Selbstgehostete Netbird-Instanz\nnetbirdSelfhostedDescription=Eine benutzerdefinierte URL bereitstellen, anstatt die in der Cloud gehostete Version zu verwenden\nnetbirdManagementUrl=Netbird Management URL\nnetbirdManagementUrlDescription=Die Verwaltungs-URL deiner selbst gehosteten Instanz\nnetbirdSetupKey=Setup-Taste\nnetbirdSetupKeyDescription=Wenn du Setup-Schlüssel verwendest, kannst du einen für die Anmeldung verwenden\nnetbirdLogin=Netbird Anmeldung\naddProfile=Profil hinzufügen\nnetbirdProfileNameAsktext=Name des neuen Netbird-Profils\nopenSftp=In SFTP-Sitzung öffnen\ncapslockWarning=Du hast Capslock aktiviert\ninherit=Erbe\nsshConfigStringSelected=Ziel-Host\nsshConfigStringSelectedDescription=Bei mehreren Hosts wird der erste als Ziel verwendet. Ordne deine Hosts neu an, um das Ziel zu ändern\ntunnelToLocalhost=Tunnel zu localhost\ntunnelToLocalhostDescription=Automatisches Tunneln des entfernten Ports zu localhost\ntags=Tags\ntag=Tag\naddNewTag=Neues Tag erstellen\ncreateTag=Tag erstellen ...\ninPlacePublicKey=Öffentlicher Schlüssel\ninPlacePublicKeyDescription=Der zugehörige öffentliche Schlüssel für den angegebenen privaten Schlüssel\nsshKeygenTitle=Neuen SSH-Schlüssel generieren\nsshKeygenAlgorithm=Algorithmus\nsshKeygenAlgorithmDescription=Der asymmetrische Schlüsselalgorithmus, der für den Schlüssel verwendet werden soll\nrsaBits=Bits\nrsaBitsDescription=Anzahl der Bits im erzeugten Schlüssel\nsshKeygenComment=Kommentar\nsshKeygenCommentDescription=Der optionale Kommentar für diesen Schlüssel\nsshKeygenPassphrase=Passphrase\nsshKeygenPassphraseDescription=Die optionale Passphrase für diesen Schlüssel\ned25519SkResident=Residenten Schlüssel erstellen\ned25519SkResidentDescription=Privaten Schlüssel auf dem Hardware-Sicherheitsschlüssel speichern\ned25519SkResidentKeyName=Residente Schlüsselbezeichnung\ned25519SkResidentKeyNameDescription=Gib dem Schlüssel eine Bezeichnung. Wird benötigt, wenn mehrere Schlüssel auf dem Sicherheitsschlüssel gespeichert werden\ned25519SkPinRequired=PIN anfordern\ned25519SkPinRequiredDescription=PIN-Eingabe bei Benutzung verlangen\ned25519SkUserPresenceRequired=Benutzeranwesenheit voraussetzen\ned25519SkUserPresenceRequiredDescription=Bei der Benutzung eine Berührung oder ähnliches erfordern. Für einige Sicherheitsschlüssel muss dies aktiviert sein\ncopyPublicKey=Öffentlichen Schlüssel kopieren\ngeneratePublicKey=Öffentlichen Schlüssel generieren\npublicKeyGenerateNotice=Kann aus dem privaten Schlüssel generiert werden\nidentityApplyTargetHost=Ziel\nidentityApplyTargetHostDescription=Das System zur Anwendung der Identität auf\nidentityApplyAuthorizedHost=SSH-Schlüssel autorisiert\nidentityApplyAuthorizedHostDescription=Der SSH-Schlüssel wird zur Datei \"authorized hosts\" hinzugefügt\nidentityApplyAuthorizedHostButton=Schlüssel an die Datei anhängen\napplyIdentityToHost=Identität auf Host anwenden ...\nidentityApplyMissingPublicKeyTitle=Fehlender öffentlicher Schlüssel\nidentityApplyMissingPublicKeyContent=Der SSH-Schlüssel der Identität ist nicht mit einem öffentlichen Schlüssel verknüpft. Sieh dir die Konfiguration für Details an.\nvalid=Gültig\nnotValid=Nicht gültig\nwarning=Warnung\nidentityApplyTitle=Identität anwenden\nidentityApplyConfigPasswordEnabled=Passwortschutz aktiviert\nidentityApplyConfigPasswordEnabledDescription=Die Passwortauthentifizierung ist in der sshd-Konfiguration immer noch aktiviert\nidentityApplyConfigPasswordDisabled=Passwortschutz deaktiviert\nidentityApplyConfigPasswordDisabledDescription=Die Passwortauthentifizierung ist in der sshd-Konfiguration immer noch deaktiviert\nidentityApplyConfigKeyEnabled=Schlüsselautorisierung aktiviert\nidentityApplyConfigKeyEnabledDescription=Die schlüsselbasierte Authentifizierung ist in der sshd-Konfiguration weiterhin aktiviert\nidentityApplyConfigKeyDisabled=Schlüsselautorisierung deaktiviert\nidentityApplyConfigKeyDisabledDescription=Die schlüsselbasierte Authentifizierung ist in der sshd-Konfiguration immer noch deaktiviert\nidentityApplyConfigRootDisabledWarning=Root-Login deaktiviert\nidentityApplyConfigRootDisabledWarningDescription=Der Root-Benutzer-Login ist in der sshd-Konfiguration nicht aktiviert\nidentityApplyConfigAdminWarning=Konfigurierte Administratorschlüssel\nidentityApplyConfigAdminWarningDescription=Für Admin-Benutzer muss der Schlüssel möglicherweise stattdessen zu administrators_authorized_keys hinzugefügt werden\nidentityApplyEditConfig=Konfiguration bearbeiten\nidentityApplyEditConfigDescription=Öffne die sshd-Konfiguration im Editor, um eventuelle Probleme zu beheben\nidentityApplyEditAuthorizedKeys=Berechtigte Schlüssel bearbeiten\nidentityApplyEditAuthorizedKeysDescription=Öffne die Datei authorized_keys im Editor, um andere Schlüssel zu bearbeiten oder zu entfernen\nidentityApplyEditConfigButton=Sshd_config öffnen\nidentityApplyEditAuthorizedKeysButton=Autorisierte_Schlüssel öffnen\nidentityApplySetStoreIdentity=Verbindungsidentitätssatz\nidentityApplySetStoreIdentityDescription=Die Identität ist so konfiguriert, dass sie von der Verbindung verwendet wird\nidentityApplySetStoreIdentityButton=Identität anwenden\ngenerateKey=Schlüssel generieren\ngroupSecretStrategy=Gruppenbasierte Zugriffskontrolle\ngroupSecretStrategyDescription=Wie man das Gruppengeheimnis abruft, das für die Ver- und Entschlüsselung der Gruppe verwendet wird. Die von dir gewählte Abrufmethode wird ausgeführt, wenn sich ein Benutzer beim Start des Tresors anmeldet.\\n\\nDiese Einstellung wird für jede Gruppe einzeln konfiguriert. Um diese Einstellung für eine andere als die derzeit aktive Gruppe zu ändern, musst du dich als Mitglied dieser Gruppe am Tresor anmelden.\nfileSecret=Dateibasiertes Geheimnis\ncommandSecret=Befehl\nhttpRequestSecret=HTTP-Antwort\nfileSecretChoice=Dateispeicherort\nfileSecretChoiceDescription=Der Pfad zu der Datei, die das Gruppenverschlüsselungsgeheimnis enthält. Da diese Datei auf allen Plattformen abgefragt werden kann, kannst du ~ im Pfad verwenden, um auf das Heimatverzeichnis zu verweisen. Die Datei muss auf allen Systemen verfügbar sein, von denen aus du den Tresor entsperrst, sonst schlägt die Anmeldung fehl.\ncommandSecretField=Skript für den Abruf\ncommandSecretFieldDescription=Der Befehl, der den geheimen Verschlüsselungsschlüssel für die aktuelle Gruppe zurückgibt. Der Befehl wird in der Standard-Shell des lokalen Systems ausgeführt und der Schlüssel sollte auf stdout ausgegeben werden.\nhttpRequestSecretField=URI anfordern\nhttpRequestSecretFieldDescription=Die URI, an die eine HTTP-Anfrage gesendet wird. Das Gruppengeheimnis wird aus dem HTTP-Antwortkörper übernommen.\nvaultAuthentication=Tresor-Authentifizierung\nvaultAuthenticationDescription=Wie man die Tresordaten authentifiziert/entsperrt. Es gibt verschiedene Möglichkeiten, Tresordaten zu verschlüsseln und zu entsperren, je nachdem, mit wem du sie teilen möchtest.\ngroupAuthFailed=Geheime Authentifizierung fehlgeschlagen\nuserAuthFailed=Passwortauthentifizierung fehlgeschlagen\nsavingChanges=Speichern von Änderungen\nawsDeviceScan=AWS\naws=AWS\nawsCliInstallTitle=AWS CLI erforderlich\nawsCliInstallContent=Für die AWS-Integration muss die AWS CLI auf deinem lokalen System installiert sein\nawsProfileCreateTitle=Neues AWS-Profil\nawsProfileAccessKey=Zugangsschlüssel\nawsProfileName=Profilname\nawsProfileNameDescription=Der Anzeigename des neuen Profils\nawsProfileRegion=Region\nawsProfileRegionDescription=Die mit dem Profil verbundene AWS-Region\nawsProfileAccessKeyId=Zugangsschlüssel-ID\nawsProfileAccessKeyIdDescription=Die ID des IAM-Benutzerzugangsschlüssels\nawsProfileSecretAccessKey=Geheimer Zugangsschlüssel\nawsProfileSecretAccessKeyDescription=Der zugehörige geheime Zugangsschlüssel\nawsInstall.displayName=AWS CLI-Installation\nawsInstall.displayDescription=Verbinde dich mit deinen AWS-Systemen über die AWS CLI\nawsProfile.displayName=AWS CLI-Profil\nawsProfile.displayDescription=Zugang zu AWS über ein bestimmtes Profil\nawsInstanceId=Instanz-ID\nawsInstanceIdDescription=Die interne ID dieser Instanz\nawsInstanceUseSsm=Verbinden über SSM\nawsInstanceUseSsmDescription=Benutze das SSM-Tool, um dich über SSH mit der Instanz zu verbinden\nawsEc2Instance.displayName=AWS EC2-Instanz\nawsEc2Instance.displayDescription=Verbindung zu einer EC2-Instanz über SSH\nawsS3Group.displayName=S3-Eimer\nawsS3Group.displayDescription=Zugriff auf S3-Buckets eines AWS-Profils\nawsS3Bucket.displayName=S3-Eimer\nawsS3Bucket.displayDescription=Zugriff auf einen S3-Bucket eines AWS-Profils\nawsEc2Group.displayName=EC2-Instanzen\nawsEc2Group.displayDescription=Zugriff auf EC2-Instanzen eines AWS-Profils\nawsEc2InstanceSsmTerminal=SSM-Terminal öffnen\ngenericS3Bucket.displayName=Generischer S3 Bucket\ngenericS3Bucket.displayDescription=Zugriff auf ein allgemeines S3-Bucket über die AWS CLI\naddFileSystem=Dateisystem ...\ngenericS3BucketHost=Host\ngenericS3BucketHostDescription=Der Host-Eintrag oder die manuelle Adresse des S3-Servers\ngenericS3BucketPortDescription=Der Port, an dem der S3-Server lauscht\ngenericS3BucketAccessKeyId=Zugangsschlüssel-ID\ngenericS3BucketAccessKeyIdDescription=Die ID des IAM-Benutzerzugangsschlüssels\ngenericS3BucketSecretAccessKey=Geheimer Zugangsschlüssel\ngenericS3BucketSecretAccessKeyDescription=Der zugehörige geheime Zugangsschlüssel\ngenericS3BucketHttps=HTTPS aktivieren\ngenericS3BucketHttpsDescription=Verwende HTTPS, um dich mit dem Server zu verbinden. Einige Anbieter verlangen möglicherweise HTTPS\ntunnelled=Getunnelt\nawsInstallSync=Konfigurationssynchronisation\nawsInstallSyncDescription=Die AWS CLI-Konfigurationsdateien mit dem Git-Tresor synchronisieren\nawsInstallLocation=Standort der Benutzerdaten\nawsInstallLocationDescription=Der Pfad, von dem aus die AWS CLI-Konfigurationsdateien bezogen werden\ninstanceActions=Instanz-Aktionen\nopenSplit=Im geteilten Terminal öffnen\nterminalSplitStrategy=Geteilte Blickrichtung\nterminalSplitStrategyDescription=Legt fest, wie Terminal-Registerkarten aufgeteilt werden, wenn du die Funktion \"Geteilte Ansicht\" im Batch-Modus verwendest, um mehrere Terminalsitzungen nebeneinander zu öffnen.\nterminalSplitStrategyDisabledDescription=Legt fest, wie Terminal-Registerkarten aufgeteilt werden, wenn du die Funktion \"Geteilte Ansicht\" im Batch-Modus verwendest, um mehrere Terminalsitzungen nebeneinander zu öffnen.\\n\\nDeine aktuelle Terminalkonfiguration unterstützt keine geteilten Ansichten.\nhorizontal=Horizontal\nvertical=Vertikal\nbalanced=Ausgewogene\nclose=Schließen\nhelpButton=$TOPIC$ dokumentationslink\nquickAccess=Schnellzugriff\ntoggleEnabled=Toggle-Status\ncurrentPath=Aktueller Pfad\ndirectoryContents=Verzeichnisinhalte\ndirectoryOptions=Verzeichnis-Optionen\nchooseConnectionType=Verbindungsart wählen\nbatchMode=Batch-Modus\ntoggleButton=Toggle-Schaltfläche\ntailscaleUseSsh=Tailscale SSH-Authentifizierung verwenden\ntailscaleUseSshDescription=Sich über den Tailscale SSH-Server selbst ohne SSH-Authentifizierung anmelden\nportDescription=Der Port, auf dem der SSH-Server läuft\nloginAs=Anmelden als\nsshGatewayType=Gateway-Typ\nsshGatewayTypeDescription=Ob die Verbindung zum Ziel über einen Tunnel oder mit der Option ProxyJump hergestellt werden soll\ngatewayTunnel=Gateway-Tunnel\nproxyJump=Proxy-Sprung\ncommandTypeAsyncBackground=Losgelöst im Hintergrund laufen lassen\ncommandTypeSyncBackground=Im Hintergrund laufen und auf das Ende warten\ncommandTypeTerminalBackground=Im Terminal öffnen\nasyncBackgroundCommand=Hintergrund-Befehl\nsyncBackgroundCommand=Blockierender Hintergrundbefehl\nterminalBackgroundCommand=Terminal-Befehl\ntestingConnection=Verbindung testen ...\nopenManagementConsole=Verwaltungskonsole öffnen\nopenLxcTerminal=LXC-Terminal öffnen\nopenContainerConsole=Serielle Konsole öffnen\nkeeper2fa=2FA-Methode\nkeeper2faDescription=Die primäre Zwei-Faktor-Authentifizierungsmethode, die für dein Konto konfiguriert ist. Aktiviere dies, wenn dein Keeper-Konto eine Zwei-Faktor-Authentifizierung für den Zugriff auf Passwörter erfordert.\nkeeperTotpDuration=Dauer des benutzerdefinierten 2FA-Codes\nkeeperTotpDurationDescription=Überschreibe die Standarddauer, wie lange ein 2FA-Code gültig ist. Gilt nur, wenn deine Unternehmensrichtlinie das Ändern der Dauer erlaubt.\\n\\nMögliche Werte sind: $VALUES$\nkeeperOtherAuth=Andere (RSA SecurID, Duo Security, Keeper DNA, etc.)\nextractReusableIdentities=Wiederverwendbare Identitäten extrahieren\nidentitiesAdded=Hinzugefügte Identitäten\nsyncMode=Sync-Modus\nsyncModeDescription=Steuert, wie Änderungen synchronisiert werden sollen.\\n\\nIm Sofortmodus werden Änderungen so schnell wie möglich gepusht und gezogen, im Start- und Beendigungsmodus werden alle während einer Sitzung vorgenommenen Änderungen auf einmal synchronisiert und im manuellen Modus erfolgt die Synchronisierung nur, wenn du sie initiierst.\ntoggleTerminalDock=Terminal-Dock umschalten\nscriptDirectory=Verzeichnisstandort\nscriptDirectoryDescription=Das lokale Verzeichnis, das Shell-Skriptdateien enthält\nscriptSourceUrl=Repository-URL\nscriptSourceUrlDescription=Die URL zu einem entfernten Git-Repository, das Shell-Skriptdateien enthält\nscriptCollectionSourceType=Quelle Typ\nscriptCollectionSourceTypeDescription=Der Typ der Quelle, aus der Shell-Skripte geladen werden sollen\nscriptCollectionSourceEntry=Quellenangabe\nscriptCollectionSourceEntryDescription=Die Quelle, aus der Shell-Skripte geladen werden sollen\ngitRepository=Git-Repository\nscriptCollectionSource.displayName=Skript-Quelle\nscriptCollectionSource.displayDescription=Shell-Skripte automatisch aus einer bestehenden Quelle importieren\ndirectorySource=Verzeichnisquelle\ngitRepositorySource=Quelle des Git-Repositorys\nrefreshSource=Quelle aktualisieren\nscriptTextSourceUrl=Skript-URL\nscriptTextSourceUrlDescription=Die URL, von der die Skriptdatei abgerufen wird\nscriptSourceType=Skript-Quelle\nscriptSourceTypeDescription=Woher soll das Skript kommen?\nscriptSourceTypeInPlace=In-Place-Skript\nscriptSourceTypeUrl=Externe URL\nscriptSourceTypeSource=Vorhandene Quelle\nimportScripts=Skripte importieren\nscriptsContained=$NUMBER$ skripte\nscriptSourceCollectionImportTitle=Skripte aus der Quelle importieren ($SELECTED$/$COUNT$)\nnoScriptsFound=Keine Skripte gefunden\ntunnel=Tunnel\nnotInitialized=Nicht initialisiert\nselectCategory=Kategorie auswählen ...\nscriptSourceName=Skriptname\nscriptSourceNameDescription=Der Dateiname des Skripts in der Quelle\nworkspaceRestartTitle=Arbeitsbereich bereit\nworkspaceRestartContent=Unter $PATH$ wurde eine Verknüpfung zu dem neuen Arbeitsbereich erstellt. Du kannst zu dieser Verknüpfung navigieren oder XPipe jetzt neu starten, um den neuen Arbeitsbereich automatisch zu öffnen.\nbrowseShortcut=Datei durchsuchen\nsyncModeInstant=Sofort synchronisieren\nsyncModeSession=Synchronisierung beim Starten und Beenden\nsyncModeManual=Manuell synchronisieren\npushChanges=Änderungen pushen\npullChanges=Änderungen ziehen\nsourcedFrom=Entnommen aus $SOURCE$\ninPlaceScript=In-Place-Skript\ngeneric=Allgemein\nsyncToPlainDirectory=Synchronisierung mit dem einfachen Verzeichnis\nsyncToPlainDirectoryDescription=Wenn du ein lokales Verzeichnis synchronisierst, kannst du dieses Verzeichnis entweder wie ein weiteres Git-Repository oder wie ein einfaches Verzeichnis behandeln. Wenn die Einstellung \"einfaches Verzeichnis\" aktiviert ist, wird das Verzeichnis nicht als Git-Repository initialisiert.\nopenSpiceSession=SPICE-Sitzung öffnen\nterminalBehaviour=Verhalten des Terminals\nnoScanPossible=Es wurden keine unterstützten Verbindungen gefunden\nnetworkSwitchPorts=Netzwerk-Ports\nnswitchGroup.displayName=Netzwerk-Ports\nnswitchGroup.displayDescription=Verfügbare Ports auf einem Netzwerkgerät auflisten\nnswitchPort.displayName=Netzwerkanschluss\nnswitchPort.displayDescription=Einen einzelnen Port eines Netzwerk-Switch-Geräts steuern\nenablePort=Port freigeben\nshutdownPort=Port abschalten\nresetPort=Anschluss zurücksetzen\nuseSystemDefault=Systemstandard verwenden\nportStatus=Port-Status\nclearCounters=Zähler löschen\nshowStatus=Status anzeigen\nshowAllPorts=Alle Ports anzeigen\nactiveLicense=Lizenz\nactiveLicenseDescription=Aktivieren eines XPipe-Lizenzschlüssels\nauthenticatorApp=Authenticator-App\nsecurityKey=Sicherheitsschlüssel\nmcpAdditionalContext=Zusätzlicher MCP-Kontext\nmcpAdditionalContextDescription=Zusätzliche Anweisungen, die an den MCP-Client weitergegeben werden. Damit kannst du das Verhalten des Agenten steuern und zusätzlichen Kontext für dein individuelles Setup liefern.\nmcpAdditionalContextSample=- Starten Sie keine Dienste und Daemons automatisch neu, ohne dies vorher zu bestätigen\\n- Wenn du eine Netzwerkschnittstelle konfigurierst, verwende immer 192.168.1.1/24 als Gateway\nprefsRestartTitle=Neustart erforderlich\nprefsRestartContent=Einige Optionen, die du geändert hast, erfordern einen Neustart der Anwendung, um angewendet zu werden. Willst du XPipe jetzt neu starten?\nbashShell=Bash-Shell\n"
  },
  {
    "path": "lang/strings/translations_en.properties",
    "content": "delete=Delete\nproperties=Properties\nusedDate=Used $DATE$\nopenDir=Open Directory\nsortLastUsed=Sort by last used date\nsortAlphabetical=Sort alphabetical by name\nsortIndexed=Sort by order index\nrestartDescription=A restart can often be a quick fix\nreportIssue=Report an issue\nreportIssueDescription=Open the integrated issue reporter\nusefulActions=Useful actions\nstored=Saved\ntroubleshootingOptions=Troubleshooting tools\ntroubleshoot=Troubleshoot\nremote=Remote File\n#context: computer shell program\naddShellStore=Add Shell ...\n#context: computer shell program\naddShellTitle=Add Shell Connection\n#context: server connections\nsavedConnections=Saved Connections\nsave=Save\n#context: verb, to get rid of dust\nclean=Clean\nmoveTo=Move to ...\naddDatabase=Database ...\nbrowseInternalStorage=Browse internal storage\naddTunnel=Tunnel ...\naddService=Service ...\naddScript=Script ...\naddHost=Remote Host ...\naddShell=Shell Environment ...\naddCommand=Command ...\naddAutomatically=Add automatically ...\naddOther=Add Other ...\nconnectionAdd=Add connection\nscriptAdd=Add script\nscriptGroupAdd=Add script group\nidentityAdd=Add identity\nnew=New\nselectType=Select Type\nselectTypeDescription=Select connection type\n#context: computer shell program\nselectShellType=Shell Type\n#context: computer shell program\nselectShellTypeDescription=Select the Type of the Shell Connection\n#context: name of an inanimate or abstract object\nname=Name\nstoreIntroHeader=Connection Hub\nstoreIntroContent=Here you can manage all your local and remote shell connections in one place. To start off, you can quickly detect available connections automatically and choose which ones to add.\nstoreIntroButton=Search for connections ...\ndragAndDropFilesHere=Or just drag and drop a file here\nconfirmDsCreationAbortTitle=Confirm abort\nconfirmDsCreationAbortHeader=Do you want to abort the data source creation?\nconfirmDsCreationAbortContent=Any data source creation progress will be lost.\nconfirmInvalidStoreTitle=Skip validation\nconfirmInvalidStoreContent=Do you want to skip connection validation? You can add this connection even if it could not be validated and fix the connection problems later on.\nexpand=Expand\naccessSubConnections=Access sub connections\n#context: noun, not rare\ncommon=Common\ncolor=Color\nalwaysConfirmElevation=Always confirm permission elevation\nalwaysConfirmElevationDescription=Controls how to handle cases when elevated permissions are required to run a command on a system, e.g. with sudo.\\n\\nBy default, any sudo credentials are cached during a session and automatically provided when needed. If this option is enabled, it will ask you to confirm the elevation access every time.\nallow=Allow\nask=Ask\ndeny=Deny\nshare=Add to git repository\nunshare=Remove from git repository\nremove=Remove\ncreateNewCategory=New subcategory\nprompt=Prompt\ncustomCommand=Custom command\nother=Other\nsetLock=Set lock\nselectConnection=Select connection\nselectEntry=Select entry\ncreateLock=Create passphrase\nchangeLock=Change passphrase\ntest=Test\n#context: verb, exit\nfinish=Finish\nerror=An error occurred\ndownloadStageDescription=Moves downloaded files into your system downloads directory and opens it.\nok=Ok\nsearch=Search\nrepeatPassword=Repeat password\naskpassAlertTitle=Askpass\nunsupportedOperation=Unsupported operation: $MSG$\nfileConflictAlertTitle=Resolve conflict\nfileConflictAlertContent=A conflict was encountered. The file $FILE$ does already exist on the target system.\\n\\nHow would you like to proceed?\nfileConflictAlertContentMultiple=A conflict was encountered. The file $FILE$ already exists.\\n\\nHow would you like to proceed? There might be more conflicts that you can automatically resolve by choosing an option that applies to all.\nmoveAlertTitle=Confirm move\nmoveAlertHeader=Do you want to move the ($COUNT$) selected elements into $TARGET$?\ndeleteAlertTitle=Confirm deletion\ndeleteAlertHeader=Do you want to delete the ($COUNT$) selected elements?\nselectedElements=Selected elements:\nmustNotBeEmpty=$VALUE$ must not be empty\nvalueMustNotBeEmpty=Value must not be empty\ntransferDescription=Drag files here to download\ndragLocalFiles=Drag downloads from here\nnull=$VALUE$ must be not null\nroots=Roots\nscripts=Scripts\nsearchFilter=Search ...\n#context: last used\nrecent=Recent\nshortcut=Shortcut\nbrowserWelcomeEmptyHeader=File browser\nbrowserWelcomeEmptyContent=You can choose on the left which systems to open in the file browser. XPipe will remember which systems and directories you have accessed previously and show them in a quick access menu here in the future.\nbrowserWelcomeEmptyButton=Open local file browser\nbrowserWelcomeSystems=You were recently connected to the following systems:\nbrowserWelcomeDocsHeader=Documentation\nbrowserWelcomeDocsContent=If you prefer a more guided approach to familiarizing yourself with XPipe, check out the documentation website.\nbrowserWelcomeDocsButton=Open documentation\nhostFeatureUnsupported=$FEATURE$ is not installed on the host\nmissingStore=$NAME$ does not exist\nconnectionName=Connection name\nconnectionNameDescription=Give this connection a custom name\nopenFileTitle=Open file\nunknown=Unknown\nscanAlertTitle=Add connections\nscanAlertChoiceHeader=Target\nscanAlertChoiceHeaderDescription=Choose where to search for connections. This will look for all available connections first.\nscanAlertHeader=Connection types\nscanAlertHeaderDescription=Select types of connections you want to automatically add for the system.\nnoInformationAvailable=No information available\nyes=Yes\nno=No\nerrorOccured=An error occured\nterminalErrorOccured=A terminal error occured\nerrorTypeOccured=An exception of type $TYPE$ was thrown\npermissionsAlertTitle=Permissions required\npermissionsAlertHeader=Additional permissions are required to perform this operation.\npermissionsAlertContent=Please follow the pop-up to give XPipe the required permissions in the settings menu.\nerrorDetails=Error details\nupdateReadyAlertTitle=Update Ready\nupdateReadyAlertHeader=An update to version $VERSION$ is ready to be installed\nupdateReadyAlertContent=This will install the new version and restart XPipe once the installation finished.\nerrorNoDetail=No error details are available\nerrorNoExceptionMessage=An error of type $TYPE$ was thrown\nupdateAvailableTitle=Update Available\nupdateAvailableContent=An XPipe update to version $VERSION$ is available to install. Even though XPipe could not be started, you can attempt to install the update to potentially fix the issue.\nclipboardActionDetectedTitle=Clipboard Action detected\nclipboardActionDetectedContent=XPipe detected content in your clipboard that can be opened. Do you want to open it now? Do you want to import your clipboard content?\ninstall=Install ...\nignore=Ignore\npossibleActions=Available actions\nreportError=Report error\nreportOnGithub=Create an issue report on GitHub\nreportOnGithubDescription=Open a new issue in the GitHub repository\nreportErrorDescription=Send an error report with optional user feedback and diagnostics info\nignoreError=Ignore error\nignoreErrorDescription=Ignore this error and continue like nothing happened\nprovideEmail=How can we contact you (optional, only if you want to get a response). Your report is anonymous by default, so you can provide contact info like an email address here.\nadditionalErrorInfo=Provide additional information (optional)\nadditionalErrorAttachments=Select attachments (optional)\ndataHandlingPolicies=Privacy policy\nsendReport=Send report\nerrorHandler=Error handler\nevents=Events\nvalidate=Validate\nstackTrace=Stack trace\npreviousStep=< Previous\nnextStep=Next >\n#context: verb, to complete a task\nfinishStep=Finish\n#context: to complete a selection\nselect=Select\nbrowseInternal=Browse Internal\ncheckOutUpdate=Check out update\nquit=Quit\nnoTerminalSet=No terminal application has been set automatically. You can do so manually in the settings menu.\nconnections=Connections\nconnectionHub=Connection hub\nsettings=Settings\nexplorePlans=License\nhelp=Help\nabout=About\ndeveloper=Developer\nbrowseFileTitle=Browse file\nbrowser=File browser\nselectFileFromComputer=Select a file from this computer\nlinks=Links\nwebsite=Website\ndiscordDescription=Join the Discord server\nredditDescription=Join the XPipe subreddit\nsecurity=Security\nsecurityPolicy=Security information\nsecurityPolicyDescription=Read the detailed security policy\nprivacy=Privacy Policy\nprivacyDescription=Read the privacy policy for the XPipe application\nslackDescription=Join the Slack workspace\nsupport=Support\ngithubDescription=Check out the GitHub repository\nopenSourceNotices=Open Source Notices\ncheckForUpdates=Check for updates\ncheckForUpdatesDescription=Download an update if there is one\nlastChecked=Last checked\nversion=Version\nbuild=Build version\nruntimeVersion=Runtime version\nvirtualMachine=Virtual machine\nupdateReady=Install update\nupdateReadyPortable=Check out update\nupdateReadyDescription=An update was downloaded and is ready to be installed\nupdateReadyDescriptionPortable=An update is available to download\nupdateRestart=Restart to update\nnever=Never\nupdateAvailableTooltip=Update available\nptbAvailableTooltip=Public Test Build available\nvisitGithubRepository=Visit GitHub repository\nupdateAvailable=Update available: $VERSION$\ndownloadUpdate=Download update\nlegalAccept=I accept the End User License Agreement\n#context: verb\nconfirm=Confirm\n#context: verb\nprint=Print\nwhatsNew=What's new in version $VERSION$ ($DATE$)\nantivirusNoticeTitle=A note on Antivirus programs\nupdateChangelogAlertTitle=Changelog\ngreetingsAlertTitle=Welcome to XPipe\neula=End User License Agreement\nnews=News\nintroduction=Introduction\nprivacyPolicy=Privacy Policy\nagree=Agree\ndisagree=Disagree\ndirectories=Directories\nlogFile=Log File\nlogFiles=Log Files\nlogFilesAttachment=Log Files\n#context: Error reporter\nissueReporter=Issue Reporter\nopenCurrentLogFile=Log files\nopenCurrentLogFileDescription=Open the log file of the current session\nopenLogsDirectory=Open logs directory\ninstallationFiles=Installation Files\nopenInstallationDirectory=Installation files\nopenInstallationDirectoryDescription=Open XPipe installation directory\nlaunchDebugMode=Debug mode\nlaunchDebugModeDescription=Restart XPipe in debug mode\nextensionInstallTitle=Download\nextensionInstallDescription=This action requires additional third party libraries that are not distributed by XPipe. You can automatically install them here. The components are then downloaded from the vendor website:\nextensionInstallLicenseNote=By performing the download and automatic installation you agree to the terms of the third party licenses:\nlicense=License\ninstallRequired=Installation Required\nrestore=Restore\nrestoreAllSessions=Restore all sessions\nlimitedTouchscreenMode=Limited touchscreen mode\nlimitedTouchscreenModeDescription=When using this application on a more exotic touchscreen interface like a phone screen, some menus might not work properly. When this option is enabled, the menu implementation uses more limited functionality to work with sparsely sent mouse/touch events.\nappearance=Appearance\ndisplay=Display\npersonalization=Personalization\ndisplayOptions=Display options\n#context: display theme\ntheme=Theme\nrdpConfiguration=Remote desktop configuration\nrdpClient=RDP client\nrdpClientDescription=The RDP client program to call when launching RDP connections.\\n\\nNote that various clients have different degrees of abilities and integrations. Some clients don't support passing passwords automatically, so you still have to fill them in on launch.\nlocalShell=Local shell\n#context: display theme\nthemeDescription=Your preferred display theme.\ndontAutomaticallyStartVmSshServer=Don't automatically start SSH server for VMs when needed\ndontAutomaticallyStartVmSshServerDescription=Any shell connection to a VM running in a hypervisor is made through SSH. XPipe can automatically start the installed SSH server when needed. If you don't want this for security reasons, then you can just disable this behavior with this option.\nconfirmGitShareTitle=Git sync\nconfirmGitShareContent=Do you want to add the selected file to your git vault repository? This will copy an encrypted version of the file into your git vault and commit your changes. You will then have access to the file on all synced desktops.\ngitShareFileTooltip=Add file to the git vault data directory so that it is automatically synced.\\n\\nThis action can only be used when the git vault is enabled in the settings.\nperformanceMode=Performance mode\nperformanceModeDescription=Disables all visual effects that are not required in order to improve the application performance.\ndontAcceptNewHostKeys=Don't accept new SSH host keys automatically\ndontAcceptNewHostKeysDescription=XPipe will automatically accept host keys by default from systems where your SSH client has no known host key already saved. If any known host key has changed however, it will refuse to connect unless you accept the new one.\\n\\nDisabling this behavior allows you to check all host keys, even if there is no conflict initially.\nuiScale=UI Scale\nuiScaleDescription=A custom scaling value that can be set independently of your system-wide display scale. Values are in percent, so e.g. value of 150 will result in a UI scale of 150%.\neditorProgram=Editor Program\neditorProgramDescription=The default text editor to use when editing any kind of text data.\nwindowOpacity=Window opacity\nwindowOpacityDescription=Changes the window opacity to keep track of what is happening in the background.\nuseSystemFont=Use system font\nopenDataDir=Vault data directory\nopenDataDirButton=Open data directory\nopenDataDirDescription=If you want to sync additional files, such as SSH keys, across systems with your git repository, you can put them into the storage data directory. Any files referenced there will have their file paths automatically adapted on any synced system.\nupdates=Updates\nselectAll=Select all\nadvanced=Advanced\nthirdParty=Open source notices\neulaDescription=Read the End User License Agreement for the XPipe application\nthirdPartyDescription=View the open source licenses of third-party libraries\nworkspaceLock=Master passphrase\nenableGitStorage=Enable synchronization\nsharing=Sharing\ngitSync=Git sync\nenableGitStorageDescription=When enabled, XPipe will initialize a git repository for the local vault and commit any changes to it. Note that this requires git to be installed and might slow down loading and saving operations.\\n\\nAny categories that should be synced must be explicitly marked as synced.\nstorageGitRemote=Remote sync URL\nstorageGitRemoteDescription=When set, XPipe will automatically pull any changes when loading and push any changes to the remote repository when saving.\\n\\nThis allows you to share your vault between multiple XPipe installations. It supports HTTP and SSH URLs, plus local directories.\n#context: a safe for secret information\nvault=Vault\nworkspaceLockDescription=Sets a custom password to encrypt any sensitive information stored in XPipe.\\n\\nThis results in increased security as it provides an additional layer of encryption for your stored sensitive information. You will then be prompted to enter the password when XPipe starts.\nuseSystemFontDescription=Controls whether to use your default system font or the Inter font, which is included with XPipe.\ntooltipDelay=Tooltip delay\ntooltipDelayDescription=The amount of milliseconds to wait until a tooltip is displayed.\nfontSize=Font size\nwindowOptions=Window Options\nsaveWindowLocation=Save window location\nsaveWindowLocationDescription=Controls whether the window coordinates should be saved and restored on restarts.\nstartupShutdown=Startup / Shutdown\nshowChildrenConnectionsInParentCategory=Show child categories in parent category\nshowChildrenConnectionsInParentCategoryDescription=Whether or not to include all connections located in sub categories when having a certain parent category is selected.\\n\\nIf this is disabled, the categories behave more like classical folders which only show their direct contents without including sub folders.\ncondenseConnectionDisplay=Condense connection display\ncondenseConnectionDisplayDescription=Make every top level connection take a less vertical space to allow for a more condensed connection list.\nopenConnectionSearchWindowOnConnectionCreation=Open connection search window on connection creation\nopenConnectionSearchWindowOnConnectionCreationDescription=Whether or not to automatically open the window to search for available subconnections upon adding a new shell connection.\nworkflow=Workflow\nsystem=System\napplication=Application\nstorage=Storage\nrunOnStartup=Run on startup\n#context: title\ncloseBehaviour=Exit behaviour\ncloseBehaviourDescription=Controls how XPipe should proceed upon closing its main window.\nlanguage=Language\nlanguageDescription=The display language to use. The translations are improved through community contributions. You can help the translation effort by submitting translation fixes on GitHub.\n#context: computer display theme for light mode\nlightTheme=Light Theme\n#context: display theme\ndarkTheme=Dark Theme\nexit=Quit XPipe\ncontinueInBackground=Continue in background\nminimizeToTray=Minimize to tray\ncloseBehaviourAlertTitle=Set closing behaviour\ncloseBehaviourAlertTitleHeader=Select what should happen when closing the window. Any active connections will be closed when the application is shut down.\nstartupBehaviour=Startup behaviour\nstartupBehaviourDescription=Controls the default behavior of the desktop application when XPipe is started.\nclearCachesAlertTitle=Clean Cache\nclearCachesAlertContent=Do you want to clean all XPipe caches? This will delete all the cache data that is stored to improve the user experience.\nstartGui=Start GUI\nstartInTray=Start in tray\nstartInBackground=Start in background\nclearCaches=Clear caches ...\nclearCachesDescription=Delete all cache data\ncancel=Cancel\nnotAnAbsolutePath=Not an absolute path\nnotADirectory=Not a directory\nnotAnEmptyDirectory=Not an empty directory\nautomaticallyCheckForUpdates=Check for updates\nautomaticallyCheckForUpdatesDescription=When enabled, new release information is automatically fetched while XPipe is running after a while. You still have to explicitly confirm any update installation.\nsendAnonymousErrorReports=Send anonymous error reports\nsendUsageStatistics=Send anonymous usage statistics\nstorageDirectory=Storage directory\nstorageDirectoryDescription=The location where XPipe should store all connection information. When changing this, the data in the old directory is not copied to the new one.\nlogLevel=Log level\nappBehaviour=Application behaviour\nlogLevelDescription=The log level that should be used when writing log files.\ndeveloperMode=Developer mode\ndeveloperModeDescription=When enabled, you will have access to a variety of additional options that are useful for development.\neditor=Editor\ncustom=Custom\npasswordManager=Password manager\nexternalPasswordManager=External password manager\npasswordManagerDescription=The locally installed password manager to integrate with.\\n\\nIf you have a password manager installed, you can configure XPipe to retrieve passwords from it so that XPipe doesn't have to store the passwords itself. When enabled, any password field for a connection can be then configured to use the password manager.\npasswordManagerCommandTest=Test password manager\npasswordManagerCommandTestDescription=You can test here whether the output looks correct if you have set up a password manager.\npreferTerminalTabs=Prefer to open new tabs\npreferTerminalTabsDescription=Controls whether XPipe will try to open new tabs in your chosen terminal instead of new windows. Not every terminal supports tabs.\ncustomRdpClientCommand=Custom command\ncustomRdpClientCommandDescription=The command to execute to start the custom RDP client.\\n\\nThe placeholder string $FILE will be replaced by the quoted absolute .rdp file name when called. Remember to quote your executable path if it contains spaces.\ncustomEditorCommand=Custom editor command\ncustomEditorCommandDescription=The command to execute to start the custom editor.\\n\\nThe placeholder string $FILE will be replaced by the quoted absolute file name when called. Remember to quote your editor executable path if it contains spaces.\neditorReloadTimeout=Editor reload timeout\neditorReloadTimeoutDescription=The amount of milliseconds to wait before reading a file after it has been updated. This avoids issues in cases where your editor is slow at writing or releasing file locks.\nencryptAllVaultData=Encrypt all vault data\nencryptAllVaultDataDescription=When enabled, every part of the vault connection data will be encrypted with your user vault encryption key as opposed to only secrets within in that data. This adds another layer of security for other parameters like usernames, hostnames, etc., that are not encrypted by default in the vault.\\n\\nThis option will render your git vault history and diffs useless as you can't see the original changes anymore, only binary changes.\nvaultSecurity=Vault security\ndeveloperDisableUpdateVersionCheck=Disable Update Version Check\ndeveloperDisableUpdateVersionCheckDescription=Controls whether the update checker will ignore the version number when looking for an update.\ndeveloperDisableGuiRestrictions=Disable GUI restrictions\ndeveloperDisableGuiRestrictionsDescription=Controls whether some disabled actions can still be executed from the user interface.\ndeveloperShowHiddenEntries=Show hidden entries\ndeveloperShowHiddenEntriesDescription=When enabled, hidden and internal data sources will be shown.\ndeveloperShowHiddenProviders=Show hidden providers\ndeveloperShowHiddenProvidersDescription=Controls whether hidden and internal connection and data source providers will be shown in the creation dialog.\ndeveloperDisableConnectorInstallationVersionCheck=Disable Connector Version Check\ndeveloperDisableConnectorInstallationVersionCheckDescription=Controls whether the update checker will ignore the version number when inspecting the version of an XPipe connector installed on a remote machine.\nshellCommandTest=Shell Command Test\nshellCommandTestDescription=Run a command in the shell session used internally by XPipe.\nterminal=Terminal\nterminalType=Terminal emulator\nterminalConfiguration=Terminal configuration\nterminalCustomization=Terminal customization\neditorConfiguration=Editor configuration\ndefaultApplication=Default application\ninitialSetup=Initial setup\nterminalTypeDescription=The default terminal to use for opening shell connections.\\n\\nThe level of feature support varies by terminal, and each one is marked as either recommended or not recommended. Your user experience will be best when using a recommended terminal.\nprogram=Program\ncustomTerminalCommand=Custom terminal command\ncustomTerminalCommandDescription=The command to execute to open the custom terminal with a given command.\\n\\nXPipe will create a temporary launcher shell script for your terminal to execute. The placeholder string $CMD in the command you supply will be replaced by the actual launcher script when called. Remember to quote your terminal executable path if it contains spaces.\nclearTerminalOnInit=Clear terminal on init\nclearTerminalOnInitDescription=When enabled, XPipe will run a clear command after a new terminal session is launched to remove any unnecessary output that was printed while starting the terminal session.\ndontCachePasswords=Don't cache prompted passwords\ndontCachePasswordsDescription=Controls whether queried passwords should be cached internally by XPipe so you don't have to enter them again in the current session.\\n\\nIf this behavior is disabled, you have to reenter any prompted credentials every time they are required by the system.\ndenyTempScriptCreation=Deny temporary script creation\ndenyTempScriptCreationDescription=To realize some of its functionality, XPipe sometimes creates temporary shell scripts on a target system to allow for an easy execution of simple commands. These do not contain any sensitive information and are just created for implementation purposes.\\n\\nIf this behavior is disabled, XPipe will not create any temporary files on a remote system. This option is useful in high-security contexts where every file system change is monitored. If this is disabled, some functionality, e.g. shell environments and scripts, will not work as intended.\ndisableCertutilUse=Disable certutil use on Windows\nuseLocalFallbackShell=Use local fallback shell\nuseLocalFallbackShellDescription=Switch to using another local shell to handle local operations. This would be PowerShell on Windows and bourne shell on other systems.\\n\\nThis option can be used in case the normal local default shell is disabled or broken to some degree. Some features might not work as expected though when this is option is enabled.\ndisableCertutilUseDescription=Due to several shortcomings and bugs in cmd.exe, temporary shell scripts are created with certutil by using it to decode base64 input as cmd.exe breaks on non-ASCII input. XPipe can also use PowerShell for that but this will be slower.\\n\\nThis disables any use of certutil on Windows systems to realize some functionality and fall back to PowerShell instead. This might please some AVs as some of them block certutil usage.\ndisableTerminalRemotePasswordPreparation=Disable terminal remote password preparation\ndisableTerminalRemotePasswordPreparationDescription=In situations where a remote shell connection that goes through multiple intermediate systems should be established in the terminal, there might be a requirement to prepare any required passwords on one of the intermediate systems to allow for an automatic filling of any prompts.\\n\\nIf you don't want the passwords to ever be transferred to any intermediate system, you can disable this behavior. Any required intermediate password will then be queried in the terminal itself when opened.\nmore=More\ntranslate=Translations\nallConnections=All connections\nallScripts=All scripts\nallIdentities=All identities\nsynced=Synced\npredefined=Predefined\nsamples=Samples\ngoodMorning=Good morning\ngoodAfternoon=Good afternoon\ngoodEvening=Good evening\naddVisual=Visual ...\naddDesktop=Desktop ...\nssh=SSH\nsshConfiguration=SSH Configuration\nsize=Size\nattributes=Attributes\n#context: title, last modified date\nmodified=Modified\nowner=Owner\nupdateReadyTitle=Update to $VERSION$ ready\n#context: digital template\ntemplates=Templates\nretry=Retry\nretryAll=Retry all\nreplace=Replace\nreplaceAll=Replace all\nhibernateBehaviour=Hibernation behaviour\nhibernateBehaviourDescription=Controls how the application behaves when your system is put into hibernation/to sleep.\noverview=Overview\nhistory=History\nskipAll=Skip all\nnotes=Notes\naddNotes=Add notes\n#context: verb, to change the sorting\norder=Reorder\nkeepFirst=Keep first\nkeepLast=Keep last\npinToTop=Pin to top\nunpinFromTop=Unpin from top\norderAheadOf=Order ahead of ...\nclearIndex=Reset index\nhttpServer=HTTP server\nmcpServer=MCP server\napiKey=API key\napiKeyDescription=The API key to authenticate XPipe daemon API requests. For more information on how to authenticate, see the general API documentation.\ndisableApiAuthentication=Disable API authentication\ndisableApiAuthenticationDescription=Disables all required authentication methods so that any unauthenticated request will be handled.\\n\\nAuthentication should only be disabled for development purposes.\napi=API\nstoreIntroImportContent=Already using XPipe on another system? Synchronize your existing connections across multiple systems through a remote git repository. You can also sync later at any time if it is not set up yet.\nstoreIntroImportButton=Sync connections ...\nstoreIntroImportHeader=Import Connections\nshowNonRunningChildren=Show non-running children\nhttpApi=HTTP API\nisOnlySupportedLimit=is only supported with a professional license when having more than $COUNT$ connections\nareOnlySupportedLimit=are only supported with a professional license when having more than $COUNT$ connections\nenabled=Enabled\nenableGitStoragePtbDisabled=Git synchronization is disabled for public test builds to prevent usage with regular release git repositories and to discourage using a PTB build as your daily driver.\ncopyId=Copy API ID\nrequireDoubleClickForConnections=Require double click for connections\nrequireDoubleClickForConnectionsDescription=If enabled, you have to double-click connections to launch them. This is useful if you're used to double-clicking things.\nclearTransferDescription=Clear selection\nselectTab=Select tab\ncloseTab=Close tab\ncloseOtherTabs=Close other tabs\ncloseAllTabs=Close all tabs\ncloseLeftTabs=Close tabs to the left\ncloseRightTabs=Close tabs to the right\n#context: Serial computer connection\naddSerial=Serial ...\nconnect=Connect\nworkspaces=Workspaces\nmanageWorkspaces=Manage workspaces\naddWorkspace=Add workspace ...\nworkspaceAdd=Add a new workspace\nworkspaceAddDescription=Workspaces are distinct configurations for running XPipe. Every workspace has a data directory where all data is stored locally. This includes connection data, settings, and more.\\n\\nIf you use the synchronization feature, you can also choose to synchronize each workspace with a different git repository.\nworkspaceName=Workspace name\nworkspaceNameDescription=The display name of the workspace\nworkspacePath=Workspace path\nworkspacePathDescription=The location of the workspace data directory\nworkspaceCreationAlertTitle=Workspace creation\ndeveloperForceSshTty=Force SSH TTY\ndeveloperForceSshTtyDescription=Make all SSH connections allocate a pty to test the support for a missing stderr and a pty.\ndeveloperDisableSshTunnelGateways=Disable SSH gateway tunneling\ndeveloperDisableSshTunnelGatewaysDescription=Don't use tunnel sessions for gateways and instead connect directly to system.\nttyWarning=The connection has forcefully allocated a pty/tty and does not provide a separate stderr stream.\\n\\nThis might lead to a few problems.\\n\\nIf you can, look into making the connection command not allocate a pty.\nxshellSetup=Xshell setup\ntermiusSetup=Termius setup\ntryPtbDescription=Try out new features early in XPipe developer builds\nconfirmVaultUnencryptTitle=Confirm vault unencryption\nconfirmVaultUnencryptContent=Do you really want to disable advanced vault encryption? This will remove the additional encryption for stored data and will overwrite existing data.\nenableHttpApi=Enable HTTP API\nenableHttpApiDescription=Enables the API, allowing external programs to call the XPipe daemon to perform actions with your managed connections.\nchooseCustomIcon=Choose custom icon\n#context: verb, to delete\ngitVault=Git vault\nfileBrowser=File browser\nconfirmAllDeletions=Confirm all deletions\nconfirmAllDeletionsDescription=Whether to show a confirmation dialog for all delete operations. By default, only directories require a confirmation.\nyesterday=Yesterday\ngreen=Green\nyellow=Yellow\nblue=Blue\nred=Red\ncyan=Cyan\npurple=Purple\nasktextAlertTitle=Prompt\nfileWriteSudoTitle=Sudo file write\nfileWriteSudoContent=The file you are trying to write does not grant write permissions to your user. Do you want to write this file as root with sudo? This will automatically elevate to root with either the existing credentials or via a prompt.\ndontAllowTerminalRestart=Don't allow terminal restart\ndontAllowTerminalRestartDescription=By default, terminal sessions can be restarted after they ended from within the terminal. To allow this, XPipe will accept these external requests from the terminal to launch the session again\\n\\nXPipe doesn't have any control over the terminal and where this call comes from, so malicious local applications can use this functionality as well to launch connections through XPipe. Disabling this functionality prevents this scenario.\n#context: verb\nopenDocumentation=Open documentation\nopenDocumentationDescription=Visit the XPipe docs page for this issue\nrenameAll=Rename all\nlogging=Logging\nenableTerminalLogging=Enable terminal logging\nenableTerminalLoggingDescription=Enables client-side logging for all terminal sessions. All inputs and outputs of the terminal session are written into a session log file. Note that any sensitive information like password prompts are not recorded.\nterminalLoggingDirectory=Terminal session logs\nterminalLoggingDirectoryDescription=All logs are stored in the XPipe data directory on your local system.\nopenSessionLogs=Open session logs\nsessionLogging=Terminal logging\nsessionActive=A background session is running for this connection.\\n\\nTo stop this session manually, click on the status indicator.\nskipValidation=Skip validation\nscriptsIntroHeader=About scripts\nscriptsIntroContent=You can run scripts on shell init, in the file browser, and on demand. You can create scripts yourself within XPipe or import existing ones from your local system or from a remote git repository.\nscriptsIntroBottomHeader=Using scripts\nscriptsIntroBottomContent=There are a variety of sample scripts to start out. You can click on the edit button of the individual scripts to see how they are implemented. Scripts first have to be enabled to run and show up in menus, there is a toggle on every script for that.\nscriptsIntroBottomButton=Get started\nscriptSourcesIntroHeader=Script sources\nscriptSourcesIntroContent=You can add custom script sources to have instant access to an entire collection of shell scripts. Both local sources and remote git repositories are supported as sources. All detected scripts from the source will become available automatically.\nscriptSourcesIntroButton=Add source ...\ncheckForSecurityUpdates=Check for security updates\ncheckForSecurityUpdatesDescription=XPipe can check for potential security updates separately from normal feature updates. When this is enabled, at least important security updates will be recommended for installation even if the normal update check is disabled.\\n\\nDisabling this setting will result in no external version request being performed, and you won't be notified about any security updates.\nclickToDock=Click to dock terminal\nterminalStarting=Waiting for terminal startup ...\n#context: tab management in software application\npinTab=Pin tab\n#context: tab management in software application\nunpinTab=Unpin tab\n#context: tab management in software application\npinned=Pinned\nenableConnectionHubTerminalDocking=Enable connection hub terminal docking\nenableConnectionHubTerminalDockingDescription=You can dock terminal windows to the XPipe application window in the connection hub to simulate a somewhat integrated terminal. The terminal windows are then managed by XPipe to always fit into the dock.\nenableFileBrowserTerminalDocking=Enable file browser terminal docking\nenableFileBrowserTerminalDockingDescription=You can dock terminal windows to the XPipe application window in the file browser to simulate a somewhat integrated terminal. The terminal windows are then managed by XPipe to always fit into the dock.\ndownloadsDirectory=Custom downloads directory\ndownloadsDirectoryDescription=The custom directory to put downloaded files into when clicking on the move to downloads button. By default, XPipe will use your user downloads directory.\npinLocalMachineOnStartup=Pin local machine tab on startup\npinLocalMachineOnStartupDescription=Automatically open a local machine tab and pin it. This is useful if you are frequently using a split file browser with the local machine and remote file system open.\nterminalErrorDescription=This error is terminal and XPipe can't continue without fixing it.\ngroupName=Group name\nchmodPermissions=New permissions\neditFilesWithDoubleClick=Edit files with double click\neditFilesWithDoubleClickDescription=When enabled, double-clicking files will straight up open them in your text editor instead of showing the context menu.\ncensorMode=Censor mode\ncensorModeDescription=Blurs out any information like hostnames, usernames, connection names, and more.\\n\\nThis is useful if you intend to screenshot or screenshare XPipe and don't want to leak any information.\naddIdentity=Identity ...\nidentities=Identities\naddMacro=Action ...\nidentitiesIntroHeader=About identities\nidentitiesIntroContent=If you are reusing common combinations of usernames, passwords, and keys, it might make sense to create reusable identities. This allows you to quickly reference them when adding new connections.\nidentitiesIntroBottomHeader=Sharing identities\nidentitiesIntroBottomContent=You can add identities locally or also sync them up in the git repository when this is enabled. This allows to selectively share identities across multiple systems and with other team members.\nidentitiesIntroBottomButton=Setup sync\nidentitiesIntroButton=Create identity\nuserName=Username\nuserAuth=User-based password authentication\ngroupAuth=Group-based secret authentication\nteam=Team\nteamSettings=Team settings\nteamVaults=Team vaults\nvaultTypeNameDefault=Default vault\nvaultTypeNameLegacy=Legacy personal vault\nvaultTypeNamePersonal=Personal vault\nvaultTypeNameTeam=Team vault\nteamVaultsDescription=Team vaults allow multiple users and groups to have secure access to a shared vault. You can configure connections and identities to either be shared for all users or only have them available for individual users and groups by encrypting them with their own key. Other vault users can't access personal and group-based connections and identities if they don't have access to the key.\nvaultTypeContentDefault=You are currently using a default vault with no user and custom passphrase set. Secrets are encrypted with the local vault key. You can upgrade to a personal vault by creating a vault user account. This allows you to encrypt vault secrets with your own personal passphrase that you have to input on each login to unlock the vault.\nvaultTypeContentLegacy=You are currently using a legacy personal vault for your user. Secrets are encrypted with your personal passphrase. This legacy compatibility has limited features and can't be upgraded to a team vault in-place.\nvaultTypeContentPersonal=You are currently using a personal vault for your user. Secrets are encrypted with your personal passphrase. You can upgrade to a team vault by adding additional vault users or add a group-based access configuration.\nvaultTypeContentTeam=You are currently using a team vault, which allows multiple users to have secure access to a shared vault. You can configure connections and identities to either be shared for all users or only have them available for your personal user or group by encrypting them with your personal or group key. Other vault users can't access your personal and group-based connections and identities if they don't have access to the key.\ngroupManagement=Group management\ngroupManagementEmpty=Group management\ngroupManagementDescription=Manage existing vault groups or create new ones. Each vault group has its own individual secret key which is used to encrypt connections and identities that should only be available to the group and not to others.\ngroupManagementEmptyDescription=Manage existing vault groups or create new ones. Each vault group has its own individual secret key which is used to encrypt connections and identities that should only be available to the group and not to others.\\n\\nGroup-based accounts for a team are supported in the professional plan.\nuserManagement=User management\nuserManagementEmpty=User management\nuserManagementDescription=Manage existing vault users or create new ones. Each vault user has its own individual password which is used to encrypt connections and identities that should only be available to the user and not to others.\nuserManagementEmptyDescription=Manage existing vault users or create new ones. Each vault user has its own individual password which is used to encrypt connections and identities that should only be available to the user and not to others. Create a user for yourself to be able to encrypt connections and identities with your personal key.\\n\\nA single user account is supported in the community edition. Multiple user accounts for a team are supported in the professional plan.\nuserIntroHeader=User management\nuserIntroContent=Create the first user account for yourself to get started. This allows you to lock this workspace with a password.\naddReusableIdentity=Add reusable identity\nusers=Users\nsyncVault=Vault synchronization\nsyncVaultDescription=To synchronize your vault with across multiple systems or with multiple team members, enable the git synchronization for this vault.\nenableGitSync=Enable git sync\nbrowseVault=Vault data\nbrowseVaultDescription=You can take a look at the vault directory yourself in your native file manager. Note that external edits are not recommended and can cause a variety of issues.\nbrowseVaultButton=Browse vault\nvaultUsers=Vault users\ncreateHeapDump=Create heap dump\ncreateHeapDumpDescription=Dump memory contents to file to troubleshoot memory usage\ninitializingApp=Loading connections\ncheckingLicense=Checking license\nloadingGit=Syncing with git repo\nloadingGpg=Starting GnuPG daemon for git\nloadingSettings=Loading settings\nloadingConnections=Loading connections\nunlockingVault=Unlocking vault\nloadingUserInterface=Loading user interface\nptbNotice=Notice for the public test build\nuserDeletionTitle=User deletion\nuserDeletionContent=Do you want to delete this vault user? This will reencrypt all your personal identities and connection secrets using the vault key that is available to all users. This will take a while and XPipe will restart to apply the user changes.\ngroupDeletionTitle=Group deletion\ngroupDeletionContent=Do you want to delete this vault group? This will reencrypt all group-only identities and connection secrets using the vault key that is available to all users. This will take a while and XPipe will restart to apply the group changes.\nkillTransfer=Kill transfer\ndestination=Destination\nconfiguration=Configuration\nnewFile=New file\nnewLink=New link\nlinkName=Link name\nscanConnections=Find available connections ...\nobserve=Start observing\nstopObserve=Stop observing\ncreateShortcut=Create desktop shortcut\nbrowseFiles=Browse Files\nclone=Clone\ntargetPath=Target path\nnewDirectory=New directory\ncopyShareLink=Copy link\nselectStore=Select Store\nsaveSource=Save for later\nexecute=Execute\ndeleteChildren=Remove all children\nscriptGroupDescriptionDescription=Give this group an optional description\nabstractHostDescriptionDescription=Give this host an optional description\nselectSource=Select Source\ncommandLineRead=Update\ncommandLineWrite=Write\nadditionalOptions=Additional Options\ninput=Input\nmachine=Machine\nopen=Open\nedit=Edit\nscriptContents=Script contents\nscriptContentsDescription=The script commands to execute\nsnippets=Script dependencies\nsnippetsDescription=Other scripts to run first\nsnippetsDependenciesDescription=All possible scripts that should be run if applicable\n#context: computer shell program\nisDefault=Run on init in all compatible shells\n#context: computer shell program\nbringToShells=Bring to all compatible shells\nisDefaultGroup=Run all group scripts on shell init\nexecutionType=Execution type\nexecutionTypeDescription=In what contexts to use this script\n#context: computer shell program\nminimumShellDialect=Shell type\n#context: computer shell program\nminimumShellDialectDescription=The shell type to run this script in\ndumbOnly=Dumb\nterminalOnly=Terminal\nboth=Both\nshouldElevate=Should elevate\nshouldElevateDescription=Whether to run this script with elevated permissions\nscript.displayName=Shell script\nscript.displayDescription=Create a reusable shell script\nscriptGroup.displayName=Script group\nscriptGroup.displayDescription=Group scripts together and organize them within\nscriptGroup=Group\nscriptGroupDescription=The group to assign this script to\nscriptGroupGroupDescription=The optional parent group to assign this script group to\nopenInNewTab=Open in new tab\nexecuteInBackground=in background\nexecuteInTerminal=in $TERM$\nback=Go back\nbrowseInWindowsExplorer=Browse in Windows explorer\nbrowseInDefaultFileManager=Browse in default file manager\nbrowseInFinder=Browse in finder\ncopy=Copy\npaste=Paste\ncopyLocation=Copy location\nabsolutePaths=Absolute paths\nabsoluteLinkPaths=Absolute link paths\nabsolutePathsQuoted=Absolute quoted paths\nfileNames=File names\nlinkFileNames=Link file names\nfileNamesQuoted=File names (Quoted)\ndeleteFile=Delete $FILE$\neditWithEditor=Edit with $EDITOR$\nfollowLink=Follow link\ngoForward=Go forward\nshowDetails=Show details\nshowDetailsDescription=Show stack trace of error\nopenFileWith=Open with ...\nopenWithDefaultApplication=Open with default application\nrename=Rename\nrun=Run\nopenInTerminal=Open in terminal\nfile=File\ndirectory=Directory\nsymbolicLink=Symbolic link\ndesktopEnvironment.displayName=Desktop environment\ndesktopEnvironment.displayDescription=Create a reusable remote desktop environment configuration\ndesktopHost=Desktop host\ndesktopHostDescription=The desktop connection to use as a base\ndesktopShellDialect=Shell dialect\ndesktopShellDialectDescription=The shell dialect to use to run scripts and applications\ndesktopSnippets=Script snippets\ndesktopSnippetsDescription=List of reusable script snippets to run first\ndesktopInitScript=Init script\ndesktopInitScriptDescription=Init commands specific to this environment\ndesktopTerminal=Terminal application\ndesktopTerminalDescription=The terminal to use on the desktop to start scripts in\ndesktopApplication.displayName=Desktop application\ndesktopApplication.displayDescription=Run an application on a remote desktop\ndesktopBase=Desktop\ndesktopBaseDescription=The desktop to run this application on\ndesktopEnvironmentBase=Desktop environment\ndesktopEnvironmentBaseDescription=The desktop environment to run this application on\ndesktopApplicationPath=Application path\ndesktopApplicationPathDescription=The path of the executable to run\ndesktopApplicationArguments=Arguments\ndesktopApplicationArgumentsDescription=The optional arguments to pass to the application\ndesktopCommand.displayName=Desktop command\ndesktopCommand.displayDescription=Run a command in a remote desktop environment\ndesktopCommandScript=Commands\ndesktopCommandScriptDescription=The commands to run in the environment\nservice.displayName=Service\nservice.displayDescription=Forward a remote service to your local machine\nserviceLocalPort=Explicit local port\nserviceLocalPortDescription=The local port to forward to, otherwise a random one is used\nserviceRemotePort=Remote port\nserviceRemotePortDescription=The port on which the service is running on\nserviceHost=Service host\nserviceHostDescription=The host entry or manual address of the server on which the service is running on\nopenWebsite=Open website\ncustomServiceGroup.displayName=Service group\ncustomServiceGroup.displayDescription=Group multiple services into one category\ninitScript=Init script - Run on shell init\nshellScript=Shell session script - Make script available to run during a shell session\nrunnableScript=Runnable script - Allow script to be run directly from the connection hub\nfileScript=File script - Allow script to be called for selected files in the file browser\nrunScript=Run script\ncopyUrl=Copy URL\nfixedServiceGroup.displayName=Service group\nfixedServiceGroup.displayDescription=List the available services on a system\nmappedService.displayName=Service\nmappedService.displayDescription=Interact with a service exposed by a container\ncustomService.displayName=Service\ncustomService.displayDescription=Automatically open or tunnel a remote service port on your local machine\nfixedService.displayName=Service\nfixedService.displayDescription=Use a predefined service\nnoServices=No available services\nhasServices=$COUNT$ available services\nhasService=$COUNT$ available service\nnoConnections=No available connections\nhasConnections=$COUNT$ available connections\nhasConnection=$COUNT$ available connection\nopenHttp=Open HTTP service\nopenHttps=Open HTTPS service\nnoScriptsAvailable=No enabled and compatible scripts available\nscriptsDisabled=Scripts disabled\nchangeIcon=Change icon\ninit=Init\n#context: computer shell program\nshell=Shell\nhub=Hub\n#context: Computer script\nscript=script\ngenericScript=Generic\ngradleTasks=Gradle tasks\nrunTask=Run task\narchiveName=Archive name\ncompress=Compress\ncompressContents=Compress contents\nuntarHere=Untar here\nuntarDirectory=Untar to $DIR$\nunzipDirectory=Unzip to $DIR$\nunzipHere=Unzip here\nrequiresRestart=Requires a restart to apply.\ndownload=Download\nservicePath=Service path\nservicePathDescription=The optional subpath when opening the URL in a browser\n#context: Currently selected\nactive=Active\n#context: Not selected\ninactive=Inactive\nstarting=Starting\nremotePort=Remote port\nremotePortNumber=Remote port $PORT$\nuserIdentity=Personal identity\nglobalIdentity=Global identity\nidentityChoice=User identity\nidentityChoiceDescription=Choose a predefined identity or specify login details just for this connection\ndefineNewIdentityOrSelect=Enter new or choose existing\nlocalIdentity.displayName=Local identity\nlocalIdentity.displayDescription=Create a reusable identity for this local desktop\nsyncedIdentity.displayName=Synced identity\nsyncedIdentity.displayDescription=Create a reusable identity that is synced across systems\nlocalIdentity=Local identity\nkeyNotSynced=Key file is not synced to git repository yet. Use the add to git button for the key file to add it.\nusernameDescription=The username to log in as\nidentity.displayName=Identity\nidentity.displayDescription=Create a reusable identity for connections\nlocal=Local\nshared=Global\nuserDescription=The username or predefined identity to log in as\nidentityAccessLevel=Access level\nidentityPerUser=Personal identity access\nidentityPerUserDescription=Restrict access to this identity and its associated connections to your vault user only\nidentityPerUserDisabled=Personal identity access (disabled)\nidentityPerUserDisabledDescription=Restrict access to this identity and its associated connections to your vault user only (Requires team to be configured)\nidentityPerGroup=Group-only identity access\nidentityPerGroupDescription=Restrict access to this identity and its associated connections to this vault group only\nlibrary=Library\nlocation=Location\nkeyAuthentication=Key-based authentication\nkeyAuthenticationDescription=The authentication method to use if key-based authentication is required\nlocationDescription=The file path of your corresponding private key\nkeyFile=Local key file\nkeyPassword=Passphrase\nkey=Key\nyubikeyPiv=Yubikey PIV\npageant=Pageant\ngpgAgent=GPG Agent\ncustomPkcs11Library=Custom PKCS#11 library\nsshAgent=OpenSSH agent\n#context: nothing selected\nnone=None\nindex=Index ...\notherExternal=Other external agent\nsync=Sync\nvaultSync=Vault sync\ncustomUsername=Username\ncustomUsernameDescription=The optional alternate user to log in as\ncustomUsernamePassword=Password\ncustomUsernamePasswordDescription=The user's password to use when sudo authentication is required\n#context: kubernetes\nshowInternalPods=Show internal pods\nshowAllNamespaces=Show all namespaces\nshowInternalContainers=Show internal containers\nrefresh=Refresh\nvmwareGui=Start GUI\nmonitorVm=Monitor VM\n#context: kubernetes\naddCluster=Add cluster ...\nshowNonRunningInstances=Show non-running instances\nvmwareGuiDescription=Whether to start a virtual machine in the background or in a window.\nvmwareEncryptionPassword=Encryption password\nvmwareEncryptionPasswordDescription=The optional password used to encrypt the VM.\nvmPasswordDescription=The required password for the guest user.\nvmPassword=User password\nvmUser=Guest user\nrunTempContainer=Run temporary container\nvmUserDescription=The username of your primary guest user\ndockerTempRunAlertTitle=Run temporary container\ndockerTempRunAlertHeader=This will run a shell process in a temporary container that will get automatically removed once it is stopped.\nimageName=Image name\nimageNameDescription=The container image identifier to use\ncontainerName=Container name\ncontainerNameDescription=The optional custom container name\nvm=Virtual machine\nvmDescription=The associated configuration file.\nvmwareScan=VMware desktop hypervisors\nvmwareMachine.displayName=VMware Virtual Machine\nvmwareMachine.displayDescription=Connect to a virtual machine via SSH\nvmwareInstallation.displayName=VMware desktop hypervisor installation\nvmwareInstallation.displayDescription=Interact with the installed VMs via its CLI\nstart=Start\nstop=Stop\npause=Pause\nrdpTunnelHost=Target host\nrdpTunnelHostDescription=The SSH connection to tunnel the RDP connection to\nrdpTunnelUsername=Username\nrdpTunnelUsernameDescription=The custom user to log in as, uses the SSH user if left empty\nrdpFileLocation=File location\nrdpFileLocationDescription=The file path of the .rdp file\nrdpPasswordAuthentication=Password authentication\nrdpFiles=RDP files\nrdpPasswordAuthenticationDescription=The password to fill in or copy to clipboard, depending on the client support\nrdpFile.displayName=RDP file\nrdpFile.displayDescription=Connect to a system via an existing .rdp file\nrequiredSshServerAlertTitle=Setup SSH server\nrequiredSshServerAlertHeader=Unable to find an installed SSH server in the VM.\nrequiredSshServerAlertContent=To connect to the VM, XPipe is looking for a running SSH server but no available SSH server was detected for the VM.\ncomputerName=Computer Name\npssComputerNameDescription=The computer name to connect to\ncredentialUser=Credential User\ncredentialUserDescription=The user to log in as.\ncredentialPassword=Credential Password\ncredentialPasswordDescription=The password of the user.\nsshConfig=SSH config files\nautostart=Automatically connect on XPipe startup\nacceptHostKey=Accept host key\nmodifyHostKeyPermissions=Modify host key permissions\nattachContainer=Attach\ncontainerLogs=Show logs\nopenSftpClient=Open in external SFTP client\nopenTermius=Open in Termius\nshowInternalInstances=Show internal instances\n#context: kubernetes\neditPod=Edit pod\nacceptHostKeyDescription=Trust the new host key and continue\nmodifyHostKeyPermissionsDescription=Attempt to remove permissions of the original file so that OpenSSH is happy\npsSession.displayName=PowerShell Remote Session\npsSession.displayDescription=Connect via New-PSSession and Enter-PSSession\nsshLocalTunnel.displayName=Local SSH tunnel\nsshLocalTunnel.displayDescription=Establish an SSH tunnel to a remote host\nsshRemoteTunnel.displayName=Remote SSH tunnel\nsshRemoteTunnel.displayDescription=Establish a reverse SSH tunnel from a remote host\nsshDynamicTunnel.displayName=Dynamic SSH tunnel\nsshDynamicTunnel.displayDescription=Establish a SOCKS proxy through an SSH connection\nshellEnvironmentGroup.displayName=Shell environments\nshellEnvironmentGroup.displayDescription=Shell environments\nshellEnvironment.displayName=Shell environment\nshellEnvironment.displayDescription=Create a customized shell startup environment\nshellEnvironment.informationFormat=$TYPE$ environment\nshellEnvironment.elevatedInformationFormat=$ELEVATION$ $TYPE$ environment\nenvironmentConnectionDescription=The base connection to create an environment for\nenvironmentScriptDescription=The optional custom init script to run in the shell\nenvironmentSnippets=Shell scripts\ncommandSnippetsDescription=The optional predefined shell scripts to run first\nenvironmentSnippetsDescription=The optional predefined shell scripts to run on initialization\nshellTypeDescription=The explicit shell type to launch\noriginPort=Origin port\noriginAddress=Origin address\nremoteAddress=Remote address\nremoteSourceAddress=Remote source address\nremoteSourcePort=Remote source port\noriginDestinationPort=Origin destination port\noriginDestinationAddress=Origin destination address\norigin=Origin\nremoteHost=Remote host\naddress=Address\nproxmox.displayName=Proxmox\nproxmox.displayDescription=Connect to systems in a Proxmox Virtual Environment\nproxmoxVm.displayName=Proxmox VM\nproxmoxVm.displayDescription=Connect to a virtual machine in a Proxmox VE via SSH\nproxmoxContainer.displayName=Proxmox Container\nproxmoxContainer.displayDescription=Connect to a container in a Proxmox VE\nsshDynamicTunnel.hostDescription=The system to use as SOCKS proxy\nsshDynamicTunnel.bindingDescription=What addresses to bind the tunnel to\nsshRemoteTunnel.hostDescription=The system from which to start the remote tunnel to the origin\nsshRemoteTunnel.bindingDescription=What addresses to bind the tunnel to\nsshLocalTunnel.hostDescription=The system to open the tunnel to\nsshLocalTunnel.bindingDescription=What addresses to bind the tunnel to\nsshLocalTunnel.localAddressDescription=The local address to bind\nsshLocalTunnel.remoteAddressDescription=The remote address to bind\ncmd.displayName=Command\ncmd.displayDescription=Execute an arbitrary command on a system\nk8sPod.displayName=Kubernetes Pod\nk8sPod.displayDescription=Connect to a pod and its containers via kubectl\nk8sContainer.displayName=Kubernetes Container\nk8sContainer.displayDescription=Open a shell to a container\nk8sCluster.displayName=Kubernetes Cluster\nk8sCluster.displayDescription=Connect to a cluster and its pods via kubectl\nsshTunnelGroup.displayName=SSH Tunnels\nsshTunnelGroup.displayCategory=All types of SSH tunnels\nlocal.displayName=Local machine\nlocal.displayDescription=The shell of the local machine\ncygwin=Cygwin\nmsys2=MSYS2\ngitWindows=Git For Windows\ngitForWindows.displayName=Git For Windows\ngitForWindows.displayDescription=Access your local Git For Windows environment\nmsys2.displayName=MSYS2\nmsys2.displayDescription=Access shells of your MSYS2 environment\ncygwin.displayName=Cygwin\ncygwin.displayDescription=Access shells of your Cygwin environment\nnamespace=Namespace\ngitVaultIdentityStrategy=Git SSH identity\ngitVaultIdentityStrategyDescription=If you chose to use an SSH git URL as the remote and your remote repository requires an SSH identity, then set this option.\\n\\nIn case you provided an HTTP url, you can ignore this option.\ndockerContainers=Docker containers\ndockerCmd.displayName=docker CLI client\ndockerCmd.displayDescription=Access Docker containers via the docker CLI client\nwslCmd.displayName=WSL install\nwslCmd.displayDescription=Access WSL instances via the wsl CLI client\nk8sCmd.displayName=kubectl client\nk8sCmd.displayDescription=Access Kubernetes clusters via kubectl\nk8sClusters=Kubernetes clusters\nshells=Available shells\ninspectContainer=Inspect\ninspectContext=Inspect\nk8sClusterNameDescription=The name of the context the cluster is in.\n#context: kubernetes\npod=Pod\npodName=Pod name\nk8sClusterContext=Context\nk8sClusterContextDescription=The name of the context the cluster is in\nk8sClusterNamespace=Namespace\nk8sClusterNamespaceDescription=The custom namespace or the default one if empty\nk8sConfigLocation=Config file\nk8sConfigLocationDescription=The custom kubeconfig file or the default one if left empty\n#context: kubernetes\ninspectPod=Inspect\nshowAllContainers=Show non-running containers\nshowAllPods=Show non-running pods\nk8sPodHostDescription=The host on which the pod is located\nk8sContainerDescription=The name of the Kubernetes container\nk8sPodDescription=The name of the Kubernetes pod\npodDescription=The pod on which the container is located\nk8sClusterHostDescription=The host through which the cluster should be accessed. Must have kubectl installed and configured to be able to access the cluster.\nconnection=Connection\nshellCommand.displayName=Custom shell command\nshellCommand.displayDescription=Open a standard shell through a custom command\nssh.displayName=SSH connection\nssh.displayDescription=Connect to a remote system via the SSH command-line client\nsshConfig.displayName=SSH config file\nsshConfig.displayDescription=Connect to hosts defined in an SSH config file\nsshConfigHost.displayName=SSH config file host\nsshConfigHost.displayDescription=Connect to a host defined in an SSH config file\nsshConfigHost.password=Password\nsshConfigHost.passwordDescription=Provide the optional password for the user login.\nsshConfigHost.identityPassphrase=Key passphrase\nsshConfigHost.identityPassphraseDescription=Provide the optional passphrase for your key.\nshellCommand.hostDescription=The host to execute the command on\nshellCommand.commandDescription=The command that will open a shell\ncommandType=Command type\ncommandTypeDescription=How to execute the command\ncommandDescription=The custom commands to execute on the host\ncommandHostDescription=The host to run the command on\ncommandDataFlowDescription=How this command handles input and output\ncommandElevationDescription=Run this command with elevated permissions\ncommandShellTypeDescription=The shell to use for this command\nlimitedSystem=This is a limited or embedded system\nlimitedSystemDescription=Don't try to identify shell type, necessary for limited embedded systems or IOT devices\nsshForwardX11=Forward X11\nsshForwardX11Description=Enables X11 forwarding for the connection\ncustomAgent=Custom agent\nidentityAgent=Identity agent\nssh.proxyDescription=The optional proxy host to use when establishing the SSH connection. Must have an ssh client installed.\nusage=Usage\nwslHostDescription=The host on which the WSL instance is located on. Must have wsl installed.\nwslDistributionDescription=The name of the WSL instance\nwslUsernameDescription=The explicit username to log in as. If not specified, the default username will be used.\nwslPasswordDescription=The user's password which can be used for sudo commands.\ndockerHostDescription=The host on which the docker container is located on. Must have docker installed.\ndockerContainerDescription=The name of the docker container\nlocalMachine=Local Machine\nrootScan=Sudo shell environment\nloginEnvironmentScan=Custom login environment\nk8sScan=Kubernetes cluster\noptions=Options\ndockerRunningScan=Running docker containers\ndockerAllScan=All docker containers\nwslScan=WSL instances\nsshScan=SSH config connections\nrunAsUser=Run as user\nrunAsUserDescription=Start this shell environment as a different user\ndefault=Default\nadministrator=Administrator\nwslHost=WSL Host\ntimeout=Timeout\ninstallLocation=Install location\ninstallLocationDescription=The location where your $NAME$ environment is installed\nwsl.displayName=Windows Subsystem for Linux\nwsl.displayDescription=Connect to a WSL instance running on Windows\ndocker.displayName=Docker Container\ndocker.displayDescription=Connect to a docker container\nport=Port\nuser=User\npassword=Password\nmethod=Method\nuri=URL\nproxy=Proxy\n#context: The software distribution type\ndistribution=Distribution\nusername=Username\nshellType=Shell type\nbrowseFile=Browse file\nopenShell=Open shell in terminal\nopenCommand=Execute command in terminal\neditFile=Edit file\ndescription=Description\nfurtherCustomization=Further customization\nfurtherCustomizationDescription=For more configuration options, use the ssh config files\nbrowse=Browse\nconfigHost=Host\nconfigHostDescription=The host on which the config is located on\nconfigLocation=Config location\nconfigLocationDescription=The file path of the config file\ngateway=Gateway\ngatewayDescription=The optional gateway to use when connecting\nconnectionInformation=Connection information\n#context: title\nconnectionInformationDescription=Which system to connect to\npasswordAuthentication=Password authentication\npasswordAuthenticationDescription=The optional password to use to authenticate\nsshConfigString.displayName=Config-based SSH connection\nsshConfigString.displayDescription=Create a customized SSH connection in the SSH config format\nsshConfigStringContent=Configuration\nsshConfigStringContentDescription=SSH options for the connection in the OpenSSH config format\nvnc.displayName=VNC connection over SSH\nvnc.displayDescription=Open a VNC session over a tunneled connection\nbinding=Binding\nvncPortDescription=The port the VNC server is listening on\nrdpPortDescription=The port the RDP server is listening on\nvncUsername=Username\nvncUsernameDescription=The optional VNC username\nvncPassword=Password\nvncPasswordDescription=The VNC password\nx11WslInstance=X11 Forward WSL instance\nx11WslInstanceDescription=The local Windows Subsystem for Linux distribution to use as an X11 server when using X11 forwarding in an SSH connection. This distribution must be a WSL2 distribution.\n#context: computer root user\nopenAsRoot=Open as root\nopenInWSL=Open in WSL\nlaunch=Launch\nsshTrustKeyContent=The host key is not known, and you have enabled manual host key verification. $CONTENT$\nsshTrustKeyTitle=Unknown host key\nrdpTunnel.displayName=RDP connection over SSH\nrdpTunnel.displayDescription=Connect via RDP over a tunneled connection\nrdpEnableDesktopIntegration=Enable desktop integration\nrdpEnableDesktopIntegrationDescription=Run remote applications assuming that the RDP allow list permits that\nrdpSetupAdminTitle=RDP setup required\nrdpSetupAllowTitle=RDP remote application\nrdpSetupAllowContent=Starting remote applications directly is currently not allowed on this system. Do you want to enable it? This will allow you to run your remote applications directly from XPipe by disabling the allow list for RDP remote applications.\nrdpServerEnableTitle=RDP server\nrdpServerEnableContent=The RDP server is disabled on the target system. Do you want to enable it in the registry in order to allow remote RDP connections?\nrdp=RDP\nrdpScan=RDP tunnel over SSH\nwslX11SetupTitle=WSL X11 setup\nwslX11SetupContent=XPipe can use your local WSL distribution to act as an X11 display server. Would you like to set up X11 on $DIST$? This will install the basic X11 packages on the WSL distribution and may take a while. You can also change which distribution is used in the settings menu.\ncommand=Command\ncommandGroup=Command group\nvncSystem=VNC target system\nvncSystemDescription=The actual system to interact with. This is usually the same as the tunnel host\nvncHost=Target VNC host\nvncHostDescription=The system on which the VNC server is running on\nvncDirectHost=Host\nvncDirectHostDescription=The host entry or manual address of the server on which the VNC server is running on\nrdpDirectHost=Host\nrdpDirectHostDescription=The host entry or manual address of the server on which the RDP server is running on\ngitVaultTitle=Git vault\ngitVaultForcePushContent=Do you want to force push to the remote repository? This will completely replace all remote repository contents with your local one, including the history.\ngitVaultOverwriteLocalContent=Do you want to override your local vault changes? This will apply all remote changes to your local repository.\nrdpSimple.displayName=Direct RDP connection\nrdpSimple.displayDescription=Connect to a host via RDP\nrdpUsername=Username\nrdpUsernameDescription=The user to log in as. Can include a domain prefix\naddressDescription=Where to connect to\nrdpAdditionalOptions=Additional RDP options\nrdpAdditionalOptionsDescription=Raw RDP options to include, formatted the same as in .rdp files\nproxmoxVncConfirmTitle=VNC access\nproxmoxVncConfirmContent=Do you want to enable VNC access for the VM? This will enable direct VNC client access in the VM config file and restart the virtual machine.\ndockerContext.displayName=Docker context\ndockerContext.displayDescription=Interact with containers located in a specific context\nvmActions=VM actions\ndockerContextActions=Context actions\nk8sPodActions=Pod actions\nopenVnc=Enable VNC access\naddVnc=Add VNC connection\ncommandGroup.displayName=Command group\ncommandGroup.displayDescription=Group available commands for a system\nserial.displayName=Serial connection\nserial.displayDescription=Open a serial connection in a terminal\nserialPort=Serial port\nserialPortDescription=The serial port / device to connect to\nbaudRate=Baud rate\ndataBits=Data bits\nstopBits=Stop bits\nparity=Parity\nflowControlWindow=Flow control\nserialImplementation=Serial implementation\nserialImplementationDescription=The tool to use to connect to the serial port\nserialHost=Host\nserialHostDescription=The system to access the serial port on\nserialPortConfiguration=Serial port configuration\nserialPortConfigurationDescription=Configuration parameters of the connected serial device\nserialInformation=Serial information\nopenXShell=Open in XShell\ntsh.displayName=Teleport\ntsh.displayDescription=Connect to your teleport nodes via tsh\ntshNode.displayName=Teleport node\n#context: kubernetes\ntshNode.displayDescription=Connect to a teleport node in a cluster\n#context: kubernetes\nteleportCluster=Cluster\n#context: kubernetes\nteleportClusterDescription=The cluster the node is in\nteleportProxy=Proxy\nteleportProxyDescription=The proxy server used to connect to the node\nteleportHost=Host\nteleportHostDescription=The host name of the node\nteleportUser=User\nteleportUserDescription=The user to log in as\nlogin=Login\nhyperVInstall.displayName=Hyper-V\nhyperVInstall.displayDescription=Connect to VMs managed by Hyper-V\nhyperVVm.displayName=Hyper-V VM\nhyperVVm.displayDescription=Connect to a Hyper-V VM via SSH or PSSession\ntrustHost=Trust host\ntrustHostDescription=Add ComputerName to trusted hosts list\ncopyIp=Copy IP\nvncDirect.displayName=Direct VNC connection\nvncDirect.displayDescription=Connect to a system via VNC directly\neditConfiguration=Edit configuration\nviewInDashboard=View in dashboard\nsetDefault=Set default\nremoveDefault=Remove default\nconnectAsOtherUser=Connect as other user\nprovideUsername=Provide alternative username to log in with\nvmIdentity=Guest identity\nvmIdentityDescription=The SSH identity authentication method to use for connecting if needed\nvmPort=Port\nvmPortDescription=The port to connect to via SSH\nforwardAgent=Forward agent\nforwardAgentDescription=Make SSH agent identities available on the remote system\nvirshUri=URI\nvirshUriDescription=The hypervisor URI, aliases are also supported\nvirshDomain.displayName=libvirt domain\nvirshDomain.displayDescription=Connect to a libvirt domain\nvirshHypervisor.displayName=libvirt hypervisor\nvirshHypervisor.displayDescription=Connect to a libvirt supported hypervisor driver\nvirshInstall.displayName=libvirt command-line client\nvirshInstall.displayDescription=Connect to all available libvirt hypervisors via virsh\naddHypervisor=Add hypervisor\ninteractiveTerminal=Interactive terminal\neditDomain=Edit domain\nlibvirt=libvirt domains\ncustomIp=Custom IP\ncustomIpDescription=Override the default local VM IP detection if you use advanced networking\nautomaticallyDetect=Automatically detect\nuserAddDialogTitle=User creation\ngroupAddDialogTitle=Group creation\npassphrase=Passphrase\nrepeatPassphrase=Repeat passphrase\ngroupSecret=Group secret\nrepeatGroupSecret=Repeat group secret\nvaultGroup=Vault group\nloginAlertTitle=Login required\nloginAlertHeader=Unlock vault to access your personal connections\nvaultUser=Vault user\n#context: dative case\nme=Me\naddGroup=Add group ...\naddGroupDescription=Create a new group for this vault\naddUser=Add user ...\naddUserDescription=Create a new user for this vault\nskip=Skip\nuserChangePasswordAlertTitle=Password change\ngroupChangeSecretAlertTitle=Secret change\ndocs=Documentation\nlxd.displayName=LXD Container\nlxd.displayDescription=Connect to a LXD container via lxc\nlxdCmd.displayName=LXD CLI client\nlxdCmd.displayDescription=Access LXD containers via the lxc CLI client\npodman.displayName=Podman Container\npodman.displayDescription=Connect to a Podman container\nincusInstall.displayName=Incus machine manager\nincusInstall.displayDescription=Access incus containers via the incus CLI client\nincusContainer.displayName=Incus container\nincusContainer.displayDescription=Connect to an incus container\npodmanCmd.displayName=Podman CLI\npodmanCmd.displayDescription=Access Podman containers via the CLI client\nlxdHostDescription=The host on which the LXD container is located on. Must have lxc installed.\nlxdContainerDescription=The name of the LXD container\npodmanContainers=Podman containers\nlxdContainers=LXD containers\nincusContainers=Incus containers\ncontainer=Container\nhost=Host\ncontainerActions=Container actions\nserialConsole=Serial console\neditRunConfiguration=Edit run configuration\ncommunityDescription=A connection power-tool perfect for your personal use cases.\nupgradeDescription=Professional connection management for your entire server infrastructure.\ndiscoverPlans=Discover upgrade options\nextendProfessional=Upgrade to latest professional features\ncommunityItem1=Unlimited connections to non-commercial systems and tools\ncommunityItem2=Seamless integration with your installed terminals and editors\ncommunityItem3=Fully featured remote file browser\ncommunityItem4=Powerful scripting system for all shells\ncommunityItem5=Git integration for synchronization and sharing connection information\nupgradeItem1=Includes all community edition features\nupgradeItem2=The Homelab plan supports unlimited hypervisors and advanced SSH features\nupgradeItem3=The Professional plan additionally supports enterprise operating systems and tools\nupgradeItem4=The Enterprise plan comes with full flexibility for your individual use case\nupgrade=Upgrade\nupgradeTitle=Available plans\nstatus=Status\ntype=Type\nlicenseAlertTitle=License required\nuseCommunity=Continue with community\npreviewDescription=Try out new features for a couple of weeks after release.\ntryPreview=Activate preview\npreviewItem1=Full access to newly released professional features for 2 weeks after release\npreviewItem2=Try out new features without any commitment\nlicensedTo=Licensed to\nemail=Email address\n#context: Apply changes\napply=Apply\n#context: Delete content\nclear=Clear\nactivate=Activate\nvalidUntil=Valid until\nlicenseActivated=License activated\nrestart=Restart\n#context: verb, to close\nlockVault=Lock vault\nrestartApp=Restart XPipe\n#context: No payment required\nfree=Free\nupgradeInfo=You can find information about upgrading to a license below.\nupgradeInfoPreview=You can find information about upgrading to a license below or try out the preview.\nenterLicenseKey=Enter license key to upgrade\nisOnlySupported=is only supported with at least a $TYPE$ license\nareOnlySupported=are only supported with at least a $TYPE$ license\nlegacyLicense=This license only included new Professional features released within one year after purchase.\npreviewExpiredLicense=This feature was recently available for free in preview, but this period has now expired.\nopenApiDocs=API documentation\nopenApiDocsDescription=The HTTP API documentation is available online, including an OpenAPI .yaml specification. You can open it in your web browser or your preferred HTTP client.\nopenApiDocsButton=Open docs\npythonApi=Python API\npersonalConnection=This connection and all its children are only available to your user as they depend on a personal identity.\ndeveloperPrintInitFiles=Print init file execution\ndeveloperPrintInitFilesDescription=Print all shell init scripts that are run when a terminal is launched.\ndeveloperShowSensitiveCommands=Log sensitive commands\ndeveloperShowSensitiveCommandsDescription=Include sensitive commands in log output for debugging.\ncheckingForUpdates=Checking for updates\ncheckingForUpdatesDescription=Fetching latest release information\ndownloadingUpdate=Retrieving release (Version $VERSION$)\ndownloadingUpdateDescription=Downloading release package\nupdateNag=You haven't updated XPipe in a while. You might be missing out on new features and fixes of newer releases.\nupdateNagTitle=Update reminder\nupdateNagButton=See releases\nrefreshServices=Refresh services\nserviceProtocolType=Service protocol type\nserviceProtocolTypeDescription=Control how to open the service\nserviceCommand=The command to run once the service is active\nserviceCommandDescription=The placeholder $PORT will be replaced with the actual tunneled local port\n#context: not the measure of importance or personal ethics\nvalue=Value\nshowAdvancedOptions=Show advanced options\nsshAdditionalConfigOptions=Additional config options\nremoteFileManager=Remote file manager\nclearUserData=Delete user data\nclearUserDataDescription=Delete all user configuration data, including connections\nclearUserDataTitle=User data deletion\nclearUserDataContent=This will delete all local user data for xpipe and restart. If you care about your connections, make sure to synchronize them first with a git repository.\nundefined=Undefined\ncopyAddress=Copy address\nnetbirdDeviceScan=Netbird connections\nnetbirdId=Peer public key\nnetbirdIdDescription=The internal netbird public key id of the peer\ntailscaleDeviceScan=Tailscale connections\ntailscaleInstall.displayName=Tailscale installation\ntailscaleInstall.displayDescription=Connect to devices in your tailnet via SSH\ntailscaleDevice.displayName=Tailscale device\ntailscaleDevice.displayDescription=Connect to a device in your tailnet via SSH\ntailscaleId=Device ID\ntailscaleIdDescription=The internal tailscale device ID\ntailscaleHostName=Host name\ntailscaleHostNameDescription=The hostname of the device in the tailnet\ntailscaleUsername=Username\ntailscaleUsernameDescription=The user to log in as\ntailscalePassword=Password\ntailscalePasswordDescription=The user password that can be used for sudo\nscriptName=Script name\nscriptNameDescription=Give this script a custom name\nscriptGroupName=Script group name\nscriptGroupNameDescription=Give this script group a custom name\nidentityName=Identity name\nidentityNameDescription=Give this identity a custom name\n#context: Tailscale VPN\ntailscaleTailnet.displayName=Tailnet\n#context: Tailscale VPN\ntailscaleTailnet.displayDescription=Connect to a specific tailnet with your account\nputtyConnections=PuTTY connections\nkittyConnections=KiTTY connections\nicons=Icons\ncustomIcons=Custom icons\niconSources=Icon sources\niconSourcesDescription=You can add your own sources for icons here. XPipe will pick up any .svg files at the added location and add them to the available set of icons.\\n\\nBoth local directories and remote git repositories or supported as icon locations.\nrefreshSources=Refresh icons\nrefreshSourcesDescription=Update all icons from the available sources\naddDirectoryIconSource=Add directory source ...\naddDirectoryIconSourceDescription=Add icons from a local directory\naddGitIconSource=Add git source ...\naddGitIconSourceDescription=Add icons located in a remote git repository\nrepositoryUrl=Git Repository URL\niconDirectory=Icon directory\naddUnsupportedKexMethod=Add unsupported key exchange method\naddUnsupportedKexMethodDescription=Allow the key exchange method $VAL$ to be used for this connection\naddUnsupportedHostKeyType=Add unsupported host key type\naddUnsupportedHostKeyTypeDescription=Allow the host key type $VAL$ to be used for this connection\naddUnsupportedMacType=Add unsupported MAC type\naddUnsupportedMacTypeDescription=Allow the MAC type $VAL$ to be used for this connection\nrunSilent=silently in background\nrunInFileBrowser=in file browser\nrunInConnectionHub=in connection hub\ncommandOutput=Command output\niconSourceDeletionTitle=Delete icon source\niconSourceDeletionContent=Do you want to delete this icon source and all associated icons with it?\nrefreshIcons=Refresh icons\nrefreshIconsDescription=Retrieving, rendering, and caching all available 1000+ icons from external sources to .png files. This may take a while ...\nvaultUserLegacy=Vault user (Limited legacy compatibility mode)\nupgradeInstructions=Upgrade instructions\nexternalActionTitle=External action request\nexternalActionContent=An external action was requested. Do you want to allow launching actions from outside XPipe?\nnoScriptStateAvailable=Refresh to determine script compatibility ...\ndocumentationDescription=Check out the documentation\ncustomEditorCommandInTerminal=Run custom command in a terminal\ncustomEditorCommandInTerminalDescription=If your editor is terminal-based, you can enable this option to automatically open a terminal and run the command in the terminal session instead.\\n\\nYou can use this option for editors like vi, vim, nvim, and others.\ndisableHttpsTlsCheck=Disable HTTPS request certificate verification\ndisableHttpsTlsCheckDescription=If your organization is decrypting your HTTPS traffic in firewalls using SSL interception, any update checks or license checks will fail due to the certificates not matching up. You can fix this by enabling this option and disabling TLS certificate validation.\nconnectionsSelected=$NUMBER$ connections selected\naddConnections=Add connections\nbrowseDirectory=Browse directory\nopenTerminal=Open terminal\ndocumentation=Documentation\nreport=Report error\nkeePassXcNotAssociated=KeePassXC link\nkeePassXcNotAssociatedDescription=XPipe is not associated with your local KeePassXC database. Click below to perform the one-time step of associating XPipe with the KeePassXC database so that XPipe can query passwords.\nkeePassXcAssociateMore=Connect more databases\nkeePassXcAssociateMoreDescription=You can be connected to multiple KeePassXC databases at the same time\nkeePassXcAssociated=KeePassXC links\nkeePassXcAssociatedDescription=XPipe is connected to the following local KeePassXC databases:\n#context: verb, link together\nkeePassXcNotAssociatedButton=Link database\nidentifier=Identifier\npasswordManagerCommand=Custom command\npasswordManagerCommandDescription=The custom command to execute to fetch passwords. The placeholder string $KEY will be replaced by the quoted password key when called. This should call your password manager CLI to print the password to stdout, e.g. mypassmgr get $KEY.\nchooseTemplate=Choose template\nkeePassXcPlaceholder=KeePassXC entry URL\nterminalEnvironment=Terminal environment\nterminalEnvironmentDescription=In case you want to use features of a local Linux-based WSL environment for your terminal customization, you can use them as the terminal environment.\\n\\nAny custom terminal init commands and terminal multiplexer configuration will then be run in this WSL distribution.\nterminalInitScript=Terminal init script\nterminalInitScriptDescription=Commands to run in the terminal environment prior to the connection being launched. You can use this to configure the terminal environment on startup.\nterminalMultiplexer=Terminal multiplexer\nterminalMultiplexerDescription=The terminal multiplexer to use as an alternative to tabs in a terminal. This will replace certain terminal handling characteristics, e.g. tab handling, with the multiplexer functionality.\\n\\nRequires the respective multiplexer executable to be installed on the system.\nterminalMultiplexerWindowsDescription=The terminal multiplexer to use as an alternative to tabs in a terminal. This will replace certain terminal handling characteristics, e.g. tab handling, with the multiplexer functionality.\\n\\nRequires the usage of a WSL terminal environment on Windows and the multiplexer executable to be installed on the WSL system.\nterminalAlwaysPauseOnExit=Always pause on exit\nterminalAlwaysPauseOnExitDescription=When enabled, exiting a terminal session will always prompt you to either restart or close the session. If disabled, XPipe will only do so for failed connections that exit with an error.\n#context: verb, database query\nquerying=Querying ...\n#context: result display of database query\nretrievedPassword=Obtained: $PASSWORD$\nrefreshOpenpubkey=Refresh openpubkey identity\nrefreshOpenpubkeyDescription=Run opkssh refresh to make the openpubkey identity valid again\nall=All\nterminalPrompt=Terminal prompt\nterminalPromptDescription=The terminal prompt tool to use in your remote terminals. Enabling a terminal prompt will automatically set up and configure the prompt tool on the target system when opening a terminal session.\\n\\nThis does not modify any existing prompt configurations or profile files on a system. This will increase the terminal loading time for the first time while the prompt is being set up on the remote system. Your terminal might need additional fonts to display the prompt correctly.\nterminalPromptConfiguration=Terminal prompt configuration\nterminalPromptConfig=Config file\nterminalPromptConfigDescription=The custom config file to apply to the prompt. This config will be automatically set up on the target system when the terminal is initialized and used as the default prompt config.\\n\\nIf you want to use the existing default config file on each system, you can leave this field empty.\npasswordManagerKey=Password manager key\npasswordManagerKeyDescription=The password manager identifier of the secret\npasswordManagerAgent=Password manager agent\ndockerComposeProject.displayName=Docker compose project\ndockerComposeProject.displayDescription=Group containers of a compose project together\nsshVerboseOutput=Enable verbose SSH output\nsshVerboseOutputDescription=This will print a lot of debug information when connecting via SSH. Useful for troubleshooting issues with SSH connections.\ndontUseGateway=Don't use gateway\ndontUseGatewayDescription=Don't use hypervisor host as a gateway and connect directly to the IP\ncategoryColor=Category color\ncategoryColorDescription=The default color to use for connections within this category\ncategorySync=Sync with git repository\ncategorySyncDescription=Sync all connections automatically with git repository. All local changes to connections will be pushed to the remote.\ncategorySyncSpecial=Sync with git repository\\n(Not configurable for special category \"$NAME$\")\ncategoryDontAllowScripts=Disable all modifications\ncategoryDontAllowScriptsDescription=Disable any command execution and other operations on systems within this category to prevent any modifications. This will disable all scripting functionality, shell environment commands, prompts, and more.\ncategoryConfirmAllModifications=Confirm all modifications\ncategoryConfirmAllModificationsDescription=Confirm any kind of modification for a connection or a file system first. This can prevent accidental operations on important systems.\ncategoryDefaultIdentity=Default identity\ncategoryDefaultIdentityDescription=If you frequently use a certain identity on many of the systems in this category, then setting a default identity will allow you to preselect it when creating new connections.\ncategoryConfigTitle=$NAME$ configuration\n#context: verb\nconfigure=Configure\naddConnection=Add connection\nnoCompatibleConnection=No compatible connection found\nnoCompatibleIdentity=No compatible identity found\nnewCategory=New category\ndockerComposeRestricted=The compose project is restricted by $NAME$ and can't be modified externally. Please use $NAME$ to manage this compose project.\nrestricted=Restricted\ndisableSshPinCaching=Disable SSH PIN caching\ndisableSshPinCachingDescription=XPipe will automatically cache any PINs that were entered for a key when using some form of hardware based authentication.\\n\\nDisabling this will result in having to reenter the PIN on every connection attempt.\ngitSyncPull=Pull to sync remote git changes\nenpassVaultFile=Vault file\nenpassVaultFileDescription=The local Enpass vault file.\n#context: No hierarchy, just a single level\nflat=Flat\nrecursive=Recursive\nrdpAllowListBlocked=The selected RemoteApp does not seem to be included in the RDP allow list for the server.\npsonoServerUrl=Server URL\npsonoServerUrlDescription=URL of the psono backend server\npsonoApiKey=API Key\npsonoApiKeyDescription=The API key to use, formatted as an uuid\npsonoApiSecretKey=API secret key\npsonoApiSecretKeyDescription=The API secret key as 64 byte hex string\npassboltServerUrl=Server URL\npassboltServerUrlDescription=URL of the passbolt backend server\npassboltPassphrase=Passphrase\npassboltPassphraseDescription=The passphrase for the vault private key\npassboltPrivateKey=Private key\npassboltPrivateKeyDescription=The private gpg key file for the vault\nfocusWindowOnNotifications=Focus window on notifications\nfocusWindowOnNotificationsDescription=Bring XPipe to the foreground when a notification or error message is shown, for example when a connection or tunnel unexpectedly terminates.\ngitUsername=Custom git username\ngitUsernameDescription=The custom user to authenticate to the git remote repository. By default, XPipe will use the currently configured credentials of the git CLI.\\n\\nThis setting will override any default credentials that are already configured for your local git CLI client.\ngitPassword=Custom git password / personal access token\ngitPasswordDescription=The password or personal access token to use to authenticate. Whether you need a password or personal access token depends on the git remote provider. This setting will override any default credentials that are already configured for your local git CLI client.\nsetReadOnly=Set read-only\nunsetReadOnly=Unset read-only\nreadOnlyStoreError=This entry's configuration is frozen. Choose a different name to save your changes to a new copy.\ncategoryFreeze=Freeze connection configurations\ncategoryFreezeDescription=Marks connection configurations as read-only. This means that no existing connection entry configuration in this category can be modified. New connections can be added though.\nupdateFail=Update installation did not succeed\nupdateFailAction=Install update manually\nupdateFailActionDescription=Check out the latest releases at GitHub\nonePasswordPlaceholder=Item name or op:// URL\ncomputeDirectorySizes=Compute directory sizes\ncomputeSize=Compute size\ncustomSpiceCommand=Custom command\ncustomSpiceCommandDescription=The custom command to execute to launch SPICE sessions. The placeholder string $FILE will be replaced by the quoted file path to the .vv file when called.\nvncClient=VNC client\nvncClientDescription=The VNC client to launch when opening VNC connections in XPipe.\\n\\nYou have the option to either use the integrated VNC client within XPipe or alternatively launch an external locally installed VNC client if you are looking for more customization.\nintegratedXPipeVncClient=Integrated XPipe VNC client\ncustomVncCommand=Custom command\ncustomVncCommandDescription=The custom command to execute to launch VNC sessions. The placeholder string $ADDRESS will be replaced by the quoted address when called.\nvncConnections=VNC connections\npasswordManagerIdentity=Password manager identity\npasswordManagerIdentity.displayName=Password manager identity\npasswordManagerIdentity.displayDescription=Retrieve username and password of an identity from your password manager\npasswordCopied=Connection password copied to clipboard\nerrorOccurred=Error occurred\nactionMacro.displayName=Action macro\nactionMacro.displayDescription=Run in action using customized triggers\nmacroAdd=Add macro\nmacroName=Macro name\nmacroNameDescription=Give this macro a custom name\nactionId=Action ID\nactionIdDescription=The action to run with this macro\nmacroRefs=Associated connections\nmacroRefsDescription=The connections with which to run the action\n#context: A duplicate\nconnectionCopy=Copy\nactionPickerTitle=Pick action\nactionPickerDescription=Click on something to perform an action. Instead of executing the action, you can create and edit shortcuts to the action in the action shortcut pick mode.\ncancelActionPicker=Cancel action pick\nactionShortcut=Action shortcut\nactionShortcuts=Action shortcuts\nactionStore=Action store\nactionStoreDescription=The store entry to run the action on\nactionStores=Action stores\nactionStoresDescription=The store entries to run the action on\nactionDesktopShortcut=Desktop shortcut\nactionDesktopShortcutDescription=Create a shortcut for this action on your desktop\nactionUrlShortcut=URL shortcut\nactionUrlShortcutDescription=Copy a URL that can trigger this actions when opened\nactionUrlShortcutDisabled=URL shortcut (Unavailable)\nactionUrlShortcutDisabledDescription=The $TYPE$ installation type does not support opening URLs\nactionApiCall=API request\nactionApiCallDescription=Call this action from the HTTP API\nactionMacro=Action macro\nactionMacroDescription=Create a macro with advanced functionality for this action\ncreateMacro=Create macro\nactionConfiguration=Parameters\nactionConfigurationDescription=The parameters to pass to the executed action\nconfirmAction=Confirm action\nactionConnections=Action connections\nactionConnectionsDescription=The connections to run the action on\nactionConnection=Action connection\nactionConnectionDescription=The connection to run the action on\nappleContainerInstall.displayName=Apple containers\nappleContainerInstall.displayDescription=Access apple container instances via the container CLI\nappleContainer.displayName=Apple container\nappleContainer.displayDescription=Access apple container instances via the container CLI\nappleContainerHostDescription=The host on which the apple container is located on\nappleContainerDescription=The name of the apple container\nappleContainers=Apple containers\n#context: To change the relative order of things\nchangeOrderIndexTitle=Change order\norderIndex=Index\norderIndexDescription=Explicit index to order this entry relative to others. Lowest indices are shown on top, highest on the bottom\n#context: Move item to top\nmoveToFirst=Move to first\n#context: Show on bottom\nmoveToLast=Move to last\ncategory=Category\nincludeRoot=Include root\nexcludeRoot=Exclude root\nfreezeConfiguration=Freeze configuration\nunfreezeConfiguration=Unfreeze configuration\nwaylandScalingTitle=Wayland scaling\nactionApiUrl=$URL$ (Copy json body)\ncopyBody=Copy request body\ngitRepoTerminalOpen=Open repository in terminal\ngitRepoTerminalOpenDescription=Take a look at the repository yourself with the command-line\ngitRepoOverwriteLocal=Overwrite local repository\ngitRepoOverwriteLocalDescription=Replace all local changes with changes from the remote\ngitRepoForcePush=Overwrite remote repository\ngitRepoForcePushDescription=Use git push --force to apply your local changes to the remote\ngitRepoDontWarn=Don't warn anymore\ngitRepoDontWarnDescription=If this is expected, make XPipe ignore this error in the future\ngitRepoTryAgain=Try again\ngitRepoTryAgainDescription=Attempt the same operation again\ngitRepoEnablePlain=Use plain directory sync\ngitRepoEnablePlainDescription=Don't initialize a git repository to sync changes to directory\ngitRepoCreateBare=Use git sync\ngitRepoCreateBareDescription=Initialize a new bare git repository in the sync directory\ngitRepoDisable=Disable git vault for now\ngitRepoDisableDescription=Don't commit any changes during this session\ngitRepoPullRefresh=Pull changes and refresh\ngitRepoPullRefreshDescription=Merge remote changes and reload data\nbreakOutCategory=Break out category\nmergeCategory=Merge category\nopenWinScp=Open in WinSCP\nuninstallApplication=Uninstall\nuninstallApplicationDescription=Runs the .pkg an installation script to fully uninstall XPipe\nk8sEditPodTitle=Apply changes\nk8sEditPodContent=Do you want to apply the changes made via the command kubectl apply? A restart is likely required for changes to apply.\nvirshEditDomainTitle=Apply changes\nvirshEditDomainContent=Do you want to apply the changes to the domain? A restart is likely required for changes to apply.\npkcs11Library=PKCS#11 library\npkcs11LibraryDescription=The path of the dynamically linked library file\nsshAgentSocket=Custom SSH agent socket\nsshAgentSocketDescription=The custom socket to use to communicate with the SSH agent. This custom agent can be used for a connection by selecting the custom agent option for it.\npublicKey=Public key identifier\npublicKeyDescription=The optional public key to force the agent to only offer the matching private key\nactions=Actions\nhcloudServer.displayName=Hetzner cloud server\nhcloudServer.displayDescription=Access a server hosted on Hetzner cloud via SSH\nhcloudInstall.displayName=Hetzner Cloud CLI\nhcloudInstall.displayDescription=Access servers hosted on Hetzner cloud via hcloud\nhcloudContext.displayName=hcloud context\nhcloudContext.displayDescription=Access servers of an hcloud context\nmetrics=Metrics\nopenInVsCode=Open in VsCode\naddCloud=Cloud ...\nhcloudToken=hcloud token\nhcloudTokenDescription=The Hetzner cloud token to use. For more information, see the documentation\nhcloudLogin=Hetzner cloud login\nclearHcloudToken=Clear hcloud token\nclearHcloudTokenDescription=Delete existing token so that you can log in again\nselectIdentity=Select identity\nenableMcpServer=Enable MCP server\nenableMcpServerDescription=Enables the XPipe MCP server, allowing external MCP clients to send requests to the MCP server. See below for the configuration details.\\n\\nNote that the HTTP API does not have to be enabled for the MCP functionality.\nenableMcpMutationTools=Enable MCP mutation tools\nenableMcpMutationToolsDescription=By default, only read-only tools are enabled in the MCP server. This is to ensure that no accidental operations can be made that potentially modify a system.\\n\\nIf you plan to make changes to systems via MCP clients, make sure to check that your MCP client is configured to confirm any potentially destructive actions before enabling this option. Requires a reconnect of any MCP clients to apply.\nmcpClientConfigurationDetails=MCP client configuration\nmcpClientConfigurationDetailsDescription=Use this configuration data to connect to the XPipe MCP server from your MCP client of choice.\nswitchHostAddress=Change host address\naddAnotherHostName=Add another host name\naddNetwork=Network scan ...\nnetworkScan=Network scan\nnetworkScanStore=Target host\nnetworkScanStoreDescription=The host for which to scan the local network\nuseAsGateway=Use host as gateway\nuseAsGatewayDescription=Whether to use the target host as a gateway for the created connections\nnetworkScanPorts=Ports to scan\nnetworkScanPortsDescription=The comma-separated list of ports to include in the scan\nnetworkScanType=Connection type\nnetworkScanTypeDescription=The type of servers to look for\nemptyDirectory=This directory looks to be empty\nhcloudConfigFile=hcloud config file\nhcloudConfigFileDescription=The location of the hcloud CLI .toml config file\npreferMonochromeIcons=Prefer monochrome icons\npreferMonochromeIconsDescription=When enabled, monochrome icon variables will be chosen over the default colored versions of an icon, assuming that a separate light or dark mode icon variant is available for an icon from a source.\\n\\nRequires an refresh of the icons to apply.\nalwaysShowSshMotd=Always show MOTD\nalwaysShowSshMotdDescription=Whether or not to show the message of the day configured on a remote system upon login in a new terminal session. Note that changing this might alter the initialization behavior of SSH connections.\nmanageSubscription=Manage subscription\nnoListeningServer=No listening server\nnetworkScanResults=Scan results\nnetworkScanResultsDescription=The list of found systems in the network\nlocalShellDialect=Local shell\nlocalShellDialectDescription=The shell that is used for local operations. In case the normal local default shell is disabled or broken to some degree, this option can be used to fall back to another alternative.\\n\\nSome configurations like custom PATH entries might not apply with the fallback shell if they are not configured in the respective shell profile files yet.\nagentSocketNotFound=No active agent socket was found\nagentSocket=Socket location\nagentSocketDescription=The path of the agent socket file\nagentSocketNotConfigured=No custom socket has been configured yet\ndownloadInProgress=$NAME$ download in progress\nenableTerminalStartupBell=Enable terminal startup bell\nenableTerminalStartupBellDescription=Play a beep/bell command in a newly terminal session. If your terminal emulator supports bells, this can be used to make identifying newly launched terminal instances easier.\ninvalidSshGatewayChain=Invalid mixed gateway chain configuration with jump gateways and non-jump gateways.\nsyncFileExists=Synced file $FILE$ already exists\nreplaceFile=Replace file\nreplaceFileDescription=Replaced the existing file with this one\nrenameFile=Rename file\nrenameFileDescription=Give this file a different name to sync\nnewFileName=New filename\nparentHostDoesNotSupportTunneling=Parent host $NAME$ does not support tunneling\nconnectionNotesTemplate=Notes template\nconnectionNotesTemplateDescription=The markdown template that should be used when adding a new notes entry to a connection.\nconnectionNotesButton=Edit notes\nrdpSmartSizing=Enable smart sizing\nrdpSmartSizingDescription=When enabled, mstsc will scale down the desktop size if the window is too small to display it in its full resolution. The aspect ratio of the desktop is preserved when scaled down.\ndisableStartOnInit=Disable automatic startup\nenableStartOnInit=Enable automatic startup\nfileReadSudoTitle=Sudo file read\nfileReadSudoContent=The file you are trying to read does not grant you current user read permissions. Do you want to read this file as the root user with sudo? This will automatically elevate to root with either the existing credentials or via a prompt.\nnetbirdInstall.displayName=Netbird installation\nnetbirdInstall.displayDescription=Connect to peers in your Netbird network\nnetbirdProfile.displayName=Netbird profile\nnetbirdProfile.displayDescription=List peers in a specific profile\nnetbirdPeer.displayName=Netbird peer\nnetbirdPeer.displayDescription=Connect to a peer via SSH\nnetbirdPublicKey=Public key\nnetbirdPublicKeyDescription=The internal public key of the peer\nnetbirdHostName=Host name\nnetbirdHostNameDescription=The hostname of the peer in the network\nvncRefSystem=Associated system\nvncRefSystemDescription=The connection entry to associate this VNC connection with. Leave empty if there is none\nabstractHost.displayName=Abstract host\nabstractHost.displayDescription=Create an entry for a host that does not support shell connections\nabstractHostAddress=Host address\nabstractHostAddressDescription=The address of the host\nabstractHostGateway=Gateway\nabstractHostGatewayDescription=The optional gateway system through which to reach this host\nabstractHostConvert=Convert to abstract host entry\nhostNoConnections=No available connections\nhostHasConnections=$COUNT$ available connections\nhostHasConnection=$COUNT$ available connection\nlargeFileWarningTitle=Large file edit\nlargeFileWarningContent=The file you want to edit is quite large with $SIZE$. Do you really want to open this file in your text editor?\nrdpAskpassUser=RDP username for host $HOST$\nrdpAskpassPassword=Password for user $USER$\ninPlaceKey=Key\ninPlaceKeyText=Private key content\ninPlaceKeyTextDescription=The private key contents\nnetbirdSelfhosted=Self-hosted netbird instance\nnetbirdSelfhostedDescription=Provide a custom URL instead of using the cloud-hosted version\nnetbirdManagementUrl=Netbird management URL\nnetbirdManagementUrlDescription=The management URL of your self-hosted instance\nnetbirdSetupKey=Setup key\nnetbirdSetupKeyDescription=If you are using setup keys, you can use one for login\nnetbirdLogin=Netbird login\naddProfile=Add profile\nnetbirdProfileNameAsktext=Name of new netbird profile\nopenSftp=Open in SFTP session\ncapslockWarning=You have capslock enabled\ninherit=Inherit\nsshConfigStringSelected=Target host\nsshConfigStringSelectedDescription=For multiple hosts, the first one is used as the target. Reorder your hosts to change the target\ntunnelToLocalhost=Tunnel to localhost\ntunnelToLocalhostDescription=Automatically tunnel the remote port to localhost\ntags=Tags\ntag=Tag\naddNewTag=Create new tag\ncreateTag=Create tag ...\ninPlacePublicKey=Public key\ninPlacePublicKeyDescription=The associated public key for the specified private key\nsshKeygenTitle=Generate new SSH key\nsshKeygenAlgorithm=Algorithm\nsshKeygenAlgorithmDescription=The asymmetric keygen algorithm to use for the key\nrsaBits=Bits\nrsaBitsDescription=Number of bits in the generated key\nsshKeygenComment=Comment\nsshKeygenCommentDescription=The optional comment for this key\nsshKeygenPassphrase=Passphrase\nsshKeygenPassphraseDescription=The optional passphrase for this key\ned25519SkResident=Make resident key\ned25519SkResidentDescription=Store private key on the hardware security key\ned25519SkResidentKeyName=Resident key label\ned25519SkResidentKeyNameDescription=Give the key a label. Needed when storing multiple keys on the security key\ned25519SkPinRequired=Require PIN\ned25519SkPinRequiredDescription=Require PIN entry on use\ned25519SkUserPresenceRequired=Require user presence\ned25519SkUserPresenceRequiredDescription=Require touch or similar on use. Some security keys require this to be enabled\ncopyPublicKey=Copy public key\ngeneratePublicKey=Generate public key\npublicKeyGenerateNotice=Can be generated from private key\nidentityApplyTargetHost=Target\nidentityApplyTargetHostDescription=The system to apply the identity to\nidentityApplyAuthorizedHost=SSH key authorized\nidentityApplyAuthorizedHostDescription=The SSH key is added to authorized hosts file\nidentityApplyAuthorizedHostButton=Append key to file\napplyIdentityToHost=Apply identity to host ...\nidentityApplyMissingPublicKeyTitle=Missing public key\nidentityApplyMissingPublicKeyContent=The identity's SSH key does not have a public key associated with it. Check out the configuration for details.\nvalid=Valid\nnotValid=Not valid\nwarning=Warning\nidentityApplyTitle=Apply identity\nidentityApplyConfigPasswordEnabled=Password auth enabled\nidentityApplyConfigPasswordEnabledDescription=Password authentication is still enabled in the sshd config\nidentityApplyConfigPasswordDisabled=Password auth disabled\nidentityApplyConfigPasswordDisabledDescription=Password authentication is still disabled in the sshd config\nidentityApplyConfigKeyEnabled=Key auth enabled\nidentityApplyConfigKeyEnabledDescription=Key-based authentication is still enabled in the sshd config\nidentityApplyConfigKeyDisabled=Key auth disabled\nidentityApplyConfigKeyDisabledDescription=Key-based authentication is still disabled in the sshd config\nidentityApplyConfigRootDisabledWarning=Root login disabled\nidentityApplyConfigRootDisabledWarningDescription=Root user login is not enabled in the sshd config\nidentityApplyConfigAdminWarning=Administrator keys configured\nidentityApplyConfigAdminWarningDescription=The key might have to be added to administrators_authorized_keys instead for admin users\nidentityApplyEditConfig=Edit config\nidentityApplyEditConfigDescription=Open the sshd config in the editor to fix any issues\nidentityApplyEditAuthorizedKeys=Edit authorized keys\nidentityApplyEditAuthorizedKeysDescription=Open the authorized_keys file in the editor to edit or remove other keys\nidentityApplyEditConfigButton=Open sshd_config\nidentityApplyEditAuthorizedKeysButton=Open authorized_keys\nidentityApplySetStoreIdentity=Connection identity set\nidentityApplySetStoreIdentityDescription=The identity is configured to be used by the connection\nidentityApplySetStoreIdentityButton=Apply identity\ngenerateKey=Generate key\ngroupSecretStrategy=Group-based access control\ngroupSecretStrategyDescription=How to retrieve the group secret used for encryption and decryption for the group. The retrieval method you choose will be run when a user logs into the vault on startup.\\n\\nThis setting is configured on a per-group basis. To change this setting for a different group other than the currently active one, you will have to log into the vault as a member of that group.\nfileSecret=File-based secret\ncommandSecret=Command\nhttpRequestSecret=HTTP response\nfileSecretChoice=File location\nfileSecretChoiceDescription=The path to the file containing the group encryption secret. Since this file can be queried on all platforms, you can use ~ in the path to refer to the home directory. The file must be available on all systems you unlock the vault from, otherwise the login will fail.\ncommandSecretField=Retrieval script\ncommandSecretFieldDescription=The command that will return the secret encryption key for the current group. The command is run in the local system default shell and the key should be printed to stdout.\nhttpRequestSecretField=Request URI\nhttpRequestSecretFieldDescription=The URI to send an HTTP request to. The group secret is taken from the HTTP response body.\nvaultAuthentication=Vault authentication\nvaultAuthenticationDescription=How to authenticate / unlock the vault data. There are multiple different ways of encrypting and unlocking vault data, depending on who you want to share the vault data with.\ngroupAuthFailed=Secret authentication failed\nuserAuthFailed=Password authentication failed\nsavingChanges=Saving changes\nawsDeviceScan=AWS\naws=AWS\nawsCliInstallTitle=AWS CLI required\nawsCliInstallContent=The AWS integration requires the AWS CLI to be installed on your local system\nawsProfileCreateTitle=New AWS profile\nawsProfileAccessKey=Access key\nawsProfileName=Profile name\nawsProfileNameDescription=The display name of the new profile\nawsProfileRegion=Region\nawsProfileRegionDescription=The AWS region associated with the profile\nawsProfileAccessKeyId=Access key ID\nawsProfileAccessKeyIdDescription=The IAM user access key ID\nawsProfileSecretAccessKey=Secret access key\nawsProfileSecretAccessKeyDescription=The associated secret access key\nawsInstall.displayName=AWS CLI installation\nawsInstall.displayDescription=Connect to your AWS systems via the AWS CLI\nawsProfile.displayName=AWS CLI profile\nawsProfile.displayDescription=Access AWS through a specific profile\nawsInstanceId=Instance ID\nawsInstanceIdDescription=The internal ID of this instance\nawsInstanceUseSsm=Connect via SSM\nawsInstanceUseSsmDescription=Use the SSM tool to connect to the instance via SSH\nawsEc2Instance.displayName=AWS EC2 instance\nawsEc2Instance.displayDescription=Connect to an EC2 instance via SSH\nawsS3Group.displayName=S3 buckets\nawsS3Group.displayDescription=Access S3 buckets of an AWS profile\nawsS3Bucket.displayName=S3 bucket\nawsS3Bucket.displayDescription=Access an S3 bucket of an AWS profile\nawsEc2Group.displayName=EC2 instances\nawsEc2Group.displayDescription=Access EC2 instances of an AWS profile\nawsEc2InstanceSsmTerminal=Open SSM terminal\ngenericS3Bucket.displayName=Generic S3 bucket\ngenericS3Bucket.displayDescription=Access a generic S3 bucket via the AWS CLI\naddFileSystem=File system ...\ngenericS3BucketHost=Host\ngenericS3BucketHostDescription=The host entry or manual address of the S3 server\ngenericS3BucketPortDescription=The port the S3 server is listening on\ngenericS3BucketAccessKeyId=Access key ID\ngenericS3BucketAccessKeyIdDescription=The IAM user access key ID\ngenericS3BucketSecretAccessKey=Secret access key\ngenericS3BucketSecretAccessKeyDescription=The associated secret access key\ngenericS3BucketHttps=Enable HTTPS\ngenericS3BucketHttpsDescription=Use HTTPS to connect to the server. Some providers might require HTTPS\ntunnelled=Tunneled\nawsInstallSync=Configuration sync\nawsInstallSyncDescription=Sync the AWS CLI config files to the git vault\nawsInstallLocation=User data location\nawsInstallLocationDescription=The path from where the AWS CLI config files are sourced\ninstanceActions=Instance actions\nopenSplit=Open in split terminal\nterminalSplitStrategy=Split view direction\nterminalSplitStrategyDescription=Controls how terminal tabs are split when using the split view functionality in batch mode to open multiple terminal sessions next to each other.\nterminalSplitStrategyDisabledDescription=Controls how terminal tabs are split when using the split view functionality in batch mode to open multiple terminal sessions next to each other.\\n\\nYour current terminal configuration does not support split views.\nhorizontal=Horizontal\nvertical=Vertical\nbalanced=Balanced\n#context: verb, to exit\nclose=Close\nhelpButton=$TOPIC$ documentation link\nquickAccess=Quick access\ntoggleEnabled=Toggle state\ncurrentPath=Current path\ndirectoryContents=Directory contents\ndirectoryOptions=Directory options\nchooseConnectionType=Choose connection type\nbatchMode=Batch mode\n#context: noun, a type of button\ntoggleButton=Toggle button\ntailscaleUseSsh=Use tailscale SSH auth\ntailscaleUseSshDescription=Log in via the tailscale SSH server itself without any SSH auth\nportDescription=The port the SSH server is running on\nloginAs=Log in as\nsshGatewayType=Gateway type\nsshGatewayTypeDescription=Whether to connect to the target via a tunnel or with the ProxyJump option\ngatewayTunnel=Gateway tunnel\n#context: SSH jump server, noun\nproxyJump=Proxy jump\ncommandTypeAsyncBackground=Run detached in background\ncommandTypeSyncBackground=Run in background and wait for finish\ncommandTypeTerminalBackground=Open in terminal\nasyncBackgroundCommand=Background command\n#context: A command that does not return until finished\nsyncBackgroundCommand=Blocking background command\nterminalBackgroundCommand=Terminal command\ntestingConnection=Testing connection ...\nopenManagementConsole=Open management console\nopenLxcTerminal=Open LXC terminal\nopenContainerConsole=Open serial console\nkeeper2fa=2FA method\nkeeper2faDescription=The primary two-factor authentication method that is configured for your account. Enable this if your Keeper account requires two-factor auth to access passwords.\nkeeperTotpDuration=Custom 2FA code duration\nkeeperTotpDurationDescription=Override the default duration on how long a 2FA code is valid. Only applies if your organization policy allows changing the duration.\\n\\nPossible values are: $VALUES$\n#context: Brand and product names\nkeeperOtherAuth=Other (RSA SecurID, Duo Security, Keeper DNA, etc.)\nextractReusableIdentities=Extract reusable identities\nidentitiesAdded=Identities added\nsyncMode=Sync mode\nsyncModeDescription=Controls how changes should be synced.\\n\\nInstant mode will push and pull changes as soon as possible, startup and exit mode will sync all changes made during a session at once, and manual mode will only sync when you initiate it.\ntoggleTerminalDock=Toggle terminal dock\nscriptDirectory=Directory location\nscriptDirectoryDescription=The local directory containing shell script files\nscriptSourceUrl=Repository URL\nscriptSourceUrlDescription=The URL to a remote git repository containing shell script files\nscriptCollectionSourceType=Source type\nscriptCollectionSourceTypeDescription=The type of source from where shell scripts should be loaded\nscriptCollectionSourceEntry=Source entry\nscriptCollectionSourceEntryDescription=The source from where shell scripts should be loaded\ngitRepository=Git repository\nscriptCollectionSource.displayName=Script source\nscriptCollectionSource.displayDescription=Automatically import shell scripts from an existing source\ndirectorySource=Directory source\ngitRepositorySource=Git repository source\nrefreshSource=Refresh source\nscriptTextSourceUrl=Script URL\nscriptTextSourceUrlDescription=The URL to retrieve the script file from\nscriptSourceType=Script source\nscriptSourceTypeDescription=From where to source the script\nscriptSourceTypeInPlace=In-place script\nscriptSourceTypeUrl=External URL\nscriptSourceTypeSource=Existing source\nimportScripts=Import scripts\nscriptsContained=$NUMBER$ scripts\nscriptSourceCollectionImportTitle=Import scripts from source ($SELECTED$/$COUNT$)\nnoScriptsFound=No scripts found\ntunnel=Tunnel\nnotInitialized=Not initialized\nselectCategory=Select category ...\nscriptSourceName=Script name\nscriptSourceNameDescription=The file name of the script in the source\nworkspaceRestartTitle=Workspace ready\nworkspaceRestartContent=A shortcut to the new workspace was created at $PATH$. You can navigate to the shortcut or restart XPipe now to open the new workspace automatically.\nbrowseShortcut=Browse file\nsyncModeInstant=Sync instantly\nsyncModeSession=Sync on startup and exit\nsyncModeManual=Sync manually\npushChanges=Push changes\npullChanges=Pull changes\nsourcedFrom=Sourced from $SOURCE$\ninPlaceScript=In-place script\ngeneric=Generic\nsyncToPlainDirectory=Sync to plain directory\nsyncToPlainDirectoryDescription=When syncing to a local directory, you can either treat this directory as another git repository or just as a plain directory. If the plain directory setting is enabled, the directory is not initialized as a git repository.\nopenSpiceSession=Open SPICE session\nterminalBehaviour=Terminal behaviour\nnoScanPossible=No supported connections were found\nnetworkSwitchPorts=Network ports\nnswitchGroup.displayName=Network ports\nnswitchGroup.displayDescription=List available ports on a network device\nnswitchPort.displayName=Network port\nnswitchPort.displayDescription=Control an individual port on a network switch device\nenablePort=Enable port\nshutdownPort=Shut down port\nresetPort=Reset port\nuseSystemDefault=Use system default\nportStatus=Port status\nclearCounters=Clear counters\nshowStatus=Show status\nshowAllPorts=Show all ports\nactiveLicense=License\nactiveLicenseDescription=Activate an XPipe license key\nauthenticatorApp=Authenticator app\nsecurityKey=Security key\nmcpAdditionalContext=Additional MCP context\nmcpAdditionalContextDescription=Additional instructions to pass to the MCP client. Use this to control the agent behaviour and supply additional context for your individual setup.\nmcpAdditionalContextSample=- Do not restart any services and daemons automatically without confirming first\\n- When configuring a network interface, always use 192.168.1.1/24 as the gateway\nprefsRestartTitle=Restart required\nprefsRestartContent=Some options you changed require an application restart to apply. Do you want to restart XPipe now?\nbashShell=Bash shell\n"
  },
  {
    "path": "lang/strings/translations_es.properties",
    "content": "delete=Borrar\nproperties=Propiedades\nusedDate=Utilizado $DATE$\nopenDir=Directorio abierto\nsortLastUsed=Ordenar por fecha de último uso\nsortAlphabetical=Ordenar alfabéticamente por nombre\nsortIndexed=Ordenar por índice de orden\nrestartDescription=Un reinicio a menudo puede ser una solución rápida\nreportIssue=Informar de un problema\nreportIssueDescription=Abre el notificador de incidencias integrado\nusefulActions=Acciones útiles\nstored=Guardado\ntroubleshootingOptions=Herramientas de solución de problemas\ntroubleshoot=Solucionar problemas\nremote=Archivo remoto\naddShellStore=Añadir Shell ...\naddShellTitle=Añadir conexión Shell\nsavedConnections=Conexiones guardadas\nsave=Guardar\nclean=Limpia\nmoveTo=Pasar a ...\naddDatabase=Base de datos ...\nbrowseInternalStorage=Explorar el almacenamiento interno\naddTunnel=Túnel ...\naddService=Servicio ...\naddScript=Guión ...\naddHost=Host remoto ...\naddShell=Entorno Shell ...\naddCommand=Comando ...\naddAutomatically=Añadir automáticamente ...\naddOther=Añadir otros ...\nconnectionAdd=Añadir conexión\nscriptAdd=Añadir script\nscriptGroupAdd=Añadir grupo de scripts\nidentityAdd=Añadir identidad\nnew=Nuevo\nselectType=Seleccionar tipo\nselectTypeDescription=Selecciona el tipo de conexión\nselectShellType=Tipo de shell\nselectShellTypeDescription=Selecciona el tipo de conexión shell\nname=Nombre\nstoreIntroHeader=Hub de conexión\nstoreIntroContent=Aquí puedes gestionar todas tus conexiones shell locales y remotas en un solo lugar. Para empezar, puedes detectar rápidamente las conexiones disponibles de forma automática y elegir cuáles añadir.\nstoreIntroButton=Buscar conexiones ...\ndragAndDropFilesHere=O simplemente arrastra y suelta un archivo aquí\nconfirmDsCreationAbortTitle=Confirmar aborto\nconfirmDsCreationAbortHeader=¿Quieres abortar la creación de la fuente de datos?\nconfirmDsCreationAbortContent=Se perderá cualquier progreso en la creación de fuentes de datos.\nconfirmInvalidStoreTitle=Omitir validación\nconfirmInvalidStoreContent=¿Quieres omitir la validación de la conexión? Puedes añadir esta conexión aunque no se haya podido validar y solucionar los problemas de conexión más adelante.\nexpand=Expandir\naccessSubConnections=Subconexiones de acceso\ncommon=Común\ncolor=Color\nalwaysConfirmElevation=Confirma siempre la elevación del permiso\nalwaysConfirmElevationDescription=Controla cómo manejar los casos en que se requieren permisos elevados para ejecutar un comando en un sistema, por ejemplo, con sudo.\\n\\nPor defecto, cualquier credencial sudo se almacena en caché durante una sesión y se proporciona automáticamente cuando se necesita. Si esta opción está activada, te pedirá que confirmes el acceso elevado cada vez.\nallow=Permitir\nask=Pregunta a\ndeny=Denegar\nshare=Añadir al repositorio git\nunshare=Eliminar del repositorio git\nremove=Elimina\ncreateNewCategory=Nueva subcategoría\nprompt=Pregunta\ncustomCommand=Comando personalizado\nother=Otros\nsetLock=Fijar bloqueo\nselectConnection=Seleccionar conexión\nselectEntry=Seleccionar entrada\ncreateLock=Crear frase de contraseña\nchangeLock=Cambiar frase de contraseña\ntest=Prueba\nfinish=Finalizar\nerror=Se ha producido un error\ndownloadStageDescription=Mueve los archivos descargados al directorio de descargas de tu sistema y ábrelo.\nok=Ok\nsearch=Busca en\nrepeatPassword=Repetir contraseña\naskpassAlertTitle=Askpass\nunsupportedOperation=Operación no admitida: $MSG$\nfileConflictAlertTitle=Resolver un conflicto\nfileConflictAlertContent=Se ha encontrado un conflicto. El archivo $FILE$ ya existe en el sistema de destino.\\n\\n¿Cómo quieres proceder?\nfileConflictAlertContentMultiple=Se ha producido un conflicto. El archivo $FILE$ ya existe.\\n\\n¿Cómo quieres proceder? Es posible que haya más conflictos que puedas resolver automáticamente eligiendo una opción que se aplique a todos.\nmoveAlertTitle=Confirmar movimiento\nmoveAlertHeader=¿Quieres mover los ($COUNT$) elementos seleccionados a $TARGET$?\ndeleteAlertTitle=Confirmar borrado\ndeleteAlertHeader=¿Quieres borrar los ($COUNT$) elementos seleccionados?\nselectedElements=Elementos seleccionados:\nmustNotBeEmpty=$VALUE$ no debe estar vacío\nvalueMustNotBeEmpty=El valor no debe estar vacío\ntransferDescription=Arrastra los archivos aquí para descargarlos\ndragLocalFiles=Arrastra las descargas desde aquí\nnull=$VALUE$ debe ser no nulo\nroots=Raíces\nscripts=Guiones\nsearchFilter=Busca ...\nrecent=Reciente\nshortcut=Atajo\nbrowserWelcomeEmptyHeader=Navegador de archivos\nbrowserWelcomeEmptyContent=Puedes elegir a la izquierda qué sistemas abrir en el explorador de archivos. XPipe recordará a qué sistemas y directorios has accedido anteriormente y los mostrará en un menú de acceso rápido aquí en el futuro.\nbrowserWelcomeEmptyButton=Abrir el explorador de archivos local\nbrowserWelcomeSystems=Hace poco te conectaste a los siguientes sistemas:\nbrowserWelcomeDocsHeader=Documentación\nbrowserWelcomeDocsContent=Si prefieres un enfoque más guiado para familiarizarte con XPipe, consulta el sitio web de documentación.\nbrowserWelcomeDocsButton=Documentación abierta\nhostFeatureUnsupported=$FEATURE$ no está instalado en el host\nmissingStore=$NAME$ no existe\nconnectionName=Nombre de la conexión\nconnectionNameDescription=Dale a esta conexión un nombre personalizado\nopenFileTitle=Abrir archivo\nunknown=Desconocido\nscanAlertTitle=Añadir conexiones\nscanAlertChoiceHeader=Objetivo\nscanAlertChoiceHeaderDescription=Elige dónde buscar las conexiones. Esto buscará primero todas las conexiones disponibles.\nscanAlertHeader=Tipos de conexión\nscanAlertHeaderDescription=Selecciona los tipos de conexiones que quieres añadir automáticamente para el sistema.\nnoInformationAvailable=No hay información disponible\nyes=Sí\nno=No\nerrorOccured=Se ha producido un error\nterminalErrorOccured=Se ha producido un error de terminal\nerrorTypeOccured=Se ha lanzado una excepción de tipo $TYPE$\npermissionsAlertTitle=Permisos necesarios\npermissionsAlertHeader=Se necesitan permisos adicionales para realizar esta operación.\npermissionsAlertContent=Por favor, sigue la ventana emergente para dar a XPipe los permisos necesarios en el menú de configuración.\nerrorDetails=Detalles del error\nupdateReadyAlertTitle=Actualizar listo\nupdateReadyAlertHeader=Una actualización a la versión $VERSION$ está lista para ser instalada\nupdateReadyAlertContent=Esto instalará la nueva versión y reiniciará XPipe una vez finalizada la instalación.\nerrorNoDetail=No hay detalles de error disponibles\nerrorNoExceptionMessage=Se ha producido un error del tipo $TYPE$\nupdateAvailableTitle=Actualización disponible\nupdateAvailableContent=Hay disponible una actualización de XPipe a la versión $VERSION$ para instalar. Aunque XPipe no pudo iniciarse, puedes intentar instalar la actualización para solucionar potencialmente el problema.\nclipboardActionDetectedTitle=Acción del portapapeles detectada\nclipboardActionDetectedContent=XPipe ha detectado contenido en tu portapapeles que se puede abrir. ¿Quieres abrirlo ahora? ¿Quieres importar el contenido de tu portapapeles?\ninstall=Instalar ...\nignore=Ignora\npossibleActions=Acciones disponibles\nreportError=Informar de un error\nreportOnGithub=Crear un informe de incidencia en GitHub\nreportOnGithubDescription=Abre una nueva incidencia en el repositorio GitHub\nreportErrorDescription=Enviar un informe de error con comentarios opcionales del usuario e información de diagnóstico\nignoreError=Ignorar error\nignoreErrorDescription=Ignora este error y continúa como si no hubiera pasado nada\nprovideEmail=Cómo podemos ponernos en contacto contigo (opcional, sólo si quieres obtener una respuesta). Tu informe es anónimo por defecto, así que puedes proporcionar aquí información de contacto como una dirección de correo electrónico.\nadditionalErrorInfo=Proporciona información adicional (opcional)\nadditionalErrorAttachments=Selecciona archivos adjuntos (opcional)\ndataHandlingPolicies=Política de privacidad\nsendReport=Enviar informe\nerrorHandler=Gestor de errores\nevents=Eventos\nvalidate=Valida\nstackTrace=Rastreo de pila\npreviousStep=< Anterior\nnextStep=Siguiente\nfinishStep=Termina\nselect=Selecciona\nbrowseInternal=Navegar internamente\ncheckOutUpdate=Comprobar actualización\nquit=Salir de\nnoTerminalSet=No se ha configurado automáticamente ninguna aplicación de terminal. Puedes hacerlo manualmente en el menú de configuración.\nconnections=Conexiones\nconnectionHub=Núcleo de conexión\nsettings=Configuración\nexplorePlans=Licencia\nhelp=Ayuda\nabout=Acerca de\ndeveloper=Desarrollador\nbrowseFileTitle=Examinar archivo\nbrowser=Navegador de archivos\nselectFileFromComputer=Selecciona un archivo de este ordenador\nlinks=Enlaces\nwebsite=Página web\ndiscordDescription=Únete al servidor Discord\nredditDescription=Únete al subreddit XPipe\nsecurity=Seguridad\nsecurityPolicy=Información de seguridad\nsecurityPolicyDescription=Lee la política de seguridad detallada\nprivacy=Política de privacidad\nprivacyDescription=Lee la política de privacidad de la aplicación XPipe\nslackDescription=Únete al espacio de trabajo Slack\nsupport=Soporte\ngithubDescription=Consulta el repositorio GitHub\nopenSourceNotices=Avisos de código abierto\ncheckForUpdates=Buscar actualizaciones\ncheckForUpdatesDescription=Descarga una actualización si la hay\nlastChecked=Última comprobación\nversion=Versión\nbuild=Versión de construcción\nruntimeVersion=Versión en tiempo de ejecución\nvirtualMachine=Máquina virtual\nupdateReady=Instalar actualización\nupdateReadyPortable=Comprobar actualización\nupdateReadyDescription=Se ha descargado una actualización y está lista para ser instalada\nupdateReadyDescriptionPortable=Se puede descargar una actualización\nupdateRestart=Reiniciar para actualizar\nnever=Nunca\nupdateAvailableTooltip=Actualización disponible\nptbAvailableTooltip=Disponible versión de prueba pública\nvisitGithubRepository=Visita el repositorio GitHub\nupdateAvailable=Actualización disponible: $VERSION$\ndownloadUpdate=Descargar actualización\nlegalAccept=Acepto el Acuerdo de Licencia de Usuario Final\nconfirm=Confirma\nprint=Imprime\nwhatsNew=Novedades de la versión $VERSION$ ($DATE$)\nantivirusNoticeTitle=Una nota sobre los programas antivirus\nupdateChangelogAlertTitle=Registro de cambios\ngreetingsAlertTitle=Bienvenido a XPipe\neula=Acuerdo de licencia de usuario final\nnews=Noticias\nintroduction=Introducción\nprivacyPolicy=Política de privacidad\nagree=Acuerda\ndisagree=En desacuerdo\ndirectories=Directorios\nlogFile=Archivo de registro\nlogFiles=Archivos de registro\nlogFilesAttachment=Archivos de registro\nissueReporter=Informador de incidencias\nopenCurrentLogFile=Archivos de registro\nopenCurrentLogFileDescription=Abrir el archivo de registro de la sesión actual\nopenLogsDirectory=Abrir directorio de registros\ninstallationFiles=Archivos de instalación\nopenInstallationDirectory=Archivos de instalación\nopenInstallationDirectoryDescription=Abrir el directorio de instalación de XPipe\nlaunchDebugMode=Modo depuración\nlaunchDebugModeDescription=Reinicia XPipe en modo depuración\nextensionInstallTitle=Descargar\nextensionInstallDescription=Esta acción requiere bibliotecas adicionales de terceros que no distribuye XPipe. Puedes instalarlas automáticamente aquí. Los componentes se descargan del sitio web del proveedor:\nextensionInstallLicenseNote=Al realizar la descarga y la instalación automática, aceptas los términos de las licencias de terceros:\nlicense=Licencia\ninstallRequired=Instalación necesaria\nrestore=Restaurar\nrestoreAllSessions=Restaurar todas las sesiones\nlimitedTouchscreenMode=Modo de pantalla táctil limitada\nlimitedTouchscreenModeDescription=Al utilizar esta aplicación en una interfaz táctil más exótica, como la pantalla de un teléfono, algunos menús podrían no funcionar correctamente. Cuando esta opción está activada, la implementación del menú utiliza una funcionalidad más limitada para trabajar con eventos de ratón/toque poco enviados.\nappearance=Apariencia\ndisplay=Mostrar\npersonalization=Personalización\ndisplayOptions=Opciones de visualización\ntheme=Tema\nrdpConfiguration=Configuración del escritorio remoto\nrdpClient=Cliente RDP\nrdpClientDescription=El programa cliente RDP al que llamar al iniciar conexiones RDP.\\n\\nTen en cuenta que los distintos clientes tienen diferentes grados de capacidades e integraciones. Algunos clientes no admiten la transmisión automática de contraseñas, por lo que tendrás que introducirlas al iniciar la conexión.\nlocalShell=Shell local\nthemeDescription=Tu tema de visualización preferido.\ndontAutomaticallyStartVmSshServer=No iniciar automáticamente el servidor SSH para las máquinas virtuales cuando sea necesario\ndontAutomaticallyStartVmSshServerDescription=Cualquier conexión shell a una máquina virtual que se ejecute en un hipervisor se realiza a través de SSH. XPipe puede iniciar automáticamente el servidor SSH instalado cuando sea necesario. Si no quieres esto por razones de seguridad, puedes desactivar este comportamiento con esta opción.\nconfirmGitShareTitle=Sincronización Git\nconfirmGitShareContent=¿Quieres añadir el archivo seleccionado a tu repositorio git vault? Esto copiará una versión encriptada del archivo en tu bóveda git y confirmará tus cambios. Entonces tendrás acceso al archivo en todos los escritorios sincronizados.\ngitShareFileTooltip=Añade un archivo al directorio de datos de la bóveda git para que se sincronice automáticamente.\\n\\nEsta acción sólo puede utilizarse cuando la bóveda git está activada en los ajustes.\nperformanceMode=Modo de funcionamiento\nperformanceModeDescription=Desactiva todos los efectos visuales que no sean necesarios para mejorar el rendimiento de la aplicación.\ndontAcceptNewHostKeys=No aceptar automáticamente nuevas claves de host SSH\ndontAcceptNewHostKeysDescription=XPipe aceptará automáticamente por defecto claves de host de sistemas en los que su cliente SSH no tenga ya guardada ninguna clave de host conocida. Sin embargo, si alguna clave de host conocida ha cambiado, se negará a conectarse a menos que aceptes la nueva.\\n\\nDesactivar este comportamiento te permite comprobar todas las claves de host, aunque inicialmente no haya ningún conflicto.\nuiScale=Escala de IU\nuiScaleDescription=Un valor de escala personalizado que puede establecerse independientemente de la escala de visualización de todo el sistema. Los valores están en porcentaje, por lo que, por ejemplo, un valor de 150 dará como resultado una escala de interfaz de usuario del 150%.\neditorProgram=Programa Editor\neditorProgramDescription=El editor de texto predeterminado que se utiliza al editar cualquier tipo de datos de texto.\nwindowOpacity=Opacidad de la ventana\nwindowOpacityDescription=Cambia la opacidad de la ventana para controlar lo que ocurre en segundo plano.\nuseSystemFont=Utiliza la fuente del sistema\nopenDataDir=Directorio de datos de la bóveda\nopenDataDirButton=Directorio de datos abierto\nopenDataDirDescription=Si quieres sincronizar archivos adicionales, como claves SSH, entre sistemas con tu repositorio git, puedes ponerlos en el directorio de datos de almacenamiento. Cualquier archivo al que se haga referencia allí tendrá sus rutas de archivo adaptadas automáticamente en cualquier sistema sincronizado.\nupdates=Actualiza\nselectAll=Seleccionar todo\nadvanced=Avanzado\nthirdParty=Avisos de código abierto\neulaDescription=Lee el Contrato de Licencia de Usuario Final de la aplicación XPipe\nthirdPartyDescription=Ver las licencias de código abierto de bibliotecas de terceros\nworkspaceLock=Frase de contraseña maestra\nenableGitStorage=Activar la sincronización\nsharing=Compartir\ngitSync=Sincronización Git\nenableGitStorageDescription=Cuando está activado, XPipe inicializará un repositorio git para la bóveda local y consignará en él cualquier cambio. Ten en cuenta que esto requiere que git esté instalado y puede ralentizar las operaciones de carga y guardado.\\n\\nLas categorías que deban sincronizarse deben marcarse explícitamente como sincronizadas.\nstorageGitRemote=URL de sincronización remota\nstorageGitRemoteDescription=Cuando se establece, XPipe extraerá automáticamente cualquier cambio al cargar y empujará cualquier cambio al repositorio remoto al guardar.\\n\\nEsto te permite compartir tu bóveda entre varias instalaciones de XPipe. Admite URL HTTP y SSH, además de directorios locales.\nvault=Bóveda\nworkspaceLockDescription=Establece una contraseña personalizada para encriptar cualquier información sensible almacenada en XPipe.\\n\\nEsto aumenta la seguridad, ya que proporciona una capa adicional de encriptación para tu información sensible almacenada. Se te pedirá que introduzcas la contraseña cuando se inicie XPipe.\nuseSystemFontDescription=Controla si se utiliza la fuente predeterminada del sistema o la fuente Inter, que se incluye con XPipe.\ntooltipDelay=Retraso de la información sobre herramientas\ntooltipDelayDescription=La cantidad de milisegundos que hay que esperar hasta que se muestre una información sobre herramientas.\nfontSize=Tamaño de letra\nwindowOptions=Opciones de ventana\nsaveWindowLocation=Guardar ubicación de la ventana\nsaveWindowLocationDescription=Controla si las coordenadas de la ventana deben guardarse y restaurarse al reiniciar.\nstartupShutdown=Inicio / Apagado\nshowChildrenConnectionsInParentCategory=Mostrar categorías hijas en la categoría padre\nshowChildrenConnectionsInParentCategoryDescription=Incluir o no todas las conexiones situadas en subcategorías cuando se selecciona una determinada categoría padre.\\n\\nSi se desactiva, las categorías se comportan más como carpetas clásicas que sólo muestran su contenido directo sin incluir las subcarpetas.\ncondenseConnectionDisplay=Condensar la visualización de la conexión\ncondenseConnectionDisplayDescription=Haz que cada conexión de nivel superior ocupe menos espacio vertical para permitir una lista de conexiones más condensada.\nopenConnectionSearchWindowOnConnectionCreation=Abrir la ventana de búsqueda de conexión al crear la conexión\nopenConnectionSearchWindowOnConnectionCreationDescription=Si abrir o no automáticamente la ventana de búsqueda de subconexiones disponibles al añadir una nueva conexión shell.\nworkflow=Flujo de trabajo\nsystem=Sistema\napplication=Aplicación\nstorage=Almacenamiento\nrunOnStartup=Ejecutar al inicio\ncloseBehaviour=Comportamiento de salida\ncloseBehaviourDescription=Controla cómo debe proceder XPipe al cerrar su ventana principal.\nlanguage=Idioma\nlanguageDescription=El idioma de visualización a utilizar. Las traducciones se mejoran gracias a las contribuciones de la comunidad. Puedes ayudar al esfuerzo de traducción enviando correcciones en GitHub.\nlightTheme=Tema Luz\ndarkTheme=Tema oscuro\nexit=Salir de XPipe\ncontinueInBackground=Continuar en segundo plano\nminimizeToTray=Minimizar a la bandeja\ncloseBehaviourAlertTitle=Establecer el comportamiento de cierre\ncloseBehaviourAlertTitleHeader=Selecciona lo que debe ocurrir al cerrar la ventana. Cualquier conexión activa se cerrará cuando se cierre la aplicación.\nstartupBehaviour=Comportamiento de inicio\nstartupBehaviourDescription=Controla el comportamiento por defecto de la aplicación de escritorio cuando se inicia XPipe.\nclearCachesAlertTitle=Limpiar caché\nclearCachesAlertContent=¿Quieres limpiar todas las cachés de XPipe? Esto eliminará todos los datos de la caché que se almacenan para mejorar la experiencia del usuario.\nstartGui=Iniciar GUI\nstartInTray=Inicio en bandeja\nstartInBackground=Iniciar en segundo plano\nclearCaches=Borrar cachés ...\nclearCachesDescription=Borrar todos los datos de la caché\ncancel=Cancelar\nnotAnAbsolutePath=No es una ruta absoluta\nnotADirectory=No es un directorio\nnotAnEmptyDirectory=No es un directorio vacío\nautomaticallyCheckForUpdates=Buscar actualizaciones\nautomaticallyCheckForUpdatesDescription=Cuando está activada, la información de las nuevas versiones se obtiene automáticamente mientras XPipe se está ejecutando al cabo de un rato. Aún así, tienes que confirmar explícitamente la instalación de cualquier actualización.\nsendAnonymousErrorReports=Enviar informes de error anónimos\nsendUsageStatistics=Enviar estadísticas de uso anónimas\nstorageDirectory=Directorio de almacenamiento\nstorageDirectoryDescription=La ubicación donde XPipe debe almacenar toda la información de conexión. Al cambiar esto, los datos del directorio antiguo no se copian en el nuevo.\nlogLevel=Nivel de registro\nappBehaviour=Comportamiento de la aplicación\nlogLevelDescription=El nivel de registro que debe utilizarse al escribir archivos de registro.\ndeveloperMode=Modo desarrollador\ndeveloperModeDescription=Cuando esté activado, tendrás acceso a una serie de opciones adicionales útiles para el desarrollo.\neditor=Editor\ncustom=Personalizado\npasswordManager=Gestor de contraseñas\nexternalPasswordManager=Gestor de contraseñas externo\npasswordManagerDescription=El gestor de contraseñas instalado localmente con el que integrarse.\\n\\nSi tienes instalado un gestor de contraseñas, puedes configurar XPipe para que recupere las contraseñas de él, de modo que XPipe no tenga que almacenarlas por sí mismo. Una vez activado, cualquier campo de contraseña de una conexión puede configurarse para utilizar el gestor de contraseñas.\npasswordManagerCommandTest=Gestor de contraseñas de prueba\npasswordManagerCommandTestDescription=Aquí puedes comprobar si la salida parece correcta si has configurado un gestor de contraseñas.\npreferTerminalTabs=Prefiero abrir nuevas pestañas\npreferTerminalTabsDescription=Controla si XPipe intentará abrir nuevas pestañas en el terminal que elijas en lugar de nuevas ventanas. No todos los terminales admiten pestañas.\ncustomRdpClientCommand=Comando personalizado\ncustomRdpClientCommandDescription=El comando a ejecutar para iniciar el cliente RDP personalizado.\\n\\nLa cadena $FILE se sustituirá por el nombre absoluto del archivo .rdp entre comillas cuando se ejecute. Recuerda entrecomillar la ruta de tu ejecutable si contiene espacios.\ncustomEditorCommand=Comando de editor personalizado\ncustomEditorCommandDescription=El comando a ejecutar para iniciar el editor personalizado.\\n\\nLa cadena de texto $FICHERO se sustituirá por el nombre absoluto del archivo entre comillas cuando se ejecute. Recuerda entrecomillar la ruta ejecutable de tu editor si contiene espacios.\neditorReloadTimeout=Tiempo de espera de recarga del editor\neditorReloadTimeoutDescription=La cantidad de milisegundos que hay que esperar antes de leer un archivo después de que se haya actualizado. Esto evita problemas en los casos en que tu editor sea lento escribiendo o liberando bloqueos de archivos.\nencryptAllVaultData=Cifra todos los datos de la caja fuerte\nencryptAllVaultDataDescription=Cuando está activada, cada parte de los datos de conexión de la bóveda se encriptará con tu clave de encriptación de la bóveda de usuario, en lugar de sólo los secretos que contengan esos datos. Esto añade otra capa de seguridad para otros parámetros como nombres de usuario, nombres de host, etc., que no están encriptados por defecto en la bóveda.\\n\\nEsta opción hará que el historial y los diffs de tu bóveda git sean inútiles, ya que no podrás ver los cambios originales, sólo los cambios binarios.\nvaultSecurity=Bóveda de seguridad\ndeveloperDisableUpdateVersionCheck=Desactivar la comprobación de la versión de actualización\ndeveloperDisableUpdateVersionCheckDescription=Controla si el comprobador de actualizaciones ignorará el número de versión al buscar una actualización.\ndeveloperDisableGuiRestrictions=Desactivar las restricciones de la GUI\ndeveloperDisableGuiRestrictionsDescription=Controla si algunas acciones desactivadas pueden seguir ejecutándose desde la interfaz de usuario.\ndeveloperShowHiddenEntries=Mostrar entradas ocultas\ndeveloperShowHiddenEntriesDescription=Cuando esté activado, se mostrarán las fuentes de datos ocultas e internas.\ndeveloperShowHiddenProviders=Mostrar proveedores ocultos\ndeveloperShowHiddenProvidersDescription=Controla si los proveedores ocultos e internos de conexión y fuente de datos se mostrarán en el diálogo de creación.\ndeveloperDisableConnectorInstallationVersionCheck=Desactivar la comprobación de la versión del conector\ndeveloperDisableConnectorInstallationVersionCheckDescription=Controla si el comprobador de actualizaciones ignorará el número de versión al inspeccionar la versión de un conector XPipe instalado en una máquina remota.\nshellCommandTest=Prueba de comandos Shell\nshellCommandTestDescription=Ejecuta un comando en la sesión shell utilizada internamente por XPipe.\nterminal=Terminal\nterminalType=Emulador de terminal\nterminalConfiguration=Configuración del terminal\nterminalCustomization=Personalización del terminal\neditorConfiguration=Configuración del editor\ndefaultApplication=Aplicación por defecto\ninitialSetup=Configuración inicial\nterminalTypeDescription=El terminal por defecto a utilizar para abrir conexiones shell.\\n\\nEl nivel de soporte de funciones varía según el terminal, y cada uno está marcado como recomendado o no recomendado. Tu experiencia de usuario será mejor si utilizas un terminal recomendado.\nprogram=Programa\ncustomTerminalCommand=Comando de terminal personalizado\ncustomTerminalCommandDescription=El comando a ejecutar para abrir el terminal personalizado con un comando determinado.\\n\\nXPipe creará un script shell lanzador temporal para que lo ejecute tu terminal. La cadena $CMD del marcador de posición del comando que proporciones será sustituida por el script lanzador real cuando sea llamado. Recuerda entrecomillar la ruta ejecutable de tu terminal si contiene espacios.\nclearTerminalOnInit=Borrar terminal al iniciar\nclearTerminalOnInitDescription=Cuando está activado, XPipe ejecutará un comando de borrado después de iniciar una nueva sesión de terminal para eliminar cualquier salida innecesaria que se haya impreso al iniciar la sesión de terminal.\ndontCachePasswords=No almacenar en caché las contraseñas solicitadas\ndontCachePasswordsDescription=Controla si las contraseñas consultadas deben ser cacheadas internamente por XPipe para que no tengas que introducirlas de nuevo en la sesión actual.\\n\\nSi este comportamiento está desactivado, tendrás que volver a introducir las credenciales solicitadas cada vez que sean requeridas por el sistema.\ndenyTempScriptCreation=Denegar la creación de un script temporal\ndenyTempScriptCreationDescription=Para realizar algunas de sus funciones, XPipe a veces crea scripts shell temporales en un sistema de destino para permitir una fácil ejecución de comandos sencillos. Éstos no contienen ninguna información sensible y sólo se crean con fines de implementación.\\n\\nSi se desactiva este comportamiento, XPipe no creará ningún archivo temporal en un sistema remoto. Esta opción es útil en contextos de alta seguridad en los que se supervisa cada cambio en el sistema de archivos. Si se desactiva, algunas funcionalidades, como los entornos shell y los scripts, no funcionarán como está previsto.\ndisableCertutilUse=Desactivar el uso de certutil en Windows\nuseLocalFallbackShell=Utilizar shell local de reserva\nuseLocalFallbackShellDescription=Pasa a utilizar otro shell local para gestionar las operaciones locales. Esto sería PowerShell en Windows y bourne shell en otros sistemas.\\n\\nEsta opción puede utilizarse en caso de que el shell local normal por defecto esté desactivado o roto en algún grado. Sin embargo, algunas funciones pueden no funcionar como se espera cuando esta opción está activada.\ndisableCertutilUseDescription=Debido a varias deficiencias y errores de cmd.exe, se crean scripts shell temporales con certutil utilizándolo para descodificar la entrada base64, ya que cmd.exe se rompe con la entrada no ASCII. XPipe también puede utilizar PowerShell para ello, pero será más lento.\\n\\nEsto deshabilita cualquier uso de certutil en sistemas Windows para realizar algunas funciones y recurrir a PowerShell en su lugar. Esto podría complacer a algunos antivirus, ya que algunos bloquean el uso de certutil.\ndisableTerminalRemotePasswordPreparation=Desactivar la preparación de la contraseña remota del terminal\ndisableTerminalRemotePasswordPreparationDescription=En situaciones en las que deba establecerse en el terminal una conexión shell remota que atraviese varios sistemas intermedios, puede ser necesario preparar las contraseñas necesarias en uno de los sistemas intermedios para permitir la cumplimentación automática de cualquier solicitud.\\n\\nSi no quieres que las contraseñas se transfieran nunca a ningún sistema intermedio, puedes desactivar este comportamiento. Cualquier contraseña intermedia requerida se consultará entonces en el propio terminal cuando se abra.\nmore=Más\ntranslate=Traducciones\nallConnections=Todas las conexiones\nallScripts=Todos los guiones\nallIdentities=Todas las identidades\nsynced=Sincronizado\npredefined=Predefinido\nsamples=Muestras\ngoodMorning=Buenos días\ngoodAfternoon=Buenas tardes\ngoodEvening=Buenas tardes\naddVisual=Visual ...\naddDesktop=Escritorio ...\nssh=SSH\nsshConfiguration=Configuración SSH\nsize=Tamaño\nattributes=Atributos\nmodified=Modificado\nowner=Propietario\nupdateReadyTitle=Actualización a $VERSION$ ready\ntemplates=Plantillas\nretry=Reintentar\nretryAll=Reintentar todo\nreplace=Sustituye\nreplaceAll=Sustituir todo\nhibernateBehaviour=Comportamiento de hibernación\nhibernateBehaviourDescription=Controla cómo se comporta la aplicación cuando tu sistema se pone en hibernación/en reposo.\noverview=Visión general\nhistory=Historia\nskipAll=Saltar todo\nnotes=Notas\naddNotes=Añadir notas\norder=Reordenar\nkeepFirst=Mantener primero\nkeepLast=Mantener último\npinToTop=Pin arriba\nunpinFromTop=Desenganchar desde arriba\norderAheadOf=Haz tu pedido antes de ...\nclearIndex=Restablecer índice\nhttpServer=Servidor HTTP\nmcpServer=Servidor MCP\napiKey=Clave API\napiKeyDescription=La clave API para autenticar las peticiones API del demonio XPipe. Para más información sobre cómo autenticarse, consulta la documentación general de la API.\ndisableApiAuthentication=Desactivar la autenticación de la API\ndisableApiAuthenticationDescription=Desactiva todos los métodos de autenticación requeridos para que se gestione cualquier solicitud no autenticada.\\n\\nLa autenticación sólo debe desactivarse con fines de desarrollo.\napi=API\nstoreIntroImportContent=¿Ya utilizas XPipe en otro sistema? Sincroniza tus conexiones existentes en varios sistemas a través de un repositorio git remoto. También puedes sincronizarlo posteriormente en cualquier momento si aún no está configurado.\nstoreIntroImportButton=Sincronizar conexiones ...\nstoreIntroImportHeader=Importar conexiones\nshowNonRunningChildren=Mostrar niños no ejecutantes\nhttpApi=API HTTP\nisOnlySupportedLimit=sólo es compatible con una licencia profesional cuando tiene más de $COUNT$ conexiones\nareOnlySupportedLimit=sólo son compatibles con una licencia profesional cuando tienen más de $COUNT$ conexiones\nenabled=Activado\nenableGitStoragePtbDisabled=La sincronización Git está desactivada para las compilaciones públicas de prueba, para evitar que se utilicen con los repositorios git de publicación regular y para desalentar el uso de una compilación PTB como tu conductor diario.\ncopyId=Copiar ID de API\nrequireDoubleClickForConnections=Requiere doble clic para las conexiones\nrequireDoubleClickForConnectionsDescription=Si está activado, tienes que hacer doble clic en las conexiones para iniciarlas. Esto es útil si estás acostumbrado a hacer doble clic en las cosas.\nclearTransferDescription=Borrar selección\nselectTab=Seleccionar pestaña\ncloseTab=Cerrar pestaña\ncloseOtherTabs=Cerrar otras pestañas\ncloseAllTabs=Cerrar todas las pestañas\ncloseLeftTabs=Cerrar pestañas a la izquierda\ncloseRightTabs=Cerrar pestañas a la derecha\naddSerial=Serie ...\nconnect=Conecta\nworkspaces=Espacios de trabajo\nmanageWorkspaces=Gestionar espacios de trabajo\naddWorkspace=Añadir espacio de trabajo ...\nworkspaceAdd=Añadir un nuevo espacio de trabajo\nworkspaceAddDescription=Los espacios de trabajo son configuraciones distintas para ejecutar XPipe. Cada espacio de trabajo tiene un directorio de datos donde se almacenan localmente todos los datos. Esto incluye datos de conexión, configuraciones y más.\\n\\nSi utilizas la función de sincronización, también puedes elegir sincronizar cada espacio de trabajo con un repositorio git diferente.\nworkspaceName=Nombre del espacio de trabajo\nworkspaceNameDescription=El nombre para mostrar del espacio de trabajo\nworkspacePath=Ruta del espacio de trabajo\nworkspacePathDescription=La ubicación del directorio de datos del espacio de trabajo\nworkspaceCreationAlertTitle=Creación de espacios de trabajo\ndeveloperForceSshTty=Forzar SSH TTY\ndeveloperForceSshTtyDescription=Haz que todas las conexiones SSH asignen una pty para probar la compatibilidad con una stderr y una pty ausentes.\ndeveloperDisableSshTunnelGateways=Desactivar el túnel de puerta de enlace SSH\ndeveloperDisableSshTunnelGatewaysDescription=No utilices sesiones de túnel para las pasarelas y, en su lugar, conéctate directamente al sistema.\nttyWarning=La conexión ha asignado forzosamente un pty/tty y no proporciona un flujo stderr separado.\\n\\nEsto puede provocar algunos problemas.\\n\\nSi puedes, intenta que el comando de conexión no asigne una pty.\nxshellSetup=Configuración de Xshell\ntermiusSetup=Configuración de Termius\ntryPtbDescription=Prueba nuevas funciones antes en las versiones para desarrolladores de XPipe\nconfirmVaultUnencryptTitle=Confirma la desencriptación de la bóveda\nconfirmVaultUnencryptContent=¿Realmente quieres desactivar la encriptación avanzada de la bóveda? Esto eliminará la encriptación adicional para los datos almacenados y sobrescribirá los datos existentes.\nenableHttpApi=Activar la API HTTP\nenableHttpApiDescription=Habilita la API, permitiendo que programas externos llamen al demonio XPipe para realizar acciones con tus conexiones gestionadas.\nchooseCustomIcon=Elegir icono personalizado\ngitVault=Bóveda Git\nfileBrowser=Navegador de archivos\nconfirmAllDeletions=Confirmar todos los borrados\nconfirmAllDeletionsDescription=Si mostrar un diálogo de confirmación para todas las operaciones de borrado. Por defecto, sólo los directorios requieren una confirmación.\nyesterday=Ayer\ngreen=Verde\nyellow=Amarillo\nblue=Azul\nred=Rojo\ncyan=Cian\npurple=Morado\nasktextAlertTitle=Pregunta\nfileWriteSudoTitle=Escritura de archivos Sudo\nfileWriteSudoContent=El archivo que intentas escribir no concede permisos de escritura a tu usuario. ¿Quieres escribir este archivo como root con sudo? Esto elevará automáticamente a root con las credenciales existentes o a través de un prompt.\ndontAllowTerminalRestart=No permitir el reinicio del terminal\ndontAllowTerminalRestartDescription=Por defecto, las sesiones de terminal pueden reiniciarse una vez finalizadas desde dentro del terminal. Para permitirlo, XPipe aceptará estas peticiones externas del terminal para iniciar de nuevo la sesión\\n\\nXPipe no tiene ningún control sobre el terminal y de dónde procede esta llamada, por lo que las aplicaciones locales maliciosas también pueden utilizar esta funcionalidad para lanzar conexiones a través de XPipe. Deshabilitar esta funcionalidad evita este escenario.\nopenDocumentation=Abrir documentación\nopenDocumentationDescription=Visita la página de documentación de XPipe sobre este tema\nrenameAll=Renombrar todo\nlogging=Registro\nenableTerminalLogging=Activar el registro de terminal\nenableTerminalLoggingDescription=Activa el registro del lado del cliente para todas las sesiones de terminal. Todas las entradas y salidas de la sesión de terminal se escriben en un archivo de registro de sesión. Ten en cuenta que no se registra ninguna información sensible, como las solicitudes de contraseña.\nterminalLoggingDirectory=Registros de sesión de terminal\nterminalLoggingDirectoryDescription=Todos los registros se almacenan en el directorio de datos de XPipe en tu sistema local.\nopenSessionLogs=Registros de sesión abiertos\nsessionLogging=Registro de terminal\nsessionActive=Se está ejecutando una sesión en segundo plano para esta conexión.\\n\\nPara detener esta sesión manualmente, pulsa sobre el indicador de estado.\nskipValidation=Omitir validación\nscriptsIntroHeader=Acerca de los guiones\nscriptsIntroContent=Puedes ejecutar scripts en shell init, en el explorador de archivos y bajo demanda. Puedes crear scripts tú mismo dentro de XPipe o importar los existentes desde tu sistema local o desde un repositorio git remoto.\nscriptsIntroBottomHeader=Utilizar guiones\nscriptsIntroBottomContent=Hay una variedad de scripts de ejemplo para empezar. Puedes hacer clic en el botón de edición de los scripts individuales para ver cómo se implementan. Primero hay que habilitar los scripts para que se ejecuten y aparezcan en los menús, para ello hay un conmutador en cada script.\nscriptsIntroBottomButton=Empezar\nscriptSourcesIntroHeader=Fuentes de script\nscriptSourcesIntroContent=Puedes añadir fuentes de scripts personalizadas para tener acceso instantáneo a una colección completa de scripts de shell. Se admiten como fuentes tanto fuentes locales como repositorios git remotos. Todos los scripts detectados de la fuente estarán disponibles automáticamente.\nscriptSourcesIntroButton=Añadir fuente ...\ncheckForSecurityUpdates=Buscar actualizaciones de seguridad\ncheckForSecurityUpdatesDescription=XPipe puede buscar posibles actualizaciones de seguridad separadamente de las actualizaciones normales de funciones. Cuando esto está activado, se recomendará la instalación de al menos las actualizaciones de seguridad importantes, incluso si la comprobación de actualizaciones normales está desactivada.\\n\\nSi desactivas esta opción, no se realizará ninguna solicitud de versión externa y no se te notificará ninguna actualización de seguridad.\nclickToDock=Haz clic para acoplar el terminal\nterminalStarting=Esperando el inicio del terminal ...\npinTab=Pestaña pin\nunpinTab=Desanclar pestaña\npinned=Fijado\nenableConnectionHubTerminalDocking=Habilitar el acoplamiento del terminal del concentrador de conexiones\nenableConnectionHubTerminalDockingDescription=Puedes acoplar ventanas de terminal a la ventana de la aplicación XPipe en el concentrador de conexiones para simular un terminal algo integrado. Las ventanas de terminal son entonces gestionadas por XPipe para que siempre quepan en el dock.\nenableFileBrowserTerminalDocking=Activar el acoplamiento de terminales del explorador de archivos\nenableFileBrowserTerminalDockingDescription=Puedes acoplar ventanas de terminal a la ventana de la aplicación XPipe en el explorador de archivos para simular un terminal algo integrado. Las ventanas de terminal son entonces gestionadas por XPipe para que siempre quepan en el dock.\ndownloadsDirectory=Directorio de descargas personalizado\ndownloadsDirectoryDescription=El directorio personalizado en el que colocar los archivos descargados al pulsar el botón mover a descargas. Por defecto, XPipe utilizará tu directorio de descargas de usuario.\npinLocalMachineOnStartup=Fijar la pestaña de máquina local al iniciar\npinLocalMachineOnStartupDescription=Abre automáticamente una pestaña de la máquina local y fíjala. Esto es útil si utilizas con frecuencia un explorador de archivos dividido con la máquina local y el sistema de archivos remoto abiertos.\nterminalErrorDescription=Este error es terminal y XPipe no puede continuar sin solucionarlo.\ngroupName=Nombre del grupo\nchmodPermissions=Nuevos permisos\neditFilesWithDoubleClick=Editar archivos con doble clic\neditFilesWithDoubleClickDescription=Cuando está activado, al hacer doble clic en los archivos se abrirán directamente en tu editor de texto en lugar de mostrar el menú contextual.\ncensorMode=Modo censor\ncensorModeDescription=Difumina cualquier información como nombres de host, nombres de usuario, nombres de conexión, etc.\\n\\nEsto es útil si pretendes hacer una captura de pantalla o compartir la pantalla de XPipe y no quieres filtrar ninguna información.\naddIdentity=Identidad ...\nidentities=Identidades\naddMacro=Acción ...\nidentitiesIntroHeader=Acerca de las identidades\nidentitiesIntroContent=Si reutilizas combinaciones comunes de nombres de usuario, contraseñas y claves, puede tener sentido crear identidades reutilizables. Esto te permite referenciarlas rápidamente al añadir nuevas conexiones.\nidentitiesIntroBottomHeader=Compartir identidades\nidentitiesIntroBottomContent=Puedes añadir identidades localmente o también sincronizarlas en el repositorio git cuando esté activado. Esto permite compartir identidades de forma selectiva en varios sistemas y con otros miembros del equipo.\nidentitiesIntroBottomButton=Configurar sincronización\nidentitiesIntroButton=Crear identidad\nuserName=Nombre de usuario\nuserAuth=Autenticación de contraseña basada en el usuario\ngroupAuth=Autenticación secreta basada en grupos\nteam=Equipo\nteamSettings=Configuración del equipo\nteamVaults=Bóvedas de equipo\nvaultTypeNameDefault=Caja fuerte por defecto\nvaultTypeNameLegacy=Bóveda personal heredada\nvaultTypeNamePersonal=Caja fuerte personal\nvaultTypeNameTeam=Bóveda de equipo\nteamVaultsDescription=Las bóvedas de equipo permiten que varios usuarios y grupos tengan acceso seguro a una bóveda compartida. Puedes configurar las conexiones e identidades para que sean compartidas por todos los usuarios o para que sólo estén disponibles para usuarios individuales y grupos, encriptándolas con su propia clave. Los demás usuarios del almacén no pueden acceder a las conexiones e identidades personales y de grupo si no tienen acceso a la clave.\nvaultTypeContentDefault=Actualmente utilizas una bóveda por defecto sin usuario y con una frase de contraseña personalizada. Los secretos se encriptan con la clave local de la bóveda. Puedes actualizar a una bóveda personal creando una cuenta de usuario de bóveda. Esto te permite encriptar los secretos de la caja fuerte con tu propia frase de contraseña personal, que tendrás que introducir en cada inicio de sesión para desbloquear la caja fuerte.\nvaultTypeContentLegacy=Actualmente utilizas una bóveda personal heredada para tu usuario. Los secretos se encriptan con tu frase de contraseña personal. Esta compatibilidad heredada tiene funciones limitadas y no puede actualizarse a una bóveda de equipo in situ.\nvaultTypeContentPersonal=Actualmente utilizas una caja fuerte personal para tu usuario. Los secretos se encriptan con tu frase de contraseña personal. Puedes pasar a una bóveda de equipo añadiendo usuarios de bóveda adicionales o añadiendo una configuración de acceso basada en grupos.\nvaultTypeContentTeam=Actualmente utilizas una bóveda de equipo, que permite que varios usuarios tengan acceso seguro a una bóveda compartida. Puedes configurar las conexiones e identidades para que sean compartidas por todos los usuarios o para que sólo estén disponibles para tu usuario personal o grupo, encriptándolas con tu clave personal o de grupo. Los demás usuarios de la bóveda no pueden acceder a tus conexiones e identidades personales y de grupo si no tienen acceso a la clave.\ngroupManagement=Gestión de grupos\ngroupManagementEmpty=Gestión de grupos\ngroupManagementDescription=Gestiona los grupos de bóvedas existentes o crea otros nuevos. Cada grupo de bóvedas tiene su propia clave secreta individual, que se utiliza para encriptar conexiones e identidades que sólo deben estar disponibles para el grupo y no para los demás.\ngroupManagementEmptyDescription=Gestiona los grupos de bóvedas existentes o crea otros nuevos. Cada grupo de bóvedas tiene su propia clave secreta individual, que se utiliza para encriptar conexiones e identidades que sólo deben estar disponibles para el grupo y no para los demás.\\n\\nLas cuentas basadas en grupos para un equipo son compatibles con el plan profesional.\nuserManagement=Gestión de usuarios\nuserManagementEmpty=Gestión de usuarios\nuserManagementDescription=Gestiona los usuarios de bóveda existentes o crea otros nuevos. Cada usuario de la bóveda tiene su propia contraseña individual, que se utiliza para encriptar conexiones e identidades que sólo deben estar disponibles para el usuario y no para otros.\nuserManagementEmptyDescription=Gestiona los usuarios de bóveda existentes o crea otros nuevos. Cada usuario de la bóveda tiene su propia clave individual que se utiliza para encriptar conexiones e identidades que sólo deben estar disponibles para el usuario y no para otros. Crea un usuario para ti para poder encriptar conexiones e identidades con tu clave personal.\\n\\nLa edición comunitaria admite una sola cuenta de usuario. El plan profesional admite varias cuentas de usuario para un equipo.\nuserIntroHeader=Gestión de usuarios\nuserIntroContent=Crea la primera cuenta de usuario para ti para empezar. Esto te permite bloquear este espacio de trabajo con una contraseña.\naddReusableIdentity=Añadir identidad reutilizable\nusers=Usuarios\nsyncVault=Sincronización de bóvedas\nsyncVaultDescription=Para sincronizar tu bóveda con varios sistemas o con varios miembros del equipo, activa la sincronización git para esta bóveda.\nenableGitSync=Activar git sync\nbrowseVault=Datos de bóveda\nbrowseVaultDescription=Puedes echar un vistazo al directorio de la bóveda tú mismo en tu gestor de archivos nativo. Ten en cuenta que las ediciones externas no son recomendables y pueden causar diversos problemas.\nbrowseVaultButton=Navegar por la bóveda\nvaultUsers=Usuarios de bóvedas\ncreateHeapDump=Crear volcado de heap\ncreateHeapDumpDescription=Volcar el contenido de la memoria a un archivo para solucionar problemas de uso de memoria\ninitializingApp=Carga de conexiones\ncheckingLicense=Comprobación de licencia\nloadingGit=Sincronización con git repo\nloadingGpg=Iniciar el demonio GnuPG para git\nloadingSettings=Configuración de carga\nloadingConnections=Carga de conexiones\nunlockingVault=Abrir la caja fuerte\nloadingUserInterface=Carga de la interfaz de usuario\nptbNotice=Aviso para la versión de prueba pública\nuserDeletionTitle=Eliminación de usuarios\nuserDeletionContent=¿Quieres eliminar este usuario de la bóveda? Esto volverá a encriptar todas tus identidades personales y secretos de conexión utilizando la clave de la bóveda que está disponible para todos los usuarios. Esto llevará un tiempo y XPipe se reiniciará para aplicar los cambios de usuario.\ngroupDeletionTitle=Eliminación de grupos\ngroupDeletionContent=¿Quieres eliminar este grupo de bóveda? Esto volverá a encriptar todas las identidades exclusivas del grupo y los secretos de conexión utilizando la clave de bóveda que está disponible para todos los usuarios. Esto llevará un tiempo y XPipe se reiniciará para aplicar los cambios de grupo.\nkillTransfer=Matar transferencia\ndestination=Destino\nconfiguration=Configuración\nnewFile=Nuevo archivo\nnewLink=Nuevo enlace\nlinkName=Nombre del enlace\nscanConnections=Encontrar conexiones disponibles ...\nobserve=Empieza a observar\nstopObserve=Deja de observar\ncreateShortcut=Crear acceso directo en el escritorio\nbrowseFiles=Examinar archivos\nclone=Clonar\ntargetPath=Ruta objetivo\nnewDirectory=Nuevo directorio\ncopyShareLink=Copiar enlace\nselectStore=Seleccionar tienda\nsaveSource=Guardar para más tarde\nexecute=Ejecuta\ndeleteChildren=Eliminar todos los niños\nscriptGroupDescriptionDescription=Dale a este grupo una descripción opcional\nabstractHostDescriptionDescription=Dale a este host una descripción opcional\nselectSource=Seleccionar fuente\ncommandLineRead=Actualiza\ncommandLineWrite=Escribe\nadditionalOptions=Opciones adicionales\ninput=Entrada\nmachine=Máquina\nopen=Abre\nedit=Edita\nscriptContents=Contenido del guión\nscriptContentsDescription=Los comandos de script a ejecutar\nsnippets=Dependencias del script\nsnippetsDescription=Otros scripts para ejecutar primero\nsnippetsDependenciesDescription=Todas las posibles secuencias de comandos que deban ejecutarse, si procede\nisDefault=Se ejecuta en init en todos los shells compatibles\nbringToShells=Lleva a todos los shells compatibles\nisDefaultGroup=Ejecutar todos los scripts de grupo en shell init\nexecutionType=Tipo de ejecución\nexecutionTypeDescription=En qué contextos utilizar este script\nminimumShellDialect=Tipo de shell\nminimumShellDialectDescription=El tipo de shell en el que ejecutar este script\ndumbOnly=Tonto\nterminalOnly=Terminal\nboth=Ambos\nshouldElevate=Debe elevar\nshouldElevateDescription=Si ejecutar este script con permisos elevados\nscript.displayName=Script de shell\nscript.displayDescription=Crea un script de shell reutilizable\nscriptGroup.displayName=Grupo de guiones\nscriptGroup.displayDescription=Agrupa guiones y organízalos dentro de\nscriptGroup=Grupo\nscriptGroupDescription=El grupo al que asignar este script\nscriptGroupGroupDescription=El grupo padre opcional al que asignar este grupo de guión\nopenInNewTab=Abrir en pestaña nueva\nexecuteInBackground=en segundo plano\nexecuteInTerminal=en $TERM$\nback=Volver atrás\nbrowseInWindowsExplorer=Navegar en el explorador de Windows\nbrowseInDefaultFileManager=Navegar en el gestor de archivos por defecto\nbrowseInFinder=Navegar en el buscador\ncopy=Copia\npaste=Pegar\ncopyLocation=Copiar ubicación\nabsolutePaths=Rutas absolutas\nabsoluteLinkPaths=Rutas de enlace absolutas\nabsolutePathsQuoted=Rutas entre comillas absolutas\nfileNames=Nombres de archivo\nlinkFileNames=Enlazar nombres de archivos\nfileNamesQuoted=Nombres de archivo (entre comillas)\ndeleteFile=Borrar $FILE$\neditWithEditor=Edita con $EDITOR$\nfollowLink=Seguir enlace\ngoForward=Avanzar\nshowDetails=Mostrar detalles\nshowDetailsDescription=Mostrar la pila de errores\nopenFileWith=Abrir con ...\nopenWithDefaultApplication=Abrir con la aplicación por defecto\nrename=Cambia el nombre de\nrun=Ejecuta\nopenInTerminal=Abrir en terminal\nfile=Archivo\ndirectory=Directorio\nsymbolicLink=Enlace simbólico\ndesktopEnvironment.displayName=Entorno de escritorio\ndesktopEnvironment.displayDescription=Crear una configuración de entorno de escritorio remoto reutilizable\ndesktopHost=Host de escritorio\ndesktopHostDescription=La conexión de escritorio a utilizar como base\ndesktopShellDialect=Dialecto shell\ndesktopShellDialectDescription=El dialecto del shell a utilizar para ejecutar scripts y aplicaciones\ndesktopSnippets=Fragmentos de guión\ndesktopSnippetsDescription=Lista de fragmentos de script reutilizables para ejecutar primero\ndesktopInitScript=Script de inicio\ndesktopInitScriptDescription=Comandos Init específicos de este entorno\ndesktopTerminal=Aplicación terminal\ndesktopTerminalDescription=El terminal a utilizar en el escritorio para iniciar scripts en\ndesktopApplication.displayName=Aplicación de escritorio\ndesktopApplication.displayDescription=Ejecutar una aplicación en un escritorio remoto\ndesktopBase=Escritorio\ndesktopBaseDescription=El escritorio en el que ejecutar esta aplicación\ndesktopEnvironmentBase=Entorno de escritorio\ndesktopEnvironmentBaseDescription=El entorno de escritorio en el que ejecutar esta aplicación\ndesktopApplicationPath=Ruta de la aplicación\ndesktopApplicationPathDescription=La ruta del ejecutable a ejecutar\ndesktopApplicationArguments=Argumentos\ndesktopApplicationArgumentsDescription=Los argumentos opcionales para pasar a la aplicación\ndesktopCommand.displayName=Comando de escritorio\ndesktopCommand.displayDescription=Ejecutar un comando en un entorno de escritorio remoto\ndesktopCommandScript=Comandos\ndesktopCommandScriptDescription=Los comandos a ejecutar en el entorno\nservice.displayName=Servicio\nservice.displayDescription=Reenviar un servicio remoto a tu máquina local\nserviceLocalPort=Puerto local explícito\nserviceLocalPortDescription=El puerto local al que reenviar, de lo contrario se utiliza uno aleatorio\nserviceRemotePort=Puerto remoto\nserviceRemotePortDescription=El puerto en el que se ejecuta el servicio\nserviceHost=Host de servicio\nserviceHostDescription=El host en el que se ejecuta el servicio\nopenWebsite=Abrir sitio web\ncustomServiceGroup.displayName=Grupo de servicios\ncustomServiceGroup.displayDescription=Agrupa varios servicios en una categoría\ninitScript=Script de init - Se ejecuta en el shell init\nshellScript=Script de sesión de shell - Hacer que un script esté disponible para ejecutarse durante una sesión de shell\nrunnableScript=Script ejecutable - Permite que el script se ejecute directamente desde el concentrador de conexiones\nfileScript=Script de archivos - Permite llamar a un script para los archivos seleccionados en el explorador de archivos\nrunScript=Ejecutar script\ncopyUrl=Copiar URL\nfixedServiceGroup.displayName=Grupo de servicios\nfixedServiceGroup.displayDescription=Enumerar los servicios disponibles en un sistema\nmappedService.displayName=Servicio\nmappedService.displayDescription=Interactúa con un servicio expuesto por un contenedor\ncustomService.displayName=Servicio\ncustomService.displayDescription=Abrir o tunelizar automáticamente un puerto de servicio remoto en tu máquina local\nfixedService.displayName=Servicio\nfixedService.displayDescription=Utilizar un servicio predefinido\nnoServices=No hay servicios disponibles\nhasServices=$COUNT$ servicios disponibles\nhasService=$COUNT$ servicio disponible\nnoConnections=No hay conexiones disponibles\nhasConnections=$COUNT$ conexiones disponibles\nhasConnection=$COUNT$ conexión disponible\nopenHttp=Servicio HTTP abierto\nopenHttps=Abrir servicio HTTPS\nnoScriptsAvailable=No hay scripts habilitados y compatibles disponibles\nscriptsDisabled=Scripts desactivados\nchangeIcon=Cambiar icono\ninit=Init\nshell=Shell\nhub=Hub\nscript=script\ngenericScript=Genérico\ngradleTasks=Tareas Gradle\nrunTask=Ejecutar tarea\narchiveName=Nombre de archivo\ncompress=Comprime\ncompressContents=Comprimir contenidos\nuntarHere=Untar aquí\nuntarDirectory=Untar a $DIR$\nunzipDirectory=Descomprimir a $DIR$\nunzipHere=Descomprimir aquí\nrequiresRestart=Requiere un reinicio para aplicarse.\ndownload=Descargar\nservicePath=Ruta de servicio\nservicePathDescription=La sub-ruta opcional al abrir la URL en un navegador\nactive=Activo\ninactive=Inactivo\nstarting=Iniciar\nremotePort=Puerto remoto\nremotePortNumber=Puerto remoto $PORT$\nuserIdentity=Identidad personal\nglobalIdentity=Identidad global\nidentityChoice=Identidad de usuario\nidentityChoiceDescription=Elige una identidad predefinida o especifica los datos de acceso sólo para esta conexión\ndefineNewIdentityOrSelect=Introduce uno nuevo o elige uno existente\nlocalIdentity.displayName=Identidad local\nlocalIdentity.displayDescription=Crea una identidad reutilizable para este escritorio local\nsyncedIdentity.displayName=Identidad sincronizada\nsyncedIdentity.displayDescription=Crear una identidad reutilizable que se sincroniza entre sistemas\nlocalIdentity=Identidad local\nkeyNotSynced=El archivo llave aún no está sincronizado con el repositorio git. Utiliza el botón añadir a git del archivo clave para añadirlo.\nusernameDescription=El nombre de usuario con el que iniciar sesión\nidentity.displayName=Identidad\nidentity.displayDescription=Crear una identidad reutilizable para las conexiones\nlocal=Local\nshared=Global\nuserDescription=El nombre de usuario o identidad predefinida con la que iniciar sesión\nidentityAccessLevel=Nivel de acceso\nidentityPerUser=Acceso a la identidad personal\nidentityPerUserDescription=Restringe el acceso a esta identidad y a sus conexiones asociadas sólo a tu usuario bóveda\nidentityPerUserDisabled=Acceso a la identidad personal (desactivado)\nidentityPerUserDisabledDescription=Restringe el acceso a esta identidad y a sus conexiones asociadas sólo a tu usuario bóveda (Requiere que el equipo esté configurado)\nidentityPerGroup=Acceso de identidad sólo para grupos\nidentityPerGroupDescription=Restringe el acceso a esta identidad y sus conexiones asociadas sólo a este grupo de bóvedas\nlibrary=Biblioteca\nlocation=Localización\nkeyAuthentication=Autenticación basada en claves\nkeyAuthenticationDescription=El método de autenticación a utilizar si se requiere una autenticación basada en claves\nlocationDescription=La ruta del archivo de tu clave privada correspondiente\nkeyFile=Archivo de clave local\nkeyPassword=Frase de contraseña\nkey=Clave\nyubikeyPiv=Yubikey PIV\npageant=Concurso\ngpgAgent=Agente GPG\ncustomPkcs11Library=Biblioteca PKCS#11 personalizada\nsshAgent=Agente OpenSSH\nnone=Ninguno\nindex=Índice ...\notherExternal=Otro agente externo\nsync=Sincroniza\nvaultSync=Sincronización de bóvedas\ncustomUsername=Nombre de usuario\ncustomUsernameDescription=El usuario alternativo opcional con el que iniciar sesión\ncustomUsernamePassword=Contraseña\ncustomUsernamePasswordDescription=La contraseña del usuario que se utilizará cuando se requiera autenticación sudo\nshowInternalPods=Mostrar pods internos\nshowAllNamespaces=Mostrar todos los espacios de nombres\nshowInternalContainers=Mostrar contenedores internos\nrefresh=Actualizar\nvmwareGui=Iniciar GUI\nmonitorVm=Monitor VM\naddCluster=Añadir clúster ...\nshowNonRunningInstances=Mostrar instancias no en ejecución\nvmwareGuiDescription=Si iniciar una máquina virtual en segundo plano o en una ventana.\nvmwareEncryptionPassword=Contraseña de encriptación\nvmwareEncryptionPasswordDescription=La contraseña opcional utilizada para encriptar la VM.\nvmPasswordDescription=La contraseña necesaria para el usuario invitado.\nvmPassword=Contraseña de usuario\nvmUser=Usuario invitado\nrunTempContainer=Ejecutar un contenedor temporal\nvmUserDescription=El nombre de usuario de tu usuario invitado principal\ndockerTempRunAlertTitle=Ejecutar un contenedor temporal\ndockerTempRunAlertHeader=Esto ejecutará un proceso shell en un contenedor temporal que se eliminará automáticamente cuando se detenga.\nimageName=Nombre de la imagen\nimageNameDescription=El identificador de imagen del contenedor a utilizar\ncontainerName=Nombre del contenedor\ncontainerNameDescription=El nombre opcional del contenedor personalizado\nvm=Máquina virtual\nvmDescription=El archivo de configuración asociado.\nvmwareScan=Hipervisores de escritorio VMware\nvmwareMachine.displayName=Máquina virtual VMware\nvmwareMachine.displayDescription=Conectarse a una máquina virtual mediante SSH\nvmwareInstallation.displayName=Instalación del hipervisor de escritorio VMware\nvmwareInstallation.displayDescription=Interactúa con las máquinas virtuales instaladas a través de su CLI\nstart=Inicia\nstop=Para\npause=Pausa\nrdpTunnelHost=Host de destino\nrdpTunnelHostDescription=La conexión SSH para tunelizar la conexión RDP\nrdpTunnelUsername=Nombre de usuario\nrdpTunnelUsernameDescription=El usuario personalizado con el que iniciar sesión, utiliza el usuario SSH si se deja vacío\nrdpFileLocation=Ubicación del archivo\nrdpFileLocationDescription=La ruta del archivo .rdp\nrdpPasswordAuthentication=Autenticación de contraseña\nrdpFiles=Archivos RDP\nrdpPasswordAuthenticationDescription=La contraseña para rellenar o copiar en el portapapeles, según el soporte del cliente\nrdpFile.displayName=Archivo RDP\nrdpFile.displayDescription=Conectarse a un sistema a través de un archivo .rdp existente\nrequiredSshServerAlertTitle=Configurar servidor SSH\nrequiredSshServerAlertHeader=No se puede encontrar un servidor SSH instalado en la máquina virtual.\nrequiredSshServerAlertContent=Para conectarse a la VM, XPipe busca un servidor SSH en ejecución, pero no se ha detectado ningún servidor SSH disponible para la VM.\ncomputerName=Nombre del ordenador\npssComputerNameDescription=El nombre del ordenador al que conectarse\ncredentialUser=Credencial de usuario\ncredentialUserDescription=El usuario con el que iniciar sesión.\ncredentialPassword=Contraseña credencial\ncredentialPasswordDescription=La contraseña del usuario.\nsshConfig=Archivos de configuración SSH\nautostart=Conectarse automáticamente al iniciar XPipe\nacceptHostKey=Aceptar clave de host\nmodifyHostKeyPermissions=Modificar los permisos de la clave del host\nattachContainer=Adjunta\ncontainerLogs=Mostrar registros\nopenSftpClient=Abrir en cliente SFTP externo\nopenTermius=Abrir en Termius\nshowInternalInstances=Mostrar instancias internas\neditPod=Editar pod\nacceptHostKeyDescription=Confía en la nueva clave de host y continúa\nmodifyHostKeyPermissionsDescription=Intenta eliminar los permisos del archivo original para que OpenSSH esté contento\npsSession.displayName=Sesión remota PowerShell\npsSession.displayDescription=Conectar mediante Nueva-PSSession y Entrar-PSSession\nsshLocalTunnel.displayName=Túnel SSH local\nsshLocalTunnel.displayDescription=Establecer un túnel SSH a un host remoto\nsshRemoteTunnel.displayName=Túnel SSH remoto\nsshRemoteTunnel.displayDescription=Establecer un túnel SSH inverso desde un host remoto\nsshDynamicTunnel.displayName=Túnel SSH dinámico\nsshDynamicTunnel.displayDescription=Establecer un proxy SOCKS a través de una conexión SSH\nshellEnvironmentGroup.displayName=Entornos Shell\nshellEnvironmentGroup.displayDescription=Entornos Shell\nshellEnvironment.displayName=Entorno Shell\nshellEnvironment.displayDescription=Crea un entorno de inicio shell personalizado\nshellEnvironment.informationFormat=$TYPE$ entorno\nshellEnvironment.elevatedInformationFormat=$ELEVATION$ $TYPE$ entorno\nenvironmentConnectionDescription=La conexión base para crear un entorno para\nenvironmentScriptDescription=El script init personalizado opcional para ejecutar en el intérprete de comandos\nenvironmentSnippets=Scripts de shell\ncommandSnippetsDescription=Los scripts shell predefinidos opcionales que se ejecutarán primero\nenvironmentSnippetsDescription=Los scripts shell predefinidos opcionales que se ejecutarán al inicializarse\nshellTypeDescription=El tipo de shell explícito a lanzar\noriginPort=Puerto de origen\noriginAddress=Dirección de origen\nremoteAddress=Dirección remota\nremoteSourceAddress=Dirección de origen remota\nremoteSourcePort=Puerto de origen remoto\noriginDestinationPort=Puerto de destino de origen\noriginDestinationAddress=Dirección de destino de origen\norigin=Origen\nremoteHost=Host remoto\naddress=Dirección\nproxmox.displayName=Proxmox\nproxmox.displayDescription=Conectarse a sistemas en un Entorno Virtual Proxmox\nproxmoxVm.displayName=Proxmox VM\nproxmoxVm.displayDescription=Conectarse a una máquina virtual en un VE Proxmox mediante SSH\nproxmoxContainer.displayName=Contenedor Proxmox\nproxmoxContainer.displayDescription=Conectarse a un contenedor en un VE Proxmox\nsshDynamicTunnel.hostDescription=El sistema a utilizar como proxy SOCKS\nsshDynamicTunnel.bindingDescription=A qué direcciones enlazar el túnel\nsshRemoteTunnel.hostDescription=El sistema desde el que iniciar el túnel remoto hacia el origen\nsshRemoteTunnel.bindingDescription=A qué direcciones enlazar el túnel\nsshLocalTunnel.hostDescription=El sistema para abrir el túnel hacia\nsshLocalTunnel.bindingDescription=A qué direcciones enlazar el túnel\nsshLocalTunnel.localAddressDescription=La dirección local a enlazar\nsshLocalTunnel.remoteAddressDescription=La dirección remota a enlazar\ncmd.displayName=Comando\ncmd.displayDescription=Ejecutar un comando arbitrario en un sistema\nk8sPod.displayName=Pod Kubernetes\nk8sPod.displayDescription=Conéctate a un pod y a sus contenedores mediante kubectl\nk8sContainer.displayName=Contenedor Kubernetes\nk8sContainer.displayDescription=Abrir un shell a un contenedor\nk8sCluster.displayName=Clúster Kubernetes\nk8sCluster.displayDescription=Conéctate a un clúster y sus pods mediante kubectl\nsshTunnelGroup.displayName=Túneles SSH\nsshTunnelGroup.displayCategory=Todos los tipos de túneles SSH\nlocal.displayName=Máquina local\nlocal.displayDescription=El shell de la máquina local\ncygwin=Cygwin\nmsys2=MSYS2\ngitWindows=Git para Windows\ngitForWindows.displayName=Git para Windows\ngitForWindows.displayDescription=Accede a tu entorno local de Git para Windows\nmsys2.displayName=MSYS2\nmsys2.displayDescription=Conchas de acceso de tu entorno MSYS2\ncygwin.displayName=Cygwin\ncygwin.displayDescription=Accede a los shells de tu entorno Cygwin\nnamespace=Espacio de nombres\ngitVaultIdentityStrategy=Identidad SSH Git\ngitVaultIdentityStrategyDescription=Si has elegido utilizar una URL git SSH como remota y tu repositorio remoto requiere una identidad SSH, activa esta opción.\\n\\nEn caso de que hayas proporcionado una url HTTP, puedes ignorar esta opción.\ndockerContainers=Contenedores Docker\ndockerCmd.displayName=cliente docker CLI\ndockerCmd.displayDescription=Accede a los contenedores Docker mediante el cliente CLI Docker\nwslCmd.displayName=Instalación WSL\nwslCmd.displayDescription=Acceder a instancias WSL mediante el cliente wsl CLI\nk8sCmd.displayName=cliente kubectl\nk8sCmd.displayDescription=Accede a clusters Kubernetes mediante kubectl\nk8sClusters=Clústeres Kubernetes\nshells=Conchas disponibles\ninspectContainer=Inspecciona\ninspectContext=Inspecciona\nk8sClusterNameDescription=El nombre del contexto en el que se encuentra el clúster.\npod=Pod\npodName=Nombre del pod\nk8sClusterContext=Contexto\nk8sClusterContextDescription=El nombre del contexto en el que se encuentra el cluster\nk8sClusterNamespace=Espacio de nombres\nk8sClusterNamespaceDescription=El espacio de nombres personalizado o el predeterminado si está vacío\nk8sConfigLocation=Archivo de configuración\nk8sConfigLocationDescription=El archivo kubeconfig personalizado o el predeterminado si se deja vacío\ninspectPod=Inspecciona\nshowAllContainers=Mostrar contenedores no en ejecución\nshowAllPods=Mostrar pods no en ejecución\nk8sPodHostDescription=El host en el que se encuentra el pod\nk8sContainerDescription=El nombre del contenedor Kubernetes\nk8sPodDescription=El nombre del pod de Kubernetes\npodDescription=La cápsula en la que se encuentra el contenedor\nk8sClusterHostDescription=El host a través del cual se debe acceder al clúster. Debe tener kubectl instalado y configurado para poder acceder al clúster.\nconnection=Conexión\nshellCommand.displayName=Comando shell personalizado\nshellCommand.displayDescription=Abrir un shell estándar mediante un comando personalizado\nssh.displayName=Conexión SSH\nssh.displayDescription=Conectarse a un sistema remoto mediante el cliente de línea de comandos SSH\nsshConfig.displayName=Archivo de configuración SSH\nsshConfig.displayDescription=Conectarse a hosts definidos en un archivo de configuración SSH\nsshConfigHost.displayName=Archivo de configuración SSH host\nsshConfigHost.displayDescription=Conectarse a un host definido en un archivo de configuración SSH\nsshConfigHost.password=Contraseña\nsshConfigHost.passwordDescription=Proporciona la contraseña opcional para el inicio de sesión del usuario.\nsshConfigHost.identityPassphrase=Frase clave\nsshConfigHost.identityPassphraseDescription=Proporciona la frase de contraseña opcional para tu clave.\nshellCommand.hostDescription=El host en el que ejecutar el comando\nshellCommand.commandDescription=El comando que abrirá un intérprete de comandos\ncommandType=Tipo de comando\ncommandTypeDescription=Cómo ejecutar el comando\ncommandDescription=Los comandos personalizados a ejecutar en el host\ncommandHostDescription=El host en el que ejecutar el comando\ncommandDataFlowDescription=Cómo gestiona este comando la entrada y la salida\ncommandElevationDescription=Ejecuta este comando con permisos elevados\ncommandShellTypeDescription=El shell a utilizar para este comando\nlimitedSystem=Se trata de un sistema limitado o integrado\nlimitedSystemDescription=No intentes identificar el tipo de shell, necesario para sistemas integrados limitados o dispositivos IOT\nsshForwardX11=Adelante X11\nsshForwardX11Description=Activa el reenvío X11 para la conexión\ncustomAgent=Agente personalizado\nidentityAgent=Agente de identidad\nssh.proxyDescription=El host proxy opcional que se utilizará al establecer la conexión SSH. Debe tener instalado un cliente ssh.\nusage=Utilización\nwslHostDescription=El host en el que se encuentra la instancia WSL. Debe tener instalado wsl.\nwslDistributionDescription=El nombre de la instancia WSL\nwslUsernameDescription=El nombre de usuario explícito con el que iniciar sesión. Si no se especifica, se utilizará el nombre de usuario por defecto.\nwslPasswordDescription=La contraseña del usuario que puede utilizarse para los comandos sudo.\ndockerHostDescription=El host en el que se encuentra el contenedor docker. Debe tener docker instalado.\ndockerContainerDescription=El nombre del contenedor docker\nlocalMachine=Máquina local\nrootScan=Entorno shell Sudo\nloginEnvironmentScan=Entorno de inicio de sesión personalizado\nk8sScan=Clúster Kubernetes\noptions=Opciones\ndockerRunningScan=Ejecutar contenedores Docker\ndockerAllScan=Todos los contenedores Docker\nwslScan=Instancias WSL\nsshScan=Conexiones de configuración SSH\nrunAsUser=Ejecutar como usuario\nrunAsUserDescription=Inicia este entorno shell como un usuario diferente\ndefault=Por defecto\nadministrator=Administrador\nwslHost=Anfitrión WSL\ntimeout=Tiempo de espera\ninstallLocation=Ubicación de la instalación\ninstallLocationDescription=La ubicación donde está instalado tu entorno $NAME$\nwsl.displayName=Subsistema Windows para Linux\nwsl.displayDescription=Conectarse a una instancia WSL que se ejecuta en Windows\ndocker.displayName=Contenedor Docker\ndocker.displayDescription=Conéctate a un contenedor Docker\nport=Puerto\nuser=Usuario\npassword=Contraseña\nmethod=Método\nuri=URL\nproxy=Proxy\ndistribution=Distribución\nusername=Nombre de usuario\nshellType=Tipo de shell\nbrowseFile=Examinar archivo\nopenShell=Abrir shell en terminal\nopenCommand=Ejecutar comando en terminal\neditFile=Editar archivo\ndescription=Descripción\nfurtherCustomization=Más personalización\nfurtherCustomizationDescription=Para más opciones de configuración, utiliza los archivos de configuración de ssh\nbrowse=Navega por\nconfigHost=Anfitrión\nconfigHostDescription=El host en el que se encuentra la configuración\nconfigLocation=Ubicación de la configuración\nconfigLocationDescription=La ruta del archivo de configuración\ngateway=Pasarela\ngatewayDescription=La pasarela opcional que se utilizará al conectarse\nconnectionInformation=Información de conexión\nconnectionInformationDescription=A qué sistema conectarse\npasswordAuthentication=Autenticación de contraseña\npasswordAuthenticationDescription=La contraseña opcional que se utilizará para autenticarse\nsshConfigString.displayName=Conexión SSH basada en la configuración\nsshConfigString.displayDescription=Crea una conexión SSH totalmente personalizada en el formato SSH config\nsshConfigStringContent=Configuración\nsshConfigStringContentDescription=Opciones SSH para la conexión en el formato de configuración OpenSSH\nvnc.displayName=Conexión VNC a través de SSH\nvnc.displayDescription=Abrir una sesión VNC a través de una conexión tunelizada\nbinding=Encuadernación\nvncPortDescription=El puerto en el que escucha el servidor VNC\nrdpPortDescription=El puerto en el que escucha el servidor RDP\nvncUsername=Nombre de usuario\nvncUsernameDescription=El nombre de usuario VNC opcional\nvncPassword=Contraseña\nvncPasswordDescription=La contraseña VNC\nx11WslInstance=Instancia X11 Forward WSL\nx11WslInstanceDescription=La distribución local del Subsistema Windows para Linux que se utilizará como servidor X11 cuando se utilice el reenvío X11 en una conexión SSH. Esta distribución debe ser una distribución WSL2.\nopenAsRoot=Abrir como root\nopenInWSL=Abrir en WSL\nlaunch=Inicia\nsshTrustKeyContent=No se conoce la clave del host y has activado la verificación manual de la clave del host. $CONTENT$\nsshTrustKeyTitle=Clave de host desconocida\nrdpTunnel.displayName=Conexión RDP sobre SSH\nrdpTunnel.displayDescription=Conectarse mediante RDP a través de una conexión tunelizada\nrdpEnableDesktopIntegration=Habilitar la integración de escritorio\nrdpEnableDesktopIntegrationDescription=Ejecutar aplicaciones remotas suponiendo que la lista de permitidos del RDP lo permite\nrdpSetupAdminTitle=Se requiere configuración RDP\nrdpSetupAllowTitle=Aplicación remota RDP\nrdpSetupAllowContent=Iniciar aplicaciones remotas directamente no está permitido actualmente en este sistema. ¿Quieres habilitarlo? Esto te permitirá ejecutar tus aplicaciones remotas directamente desde XPipe, desactivando la lista de permitidas para aplicaciones remotas RDP.\nrdpServerEnableTitle=Servidor RDP\nrdpServerEnableContent=El servidor RDP está desactivado en el sistema de destino. ¿Quieres activarlo en el registro para permitir conexiones RDP remotas?\nrdp=RDP\nrdpScan=Túnel RDP sobre SSH\nwslX11SetupTitle=Configuración WSL X11\nwslX11SetupContent=XPipe puede utilizar tu distribución local WSL para actuar como servidor de visualización X11. ¿Quieres instalar X11 en $DIST$? Esto instalará los paquetes X11 básicos en la distribución WSL y puede tardar un poco. También puedes cambiar qué distribución se utiliza en el menú de configuración.\ncommand=Comando\ncommandGroup=Grupo de comandos\nvncSystem=Sistema de destino VNC\nvncSystemDescription=El sistema real con el que interactuar. Suele ser el mismo que el host del túnel\nvncHost=Host VNC de destino\nvncHostDescription=El sistema en el que se ejecuta el servidor VNC\nvncDirectHost=Anfitrión\nvncDirectHostDescription=La entrada de host o dirección manual del servidor en el que se ejecuta el servidor VNC\nrdpDirectHost=Anfitrión\nrdpDirectHostDescription=La entrada de host o dirección manual del servidor en el que se ejecuta el servidor RDP\ngitVaultTitle=Bóveda Git\ngitVaultForcePushContent=¿Quieres forzar el push al repositorio remoto? Esto sustituirá completamente todo el contenido del repositorio remoto por el local, incluido el historial.\ngitVaultOverwriteLocalContent=¿Quieres anular los cambios de tu repositorio local? Esto aplicará todos los cambios remotos a tu repositorio local.\nrdpSimple.displayName=Conexión directa RDP\nrdpSimple.displayDescription=Conectarse a un host mediante RDP\nrdpUsername=Nombre de usuario\nrdpUsernameDescription=El usuario con el que iniciar sesión. Puede incluir un prefijo de dominio\naddressDescription=Dónde conectarse\nrdpAdditionalOptions=Opciones RDP adicionales\nrdpAdditionalOptionsDescription=Opciones RDP en bruto a incluir, con el mismo formato que en los archivos .rdp\nproxmoxVncConfirmTitle=Acceso VNC\nproxmoxVncConfirmContent=¿Quieres habilitar el acceso VNC para la VM? Esto habilitará el acceso directo del cliente VNC en el archivo de configuración de la VM y reiniciará la máquina virtual.\ndockerContext.displayName=Contexto Docker\ndockerContext.displayDescription=Interactúa con contenedores situados en un contexto específico\nvmActions=Acciones VM\ndockerContextActions=Acciones contextuales\nk8sPodActions=Acciones del pod\nopenVnc=Habilitar el acceso VNC\naddVnc=Añadir conexión VNC\ncommandGroup.displayName=Grupo de comandos\ncommandGroup.displayDescription=Agrupa los comandos disponibles para un sistema\nserial.displayName=Conexión en serie\nserial.displayDescription=Abrir una conexión serie en un terminal\nserialPort=Puerto serie\nserialPortDescription=El puerto serie / dispositivo al que conectarse\nbaudRate=Velocidad en baudios\ndataBits=Bits de datos\nstopBits=Bits de parada\nparity=Paridad\nflowControlWindow=Control de flujo\nserialImplementation=Aplicación en serie\nserialImplementationDescription=La herramienta que hay que utilizar para conectarse al puerto serie\nserialHost=Anfitrión\nserialHostDescription=El sistema para acceder al puerto serie en\nserialPortConfiguration=Configuración del puerto serie\nserialPortConfigurationDescription=Parámetros de configuración del dispositivo serie conectado\nserialInformation=Información en serie\nopenXShell=Abrir en XShell\ntsh.displayName=Teletransporte\ntsh.displayDescription=Conéctate a tus nodos de teletransporte mediante tsh\ntshNode.displayName=Nodo de teletransporte\ntshNode.displayDescription=Conectarse a un nodo teletransportador en un clúster\nteleportCluster=Clúster\nteleportClusterDescription=El clúster en el que está el nodo\nteleportProxy=Proxy\nteleportProxyDescription=El servidor proxy utilizado para conectarse al nodo\nteleportHost=Anfitrión\nteleportHostDescription=El nombre de host del nodo\nteleportUser=Usuario\nteleportUserDescription=El usuario con el que iniciar sesión\nlogin=Inicio de sesión\nhyperVInstall.displayName=Hyper-V\nhyperVInstall.displayDescription=Conectarse a máquinas virtuales gestionadas por Hyper-V\nhyperVVm.displayName=VM Hyper-V\nhyperVVm.displayDescription=Conectarse a una máquina virtual Hyper-V mediante SSH o PSSession\ntrustHost=Host de confianza\ntrustHostDescription=Añadir ComputerName a la lista de hosts de confianza\ncopyIp=Copiar IP\nvncDirect.displayName=Conexión VNC directa\nvncDirect.displayDescription=Conectarse directamente a un sistema mediante VNC\neditConfiguration=Editar configuración\nviewInDashboard=Vista en el panel de control\nsetDefault=Establecer por defecto\nremoveDefault=Eliminar por defecto\nconnectAsOtherUser=Conectarse como otro usuario\nprovideUsername=Proporcionar un nombre de usuario alternativo para iniciar sesión\nvmIdentity=Identidad de invitado\nvmIdentityDescription=El método de autenticación de identidad SSH a utilizar para conectarse si es necesario\nvmPort=Puerto\nvmPortDescription=El puerto al que conectarse mediante SSH\nforwardAgent=Agente de reenvío\nforwardAgentDescription=Hacer que las identidades del agente SSH estén disponibles en el sistema remoto\nvirshUri=URI\nvirshUriDescription=La URI del hipervisor, también se admiten alias\nvirshDomain.displayName=dominio libvirt\nvirshDomain.displayDescription=Conectarse a un dominio libvirt\nvirshHypervisor.displayName=hipervisor libvirt\nvirshHypervisor.displayDescription=Conectarse a un controlador de hipervisor compatible con libvirt\nvirshInstall.displayName=cliente de línea de comandos libvirt\nvirshInstall.displayDescription=Conéctate a todos los hipervisores libvirt disponibles mediante virsh\naddHypervisor=Añadir hipervisor\ninteractiveTerminal=Terminal interactivo\neditDomain=Editar dominio\nlibvirt=dominios libvirt\ncustomIp=IP personalizada\ncustomIpDescription=Anula la detección por defecto de la IP local de la VM si utilizas una red avanzada\nautomaticallyDetect=Detectar automáticamente\nuserAddDialogTitle=Creación de usuario\ngroupAddDialogTitle=Creación de grupos\npassphrase=Frase de contraseña\nrepeatPassphrase=Repetir frase de contraseña\ngroupSecret=Secreto de grupo\nrepeatGroupSecret=Repetir secreto de grupo\nvaultGroup=Grupo de bóvedas\nloginAlertTitle=Inicio de sesión requerido\nloginAlertHeader=Desbloquea la caja fuerte para acceder a tus conexiones personales\nvaultUser=Usuario de la caja fuerte\nme=Me\naddGroup=Añadir grupo ...\naddGroupDescription=Crea un nuevo grupo para esta caja fuerte\naddUser=Añadir usuario ...\naddUserDescription=Crea un nuevo usuario para esta caja fuerte\nskip=Saltar\nuserChangePasswordAlertTitle=Cambio de contraseña\ngroupChangeSecretAlertTitle=Cambio secreto\ndocs=Documentación\nlxd.displayName=Contenedor LXD\nlxd.displayDescription=Conéctate a un contenedor LXD mediante lxc\nlxdCmd.displayName=Cliente CLI LXD\nlxdCmd.displayDescription=Accede a los contenedores LXD mediante el cliente CLI lxc\npodman.displayName=Contenedor Podman\npodman.displayDescription=Conéctate a un contenedor Podman\nincusInstall.displayName=Gestor de máquinas Incus\nincusInstall.displayDescription=Accede a los contenedores incus mediante el cliente CLI incus\nincusContainer.displayName=Contenedor Incus\nincusContainer.displayDescription=Conectarse a un contenedor incus\npodmanCmd.displayName=Podman CLI\npodmanCmd.displayDescription=Accede a los contenedores Podman a través del cliente CLI\nlxdHostDescription=El host en el que se encuentra el contenedor LXD. Debe tener lxc instalado.\nlxdContainerDescription=El nombre del contenedor LXD\npodmanContainers=Contenedores Podman\nlxdContainers=Contenedores LXD\nincusContainers=Contenedores Incus\ncontainer=Contenedor\nhost=Anfitrión\ncontainerActions=Acciones del contenedor\nserialConsole=Consola serie\neditRunConfiguration=Editar la configuración de ejecución\ncommunityDescription=Una herramienta de conexión perfecta para tus casos de uso personal.\nupgradeDescription=Gestión profesional de conexiones para toda tu infraestructura de servidores.\ndiscoverPlans=Descubre las opciones de actualización\nextendProfessional=Actualiza a las últimas funciones profesionales\ncommunityItem1=Conexiones ilimitadas a sistemas y herramientas no comerciales\ncommunityItem2=Integración perfecta con tus terminales y editores instalados\ncommunityItem3=Navegador de archivos remoto con todas las funciones\ncommunityItem4=Potente sistema de scripts para todos los shells\ncommunityItem5=Integración de Git para sincronizar y compartir información de conexión\nupgradeItem1=Incluye todas las funciones de la edición comunitaria\nupgradeItem2=El plan Homelab admite hipervisores ilimitados y funciones SSH avanzadas\nupgradeItem3=El plan Profesional admite además sistemas operativos y herramientas empresariales\nupgradeItem4=El plan Empresa ofrece total flexibilidad para tu caso de uso individual\nupgrade=Actualiza\nupgradeTitle=Planes disponibles\nstatus=Estado\ntype=Escribe a\nlicenseAlertTitle=Licencia necesaria\nuseCommunity=Continuar con la comunidad\npreviewDescription=Prueba las nuevas funciones durante un par de semanas tras su lanzamiento.\ntryPreview=Activar vista previa\npreviewItem1=Acceso completo a las nuevas funciones profesionales durante 2 semanas después del lanzamiento\npreviewItem2=Prueba nuevas funciones sin compromiso\nlicensedTo=Con licencia para\nemail=Dirección de correo electrónico\napply=Aplica\nclear=Borrar\nactivate=Activa\nvalidUntil=Válido hasta\nlicenseActivated=Licencia activada\nrestart=Reinicia\nlockVault=Bóveda de seguridad\nrestartApp=Reiniciar XPipe\nfree=Gratis\nupgradeInfo=A continuación encontrarás información sobre cómo obtener una licencia.\nupgradeInfoPreview=A continuación puedes encontrar información sobre cómo obtener una licencia o probar la vista previa.\nenterLicenseKey=Introduce la clave de licencia para actualizar\nisOnlySupported=sólo es compatible al menos con la licencia $TYPE$\nareOnlySupported=sólo se admiten con una licencia de al menos $TYPE$\nlegacyLicense=Esta licencia sólo incluía las nuevas funciones Profesionales publicadas en el plazo de un año tras la compra.\npreviewExpiredLicense=Esta función estuvo disponible recientemente de forma gratuita en versión preliminar, pero este periodo ya ha expirado.\nopenApiDocs=Documentación API\nopenApiDocsDescription=La documentación de la API HTTP está disponible en Internet, incluida una especificación OpenAPI .yaml. Puedes abrirla en tu navegador web o en tu cliente HTTP preferido.\nopenApiDocsButton=Abrir documentos\npythonApi=API de Python\npersonalConnection=Esta conexión y todos sus hijos sólo están disponibles para tu usuario, ya que dependen de una identidad personal.\ndeveloperPrintInitFiles=Imprimir la ejecución del archivo init\ndeveloperPrintInitFilesDescription=Imprime todos los scripts init del shell que se ejecutan al iniciar un terminal.\ndeveloperShowSensitiveCommands=Registrar comandos sensibles\ndeveloperShowSensitiveCommandsDescription=Incluye comandos sensibles en la salida de registro para depuración.\ncheckingForUpdates=Comprobación de actualizaciones\ncheckingForUpdatesDescription=Obtención de información sobre la última versión\ndownloadingUpdate=Recuperación de la versión (Versión $VERSION$)\ndownloadingUpdateDescription=Descarga del paquete de lanzamiento\nupdateNag=Hace tiempo que no actualizas XPipe. Puede que te estés perdiendo nuevas funciones y correcciones de versiones más recientes.\nupdateNagTitle=Recordatorio de actualización\nupdateNagButton=Ver liberaciones\nrefreshServices=Actualizar servicios\nserviceProtocolType=Tipo de protocolo de servicio\nserviceProtocolTypeDescription=Controla cómo abrir el servicio\nserviceCommand=El comando a ejecutar una vez que el servicio esté activo\nserviceCommandDescription=El marcador de posición $PORT se sustituirá por el puerto local real tunelizado\nvalue=Valor\nshowAdvancedOptions=Mostrar opciones avanzadas\nsshAdditionalConfigOptions=Opciones de configuración adicionales\nremoteFileManager=Gestor de archivos remoto\nclearUserData=Borrar datos de usuario\nclearUserDataDescription=Borrar todos los datos de configuración del usuario, incluidas las conexiones\nclearUserDataTitle=Eliminación de datos de usuario\nclearUserDataContent=Esto borrará todos los datos de usuario locales de xpipe y se reiniciará. Si te preocupan tus conexiones, asegúrate de sincronizarlas primero con un repositorio git.\nundefined=Sin definir\ncopyAddress=Copiar dirección\nnetbirdDeviceScan=Conexiones Netbird\nnetbirdId=Clave pública par\nnetbirdIdDescription=El id de la clave pública interna de netbird del peer\ntailscaleDeviceScan=Conexiones Tailscale\ntailscaleInstall.displayName=Instalación de Tailscale\ntailscaleInstall.displayDescription=Conéctate a los dispositivos de tu tailnet mediante SSH\ntailscaleDevice.displayName=Dispositivo Tailscale\ntailscaleDevice.displayDescription=Conéctate a un dispositivo de tu tailnet mediante SSH\ntailscaleId=ID de dispositivo\ntailscaleIdDescription=El ID interno del dispositivo tailscale\ntailscaleHostName=Nombre del host\ntailscaleHostNameDescription=El nombre de host del dispositivo en la red de cola\ntailscaleUsername=Nombre de usuario\ntailscaleUsernameDescription=El usuario con el que iniciar sesión\ntailscalePassword=Contraseña\ntailscalePasswordDescription=La contraseña de usuario opcional que puede utilizarse para sudo\nscriptName=Nombre del script\nscriptNameDescription=Dale a este script un nombre personalizado\nscriptGroupName=Nombre del grupo de scripts\nscriptGroupNameDescription=Dale a este grupo de scripts un nombre personalizado\nidentityName=Nombre de identidad\nidentityNameDescription=Dale a esta identidad un nombre personalizado\ntailscaleTailnet.displayName=Tailnet\ntailscaleTailnet.displayDescription=Conéctate a una tailnet específica con tu cuenta\nputtyConnections=Conexiones PuTTY\nkittyConnections=Conexiones KiTTY\nicons=Iconos\ncustomIcons=Iconos personalizados\niconSources=Fuentes de iconos\niconSourcesDescription=Aquí puedes añadir tus propias fuentes de iconos. XPipe recogerá cualquier archivo .svg en la ubicación añadida y lo añadirá al conjunto de iconos disponibles.\\n\\nSe admiten como ubicaciones de iconos tanto directorios locales como repositorios git remotos.\nrefreshSources=Iconos de actualización\nrefreshSourcesDescription=Actualiza todos los iconos de las fuentes disponibles\naddDirectoryIconSource=Añadir fuente de directorio ...\naddDirectoryIconSourceDescription=Añadir iconos desde un directorio local\naddGitIconSource=Añadir fuente git ...\naddGitIconSourceDescription=Añade iconos situados en un repositorio git remoto\nrepositoryUrl=URL del repositorio Git\niconDirectory=Directorio de iconos\naddUnsupportedKexMethod=Añadir un método de intercambio de claves no admitido\naddUnsupportedKexMethodDescription=Permitir que se utilice el método de intercambio de claves $VAL$ para esta conexión\naddUnsupportedHostKeyType=Añadir un tipo de clave de host no compatible\naddUnsupportedHostKeyTypeDescription=Permitir que se utilice el tipo de clave de host $VAL$ para esta conexión\naddUnsupportedMacType=Añadir un tipo de MAC no compatible\naddUnsupportedMacTypeDescription=Permitir que se utilice el tipo de MAC $VAL$ para esta conexión\nrunSilent=silenciosamente en segundo plano\nrunInFileBrowser=en el explorador de archivos\nrunInConnectionHub=en centro de conexión\ncommandOutput=Salida de comandos\niconSourceDeletionTitle=Borrar fuente de iconos\niconSourceDeletionContent=¿Quieres eliminar esta fuente de iconos y todos los iconos asociados a ella?\nrefreshIcons=Iconos de actualización\nrefreshIconsDescription=Recuperar, renderizar y almacenar en caché todos los más de 1000 iconos disponibles de fuentes externas en archivos .png. Esto puede llevar un tiempo ...\nvaultUserLegacy=Usuario de bóveda (modo de compatibilidad heredado limitado)\nupgradeInstructions=Instrucciones de actualización\nexternalActionTitle=Solicitud de acción externa\nexternalActionContent=Se ha solicitado una acción externa. ¿Quieres permitir el lanzamiento de acciones desde fuera de XPipe?\nnoScriptStateAvailable=Actualizar para determinar la compatibilidad del script ...\ndocumentationDescription=Consulta la documentación\ncustomEditorCommandInTerminal=Ejecutar un comando personalizado en un terminal\ncustomEditorCommandInTerminalDescription=Si tu editor está basado en un terminal, puedes activar esta opción para abrir automáticamente un terminal y ejecutar el comando en la sesión del terminal en su lugar.\\n\\nPuedes utilizar esta opción para editores como vi, vim, nvim y otros.\ndisableHttpsTlsCheck=Desactivar la verificación del certificado de solicitud HTTPS\ndisableHttpsTlsCheckDescription=Si tu organización está descifrando tu tráfico HTTPS en cortafuegos utilizando la interceptación SSL, cualquier comprobación de actualización o de licencia fallará debido a que los certificados no coinciden. Puedes solucionarlo activando esta opción y desactivando la validación de certificados TLS.\nconnectionsSelected=$NUMBER$ conexiones seleccionadas\naddConnections=Añadir conexiones\nbrowseDirectory=Examinar directorio\nopenTerminal=Terminal abierto\ndocumentation=Documentación\nreport=Informar de un error\nkeePassXcNotAssociated=Enlace KeePassXC\nkeePassXcNotAssociatedDescription=XPipe no está asociado a tu base de datos local KeePassXC. Haz clic abajo para realizar el paso único de asociar XPipe con la base de datos KeePassXC para que XPipe pueda consultar las contraseñas.\nkeePassXcAssociateMore=Conectar más bases de datos\nkeePassXcAssociateMoreDescription=Puedes estar conectado a varias bases de datos KeePassXC al mismo tiempo\nkeePassXcAssociated=Enlaces KeePassXC\nkeePassXcAssociatedDescription=XPipe está conectado a las siguientes bases de datos locales de KeePassXC:\nkeePassXcNotAssociatedButton=Enlazar base de datos\nidentifier=Identificador\npasswordManagerCommand=Comando personalizado\npasswordManagerCommandDescription=El comando personalizado que se ejecutará para obtener las contraseñas. La cadena de texto $KEY se sustituirá por la clave de contraseña citada cuando se llame. Esto debería llamar a la CLI de tu gestor de contraseñas para que imprima la contraseña en stdout, por ejemplo, mypassmgr get $KEY.\nchooseTemplate=Elegir plantilla\nkeePassXcPlaceholder=URL de entrada de KeePassXC\nterminalEnvironment=Entorno de terminal\nterminalEnvironmentDescription=En caso de que quieras utilizar funciones de un entorno WSL local basado en Linux para la personalización de tu terminal, puedes utilizarlas como entorno de terminal.\\n\\nCualquier comando init de terminal personalizado y la configuración del multiplexor de terminal se ejecutarán entonces en esta distribución WSL.\nterminalInitScript=Script de inicio de terminal\nterminalInitScriptDescription=Comandos que se ejecutan en el entorno de terminal antes de iniciar la conexión. Puedes utilizarlo para configurar el entorno de terminal al iniciarse.\nterminalMultiplexer=Multiplexor de terminal\nterminalMultiplexerDescription=El multiplexor de terminal para utilizarlo como alternativa a las pestañas en un terminal. Esto sustituirá ciertas características de manejo del terminal, por ejemplo el manejo de pestañas, por la funcionalidad del multiplexor.\\n\\nRequiere que esté instalado en el sistema el correspondiente ejecutable del multiplexor.\nterminalMultiplexerWindowsDescription=El multiplexor de terminal para utilizarlo como alternativa a las pestañas en un terminal. Esto sustituirá ciertas características de manejo del terminal, por ejemplo el manejo de pestañas, por la funcionalidad del multiplexor.\\n\\nRequiere el uso de un entorno de terminal WSL en Windows y que el ejecutable del multiplexor esté instalado en el sistema WSL.\nterminalAlwaysPauseOnExit=Pausa siempre al salir\nterminalAlwaysPauseOnExitDescription=Si está activada, al salir de una sesión de terminal siempre se te pedirá que reinicies o cierres la sesión. Si se desactiva, XPipe sólo lo hará en el caso de conexiones fallidas que salgan con un error.\nquerying=Consulta ...\nretrievedPassword=Obtenido: $PASSWORD$\nrefreshOpenpubkey=Actualizar identidad openpubkey\nrefreshOpenpubkeyDescription=Ejecuta opkssh refresh para que la identidad openpubkey vuelva a ser válida\nall=Todos los\nterminalPrompt=Terminal prompt\nterminalPromptDescription=La herramienta prompt de terminal a utilizar en tus terminales remotos. Habilitar un prompt de terminal establecerá y configurará automáticamente la herramienta prompt en el sistema de destino al abrir una sesión de terminal.\\n\\nEsto no modifica ninguna configuración de avisos o archivos de perfil existentes en un sistema. Esto aumentará el tiempo de carga del terminal por primera vez mientras se configura el prompt en el sistema remoto. Es posible que tu terminal necesite fuentes adicionales para mostrar correctamente el prompt.\nterminalPromptConfiguration=Configuración del indicador de terminal\nterminalPromptConfig=Archivo de configuración\nterminalPromptConfigDescription=El archivo de configuración personalizado que se aplicará al prompt. Esta configuración se establecerá automáticamente en el sistema de destino cuando se inicialice el terminal y se utilizará como configuración predeterminada del indicador.\\n\\nSi quieres utilizar el archivo de configuración por defecto existente en cada sistema, puedes dejar este campo vacío.\npasswordManagerKey=Clave del gestor de contraseñas\npasswordManagerKeyDescription=El identificador del gestor de contraseñas del secreto\npasswordManagerAgent=Agente gestor de contraseñas\ndockerComposeProject.displayName=Proyecto Docker Compose\ndockerComposeProject.displayDescription=Agrupa contenedores de un proyecto de composición\nsshVerboseOutput=Activar la salida detallada SSH\nsshVerboseOutputDescription=Esto imprimirá mucha información de depuración cuando te conectes mediante SSH. Es útil para solucionar problemas con las conexiones SSH.\ndontUseGateway=No utilices puerta de enlace\ndontUseGatewayDescription=No utilices el host del hipervisor como pasarela y conéctate directamente a la IP\ncategoryColor=Color de la categoría\ncategoryColorDescription=El color por defecto a utilizar para las conexiones dentro de esta categoría\ncategorySync=Sincronizar con el repositorio git\ncategorySyncDescription=Sincroniza todas las conexiones automáticamente con el repositorio git. Todos los cambios locales en las conexiones serán empujados al remoto.\ncategorySyncSpecial=Sincronizar con repositorio git\\n(No configurable para la categoría especial \"$NAME$\")\ncategoryDontAllowScripts=Desactivar todas las modificaciones\ncategoryDontAllowScriptsDescription=Desactiva la ejecución de comandos y otras operaciones en los sistemas de esta categoría para evitar cualquier modificación. Esto deshabilitará todas las funciones de scripting, comandos del entorno shell, avisos, etc.\ncategoryConfirmAllModifications=Confirma todas las modificaciones\ncategoryConfirmAllModificationsDescription=Confirma primero cualquier tipo de modificación de una conexión o de un sistema de archivos. Esto puede evitar operaciones accidentales en sistemas importantes.\ncategoryDefaultIdentity=Identidad por defecto\ncategoryDefaultIdentityDescription=Si utilizas con frecuencia una determinada identidad en muchos de los sistemas de esta categoría, establecer una identidad por defecto te permitirá preseleccionarla al crear nuevas conexiones.\ncategoryConfigTitle=$NAME$ configuración\nconfigure=Configura\naddConnection=Añadir conexión\nnoCompatibleConnection=No se ha encontrado ninguna conexión compatible\nnoCompatibleIdentity=No se ha encontrado ninguna identidad compatible\nnewCategory=Nueva categoría\ndockerComposeRestricted=El proyecto compose está restringido por $NAME$ y no puede modificarse externamente. Por favor, utiliza $NAME$ para gestionar este proyecto de composición.\nrestricted=Restringido\ndisableSshPinCaching=Desactivar el almacenamiento en caché del PIN SSH\ndisableSshPinCachingDescription=XPipe almacenará automáticamente en caché cualquier PIN que se haya introducido para una clave cuando se utilice alguna forma de autenticación basada en hardware.\\n\\nSi desactivas esta opción, tendrás que volver a introducir el PIN en cada intento de conexión.\ngitSyncPull=Tira para sincronizar cambios git remotos\nenpassVaultFile=Archivo de bóveda\nenpassVaultFileDescription=El archivo local de la bóveda de Enpass.\nflat=Plano\nrecursive=Recursivo\nrdpAllowListBlocked=La RemoteApp seleccionada no parece estar incluida en la lista de permitidos RDP para el servidor.\npsonoServerUrl=URL del servidor\npsonoServerUrlDescription=URL del servidor psono backend\npsonoApiKey=Clave API\npsonoApiKeyDescription=La clave API a utilizar, formateada como uuid\npsonoApiSecretKey=Clave secreta API\npsonoApiSecretKeyDescription=La clave secreta de la API como cadena hexadecimal de 64 bytes\npassboltServerUrl=URL del servidor\npassboltServerUrlDescription=URL del servidor passbolt backend\npassboltPassphrase=Frase de contraseña\npassboltPassphraseDescription=La frase de contraseña para la clave privada de la cámara acorazada\npassboltPrivateKey=Clave privada\npassboltPrivateKeyDescription=El archivo de clave gpg privada de la bóveda\nfocusWindowOnNotifications=Enfocar la ventana de notificaciones\nfocusWindowOnNotificationsDescription=Trae XPipe a primer plano cuando se muestre una notificación o mensaje de error, por ejemplo cuando una conexión o túnel finaliza inesperadamente.\ngitUsername=Nombre de usuario git personalizado\ngitUsernameDescription=El usuario personalizado para autenticarse en el repositorio remoto git. Por defecto, XPipe utilizará las credenciales actualmente configuradas de la CLI de git.\\n\\nEste ajuste anulará cualquier credencial por defecto que ya esté configurada para tu cliente CLI de git local.\ngitPassword=Contraseña git personalizada / token de acceso personal\ngitPasswordDescription=La contraseña o token de acceso personal a utilizar para autenticarte. Que necesites una contraseña o un token de acceso personal depende del proveedor remoto de git. Esta configuración anulará cualquier credencial por defecto que ya esté configurada para tu cliente CLI de git local.\nsetReadOnly=Establecer sólo lectura\nunsetReadOnly=Desactivar sólo lectura\nreadOnlyStoreError=La configuración de esta entrada está congelada. Elige un nombre diferente para guardar tus cambios en una nueva copia.\ncategoryFreeze=Congelar configuraciones de conexión\ncategoryFreezeDescription=Marca las configuraciones de conexión como de sólo lectura. Esto significa que no se puede modificar ninguna configuración de entrada de conexión existente en esta categoría. Sin embargo, se pueden añadir nuevas conexiones.\nupdateFail=La instalación de la actualización no se ha realizado correctamente\nupdateFailAction=Instalar actualización manualmente\nupdateFailActionDescription=Consulta las últimas versiones en GitHub\nonePasswordPlaceholder=Nombre del elemento o URL op://\ncomputeDirectorySizes=Calcula el tamaño de los directorios\ncomputeSize=Calcula el tamaño\ncustomSpiceCommand=Comando personalizado\ncustomSpiceCommandDescription=El comando personalizado a ejecutar para lanzar sesiones SPICE. La cadena de texto $FILE se sustituirá por la ruta entre comillas del archivo .vv cuando se ejecute.\nvncClient=Cliente VNC\nvncClientDescription=El cliente VNC a lanzar al abrir conexiones VNC en XPipe.\\n\\nTienes la opción de utilizar el cliente VNC integrado en XPipe o, alternativamente, lanzar un cliente VNC externo instalado localmente si buscas más personalización.\nintegratedXPipeVncClient=Cliente VNC XPipe integrado\ncustomVncCommand=Comando personalizado\ncustomVncCommandDescription=El comando personalizado a ejecutar para iniciar sesiones VNC. La cadena de texto $ADDRESS se sustituirá por la dirección entrecomillada cuando se invoque.\nvncConnections=Conexiones VNC\npasswordManagerIdentity=Identidad del gestor de contraseñas\npasswordManagerIdentity.displayName=Identidad del gestor de contraseñas\npasswordManagerIdentity.displayDescription=Recuperar el nombre de usuario y la contraseña de una identidad de tu gestor de contraseñas\npasswordCopied=Contraseña de conexión copiada en el portapapeles\nerrorOccurred=Se ha producido un error\nactionMacro.displayName=Macro de acción\nactionMacro.displayDescription=Ejecutar en acción utilizando activadores personalizados\nmacroAdd=Añadir macro\nmacroName=Nombre de macro\nmacroNameDescription=Dale a esta macro un nombre personalizado\nactionId=ID de acción\nactionIdDescription=La acción a ejecutar con esta macro\nmacroRefs=Conexiones asociadas\nmacroRefsDescription=Las conexiones con las que ejecutar la acción\nconnectionCopy=Copia\nactionPickerTitle=Acción de selección\nactionPickerDescription=Haz clic en algo para ejecutar una acción. En lugar de ejecutar la acción, puedes crear y editar accesos directos a la acción en el modo de selección de accesos directos a la acción.\ncancelActionPicker=Cancelar selección de acción\nactionShortcut=Atajo de acción\nactionShortcuts=Atajos de acción\nactionStore=Almacén de acciones\nactionStoreDescription=La entrada de la tienda en la que ejecutar la acción\nactionStores=Acción almacena\nactionStoresDescription=Las entradas de la tienda en las que ejecutar la acción\nactionDesktopShortcut=Acceso directo del escritorio\nactionDesktopShortcutDescription=Crea un acceso directo para esta acción en tu escritorio\nactionUrlShortcut=Atajo URL\nactionUrlShortcutDescription=Copia una URL que pueda desencadenar estas acciones al abrirse\nactionUrlShortcutDisabled=Atajo URL (No disponible)\nactionUrlShortcutDisabledDescription=El tipo de instalación $TYPE$ no admite la apertura de URLs\nactionApiCall=Solicitud API\nactionApiCallDescription=Llama a esta acción desde la API HTTP\nactionMacro=Macro de acción\nactionMacroDescription=Crea una macro con funciones avanzadas para esta acción\ncreateMacro=Crear macro\nactionConfiguration=Parámetros\nactionConfigurationDescription=Los parámetros a pasar a la acción ejecutada\nconfirmAction=Confirmar acción\nactionConnections=Conexiones de acción\nactionConnectionsDescription=Las conexiones en las que ejecutar la acción\nactionConnection=Acción conexión\nactionConnectionDescription=La conexión en la que ejecutar la acción\nappleContainerInstall.displayName=Contenedores Apple\nappleContainerInstall.displayDescription=Accede a las instancias del contenedor apple mediante la CLI del contenedor\nappleContainer.displayName=Contenedor Apple\nappleContainer.displayDescription=Accede a las instancias del contenedor apple mediante la CLI del contenedor\nappleContainerHostDescription=El host en el que se encuentra el contenedor apple\nappleContainerDescription=El nombre del contenedor apple\nappleContainers=Contenedores Apple\nchangeOrderIndexTitle=Cambiar el orden\norderIndex=Índice\norderIndexDescription=Índice explícito para ordenar esta entrada en relación con otras. Los índices más bajos se muestran arriba, los más altos abajo\nmoveToFirst=Mover al primero\nmoveToLast=Mover al último\ncategory=Categoría\nincludeRoot=Incluir raíz\nexcludeRoot=Excluir raíz\nfreezeConfiguration=Congelar configuración\nunfreezeConfiguration=Descongelar la configuración\nwaylandScalingTitle=Escalado Wayland\nactionApiUrl=$URL$ (Copiar cuerpo json)\ncopyBody=Copiar el cuerpo de la solicitud\ngitRepoTerminalOpen=Abre el repositorio en el terminal\ngitRepoTerminalOpenDescription=Echa un vistazo al repositorio tú mismo con la línea de comandos\ngitRepoOverwriteLocal=Sobrescribir el repositorio local\ngitRepoOverwriteLocalDescription=Sustituye todos los cambios locales por los cambios del remoto\ngitRepoForcePush=Sobrescribir el repositorio remoto\ngitRepoForcePushDescription=Utiliza git push --force para aplicar tus cambios locales al remoto\ngitRepoDontWarn=No avises más\ngitRepoDontWarnDescription=Si esto es lo esperado, haz que XPipe ignore este error en el futuro\ngitRepoTryAgain=Inténtalo de nuevo\ngitRepoTryAgainDescription=Vuelve a intentar la misma operación\ngitRepoEnablePlain=Utilizar la sincronización de directorios simple\ngitRepoEnablePlainDescription=No inicializar un repositorio git para sincronizar los cambios con el directorio\ngitRepoCreateBare=Utilizar git sync\ngitRepoCreateBareDescription=Inicializa un nuevo repositorio git desnudo en el directorio de sincronización\ngitRepoDisable=Desactiva la bóveda git por ahora\ngitRepoDisableDescription=No realices ningún cambio durante esta sesión\ngitRepoPullRefresh=Extraer cambios y actualizar\ngitRepoPullRefreshDescription=Fusionar cambios remotos y recargar datos\nbreakOutCategory=Categoría de ruptura\nmergeCategory=Fusionar categoría\nopenWinScp=Abrir en WinSCP\nuninstallApplication=Desinstala\nuninstallApplicationDescription=Ejecuta el .pkg un script de instalación para desinstalar completamente XPipe\nk8sEditPodTitle=Aplicar cambios\nk8sEditPodContent=¿Quieres aplicar los cambios realizados mediante el comando kubectl apply? Es probable que sea necesario reiniciar para que se apliquen los cambios.\nvirshEditDomainTitle=Aplicar cambios\nvirshEditDomainContent=¿Quieres aplicar los cambios al dominio? Es probable que sea necesario reiniciar para que se apliquen los cambios.\npkcs11Library=Biblioteca PKCS#11\npkcs11LibraryDescription=La ruta del archivo de la biblioteca enlazada dinámicamente\nsshAgentSocket=Socket de agente SSH personalizado\nsshAgentSocketDescription=El socket personalizado a utilizar para comunicarse con el agente SSH. Este agente personalizado puede utilizarse para una conexión seleccionando la opción agente personalizado para él.\npublicKey=Identificador de clave pública\npublicKeyDescription=La clave pública opcional para obligar al agente a ofrecer sólo la clave privada correspondiente\nactions=Acciones\nhcloudServer.displayName=Servidor en nube de Hetzner\nhcloudServer.displayDescription=Accede a un servidor alojado en la nube de Hetzner mediante SSH\nhcloudInstall.displayName=CLI de la Nube de Hetzner\nhcloudInstall.displayDescription=Accede a servidores alojados en la nube de Hetzner a través de hcloud\nhcloudContext.displayName=contexto hcloud\nhcloudContext.displayDescription=Servidores de acceso de un contexto hcloud\nmetrics=Métricas\nopenInVsCode=Abrir en VsCode\naddCloud=Nube ...\nhcloudToken=token hcloud\nhcloudTokenDescription=El token de nube de Hetzner a utilizar. Para más información, consulta la documentación\nhcloudLogin=Inicio de sesión en la nube de Hetzner\nclearHcloudToken=Borrar token hcloud\nclearHcloudTokenDescription=Borrar el token existente para que puedas volver a conectarte\nselectIdentity=Seleccionar identidad\nenableMcpServer=Habilitar servidor MCP\nenableMcpServerDescription=Habilita el servidor MCP de XPipe, permitiendo que clientes MCP externos envíen peticiones al servidor MCP. Consulta más abajo los detalles de configuración.\\n\\nTen en cuenta que la API HTTP no tiene que estar activada para la funcionalidad MCP.\nenableMcpMutationTools=Habilitar herramientas de mutación MCP\nenableMcpMutationToolsDescription=Por defecto, en el servidor MCP sólo están habilitadas las herramientas de sólo lectura. Esto es para garantizar que no se puedan realizar operaciones accidentales que modifiquen potencialmente un sistema.\\n\\nSi tienes previsto realizar cambios en los sistemas a través de clientes MCP, asegúrate de que tu cliente MCP está configurado para confirmar cualquier acción potencialmente destructiva antes de activar esta opción. Requiere una reconexión de cualquier cliente MCP para aplicarse.\nmcpClientConfigurationDetails=Configuración del cliente MCP\nmcpClientConfigurationDetailsDescription=Utiliza estos datos de configuración para conectarte al servidor MCP XPipe desde el cliente MCP que elijas.\nswitchHostAddress=Cambiar la dirección del host\naddAnotherHostName=Añadir otro nombre de host\naddNetwork=Escaneado de red ...\nnetworkScan=Escaneado de red\nnetworkScanStore=Host de destino\nnetworkScanStoreDescription=El host para el que escanear la red local\nuseAsGateway=Utilizar el host como pasarela\nuseAsGatewayDescription=Si se debe utilizar el host de destino como pasarela para las conexiones creadas\nnetworkScanPorts=Puertos a escanear\nnetworkScanPortsDescription=La lista separada por comas de los puertos a incluir en el escaneo\nnetworkScanType=Tipo de conexión\nnetworkScanTypeDescription=El tipo de servidores que hay que buscar\nemptyDirectory=Este directorio parece estar vacío\nhcloudConfigFile=archivo de configuración hcloud\nhcloudConfigFileDescription=La ubicación del archivo de configuración .toml de hcloud CLI\npreferMonochromeIcons=Prefiero iconos monocromos\npreferMonochromeIconsDescription=Cuando está activada, las variables de icono monocromo se elegirán sobre las versiones coloreadas por defecto de un icono, suponiendo que exista una variante de icono en modo claro u oscuro para un icono de una fuente.\\n\\nRequiere una actualización de los iconos para aplicarse.\nalwaysShowSshMotd=Mostrar siempre MOTD\nalwaysShowSshMotdDescription=Si mostrar o no el mensaje del día configurado en un sistema remoto al iniciar una nueva sesión de terminal. Ten en cuenta que cambiar esto podría alterar el comportamiento de inicialización de las conexiones SSH.\nmanageSubscription=Gestionar suscripción\nnoListeningServer=Ningún servidor a la escucha\nnetworkScanResults=Resultados de la exploración\nnetworkScanResultsDescription=La lista de sistemas encontrados en la red\nlocalShellDialect=Shell local\nlocalShellDialectDescription=El shell que se utiliza para las operaciones locales. En caso de que el shell local normal por defecto esté desactivado o roto en algún grado, esta opción puede utilizarse para recurrir a otra alternativa.\\n\\nEs posible que algunas configuraciones, como las entradas PATH personalizadas, no se apliquen con el intérprete de comandos alternativo si aún no están configuradas en los respectivos archivos de perfil del intérprete de comandos.\nagentSocketNotFound=No se ha encontrado ningún socket de agente activo\nagentSocket=Ubicación del zócalo\nagentSocketDescription=La ruta del archivo de socket del agente\nagentSocketNotConfigured=Aún no se ha configurado ningún socket personalizado\ndownloadInProgress=$NAME$ descarga en curso\nenableTerminalStartupBell=Activar la campana de inicio del terminal\nenableTerminalStartupBellDescription=Reproducir un comando de pitido/timbre en una nueva sesión de terminal. Si tu emulador de terminal admite timbres, esto puede utilizarse para facilitar la identificación de las instancias de terminal recién iniciadas.\ninvalidSshGatewayChain=Configuración de cadena de pasarelas mixta no válida con pasarelas de salto y pasarelas de no salto.\nsyncFileExists=El archivo sincronizado $FILE$ ya existe\nreplaceFile=Sustituir archivo\nreplaceFileDescription=Sustituye el archivo existente por éste\nrenameFile=Renombrar archivo\nrenameFileDescription=Dale a este archivo un nombre diferente para sincronizarlo\nnewFileName=Nuevo nombre de archivo\nparentHostDoesNotSupportTunneling=El host padre $NAME$ no admite la tunelización\nconnectionNotesTemplate=Plantilla de notas\nconnectionNotesTemplateDescription=La plantilla markdown que debe utilizarse al añadir una nueva entrada de notas a una conexión.\nconnectionNotesButton=Editar notas\nrdpSmartSizing=Activar el dimensionamiento inteligente\nrdpSmartSizingDescription=Cuando está activado, mstsc reducirá el tamaño del escritorio si la ventana es demasiado pequeña para mostrarla en su resolución completa. La relación de aspecto del escritorio se conserva cuando se reduce.\ndisableStartOnInit=Desactivar el inicio automático\nenableStartOnInit=Activar el inicio automático\nfileReadSudoTitle=Lectura de archivos Sudo\nfileReadSudoContent=El archivo que intentas leer no te concede permisos de lectura como usuario actual. ¿Quieres leer este archivo como usuario root con sudo? Esto elevará automáticamente a root con las credenciales existentes o a través de un prompt.\nnetbirdInstall.displayName=Instalación de Netbird\nnetbirdInstall.displayDescription=Conéctate a compañeros de tu red Netbird\nnetbirdProfile.displayName=Perfil Netbird\nnetbirdProfile.displayDescription=Enumerar los compañeros de un perfil específico\nnetbirdPeer.displayName=Par Netbird\nnetbirdPeer.displayDescription=Conectarse a un compañero mediante SSH\nnetbirdPublicKey=Clave pública\nnetbirdPublicKeyDescription=La clave pública interna del homólogo\nnetbirdHostName=Nombre del host\nnetbirdHostNameDescription=El nombre de host del compañero en la red\nvncRefSystem=Sistema asociado\nvncRefSystemDescription=La entrada de conexión con la que asociar esta conexión VNC. Déjala vacía si no hay ninguna\nabstractHost.displayName=Anfitrión abstracto\nabstractHost.displayDescription=Crear una entrada para un host que no admite conexiones shell\nabstractHostAddress=Dirección del host\nabstractHostAddressDescription=La dirección del host\nabstractHostGateway=Pasarela\nabstractHostGatewayDescription=El sistema de pasarela opcional a través del cual llegar a este host\nabstractHostConvert=Convertir a entrada de host abstracta\nhostNoConnections=No hay conexiones disponibles\nhostHasConnections=$COUNT$ conexiones disponibles\nhostHasConnection=$COUNT$ conexión disponible\nlargeFileWarningTitle=Edición de archivos grandes\nlargeFileWarningContent=El archivo que quieres editar es bastante grande con $SIZE$. ¿Realmente quieres abrir este archivo en tu editor de texto?\nrdpAskpassUser=Nombre de usuario RDP para el host $HOST$\nrdpAskpassPassword=Contraseña de usuario $USER$\ninPlaceKey=Clave\ninPlaceKeyText=Contenido de la clave privada\ninPlaceKeyTextDescription=El contenido de la clave privada\nnetbirdSelfhosted=Instancia netbird autoalojada\nnetbirdSelfhostedDescription=Proporcionar una URL personalizada en lugar de utilizar la versión alojada en la nube\nnetbirdManagementUrl=URL de gestión de Netbird\nnetbirdManagementUrlDescription=La URL de gestión de tu instancia autoalojada\nnetbirdSetupKey=Tecla de configuración\nnetbirdSetupKeyDescription=Si utilizas claves de configuración, puedes utilizar una para iniciar sesión\nnetbirdLogin=Inicio de sesión en Netbird\naddProfile=Añadir perfil\nnetbirdProfileNameAsktext=Nombre del nuevo perfil netbird\nopenSftp=Abrir en sesión SFTP\ncapslockWarning=Tienes activado el bloqueo de mayúsculas\ninherit=Hereda\nsshConfigStringSelected=Host de destino\nsshConfigStringSelectedDescription=Para múltiples hosts, el primero se utiliza como destino. Reordena tus hosts para cambiar el objetivo\ntunnelToLocalhost=Túnel a localhost\ntunnelToLocalhostDescription=Tuneliza automáticamente el puerto remoto al localhost\ntags=Etiquetas\ntag=Etiqueta\naddNewTag=Crear una nueva etiqueta\ncreateTag=Crear etiqueta ...\ninPlacePublicKey=Clave pública\ninPlacePublicKeyDescription=La clave pública asociada a la clave privada especificada\nsshKeygenTitle=Generar nueva clave SSH\nsshKeygenAlgorithm=Algoritmo\nsshKeygenAlgorithmDescription=El algoritmo de generación de clave asimétrica que se utilizará para la clave\nrsaBits=Bits\nrsaBitsDescription=Número de bits de la clave generada\nsshKeygenComment=Comentario\nsshKeygenCommentDescription=El comentario opcional para esta clave\nsshKeygenPassphrase=Frase de contraseña\nsshKeygenPassphraseDescription=La frase de contraseña opcional para esta clave\ned25519SkResident=Hacer clave residente\ned25519SkResidentDescription=Almacena la clave privada en la clave de seguridad del hardware\ned25519SkResidentKeyName=Etiqueta de clave residente\ned25519SkResidentKeyNameDescription=Dar una etiqueta a la clave. Necesaria cuando se almacenan varias claves en la clave de seguridad\ned25519SkPinRequired=Requerir PIN\ned25519SkPinRequiredDescription=Requiere la introducción del PIN al utilizarlo\ned25519SkUserPresenceRequired=Requiere la presencia del usuario\ned25519SkUserPresenceRequiredDescription=Requiere tacto o similar al usarlo. Algunas claves de seguridad requieren que esto esté activado\ncopyPublicKey=Copia la clave pública\ngeneratePublicKey=Generar clave pública\npublicKeyGenerateNotice=Puede generarse a partir de una clave privada\nidentityApplyTargetHost=Objetivo\nidentityApplyTargetHostDescription=El sistema para aplicar la identidad a\nidentityApplyAuthorizedHost=Clave SSH autorizada\nidentityApplyAuthorizedHostDescription=La clave SSH se añade al archivo hosts autorizado\nidentityApplyAuthorizedHostButton=Añadir una clave a un archivo\napplyIdentityToHost=Aplica la identidad al host ...\nidentityApplyMissingPublicKeyTitle=Clave pública perdida\nidentityApplyMissingPublicKeyContent=La clave SSH de la identidad no tiene asociada una clave pública. Comprueba la configuración para obtener más detalles.\nvalid=Válido\nnotValid=No es válido\nwarning=Advertencia\nidentityApplyTitle=Aplicar identidad\nidentityApplyConfigPasswordEnabled=Autenticación de contraseña activada\nidentityApplyConfigPasswordEnabledDescription=La autenticación por contraseña sigue activada en la configuración de sshd\nidentityApplyConfigPasswordDisabled=Autenticación de contraseña desactivada\nidentityApplyConfigPasswordDisabledDescription=La autenticación por contraseña sigue desactivada en la configuración de sshd\nidentityApplyConfigKeyEnabled=Clave de autenticación activada\nidentityApplyConfigKeyEnabledDescription=La autenticación basada en claves sigue activada en la configuración de sshd\nidentityApplyConfigKeyDisabled=Clave de autenticación desactivada\nidentityApplyConfigKeyDisabledDescription=La autenticación basada en claves sigue desactivada en la configuración de sshd\nidentityApplyConfigRootDisabledWarning=Inicio de sesión raíz desactivado\nidentityApplyConfigRootDisabledWarningDescription=El inicio de sesión del usuario raíz no está habilitado en la configuración de sshd\nidentityApplyConfigAdminWarning=Teclas de administrador configuradas\nidentityApplyConfigAdminWarningDescription=Puede que haya que añadir la clave a administrators_authorized_keys para los usuarios administradores\nidentityApplyEditConfig=Editar configuración\nidentityApplyEditConfigDescription=Abre la configuración sshd en el editor para solucionar cualquier problema\nidentityApplyEditAuthorizedKeys=Editar claves autorizadas\nidentityApplyEditAuthorizedKeysDescription=Abre el archivo authorized_keys en el editor para editar o eliminar otras claves\nidentityApplyEditConfigButton=Abrir sshd_config\nidentityApplyEditAuthorizedKeysButton=Abrir llaves_autorizadas\nidentityApplySetStoreIdentity=Conjunto de identidades de conexión\nidentityApplySetStoreIdentityDescription=La identidad está configurada para ser utilizada por la conexión\nidentityApplySetStoreIdentityButton=Aplicar identidad\ngenerateKey=Generar clave\ngroupSecretStrategy=Control de acceso basado en grupos\ngroupSecretStrategyDescription=Cómo recuperar el secreto de grupo utilizado para la encriptación y desencriptación del grupo. El método de recuperación que elijas se ejecutará cuando un usuario se conecte a la bóveda al iniciarse.\\n\\nEsta opción se configura para cada grupo. Para cambiar esta configuración para un grupo distinto del actualmente activo, tendrás que iniciar sesión en el almacén como miembro de ese grupo.\nfileSecret=Secreto basado en archivos\ncommandSecret=Comando\nhttpRequestSecret=Respuesta HTTP\nfileSecretChoice=Ubicación del archivo\nfileSecretChoiceDescription=La ruta al archivo que contiene el secreto de encriptación del grupo. Como este archivo puede consultarse en todas las plataformas, puedes utilizar ~ en la ruta para referirte al directorio raíz. El archivo debe estar disponible en todos los sistemas desde los que desbloquees la bóveda, de lo contrario el inicio de sesión fallará.\ncommandSecretField=Script de recuperación\ncommandSecretFieldDescription=El comando que devolverá la clave secreta de encriptación del grupo actual. El comando se ejecuta en el shell por defecto del sistema local y la clave debe imprimirse en stdout.\nhttpRequestSecretField=URI de solicitud\nhttpRequestSecretFieldDescription=La URI a la que enviar una petición HTTP. El secreto de grupo se toma del cuerpo de la respuesta HTTP.\nvaultAuthentication=Autenticación de bóvedas\nvaultAuthenticationDescription=Cómo autentificar / desbloquear los datos de la bóveda. Hay varias formas diferentes de encriptar y desbloquear los datos de la bóveda, dependiendo de con quién quieras compartirlos.\ngroupAuthFailed=Fallo en la autenticación secreta\nuserAuthFailed=Error en la autenticación de contraseña\nsavingChanges=Guardar cambios\nawsDeviceScan=AWS\naws=AWS\nawsCliInstallTitle=Se requiere CLI de AWS\nawsCliInstallContent=La integración de AWS requiere que la CLI de AWS esté instalada en tu sistema local\nawsProfileCreateTitle=Nuevo perfil de AWS\nawsProfileAccessKey=Clave de acceso\nawsProfileName=Nombre del perfil\nawsProfileNameDescription=El nombre para mostrar del nuevo perfil\nawsProfileRegion=Región\nawsProfileRegionDescription=La región AWS asociada al perfil\nawsProfileAccessKeyId=ID de la clave de acceso\nawsProfileAccessKeyIdDescription=El ID de la clave de acceso del usuario IAM\nawsProfileSecretAccessKey=Clave de acceso secreta\nawsProfileSecretAccessKeyDescription=La clave de acceso secreta asociada\nawsInstall.displayName=Instalación de la CLI de AWS\nawsInstall.displayDescription=Conéctate a tus sistemas AWS mediante la CLI de AWS\nawsProfile.displayName=Perfil CLI de AWS\nawsProfile.displayDescription=Acceder a AWS a través de un perfil específico\nawsInstanceId=ID de instancia\nawsInstanceIdDescription=El ID interno de esta instancia\nawsInstanceUseSsm=Conectar a través de SSM\nawsInstanceUseSsmDescription=Utiliza la herramienta SSM para conectarte a la instancia mediante SSH\nawsEc2Instance.displayName=Instancia AWS EC2\nawsEc2Instance.displayDescription=Conectarse a una instancia EC2 mediante SSH\nawsS3Group.displayName=Buckets S3\nawsS3Group.displayDescription=Acceder a los buckets S3 de un perfil de AWS\nawsS3Bucket.displayName=Cubo S3\nawsS3Bucket.displayDescription=Acceder a un bucket S3 de un perfil AWS\nawsEc2Group.displayName=Instancias EC2\nawsEc2Group.displayDescription=Acceder a instancias EC2 de un perfil AWS\nawsEc2InstanceSsmTerminal=Abrir terminal SSM\ngenericS3Bucket.displayName=Cubo S3 genérico\ngenericS3Bucket.displayDescription=Acceder a un bucket genérico de S3 mediante la CLI de AWS\naddFileSystem=Sistema de archivos ...\ngenericS3BucketHost=Anfitrión\ngenericS3BucketHostDescription=La entrada de host o dirección manual del servidor S3\ngenericS3BucketPortDescription=El puerto en el que escucha el servidor S3\ngenericS3BucketAccessKeyId=ID de la clave de acceso\ngenericS3BucketAccessKeyIdDescription=El ID de la clave de acceso del usuario IAM\ngenericS3BucketSecretAccessKey=Clave de acceso secreta\ngenericS3BucketSecretAccessKeyDescription=La clave de acceso secreta asociada\ngenericS3BucketHttps=Activar HTTPS\ngenericS3BucketHttpsDescription=Utiliza HTTPS para conectarte al servidor. Algunos proveedores pueden requerir HTTPS\ntunnelled=En túnel\nawsInstallSync=Sincronización de la configuración\nawsInstallSyncDescription=Sincroniza los archivos de configuración de la CLI de AWS con la bóveda git\nawsInstallLocation=Localización de datos del usuario\nawsInstallLocationDescription=La ruta desde la que se obtienen los archivos de configuración de la CLI de AWS\ninstanceActions=Acciones de instancia\nopenSplit=Abrir en terminal dividido\nterminalSplitStrategy=Dirección de vista dividida\nterminalSplitStrategyDescription=Controla cómo se dividen las pestañas del terminal cuando se utiliza la funcionalidad de vista dividida en modo por lotes para abrir varias sesiones de terminal una junto a otra.\nterminalSplitStrategyDisabledDescription=Controla cómo se dividen las pestañas del terminal cuando se utiliza la funcionalidad de vista dividida en modo por lotes para abrir varias sesiones de terminal una junto a otra.\\n\\nLa configuración actual de tu terminal no admite las vistas divididas.\nhorizontal=Horizontal\nvertical=Vertical\nbalanced=Equilibrado\nclose=Cerrar\nhelpButton=$TOPIC$ enlace de documentación\nquickAccess=Acceso rápido\ntoggleEnabled=Alternar estado\ncurrentPath=Ruta actual\ndirectoryContents=Contenido del directorio\ndirectoryOptions=Opciones de directorio\nchooseConnectionType=Elige el tipo de conexión\nbatchMode=Modo por lotes\ntoggleButton=Botón de alternar\ntailscaleUseSsh=Utiliza tailscale SSH auth\ntailscaleUseSshDescription=Conéctate a través del propio servidor SSH de tailscale sin ninguna autenticación SSH\nportDescription=El puerto en el que se ejecuta el servidor SSH\nloginAs=Iniciar sesión como\nsshGatewayType=Tipo de pasarela\nsshGatewayTypeDescription=Si se conecta al objetivo a través de un túnel o con la opción ProxyJump\ngatewayTunnel=Túnel de pasarela\nproxyJump=Salto proxy\ncommandTypeAsyncBackground=Ejecutar desvinculado en segundo plano\ncommandTypeSyncBackground=Ejecutar en segundo plano y esperar a que termine\ncommandTypeTerminalBackground=Abrir en terminal\nasyncBackgroundCommand=Comando de fondo\nsyncBackgroundCommand=Comando de fondo de bloqueo\nterminalBackgroundCommand=Comando de terminal\ntestingConnection=Probar la conexión ...\nopenManagementConsole=Consola de gestión abierta\nopenLxcTerminal=Abrir terminal LXC\nopenContainerConsole=Abrir consola serie\nkeeper2fa=método 2FA\nkeeper2faDescription=El método principal de autenticación de dos factores que está configurado para tu cuenta. Actívalo si tu cuenta de Keeper requiere autenticación de dos factores para acceder a las contraseñas.\nkeeperTotpDuration=Duración del código 2FA personalizado\nkeeperTotpDurationDescription=Anula la duración por defecto de la validez de un código 2FA. Sólo se aplica si la política de tu organización permite cambiar la duración.\\n\\nLos valores posibles son: $VALUES$\nkeeperOtherAuth=Otros (RSA SecurID, Duo Security, Keeper DNA, etc.)\nextractReusableIdentities=Extraer identidades reutilizables\nidentitiesAdded=Identidades añadidas\nsyncMode=Modo de sincronización\nsyncModeDescription=Controla cómo deben sincronizarse los cambios.\\n\\nEl modo instantáneo introducirá y extraerá los cambios tan pronto como sea posible, el modo de inicio y salida sincronizará todos los cambios realizados durante una sesión a la vez, y el modo manual sólo sincronizará cuando tú lo inicies.\ntoggleTerminalDock=Conmutar el muelle del terminal\nscriptDirectory=Ubicación del directorio\nscriptDirectoryDescription=El directorio local que contiene archivos de script de shell\nscriptSourceUrl=URL del repositorio\nscriptSourceUrlDescription=La URL de un repositorio git remoto que contiene archivos de script de shell\nscriptCollectionSourceType=Tipo de fuente\nscriptCollectionSourceTypeDescription=El tipo de fuente desde donde deben cargarse los scripts de shell\nscriptCollectionSourceEntry=Fuente\nscriptCollectionSourceEntryDescription=La fuente desde la que deben cargarse los scripts de shell\ngitRepository=Un repositorio git\nscriptCollectionSource.displayName=Fuente del script\nscriptCollectionSource.displayDescription=Importa automáticamente guiones shell de una fuente existente\ndirectorySource=Fuente del directorio\ngitRepositorySource=Fuente del repositorio git\nrefreshSource=Actualizar fuente\nscriptTextSourceUrl=URL del script\nscriptTextSourceUrlDescription=La URL desde la que recuperar el archivo script\nscriptSourceType=Fuente del script\nscriptSourceTypeDescription=De dónde obtener el guión\nscriptSourceTypeInPlace=Script in situ\nscriptSourceTypeUrl=URL externa\nscriptSourceTypeSource=Fuente existente\nimportScripts=Importar scripts\nscriptsContained=$NUMBER$ scripts\nscriptSourceCollectionImportTitle=Importar scripts desde la fuente ($SELECTED$/$COUNT$)\nnoScriptsFound=No se han encontrado scripts\ntunnel=Túnel\nnotInitialized=No inicializado\nselectCategory=Selecciona la categoría ...\nscriptSourceName=Nombre del script\nscriptSourceNameDescription=El nombre de archivo del script en la fuente\nworkspaceRestartTitle=Espacio de trabajo preparado\nworkspaceRestartContent=Se ha creado un acceso directo al nuevo espacio de trabajo en $PATH$. Puedes navegar hasta el acceso directo o reiniciar XPipe ahora para abrir el nuevo espacio de trabajo automáticamente.\nbrowseShortcut=Examinar archivo\nsyncModeInstant=Sincronizar al instante\nsyncModeSession=Sincronizar al iniciar y al salir\nsyncModeManual=Sincronizar manualmente\npushChanges=Empujar cambios\npullChanges=Tirar de los cambios\nsourcedFrom=Obtenido de $SOURCE$\ninPlaceScript=Script in situ\ngeneric=Genérico\nsyncToPlainDirectory=Sincronizar con directorio plano\nsyncToPlainDirectoryDescription=Cuando sincronices con un directorio local, puedes tratar este directorio como otro repositorio git o simplemente como un directorio plano. Si la opción directorio plano está activada, el directorio no se inicializa como un repositorio git.\nopenSpiceSession=Abrir sesión SPICE\nterminalBehaviour=Comportamiento del terminal\nnoScanPossible=No se han encontrado conexiones compatibles\nnetworkSwitchPorts=Puertos de red\nnswitchGroup.displayName=Puertos de red\nnswitchGroup.displayDescription=Enumerar los puertos disponibles en un dispositivo de red\nnswitchPort.displayName=Puerto de red\nnswitchPort.displayDescription=Controlar un puerto individual en un dispositivo de conmutación de red\nenablePort=Habilitar puerto\nshutdownPort=Cerrar puerto\nresetPort=Restablecer puerto\nuseSystemDefault=Utilizar el sistema por defecto\nportStatus=Estado del puerto\nclearCounters=Borrar contadores\nshowStatus=Mostrar estado\nshowAllPorts=Mostrar todos los puertos\nactiveLicense=Licencia\nactiveLicenseDescription=Activar una clave de licencia XPipe\nauthenticatorApp=Aplicación Autenticador\nsecurityKey=Clave de seguridad\nmcpAdditionalContext=Contexto MCP adicional\nmcpAdditionalContextDescription=Instrucciones adicionales para pasar al cliente MCP. Utilízalo para controlar el comportamiento del agente y proporcionar contexto adicional para tu configuración individual.\nmcpAdditionalContextSample=- No reinicies ningún servicio o demonio automáticamente sin confirmarlo primero\\n- Al configurar una interfaz de red, utiliza siempre 192.168.1.1/24 como puerta de enlace\nprefsRestartTitle=Reinicio necesario\nprefsRestartContent=Algunas opciones que has cambiado requieren reiniciar la aplicación para aplicarse. ¿Quieres reiniciar XPipe ahora?\nbashShell=Shell Bash\n"
  },
  {
    "path": "lang/strings/translations_fr.properties",
    "content": "delete=Supprimer\nproperties=Propriétés\nusedDate=Utilisé $DATE$\nopenDir=Répertoire ouvert\nsortLastUsed=Trier par date de dernière utilisation\nsortAlphabetical=Tri alphabétique par nom\nsortIndexed=Tri par ordre d'index\nrestartDescription=Un redémarrage peut souvent être une solution rapide\nreportIssue=Signaler un problème\nreportIssueDescription=Ouvre le rapporteur de questions intégré\nusefulActions=Actions utiles\nstored=Sauvegardé\ntroubleshootingOptions=Outils de dépannage\ntroubleshoot=Dépannage\nremote=Fichier distant\n#custom\naddShellStore=Ajouter un Shell ...\naddShellTitle=Ajouter une connexion Shell\nsavedConnections=Connexions sauvegardées\nsave=Sauvegarde\nclean=Nettoyer\n#custom\nmoveTo=Déplacer vers ...\naddDatabase=Base de données ...\nbrowseInternalStorage=Parcourir le stockage interne\naddTunnel=Tunnel ...\naddService=Service ...\naddScript=Script ...\naddHost=Hôte distant ...\naddShell=Environnement Shell ...\naddCommand=Commande ...\naddAutomatically=Ajoute automatiquement ...\naddOther=Ajouter d'autres ...\nconnectionAdd=Ajouter une connexion\nscriptAdd=Ajouter un script\nscriptGroupAdd=Ajouter un groupe de scripts\nidentityAdd=Ajouter une identité\nnew=Nouveau\nselectType=Sélectionner un type\nselectTypeDescription=Sélectionne le type de connexion\n#custom\nselectShellType=Type de Shell\nselectShellTypeDescription=Sélectionne le type de connexion Shell\nname=Nom\nstoreIntroHeader=Hub de connexion\nstoreIntroContent=Ici, tu peux gérer toutes tes connexions shell locales et distantes en un seul endroit. Pour commencer, tu peux rapidement détecter automatiquement les connexions disponibles et choisir celles que tu veux ajouter.\nstoreIntroButton=Recherche de connexions ...\ndragAndDropFilesHere=Ou bien tu peux simplement faire glisser et déposer un fichier ici\nconfirmDsCreationAbortTitle=Confirmer l'abandon\nconfirmDsCreationAbortHeader=Veux-tu interrompre la création de la source de données ?\nconfirmDsCreationAbortContent=Tout progrès dans la création de la source de données sera perdu.\nconfirmInvalidStoreTitle=Sauter la validation\nconfirmInvalidStoreContent=Veux-tu ignorer la validation de la connexion ? Tu peux ajouter cette connexion même si elle n'a pas pu être validée et régler les problèmes de connexion plus tard.\nexpand=Développe\naccessSubConnections=Sous-connexions d'accès\ncommon=Commun\ncolor=Couleur\nalwaysConfirmElevation=Toujours confirmer l'élévation de la permission\nalwaysConfirmElevationDescription=Contrôle la façon de gérer les cas où des autorisations élevées sont nécessaires pour exécuter une commande sur un système, par exemple avec sudo.\\n\\nPar défaut, toutes les informations d'identification sudo sont mises en cache au cours d'une session et fournies automatiquement en cas de besoin. Si cette option est activée, il te sera demandé de confirmer l'accès à l'élévation à chaque fois.\nallow=Permettre\nask=Demande\ndeny=Refuser\nshare=Ajouter au dépôt git\nunshare=Retirer du dépôt git\nremove=Enlever\ncreateNewCategory=Nouvelle sous-catégorie\nprompt=Invite\ncustomCommand=Commande personnalisée\nother=Autre\nsetLock=Set lock\nselectConnection=Sélectionner une connexion\nselectEntry=Sélectionner une entrée\ncreateLock=Créer une phrase de passe\nchangeLock=Changer de phrase de passe\ntest=Test\nfinish=Finir\nerror=Une erreur s'est produite\ndownloadStageDescription=Déplace les fichiers téléchargés dans le répertoire des téléchargements de ton système et l'ouvre.\nok=Ok\nsearch=Recherche\nrepeatPassword=Répéter le mot de passe\n#custom\naskpassAlertTitle=Demande de mot de passe\nunsupportedOperation=Opération non prise en charge : $MSG$\nfileConflictAlertTitle=Résoudre un conflit\nfileConflictAlertContent=Un conflit a été rencontré. Le fichier $FILE$ existe déjà sur le système cible.\\n\\nComment veux-tu procéder ?\nfileConflictAlertContentMultiple=Un conflit a été rencontré. Le fichier $FILE$ existe déjà.\\n\\nComment veux-tu procéder ? Il peut y avoir d'autres conflits que tu peux résoudre automatiquement en choisissant une option qui s'applique à tous.\nmoveAlertTitle=Confirmer le déplacement\nmoveAlertHeader=Veux-tu déplacer les ($COUNT$) éléments sélectionnés dans $TARGET$?\ndeleteAlertTitle=Confirmer la suppression\ndeleteAlertHeader=Veux-tu supprimer les ($COUNT$) éléments sélectionnés ?\nselectedElements=Éléments sélectionnés :\nmustNotBeEmpty=$VALUE$ ne doit pas être vide\nvalueMustNotBeEmpty=La valeur ne doit pas être vide\ntransferDescription=Fais glisser les fichiers ici pour les télécharger\ndragLocalFiles=Téléchargements de traîneaux à partir d'ici\nnull=$VALUE$ doit être non nul\nroots=Racines\nscripts=Scripts\nsearchFilter=Recherche ...\nrecent=Récemment\nshortcut=Raccourci\nbrowserWelcomeEmptyHeader=Navigateur de fichiers\nbrowserWelcomeEmptyContent=Tu peux choisir à gauche les systèmes à ouvrir dans le navigateur de fichiers. XPipe se souviendra des systèmes et des répertoires auxquels tu as accédé précédemment et les affichera dans un menu d'accès rapide ici à l'avenir.\nbrowserWelcomeEmptyButton=Ouvrir un navigateur de fichiers local\nbrowserWelcomeSystems=Tu as récemment été connecté aux systèmes suivants :\nbrowserWelcomeDocsHeader=Documentation\nbrowserWelcomeDocsContent=Si tu préfères une approche plus guidée pour te familiariser avec XPipe, consulte le site Web de documentation.\nbrowserWelcomeDocsButton=Documentation ouverte\nhostFeatureUnsupported=$FEATURE$ n'est pas installé sur l'hôte\nmissingStore=$NAME$ n'existe pas\nconnectionName=Nom de la connexion\nconnectionNameDescription=Donne un nom personnalisé à cette connexion\nopenFileTitle=Ouvrir un fichier\nunknown=Inconnu\nscanAlertTitle=Ajouter des connexions\nscanAlertChoiceHeader=Cible\nscanAlertChoiceHeaderDescription=Choisis où rechercher les connexions. Cela permet de rechercher d'abord toutes les connexions disponibles.\nscanAlertHeader=Types de connexion\nscanAlertHeaderDescription=Sélectionne les types de connexions que tu veux ajouter automatiquement pour le système.\nnoInformationAvailable=Aucune information disponible\nyes=Oui\nno=Non\nerrorOccured=Une erreur s'est produite\nterminalErrorOccured=Une erreur de terminal s'est produite\nerrorTypeOccured=Une exception de type $TYPE$ a été lancée\npermissionsAlertTitle=Permissions requises\npermissionsAlertHeader=Des autorisations supplémentaires sont nécessaires pour effectuer cette opération.\npermissionsAlertContent=Suis le pop-up pour donner à XPipe les autorisations nécessaires dans le menu des paramètres.\nerrorDetails=Détails de l'erreur\nupdateReadyAlertTitle=Prêt pour la mise à jour\nupdateReadyAlertHeader=Une mise à jour de la version $VERSION$ est prête à être installée\nupdateReadyAlertContent=Cela installera la nouvelle version et redémarrera XPipe une fois l'installation terminée.\nerrorNoDetail=Aucun détail d'erreur n'est disponible\nerrorNoExceptionMessage=Une erreur de type $TYPE$ a été provoquée\nupdateAvailableTitle=Mise à jour disponible\nupdateAvailableContent=Une mise à jour de XPipe vers la version $VERSION$ est disponible pour installation. Même si XPipe n'a pas pu être démarré, tu peux essayer d'installer la mise à jour pour éventuellement résoudre le problème.\nclipboardActionDetectedTitle=Action du presse-papiers détectée\nclipboardActionDetectedContent=XPipe a détecté dans ton presse-papiers un contenu qui peut être ouvert. Veux-tu l'ouvrir maintenant ? Veux-tu importer le contenu de ton presse-papiers ?\ninstall=Installer ...\nignore=Ignorer\npossibleActions=Actions disponibles\nreportError=Signaler une erreur\nreportOnGithub=Créer un rapport de problème sur GitHub\nreportOnGithubDescription=Ouvre un nouveau problème dans le dépôt GitHub\nreportErrorDescription=Envoyer un rapport d'erreur avec un retour d'information optionnel de l'utilisateur et des informations de diagnostic\nignoreError=Ignorer l'erreur\nignoreErrorDescription=Ignore cette erreur et continue comme si de rien n'était\nprovideEmail=Comment pouvons-nous te contacter (facultatif, uniquement si tu veux obtenir une réponse). Ton rapport est anonyme par défaut, tu peux donc fournir des informations de contact comme une adresse électronique ici.\nadditionalErrorInfo=Fournir des informations supplémentaires (facultatif)\nadditionalErrorAttachments=Sélectionne les pièces jointes (facultatif)\ndataHandlingPolicies=Politique de confidentialité\nsendReport=Envoyer un rapport\nerrorHandler=Gestionnaire d'erreurs\nevents=Les événements\nvalidate=Valider\nstackTrace=Trace de pile\npreviousStep=< Précédent\nnextStep=Suivant >\nfinishStep=Terminer\nselect=Sélectionne\nbrowseInternal=Parcourir l'intérieur\ncheckOutUpdate=Vérifier la mise à jour\nquit=Quitter\nnoTerminalSet=Aucune application de terminal n'a été réglée automatiquement. Tu peux le faire manuellement dans le menu des paramètres.\n#custom\nconnections=Connexions\nconnectionHub=Concentrateur de connexion\nsettings=Paramètres\nexplorePlans=Licence\nhelp=Aide\nabout=A propos de\ndeveloper=Développeur\nbrowseFileTitle=Parcourir le fichier\nbrowser=Navigateur de fichiers\nselectFileFromComputer=Sélectionne un fichier à partir de cet ordinateur\nlinks=Liens\nwebsite=Site web\ndiscordDescription=Rejoins le serveur Discord\nredditDescription=Rejoins le subreddit XPipe\nsecurity=Sécurité\nsecurityPolicy=Informations sur la sécurité\nsecurityPolicyDescription=Lire la politique de sécurité détaillée\nprivacy=Politique de confidentialité\nprivacyDescription=Lis la politique de confidentialité de l'application XPipe\nslackDescription=Rejoins l'espace de travail Slack\nsupport=Support\ngithubDescription=Consulte le dépôt GitHub\nopenSourceNotices=Avis Open Source\ncheckForUpdates=Vérifier les mises à jour\ncheckForUpdatesDescription=Télécharger une mise à jour s'il y en a une\nlastChecked=Dernière vérification\nversion=Version\nbuild=Version de construction\nruntimeVersion=Version d'exécution\nvirtualMachine=Machine virtuelle\nupdateReady=Installer une mise à jour\nupdateReadyPortable=Vérifier la mise à jour\nupdateReadyDescription=Une mise à jour a été téléchargée et est prête à être installée\nupdateReadyDescriptionPortable=Une mise à jour est disponible au téléchargement\nupdateRestart=Redémarre pour mettre à jour\nnever=Jamais\nupdateAvailableTooltip=Mise à jour disponible\nptbAvailableTooltip=Test public disponible\nvisitGithubRepository=Visiter le dépôt GitHub\nupdateAvailable=Mise à jour disponible : $VERSION$\ndownloadUpdate=Télécharger la mise à jour\nlegalAccept=J'accepte le contrat de licence de l'utilisateur final\nconfirm=Confirme\nprint=Imprimer\nwhatsNew=Nouveautés de la version $VERSION$ ($DATE$)\nantivirusNoticeTitle=Une note sur les programmes antivirus\nupdateChangelogAlertTitle=Changelog\n#custom\ngreetingsAlertTitle=Bienvenue sur XPipe\neula=Contrat de licence de l'utilisateur final\n#custom\nnews=Nouveautés\nintroduction=Introduction\nprivacyPolicy=Politique de confidentialité\nagree=Accepte\ndisagree=Ne pas être d'accord\ndirectories=Répertoires\nlogFile=Fichier journal\nlogFiles=Fichiers journaux\nlogFilesAttachment=Fichiers journaux\nissueReporter=Rapporteur de problèmes\nopenCurrentLogFile=Fichiers journaux\nopenCurrentLogFileDescription=Ouvrir le fichier journal de la session en cours\nopenLogsDirectory=Répertoire des journaux ouverts\ninstallationFiles=Fichiers d'installation\nopenInstallationDirectory=Fichiers d'installation\nopenInstallationDirectoryDescription=Répertoire d'installation d'Open XPipe\nlaunchDebugMode=Mode débogage\nlaunchDebugModeDescription=Redémarre XPipe en mode débogage\nextensionInstallTitle=Télécharger\nextensionInstallDescription=Cette action nécessite des bibliothèques tierces supplémentaires qui ne sont pas distribuées par XPipe. Tu peux les installer automatiquement ici. Les composants sont ensuite téléchargés à partir du site web du fournisseur :\nextensionInstallLicenseNote=En effectuant le téléchargement et l'installation automatique, tu acceptes les termes des licences des tiers :\nlicense=Licence\ninstallRequired=Installation requise\nrestore=Restaurer\nrestoreAllSessions=Restaurer toutes les sessions\nlimitedTouchscreenMode=Mode limité de l'écran tactile\nlimitedTouchscreenModeDescription=Lorsque tu utilises cette application sur une interface tactile plus exotique comme un écran de téléphone, certains menus peuvent ne pas fonctionner correctement. Lorsque cette option est activée, l'implémentation du menu utilise une fonctionnalité plus limitée pour travailler avec des événements souris/touche envoyés de manière éparse.\nappearance=Apparence\ndisplay=Affichage\npersonalization=Personnalisation\ndisplayOptions=Options d'affichage\ntheme=Thème\nrdpConfiguration=Configuration du bureau à distance\nrdpClient=Client RDP\nrdpClientDescription=Le programme client RDP à appeler lors du lancement des connexions RDP.\\n\\nNote que les divers clients ont différents degrés de capacités et d'intégrations. Certains clients ne prennent pas en charge le passage automatique des mots de passe, tu dois donc toujours les remplir au lancement.\nlocalShell=Shell local\nthemeDescription=Ton thème d'affichage préféré.\ndontAutomaticallyStartVmSshServer=Ne démarre pas automatiquement le serveur SSH pour les machines virtuelles lorsque c'est nécessaire\ndontAutomaticallyStartVmSshServerDescription=Toute connexion shell à une VM fonctionnant dans un hyperviseur se fait par l'intermédiaire de SSH. XPipe peut démarrer automatiquement le serveur SSH installé lorsque cela est nécessaire. Si tu ne le souhaites pas pour des raisons de sécurité, tu peux simplement désactiver ce comportement avec cette option.\nconfirmGitShareTitle=Git sync\nconfirmGitShareContent=Veux-tu ajouter le fichier sélectionné à ton dépôt git ? Cela copiera une version cryptée du fichier dans ton coffre-fort git et validera tes modifications. Tu auras alors accès au fichier sur tous les bureaux synchronisés.\ngitShareFileTooltip=Ajoute un fichier au répertoire de données de git vault pour qu'il soit automatiquement synchronisé.\\n\\nCette action ne peut être utilisée que lorsque le git vault est activé dans les paramètres.\nperformanceMode=Mode de performance\nperformanceModeDescription=Désactive tous les effets visuels qui ne sont pas nécessaires afin d'améliorer les performances de l'application.\ndontAcceptNewHostKeys=N'accepte pas automatiquement les nouvelles clés d'hôte SSH\ndontAcceptNewHostKeysDescription=XPipe accepte automatiquement par défaut les clés d'hôte des systèmes pour lesquels ton client SSH n'a pas de clé d'hôte connue déjà enregistrée. Cependant, si une clé d'hôte connue a changé, il refusera de se connecter si tu n'acceptes pas la nouvelle.\\n\\nLa désactivation de ce comportement te permet de vérifier toutes les clés d'hôte, même s'il n'y a pas de conflit au départ.\nuiScale=Échelle de l'interface utilisateur\nuiScaleDescription=Une valeur d'échelle personnalisée qui peut être définie indépendamment de l'échelle d'affichage du système. Les valeurs sont exprimées en pourcentage. Ainsi, une valeur de 150 se traduira par une échelle d'affichage de 150 %.\neditorProgram=Programme d'édition\neditorProgramDescription=L'éditeur de texte par défaut à utiliser lors de l'édition de n'importe quel type de données textuelles.\nwindowOpacity=Opacité de la fenêtre\nwindowOpacityDescription=Modifie l'opacité de la fenêtre pour suivre ce qui se passe en arrière-plan.\nuseSystemFont=Utiliser la police du système\n#custom\nopenDataDir=Répertoire de données du coffre-fort\nopenDataDirButton=Répertoire de données ouvertes\nopenDataDirDescription=Si tu veux synchroniser des fichiers supplémentaires, tels que des clés SSH, entre les systèmes avec ton dépôt git, tu peux les placer dans le répertoire de données de stockage. Tous les fichiers qui y sont référencés verront leur chemin d'accès automatiquement adapté sur n'importe quel système synchronisé.\nupdates=Mises à jour\nselectAll=Sélectionne tout\nadvanced=Avancée\nthirdParty=Avis de source ouverte\neulaDescription=Lis le contrat de licence de l'utilisateur final pour l'application XPipe\nthirdPartyDescription=Affiche les licences open source des bibliothèques tierces\nworkspaceLock=Phrase de passe principale\nenableGitStorage=Activer la synchronisation\nsharing=Partage\ngitSync=Git sync\nenableGitStorageDescription=Lorsque cette option est activée, XPipe initialise un dépôt git pour le coffre-fort local et y enregistre toutes les modifications. Note que cela nécessite l'installation de git et peut ralentir les opérations de chargement et d'enregistrement.\\n\\nToutes les catégories qui doivent être synchronisées doivent être explicitement marquées comme synchronisées.\nstorageGitRemote=URL de synchronisation à distance\nstorageGitRemoteDescription=Lorsque cette option est activée, XPipe récupère automatiquement toutes les modifications lors du chargement et les transfère vers le dépôt distant lors de l'enregistrement.\\n\\nCela te permet de partager ton coffre-fort entre plusieurs installations de XPipe. Il prend en charge les URL HTTP et SSH, ainsi que les répertoires locaux.\n#custom\nvault=Coffre-fort\nworkspaceLockDescription=Définit un mot de passe personnalisé pour crypter toute information sensible stockée dans XPipe.\\n\\nCela permet d'accroître la sécurité en fournissant une couche supplémentaire de cryptage pour les informations sensibles stockées. Tu seras alors invité à saisir le mot de passe au démarrage de XPipe.\nuseSystemFontDescription=Contrôle l'utilisation de la police par défaut de ton système ou de la police Inter, qui est incluse dans XPipe.\ntooltipDelay=Délai de l'infobulle\ntooltipDelayDescription=Le nombre de millisecondes à attendre avant qu'une info-bulle ne soit affichée.\nfontSize=Taille de police\nwindowOptions=Options de la fenêtre\nsaveWindowLocation=Sauvegarder l'emplacement de la fenêtre\nsaveWindowLocationDescription=Contrôle si les coordonnées de la fenêtre doivent être sauvegardées et restaurées lors des redémarrages.\nstartupShutdown=Démarrage / Arrêt\nshowChildrenConnectionsInParentCategory=Afficher les catégories enfants dans la catégorie parent\nshowChildrenConnectionsInParentCategoryDescription=Inclure ou non toutes les connexions situées dans les sous-catégories lorsqu'une certaine catégorie parentale est sélectionnée.\\n\\nSi cette option est désactivée, les catégories se comportent davantage comme des dossiers classiques qui n'affichent que leur contenu direct sans inclure les sous-dossiers.\ncondenseConnectionDisplay=Condense l'affichage des connexions\ncondenseConnectionDisplayDescription=Faire en sorte que chaque connexion de niveau supérieur prenne moins d'espace vertical pour permettre une liste de connexions plus condensée.\nopenConnectionSearchWindowOnConnectionCreation=Ouvrir la fenêtre de recherche de connexion lors de la création de la connexion\nopenConnectionSearchWindowOnConnectionCreationDescription=Ouverture automatique ou non de la fenêtre de recherche des sous-connexions disponibles lors de l'ajout d'une nouvelle connexion shell.\nworkflow=Flux de travail\nsystem=Système\napplication=Application\nstorage=Stockage\nrunOnStartup=Exécuter au démarrage\ncloseBehaviour=Comportement de sortie\ncloseBehaviourDescription=Contrôle la façon dont XPipe doit procéder à la fermeture de sa fenêtre principale.\nlanguage=Langue\nlanguageDescription=La langue d'affichage à utiliser. Les traductions sont améliorées grâce aux contributions de la communauté. Tu peux contribuer à l'effort de traduction en soumettant des correctifs de traduction sur GitHub.\n#custom\nlightTheme=Thème clair\ndarkTheme=Thème sombre\nexit=Quitter XPipe\ncontinueInBackground=Continue en arrière-plan\nminimizeToTray=Minimiser dans la barre d'état\ncloseBehaviourAlertTitle=Définir le comportement de fermeture\ncloseBehaviourAlertTitleHeader=Sélectionne ce qui doit se passer lors de la fermeture de la fenêtre. Toutes les connexions actives seront fermées lorsque l'application sera arrêtée.\nstartupBehaviour=Comportement au démarrage\nstartupBehaviourDescription=Contrôle le comportement par défaut de l'application de bureau lorsque XPipe est démarré.\nclearCachesAlertTitle=Nettoyer le cache\nclearCachesAlertContent=Veux-tu nettoyer tous les caches de XPipe ? Cela supprimera toutes les données du cache qui sont stockées pour améliorer l'expérience de l'utilisateur.\nstartGui=Démarrer l'interface graphique\nstartInTray=Démarrer dans la barre d'état\nstartInBackground=Démarrer en arrière-plan\nclearCaches=Vider les caches ...\nclearCachesDescription=Efface toutes les données du cache\ncancel=Annuler\nnotAnAbsolutePath=Pas un chemin absolu\nnotADirectory=Pas un répertoire\nnotAnEmptyDirectory=Pas un répertoire vide\nautomaticallyCheckForUpdates=Vérifier les mises à jour\nautomaticallyCheckForUpdatesDescription=Lorsqu'elle est activée, l'information sur les nouvelles versions est automatiquement récupérée lorsque XPipe est en cours d'exécution après un certain temps. Tu dois toujours confirmer explicitement l'installation d'une mise à jour.\nsendAnonymousErrorReports=Envoyer des rapports d'erreur anonymes\nsendUsageStatistics=Envoyer des statistiques d'utilisation anonymes\nstorageDirectory=Répertoire de stockage\nstorageDirectoryDescription=L'emplacement où XPipe doit stocker toutes les informations de connexion. Lorsque l'on modifie cet emplacement, les données de l'ancien répertoire ne sont pas copiées dans le nouveau.\nlogLevel=Niveau du journal\nappBehaviour=Comportement de l'application\nlogLevelDescription=Le niveau de journal qui doit être utilisé lors de l'écriture des fichiers journaux.\ndeveloperMode=Mode développeur\ndeveloperModeDescription=Lorsque cette option est activée, tu as accès à toute une série d'options supplémentaires utiles pour le développement.\neditor=Éditeur\ncustom=Sur mesure\npasswordManager=Gestionnaire de mots de passe\nexternalPasswordManager=Gestionnaire de mots de passe externe\npasswordManagerDescription=Le gestionnaire de mots de passe installé localement à intégrer.\\n\\nSi un gestionnaire de mots de passe est installé, tu peux configurer XPipe pour qu'il récupère les mots de passe à partir de celui-ci, de sorte que XPipe n'ait pas à stocker les mots de passe lui-même. Lorsque cette option est activée, tout champ de mot de passe pour une connexion peut alors être configuré pour utiliser le gestionnaire de mots de passe.\npasswordManagerCommandTest=Test du gestionnaire de mot de passe\npasswordManagerCommandTestDescription=Tu peux tester ici si la sortie semble correcte si tu as mis en place un gestionnaire de mots de passe.\npreferTerminalTabs=Préfère ouvrir de nouveaux onglets\npreferTerminalTabsDescription=Contrôle si XPipe essaiera d'ouvrir de nouveaux onglets dans le terminal choisi au lieu de nouvelles fenêtres. Tous les terminaux ne prennent pas en charge les onglets.\ncustomRdpClientCommand=Commande personnalisée\ncustomRdpClientCommandDescription=La commande à exécuter pour démarrer le client RDP personnalisé.\\n\\nLa chaîne de caractères de remplacement $FILE sera remplacée par le nom du fichier .rdp absolu entre guillemets lorsqu'elle sera appelée. N'oublie pas de citer le chemin d'accès à l'exécutable s'il contient des espaces.\ncustomEditorCommand=Commande personnalisée de l'éditeur\ncustomEditorCommandDescription=La commande à exécuter pour démarrer l'éditeur personnalisé.\\n\\nLa chaîne de caractères de remplacement $FILE sera remplacée par le nom de fichier absolu entre guillemets lorsqu'elle sera appelée. N'oublie pas de citer le chemin d'accès à l'exécutable de ton éditeur s'il contient des espaces.\neditorReloadTimeout=Délai de rechargement de l'éditeur\neditorReloadTimeoutDescription=Le nombre de millisecondes à attendre avant de lire un fichier après sa mise à jour. Cela permet d'éviter les problèmes dans les cas où ton éditeur est lent à écrire ou à libérer les verrous de fichiers.\nencryptAllVaultData=Crypte toutes les données du coffre-fort\nencryptAllVaultDataDescription=Lorsque cette option est activée, chaque partie des données de connexion au coffre-fort est cryptée avec la clé de cryptage du coffre-fort de l'utilisateur, et non pas seulement les secrets contenus dans ces données. Cela ajoute une couche de sécurité supplémentaire pour d'autres paramètres tels que les noms d'utilisateur, les noms d'hôte, etc. qui ne sont pas cryptés par défaut dans le coffre-fort.\\n\\nCette option rendra l'historique de ton coffre-fort git et les diffs inutiles car tu ne pourras plus voir les modifications originales, seulement les modifications binaires.\n#custom\nvaultSecurity=Sécurité du coffre-fort\ndeveloperDisableUpdateVersionCheck=Désactiver la vérification de la version de la mise à jour\ndeveloperDisableUpdateVersionCheckDescription=Contrôle si le vérificateur de mise à jour ignore le numéro de version lorsqu'il recherche une mise à jour.\ndeveloperDisableGuiRestrictions=Désactiver les restrictions de l'interface graphique\ndeveloperDisableGuiRestrictionsDescription=Contrôle si certaines actions désactivées peuvent encore être exécutées à partir de l'interface utilisateur.\ndeveloperShowHiddenEntries=Afficher les entrées cachées\ndeveloperShowHiddenEntriesDescription=Lorsque cette option est activée, les sources de données cachées et internes sont affichées.\ndeveloperShowHiddenProviders=Afficher les fournisseurs cachés\ndeveloperShowHiddenProvidersDescription=Contrôle si les fournisseurs de connexion et de source de données cachés et internes seront affichés dans la boîte de dialogue de création.\ndeveloperDisableConnectorInstallationVersionCheck=Désactiver la vérification de la version du connecteur\ndeveloperDisableConnectorInstallationVersionCheckDescription=Contrôle si le vérificateur de mise à jour ignore le numéro de version lorsqu'il inspecte la version d'un connecteur XPipe installé sur une machine distante.\nshellCommandTest=Test de commande Shell\nshellCommandTestDescription=Exécute une commande dans la session shell utilisée en interne par XPipe.\nterminal=Terminal\nterminalType=Émulateur de terminal\nterminalConfiguration=Configuration du terminal\nterminalCustomization=Personnalisation du terminal\neditorConfiguration=Configuration de l'éditeur\ndefaultApplication=Application par défaut\ninitialSetup=Configuration initiale\nterminalTypeDescription=Le terminal par défaut à utiliser pour ouvrir des connexions shell.\\n\\nLe niveau de prise en charge des fonctionnalités varie d'un terminal à l'autre, et chacun d'entre eux est indiqué comme étant recommandé ou non recommandé. Ton expérience d'utilisateur sera meilleure si tu utilises un terminal recommandé.\nprogram=Programme\ncustomTerminalCommand=Commande de terminal personnalisée\ncustomTerminalCommandDescription=La commande à exécuter pour ouvrir le terminal personnalisé avec une commande donnée.\\n\\nXPipe créera un script de lancement temporaire pour ton terminal à exécuter. La chaîne de caractères $CMD de la commande que tu as fournie sera remplacée par le script de lancement lorsqu'il sera appelé. N'oublie pas de citer le chemin d'accès à l'exécutable de ton terminal s'il contient des espaces.\nclearTerminalOnInit=Effacer le terminal au démarrage\nclearTerminalOnInitDescription=Lorsque cette option est activée, XPipe exécute une commande d'effacement après le lancement d'une nouvelle session de terminal afin de supprimer toute sortie inutile qui a été imprimée lors du démarrage de la session de terminal.\ndontCachePasswords=Ne pas mettre en cache les mots de passe demandés\ndontCachePasswordsDescription=Contrôle si les mots de passe demandés doivent être mis en cache en interne par XPipe afin que tu n'aies pas à les saisir à nouveau dans la session en cours.\\n\\nSi ce comportement est désactivé, tu devras saisir à nouveau les informations d'identification demandées chaque fois qu'elles seront exigées par le système.\ndenyTempScriptCreation=Refuser la création de scripts temporaires\ndenyTempScriptCreationDescription=Pour réaliser certaines de ses fonctionnalités, XPipe crée parfois des scripts shell temporaires sur un système cible pour permettre une exécution facile de commandes simples. Ces scripts ne contiennent aucune information sensible et sont simplement créés à des fins de mise en œuvre.\\n\\nSi ce comportement est désactivé, XPipe ne créera aucun fichier temporaire sur un système distant. Cette option est utile dans les contextes de haute sécurité où chaque modification du système de fichiers est surveillée. Si cette option est désactivée, certaines fonctionnalités, par exemple les environnements shell et les scripts, ne fonctionneront pas comme prévu.\ndisableCertutilUse=Désactiver l'utilisation de certutil sur Windows\nuseLocalFallbackShell=Utiliser le shell local de repli\nuseLocalFallbackShellDescription=Passe à l'utilisation d'un autre shell local pour gérer les opérations locales. Il s'agirait de PowerShell sur Windows et de l'interpréteur de commandes bourne sur d'autres systèmes.\\n\\nCette option peut être utilisée dans le cas où le shell local normal par défaut est désactivé ou cassé dans une certaine mesure. Certaines fonctions peuvent ne pas fonctionner comme prévu lorsque cette option est activée.\ndisableCertutilUseDescription=En raison de plusieurs lacunes et bogues dans cmd.exe, des scripts shell temporaires sont créés avec certutil en l'utilisant pour décoder l'entrée base64 car cmd.exe s'interrompt sur l'entrée non ASCII. XPipe peut également utiliser PowerShell pour cela, mais cela sera plus lent.\\n\\nCela désactive toute utilisation de certutil sur les systèmes Windows pour réaliser certaines fonctionnalités et se rabat sur PowerShell à la place. Cela pourrait plaire à certains antivirus, car certains d'entre eux bloquent l'utilisation de certutil.\ndisableTerminalRemotePasswordPreparation=Désactiver la préparation du mot de passe à distance du terminal\ndisableTerminalRemotePasswordPreparationDescription=Dans les situations où une connexion shell à distance qui passe par plusieurs systèmes intermédiaires doit être établie dans le terminal, il peut être nécessaire de préparer tous les mots de passe requis sur l'un des systèmes intermédiaires pour permettre un remplissage automatique de toutes les invites.\\n\\nSi tu ne veux pas que les mots de passe soient transférés vers un système intermédiaire, tu peux désactiver ce comportement. Tout mot de passe intermédiaire requis sera alors demandé dans le terminal lui-même lorsqu'il sera ouvert.\nmore=Plus\ntranslate=Traductions\nallConnections=Toutes les connexions\nallScripts=Tous les scripts\nallIdentities=Toutes les identités\nsynced=Synchronisé\npredefined=Prédéfini\nsamples=Échantillons\ngoodMorning=Bonjour\ngoodAfternoon=Bon après-midi\ngoodEvening=Bonne soirée\naddVisual=Visual ...\naddDesktop=Desktop ...\nssh=SSH\nsshConfiguration=Configuration SSH\nsize=Taille\nattributes=Attributs\nmodified=Modifié\nowner=Propriétaire\nupdateReadyTitle=Mise à jour vers $VERSION$ ready\ntemplates=Modèles\nretry=Réessayer\nretryAll=Réessayer tout\nreplace=Remplacer\nreplaceAll=Remplacer tout\nhibernateBehaviour=Comportement d'hibernation\nhibernateBehaviourDescription=Contrôle le comportement de l'application lorsque ton système est mis en hibernation/en veille.\noverview=Vue d'ensemble\n#custom\nhistory=Historique\nskipAll=Sauter tout\nnotes=Notes\naddNotes=Ajouter des notes\norder=Réorganiser\nkeepFirst=Garder en premier\nkeepLast=Conserver en dernier\npinToTop=Épingle au sommet\nunpinFromTop=Décroche du haut\norderAheadOf=Commande en avance...\nclearIndex=Réinitialiser l'index\nhttpServer=Serveur HTTP\nmcpServer=Serveur MCP\napiKey=Clé API\napiKeyDescription=La clé API pour authentifier les demandes API du démon XPipe. Pour plus d'informations sur la manière de s'authentifier, voir la documentation générale de l'API.\ndisableApiAuthentication=Désactiver l'authentification de l'API\ndisableApiAuthenticationDescription=Désactive toutes les méthodes d'authentification requises, de sorte que toute demande non authentifiée sera traitée.\\n\\nL'authentification ne doit être désactivée qu'à des fins de développement.\napi=API\nstoreIntroImportContent=Tu utilises déjà XPipe sur un autre système ? Synchronise tes connexions existantes sur plusieurs systèmes grâce à un dépôt git distant. Tu peux aussi synchroniser plus tard à tout moment s'il n'est pas encore configuré.\nstoreIntroImportButton=Synchroniser les connexions ...\nstoreIntroImportHeader=Importer des connexions\nshowNonRunningChildren=Montrer les enfants qui ne courent pas\nhttpApi=API HTTP\nisOnlySupportedLimit=n'est pris en charge qu'avec une licence professionnelle lorsqu'il y a plus de $COUNT$ connexions\nareOnlySupportedLimit=ne sont pris en charge qu'avec une licence professionnelle lorsqu'il y a plus de $COUNT$ connexions\nenabled=Activé\nenableGitStoragePtbDisabled=La synchronisation Git est désactivée pour les versions de test publiques afin d'éviter toute utilisation avec les dépôts git des versions régulières et de décourager l'utilisation d'une version PTB comme conducteur quotidien.\ncopyId=Copier l'ID API\nrequireDoubleClickForConnections=Nécessite un double clic pour les connexions\nrequireDoubleClickForConnectionsDescription=Si cette option est activée, tu dois double-cliquer sur les connexions pour les lancer. C'est utile si tu as l'habitude de double-cliquer sur les choses.\nclearTransferDescription=Effacer la sélection\nselectTab=Onglet de sélection\ncloseTab=Fermer l'onglet\ncloseOtherTabs=Fermer d'autres onglets\ncloseAllTabs=Fermer tous les onglets\ncloseLeftTabs=Ferme les onglets à gauche\ncloseRightTabs=Ferme les onglets à droite\naddSerial=Série ...\nconnect=Connecter\nworkspaces=Espaces de travail\nmanageWorkspaces=Gérer les espaces de travail\naddWorkspace=Ajouter un espace de travail ...\nworkspaceAdd=Ajouter un nouvel espace de travail\nworkspaceAddDescription=Les espaces de travail sont des configurations distinctes pour l'exécution de XPipe. Chaque espace de travail possède un répertoire de données où toutes les données sont stockées localement. Cela comprend les données de connexion, les paramètres, et plus encore.\\n\\nSi tu utilises la fonction de synchronisation, tu peux aussi choisir de synchroniser chaque espace de travail avec un dépôt git différent.\nworkspaceName=Nom de l'espace de travail\nworkspaceNameDescription=Le nom d'affichage de l'espace de travail\nworkspacePath=Chemin d'accès à l'espace de travail\nworkspacePathDescription=L'emplacement du répertoire de données de l'espace de travail\nworkspaceCreationAlertTitle=Création d'un espace de travail\ndeveloperForceSshTty=Force SSH TTY\ndeveloperForceSshTtyDescription=Fais en sorte que toutes les connexions SSH allouent un pty pour tester la prise en charge d'un stderr et d'un pty manquants.\ndeveloperDisableSshTunnelGateways=Désactiver le tunnel de passerelle SSH\ndeveloperDisableSshTunnelGatewaysDescription=N'utilise pas de sessions tunnel pour les passerelles et connecte-toi plutôt directement au système.\nttyWarning=La connexion a alloué de force un pty/tty et ne fournit pas de flux stderr séparé.\\n\\nCela peut entraîner quelques problèmes.\\n\\nSi tu le peux, essaie de faire en sorte que la commande de connexion n'alloue pas de pty.\nxshellSetup=Configuration de Xshell\ntermiusSetup=Installation de Termius\ntryPtbDescription=Essaie les nouvelles fonctions dès le début dans les versions pour développeurs de XPipe\nconfirmVaultUnencryptTitle=Confirme le décryptage du coffre-fort\nconfirmVaultUnencryptContent=Veux-tu vraiment désactiver le cryptage avancé du coffre-fort ? Cela supprimera le cryptage supplémentaire des données stockées et écrasera les données existantes.\nenableHttpApi=Activer l'API HTTP\nenableHttpApiDescription=Active l'API, ce qui permet aux programmes externes d'appeler le démon XPipe pour effectuer des actions avec tes connexions gérées.\nchooseCustomIcon=Choisis une icône personnalisée\ngitVault=Coffre-fort Git\nfileBrowser=Navigateur de fichiers\nconfirmAllDeletions=Confirme toutes les suppressions\nconfirmAllDeletionsDescription=Affichage ou non d'une boîte de dialogue de confirmation pour toutes les opérations de suppression. Par défaut, seuls les répertoires nécessitent une confirmation.\nyesterday=Hier\ngreen=Vert\nyellow=Jaune\nblue=Bleu\nred=Rouge\ncyan=Cyan\npurple=Pourpre\nasktextAlertTitle=Invite\nfileWriteSudoTitle=Sudo file write\nfileWriteSudoContent=Le fichier que tu essaies d'écrire n'accorde pas les droits d'écriture à ton utilisateur. Veux-tu écrire ce fichier en tant que root avec sudo ? Cela te permettra d'accéder automatiquement à l'utilisateur root, soit avec les informations d'identification existantes, soit par le biais d'une invite.\ndontAllowTerminalRestart=Ne pas autoriser le redémarrage du terminal\ndontAllowTerminalRestartDescription=Par défaut, les sessions de terminal peuvent être relancées après s'être terminées depuis le terminal. Pour permettre cela, XPipe acceptera ces demandes externes du terminal pour relancer la session\\n\\nXPipe n'a aucun contrôle sur le terminal et sur la provenance de cet appel, de sorte que des applications locales malveillantes peuvent également utiliser cette fonctionnalité pour lancer des connexions par l'intermédiaire de XPipe. La désactivation de cette fonctionnalité permet d'éviter ce scénario.\nopenDocumentation=Documentation ouverte\nopenDocumentationDescription=Visite la page de documentation de XPipe pour ce problème\nrenameAll=Renommer tout\nlogging=Enregistrement\nenableTerminalLogging=Activer la journalisation du terminal\nenableTerminalLoggingDescription=Active la journalisation côté client pour toutes les sessions de terminal. Toutes les entrées et sorties de la session du terminal sont écrites dans un fichier journal de la session. Note que les informations sensibles telles que les invites de mot de passe ne sont pas enregistrées.\nterminalLoggingDirectory=Journaux de session de terminal\nterminalLoggingDirectoryDescription=Tous les journaux sont stockés dans le répertoire de données de XPipe sur ton système local.\nopenSessionLogs=Ouvrir les journaux de session\nsessionLogging=Journalisation du terminal\nsessionActive=Une session en arrière-plan est en cours pour cette connexion.\\n\\nPour arrêter cette session manuellement, clique sur l'indicateur d'état.\nskipValidation=Sauter la validation\nscriptsIntroHeader=A propos des scripts\nscriptsIntroContent=Tu peux exécuter des scripts à l'invite du shell, dans le navigateur de fichiers et à la demande. Tu peux créer des scripts toi-même dans XPipe ou importer des scripts existants à partir de ton système local ou d'un dépôt git distant.\nscriptsIntroBottomHeader=Utilisation de scripts\nscriptsIntroBottomContent=Il existe une variété d'exemples de scripts pour commencer. Tu peux cliquer sur le bouton d'édition des scripts individuels pour voir comment ils sont mis en œuvre. Les scripts doivent d'abord être activés pour s'exécuter et apparaître dans les menus.\nscriptsIntroBottomButton=Commence\nscriptSourcesIntroHeader=Sources de scripts\nscriptSourcesIntroContent=Tu peux ajouter des sources de scripts personnalisées pour avoir un accès instantané à toute une collection de scripts shell. Les sources locales et les dépôts git distants sont pris en charge en tant que sources. Tous les scripts détectés dans la source deviendront automatiquement disponibles.\nscriptSourcesIntroButton=Ajouter une source ...\ncheckForSecurityUpdates=Vérifier les mises à jour de sécurité\ncheckForSecurityUpdatesDescription=XPipe peut vérifier les mises à jour de sécurité potentielles séparément des mises à jour normales des fonctionnalités. Lorsque cette fonction est activée, il est recommandé d'installer au moins les mises à jour de sécurité importantes, même si la vérification normale des mises à jour est désactivée.\\n\\nEn désactivant ce paramètre, aucune demande de version externe ne sera effectuée et tu ne seras pas informé des mises à jour de sécurité.\nclickToDock=Cliquer pour ancrer le terminal\nterminalStarting=En attente du démarrage du terminal ...\n#custom\npinTab=Épingler l'onglet\n#custom\nunpinTab=Détacher l'onglet\npinned=Épinglé\nenableConnectionHubTerminalDocking=Activation du hub de connexion terminal docking\nenableConnectionHubTerminalDockingDescription=Tu peux ancrer des fenêtres de terminal à la fenêtre de l'application XPipe dans le hub de connexion pour simuler un terminal quelque peu intégré. Les fenêtres du terminal sont alors gérées par XPipe pour toujours tenir dans la station d'accueil.\nenableFileBrowserTerminalDocking=Activer l'ancrage terminal du navigateur de fichiers\nenableFileBrowserTerminalDockingDescription=Tu peux ancrer des fenêtres de terminal à la fenêtre de l'application XPipe dans le navigateur de fichiers pour simuler un terminal quelque peu intégré. Les fenêtres du terminal sont alors gérées par XPipe pour toujours tenir dans la station d'accueil.\ndownloadsDirectory=Répertoire de téléchargements personnalisé\ndownloadsDirectoryDescription=Le répertoire personnalisé dans lequel placer les fichiers téléchargés lorsqu'on clique sur le bouton déplacer vers les téléchargements. Par défaut, XPipe utilisera le répertoire des téléchargements de l'utilisateur.\npinLocalMachineOnStartup=Onglet de la machine locale au démarrage\npinLocalMachineOnStartupDescription=Ouvrir automatiquement un onglet de machine locale et l'épingler. C'est utile si tu utilises fréquemment un navigateur de fichiers fractionné avec la machine locale et le système de fichiers distant ouverts.\nterminalErrorDescription=Cette erreur est terminale et XPipe ne peut pas continuer sans la réparer.\ngroupName=Nom du groupe\nchmodPermissions=Nouvelles autorisations\neditFilesWithDoubleClick=Éditer des fichiers avec un double clic\neditFilesWithDoubleClickDescription=Lorsque cette option est activée, un double-clic sur les fichiers les ouvrira directement dans ton éditeur de texte au lieu d'afficher le menu contextuel.\ncensorMode=Mode censeur\ncensorModeDescription=Estompent toutes les informations telles que les noms d'hôte, les noms d'utilisateur, les noms de connexion, et plus encore.\\n\\nC'est utile si tu as l'intention de faire une capture d'écran ou un partage d'écran de XPipe et que tu ne veux pas divulguer d'informations.\naddIdentity=Identité ...\nidentities=Identités\naddMacro=Action ...\nidentitiesIntroHeader=A propos des identités\nidentitiesIntroContent=Si tu réutilises des combinaisons courantes de noms d'utilisateur, de mots de passe et de clés, il peut être judicieux de créer des identités réutilisables. Cela te permet de les référencer rapidement lorsque tu ajoutes de nouvelles connexions.\nidentitiesIntroBottomHeader=Partage d'identités\nidentitiesIntroBottomContent=Tu peux ajouter des identités localement ou également les synchroniser dans le dépôt git lorsque celui-ci est activé. Cela permet de partager sélectivement les identités sur plusieurs systèmes et avec d'autres membres de l'équipe.\nidentitiesIntroBottomButton=Synchronisation de l'installation\nidentitiesIntroButton=Créer une identité\nuserName=Nom d'utilisateur\nuserAuth=Authentification par mot de passe basée sur l'utilisateur\ngroupAuth=Authentification secrète basée sur un groupe\nteam=L'équipe\nteamSettings=Paramètres de l'équipe\nteamVaults=Coffres d'équipe\n#custom\nvaultTypeNameDefault=Coffre-fort par défaut\nvaultTypeNameLegacy=Héritage d'un coffre-fort personnel\nvaultTypeNamePersonal=Coffre-fort personnel\nvaultTypeNameTeam=Coffre-fort d'équipe\nteamVaultsDescription=Les coffres-forts d'équipe permettent à plusieurs utilisateurs et groupes d'avoir un accès sécurisé à un coffre-fort partagé. Tu peux configurer les connexions et les identités pour qu'elles soient partagées par tous les utilisateurs ou qu'elles ne soient disponibles que pour des utilisateurs et des groupes individuels en les chiffrant avec leur propre clé. Les autres utilisateurs du coffre ne peuvent pas accéder aux connexions et identités personnelles et de groupe s'ils n'ont pas accès à la clé.\nvaultTypeContentDefault=Tu utilises actuellement un coffre-fort par défaut sans utilisateur et sans phrase de passe personnalisée. Les secrets sont cryptés avec la clé locale du coffre-fort. Tu peux passer à un coffre-fort personnel en créant un compte d'utilisateur de coffre-fort. Cela te permet de crypter les secrets du coffre-fort avec ta propre phrase de passe personnelle que tu dois saisir à chaque connexion pour déverrouiller le coffre-fort.\nvaultTypeContentLegacy=Tu utilises actuellement un coffre-fort personnel hérité pour ton utilisateur. Les secrets sont cryptés avec ta phrase de passe personnelle. Cette compatibilité héritée a des fonctions limitées et ne peut pas être mise à niveau vers un coffre-fort d'équipe en place.\nvaultTypeContentPersonal=Tu utilises actuellement un coffre-fort personnel pour ton utilisateur. Les secrets sont cryptés avec ta phrase de passe personnelle. Tu peux passer à un coffre-fort d'équipe en ajoutant des utilisateurs de coffre-fort supplémentaires ou en ajoutant une configuration d'accès basée sur le groupe.\nvaultTypeContentTeam=Tu utilises actuellement un coffre-fort d'équipe, qui permet à plusieurs utilisateurs d'avoir un accès sécurisé à un coffre-fort partagé. Tu peux configurer les connexions et les identités pour qu'elles soient partagées par tous les utilisateurs ou qu'elles ne soient disponibles que pour ton utilisateur personnel ou ton groupe en les chiffrant avec ta clé personnelle ou de groupe. Les autres utilisateurs du coffre-fort ne peuvent pas accéder à tes connexions et identités personnelles et de groupe s'ils n'ont pas accès à la clé.\ngroupManagement=Gestion de groupe\ngroupManagementEmpty=Gestion de groupe\ngroupManagementDescription=Gère les groupes de coffres-forts existants ou crée-en de nouveaux. Chaque groupe de coffre-fort possède sa propre clé secrète individuelle qui est utilisée pour crypter les connexions et les identités qui ne doivent être accessibles qu'au groupe et non à d'autres.\ngroupManagementEmptyDescription=Gère les groupes de coffres-forts existants ou crée-en de nouveaux. Chaque groupe de coffre-fort possède sa propre clé secrète individuelle qui est utilisée pour crypter les connexions et les identités qui ne doivent être accessibles qu'au groupe et non à d'autres.\\n\\nLes comptes de groupe pour une équipe sont pris en charge dans le plan professionnel.\nuserManagement=Gestion des utilisateurs\nuserManagementEmpty=Gestion des utilisateurs\nuserManagementDescription=Gère les utilisateurs existants du coffre-fort ou crée-en de nouveaux. Chaque utilisateur du coffre-fort a son propre mot de passe individuel qui est utilisé pour crypter les connexions et les identités qui ne doivent être accessibles qu'à l'utilisateur et pas à d'autres.\nuserManagementEmptyDescription=Gère les utilisateurs existants du coffre-fort ou crée-en de nouveaux. Chaque utilisateur du coffre-fort a son propre mot de passe qui est utilisé pour crypter les connexions et les identités qui ne doivent être accessibles qu'à l'utilisateur et non à d'autres. Crée un utilisateur pour toi-même afin de pouvoir crypter les connexions et les identités avec ta clé personnelle.\\n\\nUn seul compte utilisateur est pris en charge dans l'édition communautaire. Plusieurs comptes d'utilisateurs pour une équipe sont pris en charge dans le plan professionnel.\nuserIntroHeader=Gestion des utilisateurs\nuserIntroContent=Crée le premier compte utilisateur pour toi-même pour commencer. Cela te permet de verrouiller cet espace de travail avec un mot de passe.\naddReusableIdentity=Ajouter une identité réutilisable\nusers=Utilisateurs\nsyncVault=Synchronisation de coffre-fort\nsyncVaultDescription=Pour synchroniser ton coffre-fort avec plusieurs systèmes ou avec plusieurs membres de l'équipe, active la synchronisation git pour ce coffre-fort.\nenableGitSync=Activer la synchronisation git\nbrowseVault=Données de coffre-fort\nbrowseVaultDescription=Tu peux jeter un coup d'œil au répertoire du coffre-fort dans ton gestionnaire de fichiers. Note que les modifications externes ne sont pas recommandées et peuvent causer divers problèmes.\nbrowseVaultButton=Parcourir le coffre-fort\nvaultUsers=Utilisateurs du coffre-fort\ncreateHeapDump=Créer un dump du tas\ncreateHeapDumpDescription=Vider le contenu de la mémoire dans un fichier pour dépanner l'utilisation de la mémoire\ninitializingApp=Chargement des connexions\ncheckingLicense=Vérification de la licence\nloadingGit=Synchronisation avec git repo\nloadingGpg=Démarrage du démon GnuPG pour git\nloadingSettings=Paramètres de chargement\nloadingConnections=Chargement des connexions\nunlockingVault=Déverrouiller le coffre-fort\nloadingUserInterface=Chargement de l'interface utilisateur\nptbNotice=Avis pour la version de test publique\nuserDeletionTitle=Suppression de l'utilisateur\nuserDeletionContent=Veux-tu supprimer cet utilisateur du coffre-fort ? Cela permettra de recrypter toutes tes identités personnelles et tes secrets de connexion à l'aide de la clé du coffre-fort qui est disponible pour tous les utilisateurs. Cela prendra un certain temps et XPipe devra redémarrer pour appliquer les changements d'utilisateur.\ngroupDeletionTitle=Suppression de groupe\ngroupDeletionContent=Veux-tu supprimer ce groupe du coffre-fort ? Cela permettra de recrypter toutes les identités et tous les secrets de connexion réservés au groupe à l'aide de la clé du coffre-fort qui est disponible pour tous les utilisateurs. Cela prendra un certain temps et XPipe devra redémarrer pour appliquer les changements de groupe.\nkillTransfer=Tuer le transfert\ndestination=Destination\nconfiguration=Configuration\nnewFile=Nouveau fichier\nnewLink=Nouveau lien\nlinkName=Nom du lien\nscanConnections=Trouver les connexions disponibles ...\nobserve=Commence à observer\nstopObserve=Arrêter d'observer\ncreateShortcut=Créer un raccourci sur le bureau\nbrowseFiles=Parcourir les fichiers\nclone=Clone\ntargetPath=Chemin cible\nnewDirectory=Nouveau répertoire\ncopyShareLink=Copier le lien\nselectStore=Sélectionner un magasin\nsaveSource=Sauvegarder pour plus tard\nexecute=Exécuter\ndeleteChildren=Retire tous les enfants\nscriptGroupDescriptionDescription=Donne à ce groupe une description facultative\nabstractHostDescriptionDescription=Donne à cet hôte une description facultative\nselectSource=Sélectionne la source\ncommandLineRead=Mise à jour\ncommandLineWrite=Ecris\nadditionalOptions=Options supplémentaires\ninput=Entrée\nmachine=Machine\nopen=Ouvrir\nedit=Éditer\nscriptContents=Contenu du script\nscriptContentsDescription=Les commandes de script à exécuter\nsnippets=Dépendances des scripts\nsnippetsDescription=Autres scripts à exécuter en premier\nsnippetsDependenciesDescription=Tous les scripts possibles qui doivent être exécutés le cas échéant\nisDefault=S'exécute en mode init dans tous les shells compatibles\n#custom\nbringToShells=Apporte à tous les shells compatibles\nisDefaultGroup=Exécute tous les scripts de groupe lors de l'initialisation de l'interpréteur de commandes\nexecutionType=Type d'exécution\nexecutionTypeDescription=Dans quels contextes utiliser ce script\n#custom\nminimumShellDialect=Type de shell\nminimumShellDialectDescription=Le type d'interpréteur de commandes dans lequel ce script doit être exécuté\ndumbOnly=Muet\nterminalOnly=Terminal\nboth=Les deux\nshouldElevate=Devrait s'élever\nshouldElevateDescription=Si ce script doit être exécuté avec des autorisations élevées\nscript.displayName=Script de l'interpréteur de commandes\nscript.displayDescription=Créer un script shell réutilisable\nscriptGroup.displayName=Groupe de scripts\nscriptGroup.displayDescription=Regrouper les scripts et les organiser dans\nscriptGroup=Groupe\nscriptGroupDescription=Le groupe à qui attribuer ce texte\nscriptGroupGroupDescription=Le groupe parent facultatif auquel assigner ce groupe de scripts\nopenInNewTab=Ouvrir dans un nouvel onglet\nexecuteInBackground=en arrière-plan\nexecuteInTerminal=dans $TERM$\n#custom\nback=Retour\nbrowseInWindowsExplorer=Naviguer dans l'explorateur Windows\nbrowseInDefaultFileManager=Parcourir dans le gestionnaire de fichiers par défaut\nbrowseInFinder=Parcourir dans finder\ncopy=Copie\npaste=Coller\ncopyLocation=Emplacement de la copie\nabsolutePaths=Chemins absolus\nabsoluteLinkPaths=Chemins d'accès absolus\nabsolutePathsQuoted=Chemins d'accès absolus\nfileNames=Noms de fichiers\nlinkFileNames=Noms de fichiers de liens\nfileNamesQuoted=Noms de fichiers (cités)\ndeleteFile=Supprimer $FILE$\neditWithEditor=Éditer avec $EDITOR$\nfollowLink=Suivre le lien\ngoForward=Va de l'avant\nshowDetails=Afficher les détails\nshowDetailsDescription=Afficher la trace de la pile de l'erreur\nopenFileWith=Ouvrir avec ...\nopenWithDefaultApplication=Ouvrir avec l'application par défaut\nrename=Renommer\nrun=Exécuter\nopenInTerminal=Ouvrir dans le terminal\nfile=Fichier\ndirectory=Répertoire\nsymbolicLink=Lien symbolique\ndesktopEnvironment.displayName=Environnement de bureau\ndesktopEnvironment.displayDescription=Créer une configuration réutilisable de l'environnement de bureau à distance\ndesktopHost=Hôte de bureau\ndesktopHostDescription=La connexion de bureau à utiliser comme base\ndesktopShellDialect=Dialecte Shell\ndesktopShellDialectDescription=Le dialecte de l'interpréteur de commandes à utiliser pour exécuter des scripts et des applications\ndesktopSnippets=Extraits de scripts\ndesktopSnippetsDescription=Liste d'extraits de scripts réutilisables à exécuter en premier\ndesktopInitScript=Script d'initialisation\ndesktopInitScriptDescription=Commandes Init spécifiques à cet environnement\ndesktopTerminal=Application de terminal\ndesktopTerminalDescription=Le terminal à utiliser sur le bureau pour lancer des scripts dans\ndesktopApplication.displayName=Application de bureau\ndesktopApplication.displayDescription=Exécuter une application sur un bureau à distance\ndesktopBase=Bureau\ndesktopBaseDescription=Le bureau sur lequel cette application doit être exécutée\ndesktopEnvironmentBase=Environnement de bureau\ndesktopEnvironmentBaseDescription=L'environnement de bureau sur lequel cette application doit être exécutée\ndesktopApplicationPath=Chemin d'accès à l'application\ndesktopApplicationPathDescription=Le chemin de l'exécutable à exécuter\ndesktopApplicationArguments=Arguments\ndesktopApplicationArgumentsDescription=Les arguments facultatifs à transmettre à l'application\ndesktopCommand.displayName=Commande de bureau\ndesktopCommand.displayDescription=Exécuter une commande dans un environnement de bureau à distance\ndesktopCommandScript=Commandes\ndesktopCommandScriptDescription=Les commandes à exécuter dans l'environnement\nservice.displayName=Service\nservice.displayDescription=Transférer un service distant vers ta machine locale\nserviceLocalPort=Port local explicite\nserviceLocalPortDescription=Le port local vers lequel transférer, sinon un port aléatoire est utilisé\nserviceRemotePort=Port distant\nserviceRemotePortDescription=Le port sur lequel le service fonctionne\nserviceHost=Hôte du service\nserviceHostDescription=L'hôte sur lequel le service est exécuté\nopenWebsite=Ouvrir un site web\ncustomServiceGroup.displayName=Groupe de service\ncustomServiceGroup.displayDescription=Regrouper plusieurs services dans une même catégorie\ninitScript=Script d'initialisation - Exécuté lors de l'initialisation de l'interpréteur de commandes\nshellScript=Script de session shell - Rendre un script disponible pour être exécuté au cours d'une session shell\nrunnableScript=Script exécutable - Permet d'exécuter un script directement à partir du concentrateur de connexion\nfileScript=Script de fichier - Permet d'appeler un script pour les fichiers sélectionnés dans le navigateur de fichiers\nrunScript=Exécuter un script\ncopyUrl=Copier l'URL\nfixedServiceGroup.displayName=Groupe de service\nfixedServiceGroup.displayDescription=Liste les services disponibles sur un système\nmappedService.displayName=Service\nmappedService.displayDescription=Interagir avec un service exposé par un conteneur\ncustomService.displayName=Service\ncustomService.displayDescription=Ouvre automatiquement ou tunnelise un port de service distant sur ta machine locale\nfixedService.displayName=Service\nfixedService.displayDescription=Utiliser un service prédéfini\nnoServices=Aucun service disponible\nhasServices=$COUNT$ services disponibles\nhasService=$COUNT$ service disponible\nnoConnections=Aucune connexion disponible\nhasConnections=$COUNT$ connexions disponibles\nhasConnection=$COUNT$ connexion disponible\nopenHttp=Service HTTP ouvert\nopenHttps=Service HTTPS ouvert\nnoScriptsAvailable=Pas de scripts activés et compatibles disponibles\nscriptsDisabled=Scripts désactivés\nchangeIcon=Changer d'icône\ninit=Init\n#custom\nshell=Shell\nhub=Hub\nscript=script\ngenericScript=Générique\ngradleTasks=Tâches Gradle\nrunTask=Exécuter une tâche\narchiveName=Nom de l'archive\ncompress=Compresser\ncompressContents=Compresser le contenu\n#custom\nuntarHere=Extraire ici (archive tar)\n#custom\nuntarDirectory=Extraire vers $DIR$ (archive tar)\n#custom\nunzipDirectory=Extraire vers $DIR$ (archive zip)\n#custom\nunzipHere=Extraire ici (archive zip)\nrequiresRestart=Nécessite un redémarrage pour s'appliquer.\ndownload=Télécharger\nservicePath=Chemin de service\nservicePathDescription=Le sous-chemin facultatif lors de l'ouverture de l'URL dans un navigateur\nactive=Actif\ninactive=Inactif\nstarting=Démarrer\nremotePort=Port distant\nremotePortNumber=Port distant $PORT$\nuserIdentity=Identité personnelle\nglobalIdentity=Identité globale\nidentityChoice=Identité de l'utilisateur\nidentityChoiceDescription=Choisis une identité prédéfinie ou spécifie des détails de connexion uniquement pour cette connexion\ndefineNewIdentityOrSelect=Saisir un nouveau texte ou choisir un texte existant\nlocalIdentity.displayName=Identité locale\nlocalIdentity.displayDescription=Créer une identité réutilisable pour ce bureau local\nsyncedIdentity.displayName=Identité synchronisée\nsyncedIdentity.displayDescription=Créer une identité réutilisable qui est synchronisée entre les systèmes\nlocalIdentity=Identité locale\nkeyNotSynced=Le fichier clé n'est pas encore synchronisé avec le dépôt git. Utilise le bouton add to git du fichier clé pour l'ajouter.\nusernameDescription=Nom d'utilisateur pour se connecter\nidentity.displayName=Identité\nidentity.displayDescription=Créer une identité réutilisable pour les connexions\nlocal=Local\nshared=Global\nuserDescription=Le nom d'utilisateur ou l'identité prédéfinie pour se connecter\nidentityAccessLevel=Niveau d'accès\nidentityPerUser=Accès à l'identité personnelle\nidentityPerUserDescription=Restreins l'accès à cette identité et à ses connexions associées à l'utilisateur de ton coffre-fort uniquement\nidentityPerUserDisabled=Accès à l'identité personnelle (désactivé)\nidentityPerUserDisabledDescription=Restreins l'accès à cette identité et à ses connexions associées à ton utilisateur de coffre-fort uniquement (Requiert la configuration de l'équipe)\nidentityPerGroup=Accès à l'identité par groupe seulement\nidentityPerGroupDescription=Restreins l'accès à cette identité et à ses connexions associées à ce groupe de coffre-fort uniquement\nlibrary=Bibliothèque\nlocation=Lieu de travail\nkeyAuthentication=Authentification par clé\nkeyAuthenticationDescription=La méthode d'authentification à utiliser si l'authentification par clé est requise\nlocationDescription=Le chemin d'accès au fichier de ta clé privée correspondante\nkeyFile=Fichier de clés locales\nkeyPassword=Phrase de passe\nkey=Clé\nyubikeyPiv=Yubikey PIV\npageant=Pageant\ngpgAgent=Agent GPG\ncustomPkcs11Library=Bibliothèque PKCS#11 personnalisée\nsshAgent=Agent OpenSSH\nnone=Aucun\nindex=Index ...\notherExternal=Autre agent externe\nsync=Sync\nvaultSync=Synchronisation de coffre-fort\ncustomUsername=Nom d'utilisateur\ncustomUsernameDescription=L'autre utilisateur facultatif pour se connecter en tant que\ncustomUsernamePassword=Mot de passe\ncustomUsernamePasswordDescription=Le mot de passe de l'utilisateur à utiliser lorsque l'authentification sudo est requise\n#custom\nshowInternalPods=Afficher les pods internes\nshowAllNamespaces=Afficher tous les espaces de noms\nshowInternalContainers=Montre les conteneurs internes\nrefresh=Rafraîchir\nvmwareGui=Démarrer l'interface graphique\nmonitorVm=Moniteur VM\n#custom\naddCluster=Ajouter un cluster ...#11 personnalisée\nshowNonRunningInstances=Afficher les instances non exécutées\nvmwareGuiDescription=S'il faut démarrer une machine virtuelle en arrière-plan ou dans une fenêtre.\nvmwareEncryptionPassword=Mot de passe de cryptage\nvmwareEncryptionPasswordDescription=Le mot de passe facultatif utilisé pour crypter la VM.\nvmPasswordDescription=Le mot de passe requis pour l'utilisateur invité.\nvmPassword=Mot de passe de l'utilisateur\nvmUser=Utilisateur invité\nrunTempContainer=Exécuter un conteneur temporaire\nvmUserDescription=Le nom d'utilisateur de ton principal utilisateur invité\ndockerTempRunAlertTitle=Exécuter un conteneur temporaire\ndockerTempRunAlertHeader=Cela permet d'exécuter un processus shell dans un conteneur temporaire qui sera automatiquement supprimé une fois qu'il sera arrêté.\nimageName=Nom de l'image\nimageNameDescription=L'identificateur d'image de conteneur à utiliser\ncontainerName=Nom du conteneur\ncontainerNameDescription=Le nom facultatif du conteneur personnalisé\nvm=Machine virtuelle\nvmDescription=Le fichier de configuration associé.\nvmwareScan=Hyperviseurs de bureau VMware\nvmwareMachine.displayName=Machine virtuelle VMware\nvmwareMachine.displayDescription=Se connecter à une machine virtuelle via SSH\nvmwareInstallation.displayName=Installation de l'hyperviseur de bureau VMware\nvmwareInstallation.displayDescription=Interagir avec les machines virtuelles installées par l'intermédiaire de son CLI\nstart=Démarrer\nstop=Arrêter\npause=Pause\nrdpTunnelHost=Hôte cible\nrdpTunnelHostDescription=La connexion SSH pour tunneler la connexion RDP vers\nrdpTunnelUsername=Nom d'utilisateur\nrdpTunnelUsernameDescription=L'utilisateur personnalisé sous lequel se connecter, utilise l'utilisateur SSH s'il n'est pas renseigné\nrdpFileLocation=Emplacement du fichier\nrdpFileLocationDescription=Le chemin d'accès au fichier .rdp\nrdpPasswordAuthentication=Authentification par mot de passe\nrdpFiles=Fichiers RDP\nrdpPasswordAuthenticationDescription=Le mot de passe à remplir ou à copier dans le presse-papiers, selon le support client\nrdpFile.displayName=Fichier RDP\nrdpFile.displayDescription=Se connecter à un système par l'intermédiaire d'un fichier .rdp existant\nrequiredSshServerAlertTitle=Configurer le serveur SSH\nrequiredSshServerAlertHeader=Impossible de trouver un serveur SSH installé dans la VM.\nrequiredSshServerAlertContent=Pour se connecter à la VM, XPipe recherche un serveur SSH en cours d'exécution mais aucun serveur SSH disponible n'a été détecté pour la VM.\ncomputerName=Nom de l'ordinateur\npssComputerNameDescription=Le nom de l'ordinateur auquel se connecter\ncredentialUser=Utilisateur de justificatifs\ncredentialUserDescription=L'utilisateur sous lequel tu dois te connecter.\ncredentialPassword=Mot de passe d'identification\ncredentialPasswordDescription=Le mot de passe de l'utilisateur.\nsshConfig=Fichiers de configuration SSH\nautostart=Se connecter automatiquement au démarrage de XPipe\nacceptHostKey=Accepter la clé d'hôte\nmodifyHostKeyPermissions=Modifier les permissions de la clé de l'hôte\nattachContainer=Attache\ncontainerLogs=Afficher les journaux\nopenSftpClient=Ouvrir dans un client SFTP externe\nopenTermius=Ouvrir dans Termius\nshowInternalInstances=Afficher les instances internes\neditPod=Editer le pod\nacceptHostKeyDescription=Fais confiance à la nouvelle clé de l'hôte et continue\nmodifyHostKeyPermissionsDescription=Tente de supprimer les permissions du fichier d'origine pour que OpenSSH soit satisfait\npsSession.displayName=Session à distance PowerShell\npsSession.displayDescription=Se connecter via New-PSSession et Enter-PSSession\nsshLocalTunnel.displayName=Tunnel SSH local\nsshLocalTunnel.displayDescription=Établir un tunnel SSH vers un hôte distant\nsshRemoteTunnel.displayName=Tunnel SSH à distance\nsshRemoteTunnel.displayDescription=Établir un tunnel SSH inverse à partir d'un hôte distant\nsshDynamicTunnel.displayName=Tunnel SSH dynamique\nsshDynamicTunnel.displayDescription=Établir un proxy SOCKS par le biais d'une connexion SSH\nshellEnvironmentGroup.displayName=Environnements Shell\nshellEnvironmentGroup.displayDescription=Environnements Shell\nshellEnvironment.displayName=Environnement shell\nshellEnvironment.displayDescription=Créer un environnement de démarrage personnalisé pour l'interpréteur de commandes\nshellEnvironment.informationFormat=$TYPE$ l'environnement\nshellEnvironment.elevatedInformationFormat=$ELEVATION$ $TYPE$ l'environnement\nenvironmentConnectionDescription=La connexion de base pour créer un environnement pour\nenvironmentScriptDescription=Le script d'initialisation personnalisé facultatif à exécuter dans l'interpréteur de commandes\nenvironmentSnippets=Scripts shell\ncommandSnippetsDescription=Les scripts shell prédéfinis facultatifs à exécuter en premier\nenvironmentSnippetsDescription=Les scripts shell prédéfinis facultatifs à exécuter lors de l'initialisation\nshellTypeDescription=Le type de shell explicite à lancer\noriginPort=Port d'origine\noriginAddress=Adresse d'origine\nremoteAddress=Adresse à distance\nremoteSourceAddress=Adresse de la source distante\nremoteSourcePort=Port source à distance\noriginDestinationPort=Origine destination port\noriginDestinationAddress=Adresse d'origine et de destination\norigin=Origine\nremoteHost=Hôte distant\naddress=Adresse\nproxmox.displayName=Proxmox\nproxmox.displayDescription=Se connecter à des systèmes dans un environnement virtuel Proxmox\nproxmoxVm.displayName=Proxmox VM\nproxmoxVm.displayDescription=Se connecter à une machine virtuelle dans un Proxmox VE via SSH\nproxmoxContainer.displayName=Conteneur Proxmox\nproxmoxContainer.displayDescription=Se connecter à un conteneur dans un Proxmox VE\nsshDynamicTunnel.hostDescription=Le système à utiliser comme proxy SOCKS\nsshDynamicTunnel.bindingDescription=A quelles adresses lier le tunnel\nsshRemoteTunnel.hostDescription=Le système à partir duquel démarrer le tunnel à distance vers l'origine\nsshRemoteTunnel.bindingDescription=A quelles adresses lier le tunnel\nsshLocalTunnel.hostDescription=Le système pour ouvrir le tunnel vers\nsshLocalTunnel.bindingDescription=A quelles adresses lier le tunnel\nsshLocalTunnel.localAddressDescription=L'adresse locale à lier\nsshLocalTunnel.remoteAddressDescription=L'adresse distante à lier\ncmd.displayName=Commande\ncmd.displayDescription=Exécuter une commande arbitraire sur un système\nk8sPod.displayName=Pod Kubernetes\nk8sPod.displayDescription=Se connecter à un pod et à ses conteneurs via kubectl\nk8sContainer.displayName=Conteneur Kubernetes\nk8sContainer.displayDescription=Ouvrir un shell à un conteneur\nk8sCluster.displayName=Cluster Kubernetes\nk8sCluster.displayDescription=Se connecter à un cluster et à ses pods via kubectl\nsshTunnelGroup.displayName=Tunnels SSH\nsshTunnelGroup.displayCategory=Tous les types de tunnels SSH\nlocal.displayName=Machine locale\nlocal.displayDescription=Le shell de la machine locale\ncygwin=Cygwin\nmsys2=MSYS2\ngitWindows=Git pour Windows\ngitForWindows.displayName=Git pour Windows\ngitForWindows.displayDescription=Accède à ton environnement local Git pour Windows\nmsys2.displayName=MSYS2\nmsys2.displayDescription=Les shells d'accès de ton environnement MSYS2\ncygwin.displayName=Cygwin\ncygwin.displayDescription=Les shells d'accès de ton environnement Cygwin\nnamespace=Espace de noms\ngitVaultIdentityStrategy=Identité SSH Git\ngitVaultIdentityStrategyDescription=Si tu as choisi d'utiliser une URL SSH git comme distant et que ton dépôt distant nécessite une identité SSH, alors définis cette option.\\n\\nSi tu as fourni une URL HTTP, tu peux ignorer cette option.\ndockerContainers=Conteneurs Docker\ndockerCmd.displayName=client CLI de docker\ndockerCmd.displayDescription=Accède aux conteneurs Docker via le client CLI de Docker\nwslCmd.displayName=Installation du WSL\nwslCmd.displayDescription=Accéder aux instances WSL via le client CLI wsl\nk8sCmd.displayName=client kubectl\nk8sCmd.displayDescription=Accéder aux clusters Kubernetes via kubectl\nk8sClusters=Clusters Kubernetes\n#custom\nshells=Shells disponibles\ninspectContainer=Inspecter\ninspectContext=Inspecter\nk8sClusterNameDescription=Le nom du contexte dans lequel se trouve le cluster.\n#custom\npod=Pod\npodName=Nom du pod\nk8sClusterContext=Contexte\nk8sClusterContextDescription=Le nom du contexte dans lequel se trouve la grappe\nk8sClusterNamespace=Espace de noms\nk8sClusterNamespaceDescription=L'espace de noms personnalisé ou l'espace de noms par défaut s'il est vide\nk8sConfigLocation=Fichier de configuration\nk8sConfigLocationDescription=Le fichier kubeconfig personnalisé ou celui par défaut s'il est laissé vide\n#custom\ninspectPod=Inspecter le pod\nshowAllContainers=Afficher les conteneurs qui ne fonctionnent pas\nshowAllPods=Afficher les pods qui ne sont pas en cours d'exécution\nk8sPodHostDescription=L'hôte sur lequel se trouve le pod\nk8sContainerDescription=Le nom du conteneur Kubernetes\nk8sPodDescription=Le nom du pod Kubernetes\npodDescription=Le pod sur lequel se trouve le conteneur\nk8sClusterHostDescription=L'hôte par lequel il faut accéder au cluster. Doit avoir kubectl installé et configuré pour pouvoir accéder au cluster.\nconnection=Connexion\nshellCommand.displayName=Commande shell personnalisée\nshellCommand.displayDescription=Ouvrir un shell standard par le biais d'une commande personnalisée\nssh.displayName=Connexion SSH\nssh.displayDescription=Se connecter à un système distant via le client de ligne de commande SSH\nsshConfig.displayName=Fichier de configuration SSH\nsshConfig.displayDescription=Se connecter aux hôtes définis dans un fichier de configuration SSH\nsshConfigHost.displayName=Fichier de configuration SSH hôte\nsshConfigHost.displayDescription=Se connecter à un hôte défini dans un fichier de configuration SSH\nsshConfigHost.password=Mot de passe\nsshConfigHost.passwordDescription=Fournis le mot de passe facultatif pour la connexion de l'utilisateur.\nsshConfigHost.identityPassphrase=Phrase de passe de la clé\nsshConfigHost.identityPassphraseDescription=Indique la phrase de passe facultative pour ta clé.\nshellCommand.hostDescription=L'hôte sur lequel exécuter la commande\nshellCommand.commandDescription=La commande qui ouvre un shell\ncommandType=Type de commande\ncommandTypeDescription=Comment exécuter la commande\ncommandDescription=Les commandes personnalisées à exécuter sur l'hôte\ncommandHostDescription=L'hôte sur lequel exécuter la commande\ncommandDataFlowDescription=Comment cette commande gère l'entrée et la sortie\ncommandElevationDescription=Exécute cette commande avec des autorisations élevées\ncommandShellTypeDescription=L'interpréteur de commandes à utiliser pour cette commande\nlimitedSystem=Il s'agit d'un système limité ou intégré\nlimitedSystemDescription=N'essaie pas d'identifier le type de coquille, nécessaire pour les systèmes embarqués limités ou les appareils IOT\nsshForwardX11=Transmettre X11\nsshForwardX11Description=Active le transfert X11 pour la connexion\ncustomAgent=Agent personnalisé\nidentityAgent=Agent d'identité\nssh.proxyDescription=L'hôte proxy facultatif à utiliser lors de l'établissement de la connexion SSH. Un client ssh doit être installé.\nusage=Utilisation\nwslHostDescription=L'hôte sur lequel se trouve l'instance WSL. Doit avoir installé wsl.\nwslDistributionDescription=Le nom de l'instance WSL\nwslUsernameDescription=Le nom d'utilisateur explicite sous lequel se connecter. S'il n'est pas spécifié, le nom d'utilisateur par défaut sera utilisé.\nwslPasswordDescription=Le mot de passe de l'utilisateur qui peut être utilisé pour les commandes sudo.\ndockerHostDescription=L'hôte sur lequel se trouve le conteneur docker. Doit avoir installé docker.\ndockerContainerDescription=Le nom du conteneur docker\nlocalMachine=Machine locale\nrootScan=Environnement shell Sudo\nloginEnvironmentScan=Environnement de connexion personnalisé\nk8sScan=Cluster Kubernetes\noptions=Options\ndockerRunningScan=L'exécution de conteneurs docker\ndockerAllScan=Tous les conteneurs docker\nwslScan=Instances WSL\nsshScan=Connexions de configuration SSH\nrunAsUser=Exécuter en tant qu'utilisateur\nrunAsUserDescription=Démarre cet environnement shell en tant qu'utilisateur différent\ndefault=Défaut\nadministrator=Administrateur\nwslHost=Hôte WSL\ntimeout=Délai d'attente\ninstallLocation=Emplacement de l'installation\ninstallLocationDescription=L'endroit où ton environnement $NAME$ est installé\nwsl.displayName=Sous-système Windows pour Linux\nwsl.displayDescription=Se connecter à une instance WSL fonctionnant sous Windows\ndocker.displayName=Conteneur Docker\ndocker.displayDescription=Se connecter à un conteneur docker\nport=Port\nuser=Utilisateur\npassword=Mot de passe\nmethod=Méthode\nuri=URL\nproxy=Proxy\ndistribution=Distribution\nusername=Nom d'utilisateur\n#custom\nshellType=Type de shell\nbrowseFile=Parcourir le fichier\nopenShell=Ouvrir un shell dans un terminal\nopenCommand=Exécuter une commande dans le terminal\neditFile=Éditer un fichier\ndescription=Description\nfurtherCustomization=Personnalisation plus poussée\nfurtherCustomizationDescription=Pour plus d'options de configuration, utilise les fichiers de configuration ssh\nbrowse=Parcourir\nconfigHost=Hôte\nconfigHostDescription=L'hôte sur lequel se trouve le config\nconfigLocation=Emplacement de la configuration\nconfigLocationDescription=Le chemin d'accès au fichier de configuration\ngateway=Passerelle\ngatewayDescription=La passerelle optionnelle à utiliser lors de la connexion\nconnectionInformation=Informations sur la connexion\nconnectionInformationDescription=Quel système connecter\npasswordAuthentication=Authentification par mot de passe\npasswordAuthenticationDescription=Le mot de passe facultatif à utiliser pour s'authentifier\nsshConfigString.displayName=Connexion SSH basée sur la configuration\nsshConfigString.displayDescription=Créer une connexion SSH entièrement personnalisée dans le format SSH config\nsshConfigStringContent=Configuration\nsshConfigStringContentDescription=Options SSH pour la connexion dans le format de configuration OpenSSH\nvnc.displayName=Connexion VNC par SSH\nvnc.displayDescription=Ouvrir une session VNC par le biais d'une connexion tunnelisée\n#custom\nbinding=Liaison\nvncPortDescription=Le port sur lequel le serveur VNC écoute\nrdpPortDescription=Le port sur lequel le serveur RDP écoute\nvncUsername=Nom d'utilisateur\nvncUsernameDescription=Le nom d'utilisateur optionnel de VNC\nvncPassword=Mot de passe\nvncPasswordDescription=Le mot de passe VNC\nx11WslInstance=Instance X11 Forward WSL\nx11WslInstanceDescription=La distribution locale de Windows Subsystem for Linux à utiliser comme serveur X11 lors de l'utilisation du transfert X11 dans une connexion SSH. Cette distribution doit être une distribution WSL2.\n#custom\nopenAsRoot=Ouvrir en tant que root\nopenInWSL=Ouvrir en WSL\nlaunch=Lancer\nsshTrustKeyContent=La clé de l'hôte n'est pas connue et tu as activé la vérification manuelle de la clé de l'hôte. $CONTENT$\nsshTrustKeyTitle=Clé d'hôte inconnue\nrdpTunnel.displayName=Connexion RDP via SSH\nrdpTunnel.displayDescription=Se connecter via RDP sur une connexion tunnelisée\nrdpEnableDesktopIntegration=Activer l'intégration du bureau\nrdpEnableDesktopIntegrationDescription=Exécuter des applications à distance en supposant que la liste d'autorisations RDP les autorise\nrdpSetupAdminTitle=Configuration RDP requise\nrdpSetupAllowTitle=Application à distance RDP\nrdpSetupAllowContent=Lancer directement des applications à distance n'est actuellement pas autorisé sur ce système. Veux-tu l'autoriser ? Cela te permettra d'exécuter tes applications distantes directement à partir de XPipe en désactivant la liste d'autorisation pour les applications distantes RDP.\nrdpServerEnableTitle=Serveur RDP\nrdpServerEnableContent=Le serveur RDP est désactivé sur le système cible. Veux-tu l'activer dans le registre afin d'autoriser les connexions RDP à distance ?\nrdp=RDP\nrdpScan=Tunnel RDP sur SSH\nwslX11SetupTitle=Configuration WSL X11\nwslX11SetupContent=XPipe peut utiliser ta distribution WSL locale pour agir en tant que serveur d'affichage X11. Veux-tu installer X11 sur $DIST$? Cela installera les paquets X11 de base sur la distribution WSL et peut prendre un certain temps. Tu peux aussi changer la distribution utilisée dans le menu des paramètres.\ncommand=Commande\ncommandGroup=Groupe de commande\nvncSystem=Système cible VNC\nvncSystemDescription=Le système réel avec lequel interagir. Il s'agit généralement du même que l'hôte du tunnel\nvncHost=Hôte VNC cible\nvncHostDescription=Le système sur lequel le serveur VNC est exécuté\nvncDirectHost=Hôte\nvncDirectHostDescription=L'entrée de l'hôte ou l'adresse manuelle du serveur sur lequel le serveur VNC est exécuté\nrdpDirectHost=Hôte\nrdpDirectHostDescription=L'entrée de l'hôte ou l'adresse manuelle du serveur sur lequel le serveur RDP est exécuté\ngitVaultTitle=Coffre-fort Git\ngitVaultForcePushContent=Veux-tu forcer la poussée vers le dépôt distant ? Cela remplacera complètement tout le contenu du dépôt distant par ton dépôt local, y compris l'historique.\ngitVaultOverwriteLocalContent=Veux-tu remplacer les modifications de ton coffre-fort local ? Cela appliquera toutes les modifications à distance à ton dépôt local.\nrdpSimple.displayName=Connexion directe RDP\nrdpSimple.displayDescription=Se connecter à un hôte via RDP\nrdpUsername=Nom d'utilisateur\nrdpUsernameDescription=L'utilisateur sous lequel se connecter. Peut inclure un préfixe de domaine\naddressDescription=Où se connecter\nrdpAdditionalOptions=Options RDP supplémentaires\nrdpAdditionalOptionsDescription=Options RDP brutes à inclure, formatées de la même manière que dans les fichiers .rdp\nproxmoxVncConfirmTitle=Accès VNC\nproxmoxVncConfirmContent=Veux-tu activer l'accès VNC pour la machine virtuelle ? Cela activera l'accès direct du client VNC dans le fichier de configuration de la VM et redémarrera la machine virtuelle.\ndockerContext.displayName=Contexte Docker\ndockerContext.displayDescription=Interagir avec des conteneurs situés dans un contexte spécifique\nvmActions=Actions VM\ndockerContextActions=Actions contextuelles\nk8sPodActions=Actions de pods\nopenVnc=Activer l'accès VNC\naddVnc=Ajouter une connexion VNC\ncommandGroup.displayName=Groupe de commande\ncommandGroup.displayDescription=Groupe de commandes disponibles pour un système\nserial.displayName=Connexion série\nserial.displayDescription=Ouvrir une connexion série dans un terminal\nserialPort=Port série\nserialPortDescription=Le port série / le périphérique à connecter\nbaudRate=Taux de bauds\ndataBits=Bits de données\nstopBits=Bits d'arrêt\nparity=Parité\nflowControlWindow=Contrôle de flux\nserialImplementation=Implémentation en série\nserialImplementationDescription=L'outil à utiliser pour se connecter au port série\nserialHost=Hôte\nserialHostDescription=Le système pour accéder au port série sur\nserialPortConfiguration=Configuration du port série\nserialPortConfigurationDescription=Paramètres de configuration de l'appareil en série connecté\nserialInformation=Informations en série\nopenXShell=Ouvrir dans XShell\ntsh.displayName=Téléportation\ntsh.displayDescription=Connecte-toi à tes nœuds de téléportation via tsh\ntshNode.displayName=Nœud de téléportation\n#custom\ntshNode.displayDescription=Se connecter à un nœud de téléportation dans un cluster\nteleportCluster=Cluster\n#custom\nteleportClusterDescription=Le cluster dans lequel se trouve le nœud\nteleportProxy=Proxy\nteleportProxyDescription=Le serveur proxy utilisé pour se connecter au nœud\nteleportHost=Hôte\nteleportHostDescription=Le nom d'hôte du nœud\nteleportUser=Utilisateur\nteleportUserDescription=L'utilisateur à connecter en tant que\nlogin=Connexion\nhyperVInstall.displayName=Hyper-V\nhyperVInstall.displayDescription=Se connecter aux machines virtuelles gérées par Hyper-V\nhyperVVm.displayName=VM Hyper-V\nhyperVVm.displayDescription=Se connecter à une VM Hyper-V via SSH ou PSSession\ntrustHost=Hôte de confiance\ntrustHostDescription=Ajoute NomOrdinateur à la liste des hôtes de confiance\ncopyIp=Copier l'IP\nvncDirect.displayName=Connexion directe VNC\nvncDirect.displayDescription=Se connecter directement à un système via VNC\neditConfiguration=Modifier la configuration\n#custom\nviewInDashboard=Voir dans le tableau de bord\nsetDefault=Définir par défaut\nremoveDefault=Supprimer la valeur par défaut\nconnectAsOtherUser=Se connecter en tant qu'autre utilisateur\nprovideUsername=Fournir un autre nom d'utilisateur pour se connecter\nvmIdentity=Identité de l'invité\nvmIdentityDescription=La méthode d'authentification de l'identité SSH à utiliser pour se connecter si nécessaire\nvmPort=Port\nvmPortDescription=Le port auquel se connecter via SSH\nforwardAgent=Agent de transfert\nforwardAgentDescription=Rendre les identités des agents SSH disponibles sur le système distant\nvirshUri=URI\nvirshUriDescription=L'URI de l'hyperviseur, les alias sont également pris en charge\nvirshDomain.displayName=domaine libvirt\nvirshDomain.displayDescription=Se connecter à un domaine libvirt\nvirshHypervisor.displayName=hyperviseur libvirt\nvirshHypervisor.displayDescription=Se connecter à un pilote d'hyperviseur pris en charge par libvirt\nvirshInstall.displayName=client de ligne de commande libvirt\nvirshInstall.displayDescription=Se connecter à tous les hyperviseurs libvirt disponibles via virsh\naddHypervisor=Ajouter un hyperviseur\ninteractiveTerminal=Terminal interactif\neditDomain=Éditer le domaine\nlibvirt=domaines libvirt\ncustomIp=IP personnalisé\ncustomIpDescription=Remplacer la détection de l'IP locale de la VM par défaut si tu utilises la mise en réseau avancée\nautomaticallyDetect=Détecter automatiquement\nuserAddDialogTitle=Création d'un utilisateur\ngroupAddDialogTitle=Création de groupe\npassphrase=Phrase de passe\nrepeatPassphrase=Répéter la phrase de passe\ngroupSecret=Secret de groupe\nrepeatGroupSecret=Répéter le secret du groupe\nvaultGroup=Groupe de coffres-forts\nloginAlertTitle=Connexion requise\nloginAlertHeader=Déverrouille le coffre-fort pour accéder à tes connexions personnelles\nvaultUser=Utilisateur du coffre-fort\nme=Me\naddGroup=Ajouter un groupe ...\naddGroupDescription=Crée un nouveau groupe pour ce coffre-fort\naddUser=Ajouter un utilisateur ...\naddUserDescription=Crée un nouvel utilisateur pour ce coffre-fort\nskip=Sauter\nuserChangePasswordAlertTitle=Changement de mot de passe\ngroupChangeSecretAlertTitle=Changement de secret\ndocs=Documentation\nlxd.displayName=Conteneur LXD\nlxd.displayDescription=Se connecter à un conteneur LXD via lxc\nlxdCmd.displayName=Client CLI LXD\nlxdCmd.displayDescription=Accéder aux conteneurs LXD via le client CLI lxc\npodman.displayName=Conteneur Podman\npodman.displayDescription=Se connecter à un conteneur Podman\nincusInstall.displayName=Gestionnaire de machine Incus\nincusInstall.displayDescription=Accède aux conteneurs incus via le client CLI incus\nincusContainer.displayName=Conteneur Incus\nincusContainer.displayDescription=Se connecter à un conteneur incus\npodmanCmd.displayName=Podman CLI\npodmanCmd.displayDescription=Accède aux conteneurs Podman via le client CLI\nlxdHostDescription=L'hôte sur lequel se trouve le conteneur LXD. Lxc doit être installé.\nlxdContainerDescription=Le nom du conteneur LXD\npodmanContainers=Conteneurs Podman\nlxdContainers=Conteneurs LXD\nincusContainers=Conteneurs Incus\ncontainer=Conteneur\nhost=Hôte\ncontainerActions=Actions du conteneur\nserialConsole=Console de série\neditRunConfiguration=Modifier la configuration de l'exécution\ncommunityDescription=Un outil puissant de connexion parfait pour tes cas d'utilisation personnels.\nupgradeDescription=Gestion professionnelle des connexions pour l'ensemble de ton infrastructure de serveurs.\ndiscoverPlans=Découvre les options de mise à niveau\nextendProfessional=Mise à jour vers les dernières fonctionnalités professionnelles\ncommunityItem1=Connexions illimitées à des systèmes et outils non commerciaux\ncommunityItem2=Intégration transparente avec les terminaux et les éditeurs que tu as installés\ncommunityItem3=Navigateur de fichiers à distance complet\ncommunityItem4=Système de script puissant pour tous les shells\ncommunityItem5=Intégration Git pour la synchronisation et le partage des informations de connexion\nupgradeItem1=Comprend toutes les fonctionnalités de l'édition communautaire\nupgradeItem2=Le plan Homelab prend en charge un nombre illimité d'hyperviseurs et des fonctions SSH avancées\nupgradeItem3=Le plan professionnel prend en outre en charge les systèmes d'exploitation et les outils d'entreprise\nupgradeItem4=Le plan Entreprise est assorti d'une flexibilité totale pour ton cas d'utilisation individuel\nupgrade=Mise à niveau\nupgradeTitle=Plans disponibles\nstatus=Statut\ntype=Type de texte\nlicenseAlertTitle=Licence requise\nuseCommunity=Continue avec la communauté\npreviewDescription=Essaie les nouvelles fonctionnalités pendant quelques semaines après leur sortie.\ntryPreview=Activer l'aperçu\npreviewItem1=Accès complet aux fonctionnalités professionnelles nouvellement publiées pendant 2 semaines après la sortie de l'application\npreviewItem2=Essaie les nouvelles fonctions sans t'engager\nlicensedTo=Sous licence\nemail=Adresse électronique\napply=Appliquer\nclear=Effacer\nactivate=Activer\nvalidUntil=Valable jusqu'au\nlicenseActivated=Licence activée\nrestart=Redémarrer\nlockVault=Verrouiller le coffre-fort\nrestartApp=Redémarrer XPipe\nfree=Gratuit\nupgradeInfo=Tu trouveras ci-dessous des informations sur la mise à niveau vers une licence.\nupgradeInfoPreview=Tu peux trouver des informations sur la mise à niveau vers une licence ci-dessous ou essayer l'aperçu.\nenterLicenseKey=Saisis la clé de licence pour la mise à niveau\nisOnlySupported=n'est pris en charge qu'avec au moins une licence $TYPE$\nareOnlySupported=ne sont pris en charge qu'avec au moins une licence $TYPE$\nlegacyLicense=Cette licence n'inclut que les nouvelles fonctionnalités professionnelles publiées dans l'année qui suit l'achat.\npreviewExpiredLicense=Cette fonctionnalité était récemment disponible gratuitement en avant-première, mais cette période a maintenant expiré.\nopenApiDocs=Documentation de l'API\nopenApiDocsDescription=La documentation de l'API HTTP est disponible en ligne, y compris une spécification OpenAPI .yaml. Tu peux l'ouvrir dans ton navigateur web ou dans ton client HTTP préféré.\n#custom\nopenApiDocsButton=Ouvrir la documentation\npythonApi=API Python\npersonalConnection=Cette connexion et tous ses enfants ne sont disponibles que pour ton utilisateur car ils dépendent d'une identité personnelle.\ndeveloperPrintInitFiles=Exécution d'un fichier d'initialisation d'impression\ndeveloperPrintInitFilesDescription=Imprime tous les scripts d'initialisation de l'interpréteur de commandes qui sont exécutés lorsqu'un terminal est lancé.\ndeveloperShowSensitiveCommands=Commandes sensibles du journal\ndeveloperShowSensitiveCommandsDescription=Inclure des commandes sensibles dans la sortie du journal pour le débogage.\ncheckingForUpdates=Vérification des mises à jour\ncheckingForUpdatesDescription=Récupérer les informations sur la dernière version\ndownloadingUpdate=Récupération de la version (Version $VERSION$)\ndownloadingUpdateDescription=Téléchargement d'un paquet de versions\nupdateNag=Tu n'as pas mis à jour XPipe depuis un certain temps. Il se peut que tu passes à côté des nouvelles fonctionnalités et des correctifs des versions plus récentes.\nupdateNagTitle=Rappel de mise à jour\nupdateNagButton=Voir les communiqués\nrefreshServices=Services de rafraîchissement\nserviceProtocolType=Type de protocole de service\nserviceProtocolTypeDescription=Contrôle la façon d'ouvrir le service\nserviceCommand=La commande à exécuter une fois que le service est actif\nserviceCommandDescription=L'espace réservé $PORT sera remplacé par le port local tunnelisé\nvalue=Valeur\nshowAdvancedOptions=Afficher les options avancées\nsshAdditionalConfigOptions=Options de configuration supplémentaires\nremoteFileManager=Gestionnaire de fichiers à distance\nclearUserData=Effacer les données de l'utilisateur\nclearUserDataDescription=Supprimer toutes les données de configuration de l'utilisateur, y compris les connexions\nclearUserDataTitle=Suppression des données de l'utilisateur\nclearUserDataContent=Cela supprimera toutes les données utilisateur locales pour xpipe et redémarrera. Si tu tiens à tes connexions, assure-toi de les synchroniser d'abord avec un dépôt git.\nundefined=Non défini\ncopyAddress=Adresse de copie\nnetbirdDeviceScan=Connexions Netbird\nnetbirdId=Clé publique de l'homologue\nnetbirdIdDescription=L'identifiant de la clé publique interne netbird de l'homologue\ntailscaleDeviceScan=Connexions Tailscale\ntailscaleInstall.displayName=Installation de Tailscale\ntailscaleInstall.displayDescription=Connecte-toi aux appareils de ton tailnet via SSH\ntailscaleDevice.displayName=Dispositif Tailscale\ntailscaleDevice.displayDescription=Connecte-toi à un appareil de ton tailnet via SSH\ntailscaleId=ID de l'appareil\ntailscaleIdDescription=L'identifiant interne de l'appareil Tailscale\ntailscaleHostName=Nom d'hôte\ntailscaleHostNameDescription=Le nom d'hôte de l'appareil dans le tailnet\ntailscaleUsername=Nom d'utilisateur\ntailscaleUsernameDescription=L'utilisateur à connecter en tant que\ntailscalePassword=Mot de passe\ntailscalePasswordDescription=Le mot de passe optionnel de l'utilisateur qui peut être utilisé pour sudo\nscriptName=Nom du script\nscriptNameDescription=Donne à ce script un nom personnalisé\nscriptGroupName=Nom du groupe de script\nscriptGroupNameDescription=Donne un nom personnalisé à ce groupe de scripts\nidentityName=Nom d'identité\nidentityNameDescription=Donne à cette identité un nom personnalisé\ntailscaleTailnet.displayName=Tailnet\ntailscaleTailnet.displayDescription=Connecte-toi à un tailnet spécifique avec ton compte\nputtyConnections=Connexions PuTTY\nkittyConnections=Connexions KiTTY\nicons=Icônes\ncustomIcons=Icônes personnalisées\niconSources=Sources d'icônes\niconSourcesDescription=Tu peux ajouter tes propres sources d'icônes ici. XPipe récupérera tous les fichiers .svg à l'emplacement ajouté et les ajoutera à l'ensemble des icônes disponibles.\\n\\nLes répertoires locaux et les dépôts git distants sont pris en charge en tant qu'emplacements d'icônes.\nrefreshSources=Icônes de rafraîchissement\nrefreshSourcesDescription=Mets à jour toutes les icônes à partir des sources disponibles\naddDirectoryIconSource=Ajouter une source de répertoire ...\naddDirectoryIconSourceDescription=Ajouter des icônes à partir d'un répertoire local\n#custom\naddGitIconSource=Ajouter une source git ...\naddGitIconSourceDescription=Ajouter des icônes situées dans un dépôt git distant\nrepositoryUrl=URL du dépôt Git\niconDirectory=Répertoire d'icônes\naddUnsupportedKexMethod=Ajoute une méthode d'échange de clés non prise en charge\naddUnsupportedKexMethodDescription=Autorise l'utilisation de la méthode d'échange de clés $VAL$ pour cette connexion\naddUnsupportedHostKeyType=Ajouter un type de clé d'hôte non pris en charge\naddUnsupportedHostKeyTypeDescription=Autorise l'utilisation du type de clé hôte $VAL$ pour cette connexion\naddUnsupportedMacType=Ajouter un type de MAC non pris en charge\naddUnsupportedMacTypeDescription=Autorise l'utilisation du type de MAC $VAL$ pour cette connexion\nrunSilent=silencieusement en arrière-plan\nrunInFileBrowser=dans un navigateur de fichiers\nrunInConnectionHub=dans un hub de connexion\ncommandOutput=Sortie de commande\niconSourceDeletionTitle=Supprimer la source de l'icône\niconSourceDeletionContent=Veux-tu supprimer cette source d'icônes et toutes les icônes qui y sont associées ?\nrefreshIcons=Icônes de rafraîchissement\nrefreshIconsDescription=Récupérer, rendre et mettre en cache toutes les icônes disponibles (plus de 1000) à partir de sources externes dans des fichiers .png. Cela peut prendre un certain temps...\n#custom\nvaultUserLegacy=Utilisateur du coffre-fort (mode de compatibilité héritée limité)\nupgradeInstructions=Instructions de mise à niveau\nexternalActionTitle=Demande d'action externe\nexternalActionContent=Une action externe a été demandée. Veux-tu autoriser le lancement d'actions depuis l'extérieur de XPipe ?\nnoScriptStateAvailable=Actualise pour déterminer la compatibilité des scripts...\ndocumentationDescription=Vérifie la documentation\ncustomEditorCommandInTerminal=Exécuter une commande personnalisée dans un terminal\ncustomEditorCommandInTerminalDescription=Si ton éditeur est basé sur un terminal, tu peux activer cette option pour ouvrir automatiquement un terminal et exécuter la commande dans la session du terminal à la place.\\n\\nTu peux utiliser cette option pour des éditeurs comme vi, vim, nvim et d'autres.\ndisableHttpsTlsCheck=Désactiver la vérification des certificats des requêtes HTTPS\ndisableHttpsTlsCheckDescription=Si ton organisation décrypte ton trafic HTTPS dans les pare-feux en utilisant l'interception SSL, toutes les vérifications de mise à jour ou de licence échoueront parce que les certificats ne correspondent pas. Tu peux résoudre ce problème en activant cette option et en désactivant la validation des certificats TLS.\nconnectionsSelected=$NUMBER$ connexions sélectionnées\naddConnections=Ajouter des connexions\nbrowseDirectory=Répertoire de navigation\nopenTerminal=Terminal ouvert\n#custom\ndocumentation=Documentation\nreport=Signaler une erreur\nkeePassXcNotAssociated=Lien KeePassXC\nkeePassXcNotAssociatedDescription=XPipe n'est pas associé à ta base de données KeePassXC locale. Clique ci-dessous pour associer XPipe à la base de données KeePassXC afin qu'il puisse interroger les mots de passe.\nkeePassXcAssociateMore=Connecte d'autres bases de données\nkeePassXcAssociateMoreDescription=Tu peux être connecté à plusieurs bases de données KeePassXC en même temps\nkeePassXcAssociated=Liens KeePassXC\nkeePassXcAssociatedDescription=XPipe est connecté aux bases de données locales KeePassXC suivantes :\nkeePassXcNotAssociatedButton=Base de données de liens\nidentifier=Identificateur\npasswordManagerCommand=Commande personnalisée\npasswordManagerCommandDescription=La commande personnalisée à exécuter pour récupérer les mots de passe. La chaîne de caractères $KEY sera remplacée par la clé du mot de passe citée lorsqu'elle sera appelée. Cette commande devrait appeler ton gestionnaire de mots de passe CLI pour imprimer le mot de passe sur stdout, par exemple mypassmgr get $KEY.\nchooseTemplate=Choisir un modèle\nkeePassXcPlaceholder=URL d'entrée de KeePassXC\nterminalEnvironment=Environnement du terminal\nterminalEnvironmentDescription=Si tu veux utiliser les fonctions d'un environnement WSL local basé sur Linux pour personnaliser ton terminal, tu peux les utiliser comme environnement de terminal.\\n\\nToutes les commandes d'initialisation de terminal personnalisées et la configuration du multiplexeur de terminal seront alors exécutées dans cette distribution WSL.\nterminalInitScript=Script d'initialisation du terminal\nterminalInitScriptDescription=Commandes à exécuter dans l'environnement du terminal avant le lancement de la connexion. Tu peux l'utiliser pour configurer l'environnement du terminal au démarrage.\nterminalMultiplexer=Multiplexeur de terminaux\nterminalMultiplexerDescription=Le multiplexeur de terminal à utiliser comme alternative aux onglets dans un terminal. Cela remplacera certaines caractéristiques de manipulation du terminal, par exemple la manipulation des onglets, par la fonctionnalité du multiplexeur.\\n\\nIl faut que l'exécutable du multiplexeur correspondant soit installé sur le système.\nterminalMultiplexerWindowsDescription=Le multiplexeur de terminal à utiliser comme alternative aux onglets dans un terminal. Cela remplacera certaines caractéristiques de manipulation du terminal, par exemple la manipulation des onglets, par la fonctionnalité du multiplexeur.\\n\\nL'utilisation d'un environnement terminal WSL sous Windows et l'installation de l'exécutable du multiplexeur sur le système WSL sont nécessaires.\nterminalAlwaysPauseOnExit=Toujours faire une pause à la sortie\nterminalAlwaysPauseOnExitDescription=Lorsque cette option est activée, la sortie d'une session de terminal te demandera toujours de redémarrer ou de fermer la session. S'il est désactivé, XPipe ne le fera que pour les connexions échouées qui se terminent par une erreur.\nquerying=Interroger ...\nretrievedPassword=Obtenu : $PASSWORD$\nrefreshOpenpubkey=Rafraîchir l'identité openpubkey\nrefreshOpenpubkeyDescription=Exécute opkssh refresh pour que l'identité openpubkey soit à nouveau valide\nall=Tous\nterminalPrompt=Invite du terminal\nterminalPromptDescription=L'outil d'invite de terminal à utiliser dans tes terminaux distants. L'activation d'une invite de terminal permet d'installer et de configurer automatiquement l'outil d'invite sur le système cible lors de l'ouverture d'une session de terminal.\\n\\nCela ne modifie pas les configurations d'invite ou les fichiers de profil existants sur un système. Cela augmentera le temps de chargement du terminal pour la première fois pendant que l'invite est configurée sur le système distant. Ton terminal peut avoir besoin de polices supplémentaires pour afficher correctement l'invite.\nterminalPromptConfiguration=Configuration de l'invite du terminal\nterminalPromptConfig=Fichier de configuration\nterminalPromptConfigDescription=Le fichier de configuration personnalisé à appliquer à l'invite. Cette configuration sera automatiquement mise en place sur le système cible lors de l'initialisation du terminal et utilisée comme configuration par défaut de l'invite.\\n\\nSi tu veux utiliser le fichier de config par défaut existant sur chaque système, tu peux laisser ce champ vide.\npasswordManagerKey=Clé du gestionnaire de mots de passe\npasswordManagerKeyDescription=L'identifiant du gestionnaire de mot de passe du secret\npasswordManagerAgent=Agent du gestionnaire de mot de passe\ndockerComposeProject.displayName=Projet Docker compose\ndockerComposeProject.displayDescription=Regrouper les conteneurs d'un projet de composition\nsshVerboseOutput=Activer la sortie verbeuse de SSH\nsshVerboseOutputDescription=Ceci imprimera beaucoup d'informations de débogage lors d'une connexion via SSH. Utile pour résoudre les problèmes liés aux connexions SSH.\ndontUseGateway=N'utilise pas de passerelle\ndontUseGatewayDescription=N'utilise pas l'hôte de l'hyperviseur comme passerelle et connecte-toi directement à l'IP\ncategoryColor=Couleur de la catégorie\ncategoryColorDescription=La couleur par défaut à utiliser pour les connexions de cette catégorie\ncategorySync=Synchronisation avec le dépôt git\ncategorySyncDescription=Synchronise automatiquement toutes les connexions avec le dépôt git. Toutes les modifications locales apportées aux connexions seront poussées vers le dépôt distant.\ncategorySyncSpecial=Synchronisation avec le dépôt git\\n(Non configurable pour la catégorie spéciale \"$NAME$\")\ncategoryDontAllowScripts=Désactive toutes les modifications\ncategoryDontAllowScriptsDescription=Désactive toute exécution de commande et autres opérations sur les systèmes de cette catégorie pour empêcher toute modification. Cela désactivera toutes les fonctionnalités de script, les commandes de l'environnement shell, les invites, etc.\ncategoryConfirmAllModifications=Confirme toutes les modifications\ncategoryConfirmAllModificationsDescription=Confirme d'abord tout type de modification pour une connexion ou un système de fichiers. Cela peut éviter des opérations accidentelles sur des systèmes importants.\ncategoryDefaultIdentity=Identité par défaut\ncategoryDefaultIdentityDescription=Si tu utilises fréquemment une certaine identité sur plusieurs des systèmes de cette catégorie, le fait de définir une identité par défaut te permettra de la présélectionner lors de la création de nouvelles connexions.\ncategoryConfigTitle=$NAME$ configuration\nconfigure=Configurer\naddConnection=Ajouter une connexion\nnoCompatibleConnection=Aucune connexion compatible trouvée\nnoCompatibleIdentity=Aucune identité compatible trouvée\nnewCategory=Nouvelle catégorie\ndockerComposeRestricted=Le projet compose est restreint par $NAME$ et ne peut pas être modifié de l'extérieur. Utilise $NAME$ pour gérer ce projet compose.\nrestricted=Restreint\ndisableSshPinCaching=Désactiver la mise en cache du code PIN SSH\ndisableSshPinCachingDescription=XPipe met automatiquement en cache tous les codes PIN qui ont été saisis pour une clé lors de l'utilisation d'une forme d'authentification basée sur le matériel.\\n\\nSi tu désactives cette fonction, tu devras ressaisir le code PIN à chaque tentative de connexion.\ngitSyncPull=Pull pour synchroniser les changements git à distance\n#custom\nenpassVaultFile=Fichier du coffre-fort\nenpassVaultFileDescription=Le fichier local du coffre-fort Enpass.\nflat=Appartement\nrecursive=Récursif\nrdpAllowListBlocked=La RemoteApp sélectionnée ne semble pas être incluse dans la liste des autorisations RDP pour le serveur.\npsonoServerUrl=URL du serveur\npsonoServerUrlDescription=URL du serveur backend psono\npsonoApiKey=Clé API\npsonoApiKeyDescription=La clé API à utiliser, formatée sous forme d'uuid\npsonoApiSecretKey=Clé secrète de l'API\npsonoApiSecretKeyDescription=La clé secrète de l'API sous forme de chaîne hexagonale de 64 octets\npassboltServerUrl=URL du serveur\npassboltServerUrlDescription=URL du serveur dorsal passbolt\npassboltPassphrase=Phrase de passe\npassboltPassphraseDescription=La phrase de passe de la clé privée du coffre-fort\npassboltPrivateKey=Clé privée\npassboltPrivateKeyDescription=Le fichier de clés gpg privées pour le coffre-fort\nfocusWindowOnNotifications=Fenêtre de mise au point sur les notifications\nfocusWindowOnNotificationsDescription=Fais passer XPipe au premier plan lorsqu'une notification ou un message d'erreur s'affiche, par exemple lorsqu'une connexion ou un tunnel se termine de manière inattendue.\ngitUsername=Nom d'utilisateur git personnalisé\ngitUsernameDescription=L'utilisateur personnalisé pour s'authentifier auprès du dépôt distant git. Par défaut, XPipe utilisera les informations d'identification actuellement configurées de la CLI git.\\n\\nCe paramètre remplacera toutes les informations d'identification par défaut qui sont déjà configurées pour ton client CLI git local.\ngitPassword=Mot de passe git personnalisé / jeton d'accès personnel\ngitPasswordDescription=Le mot de passe ou le jeton d'accès personnel à utiliser pour s'authentifier. La nécessité d'un mot de passe ou d'un jeton d'accès personnel dépend du fournisseur distant de git. Ce paramètre remplacera les informations d'identification par défaut déjà configurées pour ton client CLI git local.\nsetReadOnly=Définir en lecture seule\nunsetReadOnly=Lecture seule non paramétrée\nreadOnlyStoreError=La configuration de cette entrée est gelée. Choisis un autre nom pour enregistrer tes modifications sur une nouvelle copie.\ncategoryFreeze=Geler les configurations de connexion\ncategoryFreezeDescription=Marque les configurations de connexion comme étant en lecture seule. Cela signifie qu'aucune configuration d'entrée de connexion existante dans cette catégorie ne peut être modifiée. De nouvelles connexions peuvent cependant être ajoutées.\nupdateFail=L'installation de la mise à jour n'a pas réussi\nupdateFailAction=Installer la mise à jour manuellement\nupdateFailActionDescription=Consulte les dernières versions sur GitHub\nonePasswordPlaceholder=Nom de l'article ou URL op://\ncomputeDirectorySizes=Calculer la taille des répertoires\ncomputeSize=Calculer la taille\ncustomSpiceCommand=Commande personnalisée\ncustomSpiceCommandDescription=La commande personnalisée à exécuter pour lancer les sessions SPICE. La chaîne de caractères de remplacement $FILE sera remplacée par le chemin d'accès au fichier .vv lorsqu'elle sera appelée.\nvncClient=Client VNC\nvncClientDescription=Le client VNC à lancer lors de l'ouverture de connexions VNC dans XPipe.\\n\\nTu as la possibilité d'utiliser le client VNC intégré à XPipe ou de lancer un client VNC externe installé localement si tu souhaites plus de personnalisation.\nintegratedXPipeVncClient=Client VNC XPipe intégré\ncustomVncCommand=Commande personnalisée\ncustomVncCommandDescription=La commande personnalisée à exécuter pour lancer des sessions VNC. La chaîne de caractères de remplacement $ADDRESS sera remplacée par l'adresse citée lorsqu'elle sera appelée.\nvncConnections=Connexions VNC\npasswordManagerIdentity=Identité du gestionnaire de mots de passe\npasswordManagerIdentity.displayName=Identité du gestionnaire de mots de passe\npasswordManagerIdentity.displayDescription=Récupère le nom d'utilisateur et le mot de passe d'une identité dans ton gestionnaire de mots de passe\npasswordCopied=Mot de passe de connexion copié dans le presse-papiers\nerrorOccurred=Une erreur s'est produite\nactionMacro.displayName=Macro d'action\nactionMacro.displayDescription=Exécute en action à l'aide de déclencheurs personnalisés\nmacroAdd=Ajouter une macro\nmacroName=Nom de macro\nmacroNameDescription=Donne à cette macro un nom personnalisé\nactionId=ID d'action\nactionIdDescription=L'action à exécuter avec cette macro\nmacroRefs=Connexions associées\nmacroRefsDescription=Les connexions avec lesquelles exécuter l'action\nconnectionCopy=Copie\nactionPickerTitle=Action de choisir\nactionPickerDescription=Clique sur quelque chose pour effectuer une action. Au lieu d'exécuter l'action, tu peux créer et modifier des raccourcis vers l'action dans le mode de sélection des raccourcis d'action.\ncancelActionPicker=Annuler la sélection d'action\nactionShortcut=Raccourci d'action\nactionShortcuts=Raccourcis d'action\nactionStore=Magasin d'action\nactionStoreDescription=L'entrée du magasin sur laquelle l'action doit être exécutée\nactionStores=Magasins d'action\nactionStoresDescription=Les entrées du magasin sur lesquelles l'action doit être exécutée\nactionDesktopShortcut=Raccourci clavier\nactionDesktopShortcutDescription=Crée un raccourci pour cette action sur ton bureau\nactionUrlShortcut=Raccourci URL\nactionUrlShortcutDescription=Copie une URL qui peut déclencher ces actions lorsqu'elle est ouverte\nactionUrlShortcutDisabled=Raccourci URL (Indisponible)\nactionUrlShortcutDisabledDescription=Le type d'installation $TYPE$ ne prend pas en charge l'ouverture des URL\nactionApiCall=Demande d'API\nactionApiCallDescription=Appelle cette action à partir de l'API HTTP\nactionMacro=Macro d'action\nactionMacroDescription=Crée une macro avec des fonctionnalités avancées pour cette action\ncreateMacro=Créer une macro\nactionConfiguration=Paramètres\nactionConfigurationDescription=Les paramètres à transmettre à l'action exécutée\nconfirmAction=Confirmer l'action\nactionConnections=Connexions d'action\nactionConnectionsDescription=Les connexions sur lesquelles exécuter l'action\nactionConnection=Connexion d'action\nactionConnectionDescription=La connexion sur laquelle l'action doit être exécutée\nappleContainerInstall.displayName=Conteneurs Apple\nappleContainerInstall.displayDescription=Accède aux instances de conteneurs d'apple via l'interface de programmation des conteneurs\nappleContainer.displayName=Conteneur Apple\nappleContainer.displayDescription=Accède aux instances de conteneurs d'apple via l'interface de programmation des conteneurs\nappleContainerHostDescription=L'hôte sur lequel se trouve le conteneur apple\nappleContainerDescription=Le nom du conteneur apple\nappleContainers=Conteneurs Apple\nchangeOrderIndexTitle=Changer l'ordre\norderIndex=Index\norderIndexDescription=Indice explicite permettant d'ordonner cette entrée par rapport aux autres. Les indices les plus faibles sont indiqués en haut, les plus élevés en bas\nmoveToFirst=Déplacer vers le premier\nmoveToLast=Passer en dernier\ncategory=Catégorie\nincludeRoot=Inclure la racine\nexcludeRoot=Exclure la racine\nfreezeConfiguration=Gel de la configuration\nunfreezeConfiguration=Décongeler la configuration\nwaylandScalingTitle=Mise à l'échelle de Wayland\nactionApiUrl=$URL$ (Copier le corps json)\ncopyBody=Corps de la demande de copie\ngitRepoTerminalOpen=Ouvre le dépôt dans le terminal\ngitRepoTerminalOpenDescription=Jette un coup d'œil au dépôt toi-même à l'aide de la ligne de commande\ngitRepoOverwriteLocal=Ecraser le dépôt local\ngitRepoOverwriteLocalDescription=Remplacer toutes les modifications locales par des modifications provenant de l'ordinateur distant\ngitRepoForcePush=Écraser le dépôt distant\ngitRepoForcePushDescription=Utilise git push --force pour appliquer tes modifications locales à l'ordinateur distant\ngitRepoDontWarn=N'avertis plus\ngitRepoDontWarnDescription=Si cela est attendu, fais en sorte que XPipe ignore cette erreur à l'avenir\ngitRepoTryAgain=Essaie encore\ngitRepoTryAgainDescription=Tenter à nouveau la même opération\ngitRepoEnablePlain=Utiliser la synchronisation des répertoires simples\ngitRepoEnablePlainDescription=N'initialise pas un dépôt git pour synchroniser les changements dans le répertoire\ngitRepoCreateBare=Utiliser git sync\ngitRepoCreateBareDescription=Initialise un nouveau dépôt git nu dans le répertoire sync\ngitRepoDisable=Désactive le coffre-fort de git pour l'instant\ngitRepoDisableDescription=N'effectue aucune modification pendant cette session\ngitRepoPullRefresh=Retirer les changements et rafraîchir\ngitRepoPullRefreshDescription=Fusionner les modifications à distance et recharger les données\nbreakOutCategory=Catégorie de rupture\nmergeCategory=Fusionner la catégorie\nopenWinScp=Ouvrir dans WinSCP\nuninstallApplication=Désinstaller\nuninstallApplicationDescription=Exécute le script d'installation .pkg pour désinstaller complètement XPipe\nk8sEditPodTitle=Appliquer les changements\nk8sEditPodContent=Veux-tu appliquer les modifications apportées via la commande kubectl apply ? Un redémarrage est probablement nécessaire pour que les changements s'appliquent.\nvirshEditDomainTitle=Appliquer les changements\nvirshEditDomainContent=Veux-tu appliquer les changements au domaine ? Un redémarrage est probablement nécessaire pour que les changements s'appliquent.\npkcs11Library=Bibliothèque PKCS#11\npkcs11LibraryDescription=Le chemin du fichier de la bibliothèque liée dynamiquement\nsshAgentSocket=Socket d'agent SSH personnalisé\nsshAgentSocketDescription=La prise personnalisée à utiliser pour communiquer avec l'agent SSH. Cet agent personnalisé peut être utilisé pour une connexion en sélectionnant l'option d'agent personnalisé pour lui.\npublicKey=Identificateur de clé publique\npublicKeyDescription=La clé publique optionnelle pour forcer l'agent à n'offrir que la clé privée correspondante\nactions=Actions\nhcloudServer.displayName=Serveur en nuage Hetzner\nhcloudServer.displayDescription=Accède à un serveur hébergé sur le nuage Hetzner via SSH\nhcloudInstall.displayName=Hetzner Cloud CLI\nhcloudInstall.displayDescription=Accède aux serveurs hébergés sur le nuage Hetzner via hcloud\nhcloudContext.displayName=contexte hcloud\nhcloudContext.displayDescription=Serveurs d'accès d'un contexte hcloud\nmetrics=Métriques\n#custom\nopenInVsCode=Ouvrir dans VSCode\naddCloud=Cloud ...\nhcloudToken=jeton hcloud\nhcloudTokenDescription=Le jeton de nuage Hetzner à utiliser. Pour plus d'informations, voir la documentation\nhcloudLogin=Connexion au nuage Hetzner\nclearHcloudToken=Effacer le jeton hcloud\nclearHcloudTokenDescription=Supprime le jeton existant pour que tu puisses te connecter à nouveau\nselectIdentity=Sélectionne l'identité\nenableMcpServer=Activer le serveur MCP\nenableMcpServerDescription=Active le serveur XPipe MCP, ce qui permet aux clients MCP externes d'envoyer des demandes au serveur MCP. Voir ci-dessous pour les détails de la configuration.\\n\\nNote que l'API HTTP n'a pas besoin d'être activée pour la fonctionnalité MCP.\nenableMcpMutationTools=Activer les outils de mutation MCP\nenableMcpMutationToolsDescription=Par défaut, seuls les outils en lecture seule sont activés dans le serveur MCP. Cela permet de s'assurer qu'aucune opération accidentelle ne peut être effectuée pour modifier potentiellement un système.\\n\\nSi tu prévois d'apporter des modifications aux systèmes par l'intermédiaire de clients MCP, veille à vérifier que ton client MCP est configuré pour confirmer toute action potentiellement destructrice avant d'activer cette option. Nécessite une reconnexion de tous les clients MCP pour s'appliquer.\nmcpClientConfigurationDetails=Configuration du client MCP\nmcpClientConfigurationDetailsDescription=Utilise ces données de configuration pour te connecter au serveur XPipe MCP à partir du client MCP de ton choix.\nswitchHostAddress=Changer l'adresse de l'hôte\naddAnotherHostName=Ajouter un autre nom d'hôte\naddNetwork=Analyse du réseau ...\nnetworkScan=Balayage du réseau\nnetworkScanStore=Hôte cible\nnetworkScanStoreDescription=L'hôte pour lequel il faut scanner le réseau local\nuseAsGateway=Utiliser l'hôte comme passerelle\nuseAsGatewayDescription=S'il faut utiliser l'hôte cible comme passerelle pour les connexions créées\nnetworkScanPorts=Ports à scanner\nnetworkScanPortsDescription=La liste des ports séparés par des virgules à inclure dans l'analyse\nnetworkScanType=Type de connexion\nnetworkScanTypeDescription=Le type de serveurs à rechercher\nemptyDirectory=Ce répertoire semble être vide\nhcloudConfigFile=fichier de configuration hcloud\nhcloudConfigFileDescription=L'emplacement du fichier de configuration .toml de la CLI hcloud\npreferMonochromeIcons=Préfère les icônes monochromes\npreferMonochromeIconsDescription=Lorsque cette option est activée, les variables d'icônes monochromes seront choisies plutôt que les versions colorées par défaut d'une icône, en supposant qu'une variante d'icône distincte en mode clair ou foncé soit disponible pour une icône à partir d'une source.\\n\\nNécessite un rafraîchissement des icônes à appliquer.\nalwaysShowSshMotd=Toujours montrer MOTD\nalwaysShowSshMotdDescription=Affichage ou non du message du jour configuré sur un système distant lors de la connexion dans une nouvelle session de terminal. Note que la modification de ce paramètre peut altérer le comportement d'initialisation des connexions SSH.\nmanageSubscription=Gérer l'abonnement\nnoListeningServer=Pas de serveur d'écoute\nnetworkScanResults=Résultats de l'analyse\nnetworkScanResultsDescription=La liste des systèmes trouvés dans le réseau\nlocalShellDialect=Shell local\nlocalShellDialectDescription=L'interprète de commandes utilisé pour les opérations locales. Si l'interpréteur de commandes local par défaut normal est désactivé ou cassé dans une certaine mesure, cette option peut être utilisée pour se rabattre sur une autre alternative.\\n\\nCertaines configurations, comme les entrées PATH personnalisées, peuvent ne pas s'appliquer à l'interpréteur de commandes de repli si elles ne sont pas encore configurées dans les fichiers de profil de l'interpréteur de commandes respectifs.\nagentSocketNotFound=Aucune prise d'agent actif n'a été trouvée\nagentSocket=Emplacement de la prise\nagentSocketDescription=Le chemin d'accès au fichier socket de l'agent\nagentSocketNotConfigured=Aucune prise personnalisée n'a encore été configurée\ndownloadInProgress=$NAME$ téléchargement en cours\nenableTerminalStartupBell=Activer la cloche de démarrage du terminal\nenableTerminalStartupBellDescription=Joue une commande de bip/de cloche dans une nouvelle session de terminal. Si ton émulateur de terminal prend en charge les cloches, cela peut être utilisé pour faciliter l'identification des instances de terminal nouvellement lancées.\ninvalidSshGatewayChain=Configuration invalide de la chaîne de passerelles mixtes avec des passerelles de saut et des passerelles sans saut.\nsyncFileExists=Le fichier synchronisé $FILE$ existe déjà\nreplaceFile=Remplacer un fichier\nreplaceFileDescription=Remplace le fichier existant par celui-ci\nrenameFile=Renommer un fichier\nrenameFileDescription=Donne à ce fichier un nom différent pour le synchroniser\nnewFileName=Nouveau nom de fichier\nparentHostDoesNotSupportTunneling=L'hôte parent $NAME$ ne prend pas en charge le tunneling\nconnectionNotesTemplate=Modèle de notes\nconnectionNotesTemplateDescription=Le modèle markdown qui doit être utilisé lors de l'ajout d'une nouvelle entrée de notes à une connexion.\nconnectionNotesButton=Modifier les notes\nrdpSmartSizing=Activer le dimensionnement intelligent\nrdpSmartSizingDescription=Lorsque cette option est activée, mstsc réduit la taille du bureau si la fenêtre est trop petite pour l'afficher dans sa pleine résolution. Le rapport d'aspect du bureau est préservé lorsqu'il est réduit.\ndisableStartOnInit=Désactive le démarrage automatique\nenableStartOnInit=Activer le démarrage automatique\nfileReadSudoTitle=Lecture de fichiers Sudo\nfileReadSudoContent=Le fichier que tu essaies de lire ne t'accorde pas les permissions de lecture de l'utilisateur actuel. Veux-tu lire ce fichier en tant qu'utilisateur root avec sudo ? Cela te permettra d'accéder automatiquement à l'utilisateur root avec les informations d'identification existantes ou par le biais d'une invite.\nnetbirdInstall.displayName=Installation de Netbird\nnetbirdInstall.displayDescription=Connecte-toi à des pairs dans ton réseau Netbird\nnetbirdProfile.displayName=Profil Netbird\nnetbirdProfile.displayDescription=Liste des pairs d'un profil spécifique\nnetbirdPeer.displayName=Netbird peer\nnetbirdPeer.displayDescription=Se connecter à un pair via SSH\nnetbirdPublicKey=Clé publique\nnetbirdPublicKeyDescription=La clé publique interne de l'homologue\nnetbirdHostName=Nom d'hôte\nnetbirdHostNameDescription=Le nom d'hôte de l'homologue dans le réseau\nvncRefSystem=Système associé\nvncRefSystemDescription=L'entrée de connexion à laquelle associer cette connexion VNC. Laisser vide s'il n'y en a pas\nabstractHost.displayName=Hôte du résumé\nabstractHost.displayDescription=Créer une entrée pour un hôte qui ne prend pas en charge les connexions shell\nabstractHostAddress=Adresse de l'hôte\nabstractHostAddressDescription=L'adresse de l'hôte\nabstractHostGateway=Passerelle\nabstractHostGatewayDescription=Le système de passerelle optionnel par lequel on peut atteindre cet hôte\nabstractHostConvert=Convertir en entrée d'hôte abstraite\nhostNoConnections=Aucune connexion disponible\nhostHasConnections=$COUNT$ connexions disponibles\nhostHasConnection=$COUNT$ connexion disponible\nlargeFileWarningTitle=Édition d'un grand fichier\nlargeFileWarningContent=Le fichier que tu veux éditer est assez volumineux avec $SIZE$. Veux-tu vraiment ouvrir ce fichier dans ton éditeur de texte ?\nrdpAskpassUser=Nom d'utilisateur RDP pour l'hôte $HOST$\nrdpAskpassPassword=Mot de passe pour l'utilisateur $USER$\ninPlaceKey=Clé\ninPlaceKeyText=Contenu de la clé privée\ninPlaceKeyTextDescription=Le contenu de la clé privée\nnetbirdSelfhosted=Instance Netbird auto-hébergée\nnetbirdSelfhostedDescription=Fournir une URL personnalisée au lieu d'utiliser la version hébergée dans le nuage\nnetbirdManagementUrl=URL de gestion de Netbird\nnetbirdManagementUrlDescription=L'URL de gestion de ton instance auto-hébergée\nnetbirdSetupKey=Touche de configuration\nnetbirdSetupKeyDescription=Si tu utilises des clés de configuration, tu peux en utiliser une pour la connexion\nnetbirdLogin=Connexion Netbird\naddProfile=Ajouter un profil\nnetbirdProfileNameAsktext=Nom du nouveau profil netbird\nopenSftp=Ouvrir une session SFTP\ncapslockWarning=Tu as activé le verrouillage des caps\ninherit=Hériter\nsshConfigStringSelected=Hôte cible\nsshConfigStringSelectedDescription=Pour plusieurs hôtes, le premier est utilisé comme cible. Réorganise tes hôtes pour changer la cible\ntunnelToLocalhost=Tunnel vers localhost\ntunnelToLocalhostDescription=Tunnelise automatiquement le port distant vers localhost\ntags=Tags\ntag=Étiquette\naddNewTag=Créer une nouvelle étiquette\ncreateTag=Créer une étiquette ...\ninPlacePublicKey=Clé publique\ninPlacePublicKeyDescription=La clé publique associée à la clé privée spécifiée\nsshKeygenTitle=Générer une nouvelle clé SSH\nsshKeygenAlgorithm=Algorithme\nsshKeygenAlgorithmDescription=L'algorithme de génération de clés asymétriques à utiliser pour la clé\nrsaBits=Bits\nrsaBitsDescription=Nombre de bits dans la clé générée\nsshKeygenComment=Commentaire\nsshKeygenCommentDescription=Le commentaire facultatif pour cette clé\nsshKeygenPassphrase=Phrase de passe\nsshKeygenPassphraseDescription=La phrase de passe optionnelle pour cette clé\ned25519SkResident=Faire une clé de résident\ned25519SkResidentDescription=Stocke la clé privée sur la clé de sécurité matérielle\ned25519SkResidentKeyName=Étiquette de clé résidente\ned25519SkResidentKeyNameDescription=Donne une étiquette à la clé. Nécessaire lorsque l'on stocke plusieurs clés sur la clé de sécurité\ned25519SkPinRequired=Exiger un NIP\ned25519SkPinRequiredDescription=Exiger la saisie d'un code PIN lors de l'utilisation\ned25519SkUserPresenceRequired=Exiger la présence de l'utilisateur\ned25519SkUserPresenceRequiredDescription=Nécessite un toucher ou un élément similaire lors de l'utilisation. Certaines clés de sécurité exigent que cette fonction soit activée\ncopyPublicKey=Copier la clé publique\ngeneratePublicKey=Générer une clé publique\npublicKeyGenerateNotice=Peut être généré à partir de la clé privée\nidentityApplyTargetHost=Cible\nidentityApplyTargetHostDescription=Le système pour appliquer l'identité à\nidentityApplyAuthorizedHost=Clé SSH autorisée\nidentityApplyAuthorizedHostDescription=La clé SSH est ajoutée au fichier hosts autorisé\nidentityApplyAuthorizedHostButton=Ajouter une clé à un fichier\napplyIdentityToHost=Appliquer l'identité à l'hôte ...\nidentityApplyMissingPublicKeyTitle=Clé publique manquante\nidentityApplyMissingPublicKeyContent=La clé SSH de l'identité n'est pas associée à une clé publique. Vérifie la configuration pour plus de détails.\nvalid=Valable\nnotValid=Non valide\nwarning=Avertissement\nidentityApplyTitle=Appliquer l'identité\nidentityApplyConfigPasswordEnabled=Autorisation de mot de passe activée\nidentityApplyConfigPasswordEnabledDescription=L'authentification par mot de passe est toujours activée dans la configuration de sshd\nidentityApplyConfigPasswordDisabled=Authentification par mot de passe désactivée\nidentityApplyConfigPasswordDisabledDescription=L'authentification par mot de passe est toujours désactivée dans la configuration de sshd\nidentityApplyConfigKeyEnabled=Authentification des clés activée\nidentityApplyConfigKeyEnabledDescription=L'authentification par clé est toujours activée dans la configuration de sshd\nidentityApplyConfigKeyDisabled=Authentification des clés désactivée\nidentityApplyConfigKeyDisabledDescription=L'authentification par clé est toujours désactivée dans la configuration de sshd\nidentityApplyConfigRootDisabledWarning=Connexion racine désactivée\nidentityApplyConfigRootDisabledWarningDescription=La connexion de l'utilisateur root n'est pas activée dans la configuration de sshd\nidentityApplyConfigAdminWarning=Clés d'administrateur configurées\nidentityApplyConfigAdminWarningDescription=La clé peut être ajoutée à administrators_authorized_keys pour les utilisateurs administrateurs\nidentityApplyEditConfig=Modifier la configuration\nidentityApplyEditConfigDescription=Ouvre la configuration sshd dans l'éditeur pour résoudre les problèmes\nidentityApplyEditAuthorizedKeys=Modifier les clés autorisées\nidentityApplyEditAuthorizedKeysDescription=Ouvre le fichier authorized_keys dans l'éditeur pour modifier ou supprimer d'autres clés\nidentityApplyEditConfigButton=Ouvrir sshd_config\nidentityApplyEditAuthorizedKeysButton=Ouvrir les clés autorisées\nidentityApplySetStoreIdentity=Jeu d'identité de connexion\nidentityApplySetStoreIdentityDescription=L'identité est configurée pour être utilisée par la connexion\nidentityApplySetStoreIdentityButton=Appliquer l'identité\ngenerateKey=Générer une clé\ngroupSecretStrategy=Contrôle d'accès par groupe\ngroupSecretStrategyDescription=Comment récupérer le secret de groupe utilisé pour le cryptage et le décryptage pour le groupe. La méthode de récupération que tu choisis sera exécutée lorsqu'un utilisateur se connectera au coffre-fort au démarrage.\\n\\nCe paramètre est configuré pour chaque groupe. Pour modifier ce paramètre pour un groupe différent de celui qui est actuellement actif, tu devras te connecter au coffre-fort en tant que membre de ce groupe.\nfileSecret=Secret de fichier\ncommandSecret=Commande\nhttpRequestSecret=Réponse HTTP\nfileSecretChoice=Emplacement du fichier\nfileSecretChoiceDescription=Le chemin d'accès au fichier contenant le secret de cryptage du groupe. Comme ce fichier peut être interrogé sur toutes les plateformes, tu peux utiliser ~ dans le chemin pour faire référence au répertoire personnel. Le fichier doit être disponible sur tous les systèmes à partir desquels tu déverrouilles le coffre, sinon la connexion échouera.\ncommandSecretField=Script de récupération\ncommandSecretFieldDescription=La commande qui renverra la clé de cryptage secrète pour le groupe actuel. La commande est exécutée dans le shell par défaut du système local et la clé doit être imprimée sur stdout.\nhttpRequestSecretField=URI de demande\nhttpRequestSecretFieldDescription=L'URI à laquelle envoyer une requête HTTP. Le secret du groupe est extrait du corps de la réponse HTTP.\nvaultAuthentication=Authentification du coffre-fort\nvaultAuthenticationDescription=Comment authentifier / déverrouiller les données du coffre-fort. Il existe de multiples façons de crypter et de déverrouiller les données du coffre-fort, en fonction de la personne avec laquelle tu veux partager les données du coffre-fort.\ngroupAuthFailed=L'authentification secrète a échoué\nuserAuthFailed=L'authentification du mot de passe a échoué\nsavingChanges=Sauvegarde des changements\nawsDeviceScan=AWS\naws=AWS\nawsCliInstallTitle=AWS CLI requis\nawsCliInstallContent=L'intégration AWS nécessite l'installation de l'AWS CLI sur ton système local\nawsProfileCreateTitle=Nouveau profil AWS\nawsProfileAccessKey=Clé d'accès\nawsProfileName=Nom du profil\nawsProfileNameDescription=Le nom d'affichage du nouveau profil\nawsProfileRegion=Région\nawsProfileRegionDescription=La région AWS associée au profil\nawsProfileAccessKeyId=Clé d'accès ID\nawsProfileAccessKeyIdDescription=L'ID de la clé d'accès de l'utilisateur IAM\nawsProfileSecretAccessKey=Clé d'accès secrète\nawsProfileSecretAccessKeyDescription=La clé d'accès secrète associée\nawsInstall.displayName=Installation de la CLI d'AWS\nawsInstall.displayDescription=Connecte-toi à tes systèmes AWS via le CLI AWS\nawsProfile.displayName=Profil CLI AWS\nawsProfile.displayDescription=Accéder à AWS par le biais d'un profil spécifique\nawsInstanceId=ID d'instance\nawsInstanceIdDescription=L'ID interne de cette instance\nawsInstanceUseSsm=Se connecter via SSM\nawsInstanceUseSsmDescription=Utilise l'outil SSM pour te connecter à l'instance via SSH\nawsEc2Instance.displayName=Instance AWS EC2\nawsEc2Instance.displayDescription=Se connecter à une instance EC2 via SSH\nawsS3Group.displayName=Seaux S3\nawsS3Group.displayDescription=Accéder aux buckets S3 d'un profil AWS\nawsS3Bucket.displayName=Seau S3\nawsS3Bucket.displayDescription=Accéder à un bucket S3 d'un profil AWS\nawsEc2Group.displayName=Instances EC2\nawsEc2Group.displayDescription=Accéder aux instances EC2 d'un profil AWS\nawsEc2InstanceSsmTerminal=Ouvrir le terminal SSM\ngenericS3Bucket.displayName=Seau S3 générique\ngenericS3Bucket.displayDescription=Accéder à un seau S3 générique via la CLI AWS\naddFileSystem=Système de fichiers ...\ngenericS3BucketHost=Hôte\ngenericS3BucketHostDescription=L'entrée de l'hôte ou l'adresse manuelle du serveur S3\ngenericS3BucketPortDescription=Le port sur lequel le serveur S3 écoute\ngenericS3BucketAccessKeyId=Clé d'accès ID\ngenericS3BucketAccessKeyIdDescription=L'ID de la clé d'accès de l'utilisateur IAM\ngenericS3BucketSecretAccessKey=Clé d'accès secrète\ngenericS3BucketSecretAccessKeyDescription=La clé d'accès secrète associée\ngenericS3BucketHttps=Activer HTTPS\ngenericS3BucketHttpsDescription=Utilise le protocole HTTPS pour te connecter au serveur. Certains fournisseurs peuvent exiger l'utilisation de HTTPS\ntunnelled=Par tunnel\nawsInstallSync=Synchronisation de la configuration\nawsInstallSyncDescription=Synchronise les fichiers de configuration de l'interface de programmation AWS avec le coffre-fort git\nawsInstallLocation=Emplacement des données de l'utilisateur\nawsInstallLocationDescription=Le chemin d'où proviennent les fichiers de configuration de la CLI AWS\ninstanceActions=Actions d'instance\nopenSplit=Ouvrir dans un terminal divisé\nterminalSplitStrategy=Sens de la vue fractionnée\nterminalSplitStrategyDescription=Contrôle la façon dont les onglets du terminal sont divisés lors de l'utilisation de la fonctionnalité d'affichage fractionné en mode batch pour ouvrir plusieurs sessions de terminal l'une à côté de l'autre.\nterminalSplitStrategyDisabledDescription=Contrôle la façon dont les onglets du terminal sont divisés lors de l'utilisation de la fonctionnalité d'affichage fractionné en mode batch pour ouvrir plusieurs sessions de terminal l'une à côté de l'autre.\\n\\nLa configuration actuelle de ton terminal ne prend pas en charge les vues fractionnées.\nhorizontal=Horizontal\nvertical=Vertical\nbalanced=Équilibré\nclose=Fermer\nhelpButton=$TOPIC$ lien de documentation\nquickAccess=Accès rapide\ntoggleEnabled=État de basculement\ncurrentPath=Chemin actuel\ndirectoryContents=Contenu du répertoire\ndirectoryOptions=Options de l'annuaire\nchooseConnectionType=Choisir le type de connexion\nbatchMode=Mode par lots\ntoggleButton=Bouton de basculement\ntailscaleUseSsh=Utiliser l'authentification SSH de Tailscale\ntailscaleUseSshDescription=Se connecter via le serveur SSH tailscale lui-même sans aucune authentification SSH\nportDescription=Le port sur lequel le serveur SSH fonctionne\nloginAs=Connecte-toi en tant que\nsshGatewayType=Type de passerelle\nsshGatewayTypeDescription=S'il faut se connecter à la cible via un tunnel ou avec l'option ProxyJump\ngatewayTunnel=Tunnel de passerelle\nproxyJump=Saut de proxy\ncommandTypeAsyncBackground=Exécuter détaché en arrière-plan\ncommandTypeSyncBackground=Fonctionne en arrière-plan et attends la fin de l'opération\ncommandTypeTerminalBackground=Ouvrir dans le terminal\nasyncBackgroundCommand=Commande d'arrière-plan\nsyncBackgroundCommand=Commande de blocage de l'arrière-plan\nterminalBackgroundCommand=Commande de terminal\ntestingConnection=Test de connexion ...\nopenManagementConsole=Console de gestion ouverte\nopenLxcTerminal=Ouvrir le terminal LXC\nopenContainerConsole=Ouvrir une console série\nkeeper2fa=méthode 2FA\nkeeper2faDescription=La principale méthode d'authentification à deux facteurs qui est configurée pour ton compte. Active cette option si ton compte Keeper nécessite une authentification à deux facteurs pour accéder aux mots de passe.\nkeeperTotpDuration=Durée du code 2FA personnalisé\nkeeperTotpDurationDescription=Remplacer la durée par défaut de la validité d'un code 2FA. Ne s'applique que si la politique de ton organisation permet de modifier la durée.\\n\\nLes valeurs possibles sont : $VALUES$\nkeeperOtherAuth=Autre (RSA SecurID, Duo Security, Keeper DNA, etc.)\nextractReusableIdentities=Extraire des identités réutilisables\nidentitiesAdded=Identités ajoutées\nsyncMode=Mode de synchronisation\nsyncModeDescription=Contrôle la façon dont les changements doivent être synchronisés.\\n\\nLe mode instantané pousse et tire les changements dès que possible, le mode de démarrage et de sortie synchronise toutes les modifications effectuées au cours d'une session en une seule fois, et le mode manuel ne synchronise que lorsque tu l'inities.\ntoggleTerminalDock=Dock de terminal à bascule\nscriptDirectory=Emplacement du répertoire\nscriptDirectoryDescription=Le répertoire local contenant les fichiers de scripts de l'interpréteur de commandes\nscriptSourceUrl=URL du dépôt\nscriptSourceUrlDescription=L'URL d'un dépôt git distant contenant des fichiers de scripts shell\nscriptCollectionSourceType=Type de source\nscriptCollectionSourceTypeDescription=Le type de source à partir de laquelle les scripts de l'interpréteur de commandes doivent être chargés\nscriptCollectionSourceEntry=Entrée de source\nscriptCollectionSourceEntryDescription=La source à partir de laquelle les scripts de l'interpréteur de commandes doivent être chargés\ngitRepository=Dépôt Git\nscriptCollectionSource.displayName=Source du script\nscriptCollectionSource.displayDescription=Importer automatiquement des scripts shell à partir d'une source existante\ndirectorySource=Source du répertoire\ngitRepositorySource=Source d'un dépôt Git\nrefreshSource=Rafraîchir la source\nscriptTextSourceUrl=URL du script\nscriptTextSourceUrlDescription=L'URL à partir de laquelle tu peux récupérer le fichier script\nscriptSourceType=Source du script\nscriptSourceTypeDescription=D'où vient le texte ?\nscriptSourceTypeInPlace=Script en place\nscriptSourceTypeUrl=URL externe\nscriptSourceTypeSource=Source existante\nimportScripts=Importation de scripts\nscriptsContained=$NUMBER$ scripts\nscriptSourceCollectionImportTitle=Importer des scripts à partir d'une source ($SELECTED$/$COUNT$)\nnoScriptsFound=Aucun script trouvé\ntunnel=Tunnel\nnotInitialized=Non initialisé\nselectCategory=Sélectionne la catégorie ...\nscriptSourceName=Nom du script\nscriptSourceNameDescription=Le nom de fichier du script dans la source\nworkspaceRestartTitle=Espace de travail prêt\nworkspaceRestartContent=Un raccourci vers le nouvel espace de travail a été créé à l'adresse $PATH$. Tu peux naviguer vers le raccourci ou redémarrer XPipe maintenant pour ouvrir automatiquement le nouvel espace de travail.\nbrowseShortcut=Parcourir le fichier\nsyncModeInstant=Synchroniser instantanément\nsyncModeSession=Synchronisation au démarrage et à la sortie\nsyncModeManual=Synchroniser manuellement\npushChanges=Pousser les changements\npullChanges=Changements de tirage\nsourcedFrom=Tiré de $SOURCE$\ninPlaceScript=Script en place\ngeneric=Générique\nsyncToPlainDirectory=Synchronisation avec un répertoire simple\nsyncToPlainDirectoryDescription=Lors de la synchronisation avec un répertoire local, tu peux traiter ce répertoire comme un autre dépôt git ou comme un simple répertoire. Si le paramètre répertoire ordinaire est activé, le répertoire n'est pas initialisé en tant que dépôt git.\nopenSpiceSession=Ouvrir une session SPICE\nterminalBehaviour=Comportement du terminal\nnoScanPossible=Aucune connexion prise en charge n'a été trouvée\nnetworkSwitchPorts=Ports de réseau\nnswitchGroup.displayName=Ports de réseau\nnswitchGroup.displayDescription=Liste des ports disponibles sur un périphérique réseau\nnswitchPort.displayName=Port réseau\nnswitchPort.displayDescription=Contrôler un port individuel sur un commutateur de réseau\nenablePort=Activer le port\nshutdownPort=Ferme le port\nresetPort=Port de réinitialisation\nuseSystemDefault=Utiliser la valeur par défaut du système\nportStatus=Statut du port\nclearCounters=Effacer les compteurs\nshowStatus=Afficher l'état\nshowAllPorts=Afficher tous les ports\nactiveLicense=Licence\nactiveLicenseDescription=Activer une clé de licence XPipe\nauthenticatorApp=Application Authenticator\nsecurityKey=Clé de sécurité\nmcpAdditionalContext=Contexte MCP supplémentaire\nmcpAdditionalContextDescription=Instructions supplémentaires à transmettre au client MCP. Sert à contrôler le comportement de l'agent et à fournir un contexte supplémentaire pour ta configuration individuelle.\nmcpAdditionalContextSample=- Ne redémarre pas automatiquement les services et les démons sans confirmer au préalable\\n- Lors de la configuration d'une interface réseau, utilise toujours 192.168.1.1/24 comme passerelle\nprefsRestartTitle=Redémarrage nécessaire\nprefsRestartContent=Certaines options que tu as modifiées nécessitent un redémarrage de l'application pour être appliquées. Veux-tu redémarrer XPipe maintenant ?\nbashShell=Shell Bash\n"
  },
  {
    "path": "lang/strings/translations_id.properties",
    "content": "delete=Menghapus\nproperties=Properti\nusedDate=Digunakan $DATE$\nopenDir=Direktori Terbuka\nsortLastUsed=Mengurutkan berdasarkan tanggal terakhir digunakan\nsortAlphabetical=Mengurutkan abjad berdasarkan nama\nsortIndexed=Mengurutkan berdasarkan indeks urutan\nrestartDescription=Memulai ulang sering kali dapat menjadi solusi cepat\nreportIssue=Melaporkan masalah\nreportIssueDescription=Membuka pelapor masalah terintegrasi\nusefulActions=Tindakan yang berguna\nstored=Disimpan\ntroubleshootingOptions=Alat bantu pemecahan masalah\ntroubleshoot=Memecahkan masalah\nremote=File Jarak Jauh\naddShellStore=Menambahkan Shell ...\naddShellTitle=Menambahkan Koneksi Shell\nsavedConnections=Koneksi yang Disimpan\nsave=Menyimpan\nclean=Bersih\nmoveTo=Pindah ke ...\naddDatabase=Basis data ...\nbrowseInternalStorage=Menelusuri penyimpanan internal\naddTunnel=Terowongan ...\naddService=Layanan ...\naddScript=Naskah ...\naddHost=Host Jarak Jauh ...\naddShell=Lingkungan Cangkang ...\naddCommand=Perintah ...\naddAutomatically=Menambahkan secara otomatis ...\naddOther=Tambahkan Lainnya ...\nconnectionAdd=Menambahkan koneksi\nscriptAdd=Menambahkan skrip\nscriptGroupAdd=Menambahkan grup skrip\nidentityAdd=Menambahkan identitas\nnew=Baru\nselectType=Pilih Jenis\nselectTypeDescription=Memilih jenis koneksi\nselectShellType=Jenis Shell\nselectShellTypeDescription=Memilih Jenis Koneksi Shell\nname=Nama\nstoreIntroHeader=Hub Koneksi\nstoreIntroContent=Di sini Anda dapat mengelola semua koneksi shell lokal dan jarak jauh di satu tempat. Untuk memulai, Anda dapat dengan cepat mendeteksi koneksi yang tersedia secara otomatis dan memilih koneksi mana yang akan ditambahkan.\nstoreIntroButton=Mencari koneksi ...\ndragAndDropFilesHere=Atau cukup seret dan jatuhkan file di sini\nconfirmDsCreationAbortTitle=Konfirmasi pembatalan\nconfirmDsCreationAbortHeader=Apakah Anda ingin membatalkan pembuatan sumber data?\nconfirmDsCreationAbortContent=Setiap kemajuan pembuatan sumber data akan hilang.\nconfirmInvalidStoreTitle=Lewati validasi\nconfirmInvalidStoreContent=Apakah Anda ingin melewatkan validasi koneksi? Anda dapat menambahkan koneksi ini meskipun tidak dapat divalidasi dan memperbaiki masalah koneksi di kemudian hari.\nexpand=Memperluas\naccessSubConnections=Mengakses sub koneksi\ncommon=Umum\ncolor=Warna\nalwaysConfirmElevation=Selalu konfirmasikan peningkatan izin\nalwaysConfirmElevationDescription=Mengontrol cara menangani kasus ketika izin yang lebih tinggi diperlukan untuk menjalankan perintah pada sistem, misalnya dengan sudo.\\n\\nSecara default, semua kredensial sudo disimpan dalam cache selama sesi dan secara otomatis diberikan saat diperlukan. Jika opsi ini diaktifkan, Anda akan diminta untuk mengonfirmasi akses elevasi setiap saat.\nallow=Mengizinkan\nask=Menanyakan\ndeny=Menolak\nshare=Menambahkan ke repositori git\nunshare=Menghapus dari repositori git\nremove=Menghapus\ncreateNewCategory=Subkategori baru\nprompt=Prompt\ncustomCommand=Perintah khusus\nother=Lainnya\nsetLock=Mengatur kunci\nselectConnection=Memilih koneksi\nselectEntry=Memilih entri\ncreateLock=Membuat kata sandi\nchangeLock=Mengubah kata sandi\ntest=Tes\nfinish=Selesai\nerror=Terjadi kesalahan\ndownloadStageDescription=Memindahkan file yang diunduh ke dalam direktori unduhan sistem dan membukanya.\nok=Baik\nsearch=Pencarian\nrepeatPassword=Mengulang kata sandi\naskpassAlertTitle=Askpass\nunsupportedOperation=Operasi yang tidak didukung: $MSG$\nfileConflictAlertTitle=Menyelesaikan konflik\nfileConflictAlertContent=Terjadi konflik. File $FILE$ sudah ada di sistem target.\\n\\nBagaimana Anda ingin melanjutkan?\nfileConflictAlertContentMultiple=Terjadi konflik. File $FILE$ sudah ada.\\n\\nBagaimana Anda ingin melanjutkan? Mungkin ada lebih banyak konflik yang dapat Anda selesaikan secara otomatis dengan memilih opsi yang berlaku untuk semua.\nmoveAlertTitle=Konfirmasi pemindahan\nmoveAlertHeader=Apakah Anda ingin memindahkan ($COUNT$) elemen yang dipilih ke dalam $TARGET$?\ndeleteAlertTitle=Mengonfirmasi penghapusan\ndeleteAlertHeader=Apakah Anda ingin menghapus ($COUNT$) elemen yang dipilih?\nselectedElements=Elemen yang dipilih:\nmustNotBeEmpty=$VALUE$ tidak boleh kosong\nvalueMustNotBeEmpty=Nilai tidak boleh kosong\ntransferDescription=Seret file ke sini untuk mengunduh\ndragLocalFiles=Seret unduhan dari sini\nnull=$VALUE$ tidak boleh bernilai nol\nroots=Akar\nscripts=Skrip\nsearchFilter=Cari ...\nrecent=Terbaru\nshortcut=Pintasan\nbrowserWelcomeEmptyHeader=Peramban file\nbrowserWelcomeEmptyContent=Anda dapat memilih di sebelah kiri sistem mana yang akan dibuka di peramban file. XPipe akan mengingat sistem dan direktori mana yang telah Anda akses sebelumnya dan menampilkannya dalam menu akses cepat di sini di masa mendatang.\nbrowserWelcomeEmptyButton=Membuka peramban file lokal\nbrowserWelcomeSystems=Anda baru saja tersambung ke sistem berikut ini:\nbrowserWelcomeDocsHeader=Dokumentasi\nbrowserWelcomeDocsContent=Jika Anda lebih suka pendekatan yang lebih terpandu untuk membiasakan diri dengan XPipe, lihat situs web dokumentasi.\nbrowserWelcomeDocsButton=Buka dokumentasi\nhostFeatureUnsupported=$FEATURE$ tidak terinstal pada host\nmissingStore=$NAME$ tidak ada\nconnectionName=Nama koneksi\nconnectionNameDescription=Berikan nama khusus pada sambungan ini\nopenFileTitle=Buka file\nunknown=Tidak diketahui\nscanAlertTitle=Menambahkan koneksi\nscanAlertChoiceHeader=Target\nscanAlertChoiceHeaderDescription=Pilih tempat untuk mencari koneksi. Ini akan mencari semua koneksi yang tersedia terlebih dahulu.\nscanAlertHeader=Jenis koneksi\nscanAlertHeaderDescription=Pilih jenis koneksi yang ingin Anda tambahkan secara otomatis untuk sistem.\nnoInformationAvailable=Tidak ada informasi yang tersedia\nyes=Ya\nno=Tidak\nerrorOccured=Terjadi kesalahan\nterminalErrorOccured=Terjadi kesalahan terminal\nerrorTypeOccured=Pengecualian tipe $TYPE$ dilemparkan\npermissionsAlertTitle=Izin yang diperlukan\npermissionsAlertHeader=Diperlukan izin tambahan untuk melakukan operasi ini.\npermissionsAlertContent=Ikuti pop-up untuk memberikan izin yang diperlukan pada XPipe di menu pengaturan.\nerrorDetails=Rincian kesalahan\nupdateReadyAlertTitle=Siap Memperbarui\nupdateReadyAlertHeader=Pembaruan ke versi $VERSION$ siap untuk diinstal\nupdateReadyAlertContent=Ini akan menginstal versi baru dan memulai ulang XPipe setelah instalasi selesai.\nerrorNoDetail=Tidak ada detail kesalahan yang tersedia\nerrorNoExceptionMessage=Kesalahan tipe $TYPE$ dilemparkan\nupdateAvailableTitle=Pembaruan Tersedia\nupdateAvailableContent=Pembaruan XPipe ke versi $VERSION$ tersedia untuk diinstal. Meskipun XPipe tidak dapat dijalankan, Anda dapat mencoba menginstal pembaruan untuk memperbaiki masalah tersebut.\nclipboardActionDetectedTitle=Tindakan Clipboard terdeteksi\nclipboardActionDetectedContent=XPipe mendeteksi konten di papan klip yang dapat dibuka. Apakah Anda ingin membukanya sekarang? Apakah Anda ingin mengimpor konten clipboard Anda?\ninstall=Menginstal ...\nignore=Abaikan\npossibleActions=Tindakan yang tersedia\nreportError=Melaporkan kesalahan\nreportOnGithub=Membuat laporan masalah di GitHub\nreportOnGithubDescription=Membuka isu baru di repositori GitHub\nreportErrorDescription=Mengirim laporan kesalahan dengan umpan balik pengguna opsional dan info diagnostik\nignoreError=Abaikan kesalahan\nignoreErrorDescription=Abaikan kesalahan ini dan lanjutkan seolah-olah tidak terjadi apa-apa\nprovideEmail=Bagaimana kami dapat menghubungi Anda (opsional, hanya jika Anda ingin mendapatkan tanggapan). Laporan Anda bersifat anonim secara default, sehingga Anda dapat memberikan informasi kontak seperti alamat email di sini.\nadditionalErrorInfo=Memberikan informasi tambahan (opsional)\nadditionalErrorAttachments=Memilih lampiran (opsional)\ndataHandlingPolicies=Kebijakan privasi\nsendReport=Mengirim laporan\nerrorHandler=Penangan kesalahan\nevents=Acara\nvalidate=Memvalidasi\nstackTrace=Jejak tumpukan\npreviousStep=< Sebelumnya\nnextStep=Berikutnya >\nfinishStep=Selesai\nselect=Pilih\nbrowseInternal=Jelajahi Internal\ncheckOutUpdate=Lihat pembaruan\nquit=Keluar\nnoTerminalSet=Tidak ada aplikasi terminal yang diatur secara otomatis. Anda dapat melakukannya secara manual di menu pengaturan.\nconnections=Koneksi\nconnectionHub=Hub koneksi\nsettings=Pengaturan\nexplorePlans=Lisensi\nhelp=Bantuan\nabout=Tentang\ndeveloper=Pengembang\nbrowseFileTitle=Menelusuri file\nbrowser=Peramban file\nselectFileFromComputer=Memilih file dari komputer ini\nlinks=Tautan\nwebsite=Situs web\ndiscordDescription=Bergabung dengan server Discord\nredditDescription=Bergabunglah dengan subreddit XPipe\nsecurity=Keamanan\nsecurityPolicy=Informasi keamanan\nsecurityPolicyDescription=Baca kebijakan keamanan yang terperinci\nprivacy=Kebijakan Privasi\nprivacyDescription=Baca kebijakan privasi untuk aplikasi XPipe\nslackDescription=Bergabung dengan ruang kerja Slack\nsupport=Dukungan\ngithubDescription=Lihat repositori GitHub\nopenSourceNotices=Pemberitahuan Sumber Terbuka\ncheckForUpdates=Memeriksa pembaruan\ncheckForUpdatesDescription=Mengunduh pembaruan jika ada\nlastChecked=Terakhir diperiksa\nversion=Versi\nbuild=Versi build\nruntimeVersion=Versi runtime\nvirtualMachine=Mesin virtual\nupdateReady=Menginstal pembaruan\nupdateReadyPortable=Lihat pembaruan\nupdateReadyDescription=Pembaruan telah diunduh dan siap diinstal\nupdateReadyDescriptionPortable=Pembaruan tersedia untuk diunduh\nupdateRestart=Mulai ulang untuk memperbarui\nnever=Tidak pernah\nupdateAvailableTooltip=Pembaruan tersedia\nptbAvailableTooltip=Tersedia Uji Coba Publik\nvisitGithubRepository=Kunjungi repositori GitHub\nupdateAvailable=Pembaruan tersedia: $VERSION$\ndownloadUpdate=Unduh pembaruan\nlegalAccept=Saya menerima Perjanjian Lisensi Pengguna Akhir\nconfirm=Konfirmasi\nprint=Mencetak\nwhatsNew=Apa yang baru dalam versi $VERSION$ ($DATE$)\nantivirusNoticeTitle=Catatan tentang program Antivirus\nupdateChangelogAlertTitle=Changelog\ngreetingsAlertTitle=Selamat datang di XPipe\neula=Perjanjian Lisensi Pengguna Akhir\nnews=Berita\nintroduction=Pendahuluan\nprivacyPolicy=Kebijakan Privasi\nagree=Setuju\ndisagree=Tidak setuju\ndirectories=Direktori\nlogFile=File Log\nlogFiles=File Log\nlogFilesAttachment=File Log\nissueReporter=Pelapor masalah\nopenCurrentLogFile=File log\nopenCurrentLogFileDescription=Membuka file log dari sesi saat ini\nopenLogsDirectory=Membuka direktori log\ninstallationFiles=File Instalasi\nopenInstallationDirectory=File instalasi\nopenInstallationDirectoryDescription=Buka direktori instalasi XPipe\nlaunchDebugMode=Mode debug\nlaunchDebugModeDescription=Mulai ulang XPipe dalam mode debug\nextensionInstallTitle=Unduh\nextensionInstallDescription=Tindakan ini memerlukan pustaka pihak ketiga tambahan yang tidak didistribusikan oleh XPipe. Anda dapat menginstalnya secara otomatis di sini. Komponen-komponen tersebut kemudian diunduh dari situs web vendor:\nextensionInstallLicenseNote=Dengan melakukan pengunduhan dan instalasi otomatis, Anda menyetujui persyaratan lisensi pihak ketiga:\nlicense=Lisensi\ninstallRequired=Diperlukan Instalasi\nrestore=Mengembalikan\nrestoreAllSessions=Memulihkan semua sesi\nlimitedTouchscreenMode=Mode layar sentuh terbatas\nlimitedTouchscreenModeDescription=Apabila menggunakan aplikasi ini pada antarmuka layar sentuh yang lebih eksotis, seperti layar ponsel, sebagian menu mungkin tidak berfungsi dengan baik. Apabila opsi ini diaktifkan, implementasi menu menggunakan fungsionalitas yang lebih terbatas untuk bekerja dengan peristiwa mouse/sentuh yang jarang dikirim.\nappearance=Penampilan\ndisplay=Tampilan\npersonalization=Personalisasi\ndisplayOptions=Opsi tampilan\ntheme=Tema\nrdpConfiguration=Konfigurasi desktop jarak jauh\nrdpClient=Klien RDP\nrdpClientDescription=Program klien RDP yang akan dipanggil saat meluncurkan koneksi RDP.\\n\\nPerhatikan bahwa berbagai klien memiliki tingkat kemampuan dan integrasi yang berbeda. Beberapa klien tidak mendukung pemberian kata sandi secara otomatis, jadi Anda masih harus mengisinya saat peluncuran.\nlocalShell=Cangkang lokal\nthemeDescription=Tema tampilan pilihan Anda.\ndontAutomaticallyStartVmSshServer=Jangan memulai server SSH untuk VM secara otomatis saat dibutuhkan\ndontAutomaticallyStartVmSshServerDescription=Semua koneksi shell ke VM yang berjalan di hypervisor dibuat melalui SSH. XPipe dapat secara otomatis memulai server SSH yang terinstal bila diperlukan. Jika Anda tidak menginginkan hal ini karena alasan keamanan, maka Anda dapat menonaktifkan perilaku ini dengan opsi ini.\nconfirmGitShareTitle=Sinkronisasi git\nconfirmGitShareContent=Apakah Anda ingin menambahkan berkas yang dipilih ke repositori git vault Anda? Ini akan menyalin versi terenkripsi dari berkas ke dalam brankas git Anda dan mengomit perubahan Anda. Anda kemudian akan memiliki akses ke file tersebut di semua desktop yang disinkronkan.\ngitShareFileTooltip=Menambahkan file ke direktori data git vault agar disinkronkan secara otomatis.\\n\\nTindakan ini hanya dapat digunakan bila git vault diaktifkan di pengaturan.\nperformanceMode=Mode kinerja\nperformanceModeDescription=Menonaktifkan semua efek visual yang tidak diperlukan untuk meningkatkan kinerja aplikasi.\ndontAcceptNewHostKeys=Jangan menerima kunci host SSH baru secara otomatis\ndontAcceptNewHostKeysDescription=XPipe akan secara otomatis menerima kunci host secara default dari sistem di mana klien SSH Anda tidak memiliki kunci host yang diketahui dan sudah disimpan. Namun, jika kunci host yang diketahui telah berubah, maka klien akan menolak untuk menyambung kecuali Anda menerima kunci host yang baru.\\n\\nDengan menonaktifkan perilaku ini, Anda dapat memeriksa semua kunci host, bahkan jika pada awalnya tidak ada konflik.\nuiScale=Skala UI\nuiScaleDescription=Nilai penskalaan khusus yang dapat ditetapkan secara independen dari skala tampilan di seluruh sistem. Nilai dalam persen, misalnya nilai 150 akan menghasilkan skala UI 150%.\neditorProgram=Program Editor\neditorProgramDescription=Editor teks default yang digunakan saat mengedit data teks apa pun.\nwindowOpacity=Keburaman jendela\nwindowOpacityDescription=Mengubah opasitas jendela untuk melacak apa yang terjadi di latar belakang.\nuseSystemFont=Menggunakan font sistem\nopenDataDir=Direktori data brankas\nopenDataDirButton=Membuka direktori data\nopenDataDirDescription=Jika Anda ingin menyinkronkan file tambahan, seperti kunci SSH, di seluruh sistem dengan repositori git Anda, Anda dapat memasukkannya ke dalam direktori data penyimpanan. Semua file yang dirujuk di sana akan memiliki jalur file yang secara otomatis disesuaikan pada sistem yang disinkronkan.\nupdates=Pembaruan\nselectAll=Pilih semua\nadvanced=Tingkat Lanjut\nthirdParty=Pemberitahuan sumber terbuka\neulaDescription=Baca Perjanjian Lisensi Pengguna Akhir untuk aplikasi XPipe\nthirdPartyDescription=Melihat lisensi sumber terbuka dari pustaka pihak ketiga\nworkspaceLock=Kata sandi utama\nenableGitStorage=Mengaktifkan sinkronisasi\nsharing=Berbagi\ngitSync=Sinkronisasi git\nenableGitStorageDescription=Ketika diaktifkan, XPipe akan menginisialisasi repositori git untuk brankas lokal dan melakukan perubahan apa pun padanya. Perhatikan bahwa hal ini membutuhkan git untuk diinstal dan mungkin memperlambat operasi pemuatan dan penyimpanan.\\n\\nSetiap kategori yang harus disinkronkan harus secara eksplisit ditandai sebagai disinkronkan.\nstorageGitRemote=URL sinkronisasi jarak jauh\nstorageGitRemoteDescription=Ketika diatur, XPipe akan secara otomatis menarik perubahan apa pun ketika memuat dan mendorong perubahan apa pun ke repositori jarak jauh ketika menyimpan.\\n\\nHal ini memungkinkan Anda untuk berbagi brankas di antara beberapa instalasi XPipe. Mendukung URL HTTP dan SSH, ditambah direktori lokal.\nvault=Brankas\nworkspaceLockDescription=Menetapkan kata sandi khusus untuk mengenkripsi informasi sensitif yang disimpan di XPipe.\\n\\nHal ini akan meningkatkan keamanan karena menyediakan lapisan enkripsi tambahan untuk informasi sensitif yang tersimpan. Anda kemudian akan diminta untuk memasukkan kata sandi saat XPipe dimulai.\nuseSystemFontDescription=Mengontrol apakah akan menggunakan font sistem default atau font Inter, yang disertakan dengan XPipe.\ntooltipDelay=Penundaan keterangan alat\ntooltipDelayDescription=Jumlah milidetik untuk menunggu hingga keterangan alat ditampilkan.\nfontSize=Ukuran huruf\nwindowOptions=Opsi Jendela\nsaveWindowLocation=Menyimpan lokasi jendela\nsaveWindowLocationDescription=Mengontrol apakah koordinat jendela harus disimpan dan dipulihkan saat memulai ulang.\nstartupShutdown=Pengaktifan / Penonaktifan\nshowChildrenConnectionsInParentCategory=Menampilkan kategori anak dalam kategori induk\nshowChildrenConnectionsInParentCategoryDescription=Apakah akan menyertakan semua koneksi yang berada dalam sub kategori atau tidak ketika memilih kategori induk tertentu.\\n\\nJika ini dinonaktifkan, kategori akan berperilaku seperti folder klasik yang hanya menampilkan konten langsung tanpa menyertakan sub folder.\ncondenseConnectionDisplay=Memadatkan tampilan koneksi\ncondenseConnectionDisplayDescription=Buatlah setiap koneksi tingkat atas mengambil ruang vertikal yang lebih sedikit untuk memungkinkan daftar koneksi yang lebih ringkas.\nopenConnectionSearchWindowOnConnectionCreation=Membuka jendela pencarian koneksi pada pembuatan koneksi\nopenConnectionSearchWindowOnConnectionCreationDescription=Apakah akan membuka jendela secara otomatis untuk mencari subkoneksi yang tersedia saat menambahkan koneksi shell baru atau tidak.\nworkflow=Alur kerja\nsystem=Sistem\napplication=Aplikasi\nstorage=Penyimpanan\nrunOnStartup=Jalankan saat pengaktifan\ncloseBehaviour=Perilaku keluar\ncloseBehaviourDescription=Mengontrol bagaimana XPipe akan melanjutkan setelah menutup jendela utamanya.\nlanguage=Bahasa\nlanguageDescription=Bahasa tampilan yang akan digunakan. Terjemahan ditingkatkan melalui kontribusi komunitas. Anda dapat membantu upaya penerjemahan dengan mengirimkan perbaikan terjemahan di GitHub.\nlightTheme=Tema Cahaya\ndarkTheme=Tema Gelap\nexit=Keluar dari XPipe\ncontinueInBackground=Lanjutkan di latar belakang\nminimizeToTray=Meminimalkan ke baki\ncloseBehaviourAlertTitle=Mengatur perilaku penutupan\ncloseBehaviourAlertTitleHeader=Pilih apa yang seharusnya terjadi saat menutup jendela. Semua koneksi aktif akan ditutup saat aplikasi ditutup.\nstartupBehaviour=Perilaku pengaktifan\nstartupBehaviourDescription=Mengontrol perilaku default aplikasi desktop saat XPipe dimulai.\nclearCachesAlertTitle=Bersihkan Cache\nclearCachesAlertContent=Apakah Anda ingin membersihkan semua cache XPipe? Ini akan menghapus semua data cache yang disimpan untuk meningkatkan pengalaman pengguna.\nstartGui=Mulai GUI\nstartInTray=Mulai di baki\nstartInBackground=Mulai di latar belakang\nclearCaches=Menghapus cache ...\nclearCachesDescription=Menghapus semua data cache\ncancel=Membatalkan\nnotAnAbsolutePath=Bukan jalur absolut\nnotADirectory=Bukan direktori\nnotAnEmptyDirectory=Bukan direktori kosong\nautomaticallyCheckForUpdates=Memeriksa pembaruan\nautomaticallyCheckForUpdatesDescription=Bila diaktifkan, informasi rilis baru secara otomatis diambil saat XPipe berjalan setelah beberapa saat. Anda masih harus mengkonfirmasi secara eksplisit setiap instalasi pembaruan.\nsendAnonymousErrorReports=Mengirim laporan kesalahan anonim\nsendUsageStatistics=Mengirim statistik penggunaan anonim\nstorageDirectory=Direktori penyimpanan\nstorageDirectoryDescription=Lokasi di mana XPipe harus menyimpan semua informasi koneksi. Saat mengubahnya, data di direktori lama tidak disalin ke direktori baru.\nlogLevel=Tingkat log\nappBehaviour=Perilaku aplikasi\nlogLevelDescription=Tingkat log yang harus digunakan saat menulis file log.\ndeveloperMode=Mode pengembang\ndeveloperModeDescription=Apabila diaktifkan, Anda akan memiliki akses ke berbagai opsi tambahan yang berguna untuk pengembangan.\neditor=Editor\ncustom=Kustom\npasswordManager=Manajer kata sandi\nexternalPasswordManager=Pengelola kata sandi eksternal\npasswordManagerDescription=Pengelola kata sandi yang diinstal secara lokal untuk diintegrasikan.\\n\\nJika Anda memiliki pengelola kata sandi yang terinstal, Anda dapat mengonfigurasi XPipe untuk mengambil kata sandi dari pengelola tersebut sehingga XPipe tidak perlu menyimpan kata sandi itu sendiri. Bila diaktifkan, bidang kata sandi apa pun untuk koneksi dapat dikonfigurasi untuk menggunakan pengelola kata sandi.\npasswordManagerCommandTest=Menguji pengelola kata sandi\npasswordManagerCommandTestDescription=Anda dapat menguji di sini apakah output terlihat benar jika Anda telah mengatur pengelola kata sandi.\npreferTerminalTabs=Lebih suka membuka tab baru\npreferTerminalTabsDescription=Mengontrol apakah XPipe akan mencoba membuka tab baru di terminal yang Anda pilih, bukan jendela baru. Tidak semua terminal mendukung tab.\ncustomRdpClientCommand=Perintah khusus\ncustomRdpClientCommandDescription=Perintah yang harus dijalankan untuk memulai klien RDP khusus.\\n\\nString penampung $FILE akan diganti dengan nama file .rdp absolut yang dikutip saat dipanggil. Ingatlah untuk mengutip jalur yang dapat dieksekusi jika mengandung spasi.\ncustomEditorCommand=Perintah editor khusus\ncustomEditorCommandDescription=Perintah yang harus dijalankan untuk memulai editor khusus.\\n\\nString penampung $FILE akan diganti dengan nama file absolut yang dikutip saat dipanggil. Ingatlah untuk mengutip jalur eksekusi editor Anda jika mengandung spasi.\neditorReloadTimeout=Batas waktu muat ulang editor\neditorReloadTimeoutDescription=Jumlah milidetik untuk menunggu sebelum membaca file setelah diperbarui. Hal ini untuk menghindari masalah jika editor Anda lambat dalam menulis atau melepaskan kunci file.\nencryptAllVaultData=Mengenkripsi semua data brankas\nencryptAllVaultDataDescription=Ketika diaktifkan, setiap bagian dari data koneksi vault akan dienkripsi dengan kunci enkripsi vault pengguna Anda dan bukan hanya rahasia yang ada di dalam data tersebut. Hal ini menambahkan lapisan keamanan lain untuk parameter lain seperti nama pengguna, nama host, dll., yang tidak dienkripsi secara default di dalam vault.\\n\\nOpsi ini akan membuat riwayat git vault dan perbedaannya tidak berguna karena Anda tidak dapat melihat perubahan aslinya lagi, hanya perubahan biner.\nvaultSecurity=Keamanan lemari besi\ndeveloperDisableUpdateVersionCheck=Menonaktifkan Pemeriksaan Versi Pembaruan\ndeveloperDisableUpdateVersionCheckDescription=Mengontrol apakah pemeriksa pembaruan akan mengabaikan nomor versi saat mencari pembaruan.\ndeveloperDisableGuiRestrictions=Menonaktifkan pembatasan GUI\ndeveloperDisableGuiRestrictionsDescription=Mengontrol apakah beberapa tindakan yang dinonaktifkan masih dapat dieksekusi dari antarmuka pengguna.\ndeveloperShowHiddenEntries=Menampilkan entri tersembunyi\ndeveloperShowHiddenEntriesDescription=Bila diaktifkan, sumber data yang tersembunyi dan internal akan ditampilkan.\ndeveloperShowHiddenProviders=Menampilkan penyedia tersembunyi\ndeveloperShowHiddenProvidersDescription=Mengontrol apakah koneksi tersembunyi dan internal serta penyedia sumber data akan ditampilkan dalam dialog pembuatan.\ndeveloperDisableConnectorInstallationVersionCheck=Menonaktifkan Pemeriksaan Versi Konektor\ndeveloperDisableConnectorInstallationVersionCheckDescription=Mengontrol apakah pemeriksa pembaruan akan mengabaikan nomor versi saat memeriksa versi konektor XPipe yang dipasang pada mesin jarak jauh.\nshellCommandTest=Tes Perintah Shell\nshellCommandTestDescription=Menjalankan perintah dalam sesi shell yang digunakan secara internal oleh XPipe.\nterminal=Terminal\nterminalType=Emulator terminal\nterminalConfiguration=Konfigurasi terminal\nterminalCustomization=Kustomisasi terminal\neditorConfiguration=Konfigurasi editor\ndefaultApplication=Aplikasi default\ninitialSetup=Penyiapan awal\nterminalTypeDescription=Terminal default yang digunakan untuk membuka koneksi shell.\\n\\nTingkat dukungan fitur bervariasi menurut terminal, dan masing-masing terminal ditandai sebagai direkomendasikan atau tidak direkomendasikan. Pengalaman pengguna Anda akan menjadi yang terbaik bila menggunakan terminal yang direkomendasikan.\nprogram=Program\ncustomTerminalCommand=Perintah terminal khusus\ncustomTerminalCommandDescription=Perintah yang harus dijalankan untuk membuka terminal khusus dengan perintah tertentu.\\n\\nXPipe akan membuat skrip shell peluncur sementara untuk dieksekusi oleh terminal Anda. String penampung $CMD dalam perintah yang Anda berikan akan digantikan oleh skrip peluncur yang sebenarnya ketika dipanggil. Ingatlah untuk mengutip jalur eksekusi terminal Anda jika mengandung spasi.\nclearTerminalOnInit=Menghapus terminal saat init\nclearTerminalOnInitDescription=Bila diaktifkan, XPipe akan menjalankan perintah clear setelah sesi terminal baru diluncurkan untuk menghapus output yang tidak perlu yang dicetak saat memulai sesi terminal.\ndontCachePasswords=Jangan menyimpan kata sandi yang diminta dalam cache\ndontCachePasswordsDescription=Mengontrol apakah kata sandi yang ditanyakan harus di-cache secara internal oleh XPipe sehingga Anda tidak perlu memasukkannya lagi dalam sesi saat ini.\\n\\nJika perilaku ini dinonaktifkan, Anda harus memasukkan kembali kredensial yang diminta setiap kali diminta oleh sistem.\ndenyTempScriptCreation=Menolak pembuatan skrip sementara\ndenyTempScriptCreationDescription=Untuk mewujudkan beberapa fungsinya, XPipe terkadang membuat skrip shell sementara pada sistem target untuk memudahkan eksekusi perintah sederhana. Skrip ini tidak mengandung informasi sensitif dan hanya dibuat untuk tujuan implementasi.\\n\\nJika perilaku ini dinonaktifkan, XPipe tidak akan membuat file sementara pada sistem jarak jauh. Opsi ini berguna dalam konteks keamanan tinggi di mana setiap perubahan sistem file dimonitor. Jika ini dinonaktifkan, beberapa fungsionalitas, misalnya lingkungan shell dan skrip, tidak akan berfungsi sebagaimana mestinya.\ndisableCertutilUse=Menonaktifkan penggunaan certutil pada Windows\nuseLocalFallbackShell=Gunakan shell fallback lokal\nuseLocalFallbackShellDescription=Beralih menggunakan shell lokal lain untuk menangani operasi lokal. Misalnya, PowerShell pada Windows dan bourne shell pada sistem lain.\\n\\nOpsi ini dapat digunakan jika shell default lokal normal dinonaktifkan atau rusak pada tingkat tertentu. Beberapa fitur mungkin tidak berfungsi seperti yang diharapkan ketika opsi ini diaktifkan.\ndisableCertutilUseDescription=Karena beberapa kekurangan dan bug pada cmd.exe, skrip shell sementara dibuat dengan certutil dengan menggunakannya untuk memecahkan kode input base64 karena cmd.exe rusak pada input non-ASCII. XPipe juga dapat menggunakan PowerShell untuk itu tetapi ini akan lebih lambat.\\n\\nHal ini menonaktifkan penggunaan certutil pada sistem Windows untuk merealisasikan beberapa fungsionalitas dan kembali ke PowerShell. Hal ini mungkin akan menyenangkan beberapa AV karena beberapa di antaranya memblokir penggunaan certutil.\ndisableTerminalRemotePasswordPreparation=Menonaktifkan persiapan kata sandi jarak jauh terminal\ndisableTerminalRemotePasswordPreparationDescription=Dalam situasi di mana koneksi shell jarak jauh yang melewati beberapa sistem perantara harus dibuat di terminal, mungkin ada persyaratan untuk menyiapkan kata sandi yang diperlukan pada salah satu sistem perantara untuk memungkinkan pengisian otomatis setiap permintaan.\\n\\nJika Anda tidak ingin kata sandi ditransfer ke sistem perantara mana pun, Anda dapat menonaktifkan perilaku ini. Kata sandi perantara yang diperlukan kemudian akan ditanyakan di terminal itu sendiri ketika dibuka.\nmore=Lainnya\ntranslate=Terjemahan\nallConnections=Semua koneksi\nallScripts=Semua skrip\nallIdentities=Semua identitas\nsynced=Disinkronkan\npredefined=Telah ditentukan sebelumnya\nsamples=Sampel\ngoodMorning=Selamat pagi\ngoodAfternoon=Selamat siang\ngoodEvening=Selamat malam\naddVisual=Visual ...\naddDesktop=Desktop ...\nssh=SSH\nsshConfiguration=Konfigurasi SSH\nsize=Ukuran\nattributes=Atribut\nmodified=Dimodifikasi\nowner=Pemilik\nupdateReadyTitle=Pembaruan ke $VERSION$ siap\ntemplates=Templat\nretry=Coba lagi\nretryAll=Coba lagi semua\nreplace=Menggantikan\nreplaceAll=Ganti semua\nhibernateBehaviour=Perilaku hibernasi\nhibernateBehaviourDescription=Mengontrol perilaku aplikasi saat sistem Anda masuk ke mode hibernasi/tidur.\noverview=Ikhtisar\nhistory=Sejarah\nskipAll=Lewati semua\nnotes=Catatan\naddNotes=Menambahkan catatan\norder=Susun ulang\nkeepFirst=Simpan dulu\nkeepLast=Simpan yang terakhir\npinToTop=Sematkan ke atas\nunpinFromTop=Lepaskan pin dari atas\norderAheadOf=Memesan di depan ...\nclearIndex=Setel ulang indeks\nhttpServer=Server HTTP\nmcpServer=Server MCP\napiKey=Kunci API\napiKeyDescription=Kunci API untuk mengautentikasi permintaan API daemon XPipe. Untuk informasi lebih lanjut tentang cara mengautentikasi, lihat dokumentasi API umum.\ndisableApiAuthentication=Menonaktifkan autentikasi API\ndisableApiAuthenticationDescription=Menonaktifkan semua metode autentikasi yang diperlukan agar permintaan yang tidak diautentikasi dapat ditangani.\\n\\nAutentikasi sebaiknya hanya dinonaktifkan untuk tujuan pengembangan.\napi=API\nstoreIntroImportContent=Sudah menggunakan XPipe di sistem lain? Sinkronkan koneksi Anda yang ada di beberapa sistem melalui repositori git jarak jauh. Anda juga dapat menyinkronkan nanti kapan saja jika belum diatur.\nstoreIntroImportButton=Menyinkronkan koneksi ...\nstoreIntroImportHeader=Mengimpor Koneksi\nshowNonRunningChildren=Menampilkan anak yang tidak berjalan\nhttpApi=API HTTP\nisOnlySupportedLimit=hanya didukung dengan lisensi profesional bila memiliki lebih dari $COUNT$ koneksi\nareOnlySupportedLimit=hanya didukung dengan lisensi profesional jika memiliki lebih dari $COUNT$ koneksi\nenabled=Diaktifkan\nenableGitStoragePtbDisabled=Sinkronisasi Git dinonaktifkan untuk build uji publik untuk mencegah penggunaan dengan repositori git rilis reguler dan untuk mencegah penggunaan build PTB sebagai driver harian Anda.\ncopyId=Salin ID API\nrequireDoubleClickForConnections=Memerlukan klik dua kali untuk koneksi\nrequireDoubleClickForConnectionsDescription=Jika diaktifkan, Anda harus mengeklik dua kali koneksi untuk meluncurkannya. Hal ini berguna jika Anda terbiasa mengklik dua kali.\nclearTransferDescription=Pilihan yang jelas\nselectTab=Pilih tab\ncloseTab=Menutup tab\ncloseOtherTabs=Menutup tab lain\ncloseAllTabs=Menutup semua tab\ncloseLeftTabs=Menutup tab di sebelah kiri\ncloseRightTabs=Menutup tab di sebelah kanan\naddSerial=Serial ...\nconnect=Menghubungkan\nworkspaces=Ruang kerja\nmanageWorkspaces=Mengelola ruang kerja\naddWorkspace=Menambahkan ruang kerja ...\nworkspaceAdd=Menambahkan ruang kerja baru\nworkspaceAddDescription=Ruang kerja adalah konfigurasi yang berbeda untuk menjalankan XPipe. Setiap ruang kerja memiliki direktori data di mana semua data disimpan secara lokal. Ini termasuk data koneksi, pengaturan, dan banyak lagi.\\n\\nJika Anda menggunakan fitur sinkronisasi, Anda juga dapat memilih untuk menyinkronkan setiap workspace dengan repositori git yang berbeda.\nworkspaceName=Nama ruang kerja\nworkspaceNameDescription=Nama tampilan ruang kerja\nworkspacePath=Jalur ruang kerja\nworkspacePathDescription=Lokasi direktori data ruang kerja\nworkspaceCreationAlertTitle=Pembuatan ruang kerja\ndeveloperForceSshTty=Memaksa SSH TTY\ndeveloperForceSshTtyDescription=Buatlah semua sambungan SSH mengalokasikan sebuah pty untuk menguji dukungan stderr dan pty yang hilang.\ndeveloperDisableSshTunnelGateways=Menonaktifkan kanalisasi gateway SSH\ndeveloperDisableSshTunnelGatewaysDescription=Jangan gunakan sesi terowongan untuk gateway dan sebagai gantinya sambungkan langsung ke sistem.\nttyWarning=Sambungan telah mengalokasikan pty/tty secara paksa dan tidak menyediakan aliran stderr yang terpisah.\\n\\nHal ini dapat menyebabkan beberapa masalah.\\n\\nJika bisa, cari cara agar perintah koneksi tidak mengalokasikan pty.\nxshellSetup=Pengaturan Xshell\ntermiusSetup=Pengaturan Termius\ntryPtbDescription=Mencoba fitur-fitur baru lebih awal dalam pengembangan XPipe yang dibuat oleh pengembang\nconfirmVaultUnencryptTitle=Mengonfirmasi penguraian enkripsi brankas\nconfirmVaultUnencryptContent=Apakah Anda benar-benar ingin menonaktifkan enkripsi brankas tingkat lanjut? Ini akan menghapus enkripsi tambahan untuk data yang tersimpan dan akan menimpa data yang ada.\nenableHttpApi=Mengaktifkan API HTTP\nenableHttpApiDescription=Mengaktifkan API, sehingga program eksternal dapat memanggil daemon XPipe untuk melakukan tindakan dengan koneksi terkelola Anda.\nchooseCustomIcon=Memilih ikon khusus\ngitVault=Brankas git\nfileBrowser=Peramban file\nconfirmAllDeletions=Mengonfirmasi semua penghapusan\nconfirmAllDeletionsDescription=Apakah akan menampilkan dialog konfirmasi untuk semua operasi penghapusan. Secara default, hanya direktori yang memerlukan konfirmasi.\nyesterday=Kemarin\ngreen=Hijau\nyellow=Kuning\nblue=Biru\nred=Merah\ncyan=Cyan\npurple=Ungu\nasktextAlertTitle=Prompt\nfileWriteSudoTitle=Menulis file Sudo\nfileWriteSudoContent=Berkas yang Anda coba tulis tidak memberikan izin tulis kepada pengguna Anda. Apakah Anda ingin menulis berkas ini sebagai root dengan sudo? Ini akan secara otomatis meningkatkannya menjadi root dengan kredensial yang ada atau melalui prompt.\ndontAllowTerminalRestart=Jangan izinkan pengaktifan ulang terminal\ndontAllowTerminalRestartDescription=Secara default, sesi terminal dapat dimulai kembali setelah sesi tersebut diakhiri dari dalam terminal. Untuk mengizinkan hal ini, XPipe akan menerima permintaan eksternal ini dari terminal untuk meluncurkan sesi lagi\\n\\nXPipe tidak memiliki kontrol atas terminal dan dari mana panggilan ini berasal, sehingga aplikasi lokal yang berbahaya dapat menggunakan fungsionalitas ini juga untuk meluncurkan koneksi melalui XPipe. Menonaktifkan fungsi ini akan mencegah skenario ini.\nopenDocumentation=Buka dokumentasi\nopenDocumentationDescription=Kunjungi halaman dokumen XPipe untuk masalah ini\nrenameAll=Ganti nama semua\nlogging=Penebangan\nenableTerminalLogging=Mengaktifkan pencatatan terminal\nenableTerminalLoggingDescription=Mengaktifkan pencatatan sisi klien untuk semua sesi terminal. Semua input dan output sesi terminal ditulis ke dalam file log sesi. Perhatikan bahwa informasi sensitif apa pun seperti permintaan kata sandi tidak direkam.\nterminalLoggingDirectory=Log sesi terminal\nterminalLoggingDirectoryDescription=Semua log disimpan dalam direktori data XPipe pada sistem lokal Anda.\nopenSessionLogs=Membuka log sesi\nsessionLogging=Pencatatan terminal\nsessionActive=Sesi latar belakang sedang berjalan untuk koneksi ini.\\n\\nUntuk menghentikan sesi ini secara manual, klik indikator status.\nskipValidation=Lewati validasi\nscriptsIntroHeader=Tentang skrip\nscriptsIntroContent=Anda dapat menjalankan skrip pada shell init, di peramban file, dan sesuai permintaan. Anda dapat membuat skrip sendiri di dalam XPipe atau mengimpor skrip yang sudah ada dari sistem lokal atau dari repositori git jarak jauh.\nscriptsIntroBottomHeader=Menggunakan skrip\nscriptsIntroBottomContent=Terdapat berbagai contoh skrip untuk memulai. Anda dapat mengklik tombol edit pada masing-masing skrip untuk melihat bagaimana skrip tersebut diimplementasikan. Skrip pertama-tama harus diaktifkan agar dapat berjalan dan muncul di menu, ada tombol untuk itu pada setiap skrip.\nscriptsIntroBottomButton=Memulai\nscriptSourcesIntroHeader=Sumber naskah\nscriptSourcesIntroContent=Anda dapat menambahkan sumber skrip khusus untuk mendapatkan akses instan ke seluruh koleksi skrip shell. Sumber lokal dan repositori git jarak jauh didukung sebagai sumber. Semua skrip yang terdeteksi dari sumber akan tersedia secara otomatis.\nscriptSourcesIntroButton=Menambahkan sumber ...\ncheckForSecurityUpdates=Memeriksa pembaruan keamanan\ncheckForSecurityUpdatesDescription=XPipe dapat memeriksa potensi pembaruan keamanan secara terpisah dari pembaruan fitur normal. Bila ini diaktifkan, setidaknya pembaruan keamanan yang penting akan direkomendasikan untuk diinstal meskipun pemeriksaan pembaruan normal dinonaktifkan.\\n\\nMenonaktifkan pengaturan ini akan mengakibatkan tidak ada permintaan versi eksternal yang dilakukan, dan Anda tidak akan diberitahu tentang pembaruan keamanan apa pun.\nclickToDock=Klik untuk membuka terminal dok\nterminalStarting=Menunggu pengaktifan terminal ...\npinTab=Tab pin\nunpinTab=Membuka pin tab\npinned=Disematkan\nenableConnectionHubTerminalDocking=Mengaktifkan docking terminal hub koneksi\nenableConnectionHubTerminalDockingDescription=Anda dapat menyambungkan jendela terminal ke jendela aplikasi XPipe di hub koneksi untuk mensimulasikan terminal yang agak terintegrasi. Jendela terminal kemudian dikelola oleh XPipe agar selalu sesuai dengan dok.\nenableFileBrowserTerminalDocking=Mengaktifkan docking terminal peramban file\nenableFileBrowserTerminalDockingDescription=Anda dapat menyambungkan jendela terminal ke jendela aplikasi XPipe di peramban file untuk mensimulasikan terminal yang agak terintegrasi. Jendela terminal kemudian dikelola oleh XPipe agar selalu sesuai dengan dok.\ndownloadsDirectory=Direktori unduhan khusus\ndownloadsDirectoryDescription=Direktori khusus untuk meletakkan file yang diunduh ketika mengklik tombol pindah ke unduhan. Secara default, XPipe akan menggunakan direktori unduhan pengguna Anda.\npinLocalMachineOnStartup=Menyematkan tab mesin lokal saat pengaktifan\npinLocalMachineOnStartupDescription=Secara otomatis membuka tab mesin lokal dan menyematkannya. Hal ini berguna jika Anda sering menggunakan peramban file terpisah dengan mesin lokal dan sistem file jarak jauh terbuka.\nterminalErrorDescription=Kesalahan ini bersifat terminal dan XPipe tidak dapat dilanjutkan tanpa memperbaikinya.\ngroupName=Nama grup\nchmodPermissions=Izin baru\neditFilesWithDoubleClick=Mengedit file dengan klik dua kali\neditFilesWithDoubleClickDescription=Apabila diaktifkan, mengeklik dua kali file akan langsung membukanya di editor teks, bukannya menampilkan menu konteks.\ncensorMode=Mode sensor\ncensorModeDescription=Mengaburkan informasi apa pun seperti nama host, nama pengguna, nama koneksi, dan lainnya.\\n\\nIni berguna jika Anda berniat mengambil tangkapan layar atau melakukan screenshot atau berbagi layar XPipe dan tidak ingin membocorkan informasi apa pun.\naddIdentity=Identitas ...\nidentities=Identitas\naddMacro=Tindakan ...\nidentitiesIntroHeader=Tentang identitas\nidentitiesIntroContent=Jika Anda menggunakan kembali kombinasi umum nama pengguna, kata sandi, dan kunci, mungkin masuk akal untuk membuat identitas yang dapat digunakan kembali. Hal ini memungkinkan Anda untuk dengan cepat mereferensikannya saat menambahkan koneksi baru.\nidentitiesIntroBottomHeader=Berbagi identitas\nidentitiesIntroBottomContent=Anda dapat menambahkan identitas secara lokal atau juga menyinkronkannya di repositori git ketika ini diaktifkan. Hal ini memungkinkan untuk berbagi identitas secara selektif di beberapa sistem dan dengan anggota tim lainnya.\nidentitiesIntroBottomButton=Menyiapkan sinkronisasi\nidentitiesIntroButton=Membuat identitas\nuserName=Nama pengguna\nuserAuth=Autentikasi kata sandi berbasis pengguna\ngroupAuth=Autentikasi rahasia berbasis grup\nteam=Tim\nteamSettings=Pengaturan tim\nteamVaults=Brankas tim\nvaultTypeNameDefault=Brankas default\nvaultTypeNameLegacy=Brankas pribadi warisan\nvaultTypeNamePersonal=Brankas pribadi\nvaultTypeNameTeam=Brankas tim\nteamVaultsDescription=Brankas tim memungkinkan beberapa pengguna dan grup memiliki akses yang aman ke brankas bersama. Anda dapat mengonfigurasi koneksi dan identitas untuk dibagikan kepada semua pengguna atau hanya tersedia bagi pengguna dan grup tertentu dengan mengenkripsinya dengan kunci mereka sendiri. Pengguna vault lainnya tidak dapat mengakses koneksi dan identitas berbasis pribadi dan grup jika mereka tidak memiliki akses ke kunci.\nvaultTypeContentDefault=Anda saat ini menggunakan brankas default tanpa pengguna dan kata sandi khusus yang ditetapkan. Rahasia dienkripsi dengan kunci brankas lokal. Anda dapat meningkatkan ke brankas pribadi dengan membuat akun pengguna brankas. Hal ini memungkinkan Anda untuk mengenkripsi rahasia vault dengan kata sandi pribadi yang harus Anda masukkan pada setiap login untuk membuka kunci vault.\nvaultTypeContentLegacy=Anda saat ini menggunakan brankas pribadi lama untuk pengguna Anda. Rahasia dienkripsi dengan kata sandi pribadi Anda. Kompatibilitas lama ini memiliki fitur yang terbatas dan tidak dapat ditingkatkan ke brankas tim di tempat.\nvaultTypeContentPersonal=Anda saat ini menggunakan brankas pribadi untuk pengguna Anda. Rahasia dienkripsi dengan kata sandi pribadi Anda. Anda dapat meningkatkan ke brankas tim dengan menambahkan pengguna brankas tambahan atau menambahkan konfigurasi akses berbasis grup.\nvaultTypeContentTeam=Saat ini Anda menggunakan brankas tim, yang memungkinkan beberapa pengguna memiliki akses aman ke brankas bersama. Anda dapat mengonfigurasi koneksi dan identitas untuk dibagikan kepada semua pengguna atau hanya tersedia untuk pengguna pribadi atau grup dengan mengenkripsinya dengan kunci pribadi atau grup. Pengguna vault lainnya tidak dapat mengakses koneksi dan identitas berbasis pribadi dan grup Anda jika mereka tidak memiliki akses ke kunci tersebut.\ngroupManagement=Manajemen grup\ngroupManagementEmpty=Manajemen grup\ngroupManagementDescription=Mengelola grup vault yang sudah ada atau membuat yang baru. Setiap grup vault memiliki kunci rahasianya masing-masing yang digunakan untuk mengenkripsi koneksi dan identitas yang seharusnya hanya tersedia untuk grup tersebut dan tidak untuk orang lain.\ngroupManagementEmptyDescription=Mengelola grup vault yang sudah ada atau membuat yang baru. Setiap grup vault memiliki kunci rahasianya masing-masing yang digunakan untuk mengenkripsi koneksi dan identitas yang seharusnya hanya tersedia untuk grup tersebut dan tidak untuk orang lain.\\n\\nAkun berbasis grup untuk sebuah tim didukung dalam paket profesional.\nuserManagement=Manajemen pengguna\nuserManagementEmpty=Manajemen pengguna\nuserManagementDescription=Mengelola pengguna brankas yang sudah ada atau membuat yang baru. Setiap pengguna brankas memiliki kata sandi tersendiri yang digunakan untuk mengenkripsi koneksi dan identitas yang hanya boleh diketahui oleh pengguna tersebut dan tidak boleh diketahui oleh orang lain.\nuserManagementEmptyDescription=Mengelola pengguna brankas yang sudah ada atau membuat yang baru. Setiap pengguna brankas memiliki kata sandi tersendiri yang digunakan untuk mengenkripsi koneksi dan identitas yang seharusnya hanya dapat diakses oleh pengguna tersebut dan tidak dapat diakses oleh orang lain. Buat pengguna untuk diri Anda sendiri untuk dapat mengenkripsi koneksi dan identitas dengan kunci pribadi Anda.\\n\\nSatu akun pengguna didukung dalam edisi komunitas. Beberapa akun pengguna untuk sebuah tim didukung dalam paket profesional.\nuserIntroHeader=Manajemen pengguna\nuserIntroContent=Buat akun pengguna pertama untuk diri Anda sendiri untuk memulai. Hal ini memungkinkan Anda untuk mengunci ruang kerja ini dengan kata sandi.\naddReusableIdentity=Menambahkan identitas yang dapat digunakan kembali\nusers=Pengguna\nsyncVault=Sinkronisasi brankas\nsyncVaultDescription=Untuk menyinkronkan brankas Anda dengan beberapa sistem atau dengan beberapa anggota tim, aktifkan sinkronisasi git untuk brankas ini.\nenableGitSync=Mengaktifkan sinkronisasi git\nbrowseVault=Data brankas\nbrowseVaultDescription=Anda dapat melihat sendiri direktori vault di manajer file asli Anda. Perhatikan bahwa pengeditan eksternal tidak disarankan dan dapat menyebabkan berbagai masalah.\nbrowseVaultButton=Jelajahi brankas\nvaultUsers=Pengguna brankas\ncreateHeapDump=Membuat tempat pembuangan sampah\ncreateHeapDumpDescription=Membuang konten memori ke file untuk memecahkan masalah penggunaan memori\ninitializingApp=Memuat koneksi\ncheckingLicense=Memeriksa lisensi\nloadingGit=Menyinkronkan dengan repo git\nloadingGpg=Memulai daemon GnuPG untuk git\nloadingSettings=Memuat pengaturan\nloadingConnections=Memuat koneksi\nunlockingVault=Membuka kunci brankas\nloadingUserInterface=Memuat antarmuka pengguna\nptbNotice=Pemberitahuan untuk uji coba publik\nuserDeletionTitle=Penghapusan pengguna\nuserDeletionContent=Apakah Anda ingin menghapus pengguna brankas ini? Ini akan mengenkripsi ulang semua identitas pribadi dan rahasia koneksi Anda menggunakan kunci brankas yang tersedia untuk semua pengguna. Ini akan memakan waktu beberapa saat dan XPipe akan memulai ulang untuk menerapkan perubahan pengguna.\ngroupDeletionTitle=Penghapusan grup\ngroupDeletionContent=Apakah Anda ingin menghapus grup brankas ini? Ini akan mengenkripsi ulang semua identitas khusus grup dan rahasia koneksi menggunakan kunci brankas yang tersedia untuk semua pengguna. Ini akan memakan waktu beberapa saat dan XPipe akan memulai ulang untuk menerapkan perubahan grup.\nkillTransfer=Membunuh transfer\ndestination=Tujuan\nconfiguration=Konfigurasi\nnewFile=File baru\nnewLink=Tautan baru\nlinkName=Nama tautan\nscanConnections=Menemukan koneksi yang tersedia ...\nobserve=Mulai mengamati\nstopObserve=Berhenti mengamati\ncreateShortcut=Membuat pintasan desktop\nbrowseFiles=Menelusuri File\nclone=Klon\ntargetPath=Jalur target\nnewDirectory=Direktori baru\ncopyShareLink=Salin tautan\nselectStore=Pilih Simpan\nsaveSource=Simpan untuk nanti\nexecute=Menjalankan\ndeleteChildren=Hapus semua anak\nscriptGroupDescriptionDescription=Berikan deskripsi opsional pada grup ini\nabstractHostDescriptionDescription=Berikan deskripsi opsional pada host ini\nselectSource=Pilih Sumber\ncommandLineRead=Memperbarui\ncommandLineWrite=Menulis\nadditionalOptions=Opsi Tambahan\ninput=Masukan\nmachine=Mesin\nopen=Buka\nedit=Mengedit\nscriptContents=Isi naskah\nscriptContentsDescription=Perintah skrip untuk dijalankan\nsnippets=Ketergantungan skrip\nsnippetsDescription=Skrip lain yang harus dijalankan terlebih dahulu\nsnippetsDependenciesDescription=Semua skrip yang mungkin harus dijalankan jika ada\nisDefault=Jalankan saat init di semua shell yang kompatibel\nbringToShells=Membawa ke semua shell yang kompatibel\nisDefaultGroup=Menjalankan semua skrip grup pada shell init\nexecutionType=Jenis eksekusi\nexecutionTypeDescription=Dalam konteks apa untuk menggunakan skrip ini\nminimumShellDialect=Jenis shell\nminimumShellDialectDescription=Jenis shell untuk menjalankan skrip ini di\ndumbOnly=Bodoh\nterminalOnly=Terminal\nboth=Keduanya\nshouldElevate=Harus meninggikan\nshouldElevateDescription=Apakah akan menjalankan skrip ini dengan izin yang lebih tinggi\nscript.displayName=Skrip shell\nscript.displayDescription=Membuat skrip shell yang dapat digunakan kembali\nscriptGroup.displayName=Kelompok skrip\nscriptGroup.displayDescription=Mengelompokkan skrip bersama dan mengaturnya di dalam\nscriptGroup=Kelompok\nscriptGroupDescription=Grup yang akan ditugaskan skrip ini\nscriptGroupGroupDescription=Grup induk opsional untuk menetapkan grup skrip ini\nopenInNewTab=Buka di tab baru\nexecuteInBackground=di latar belakang\nexecuteInTerminal=dalam $TERM$\nback=Kembali\nbrowseInWindowsExplorer=Menjelajah di penjelajah Windows\nbrowseInDefaultFileManager=Menelusuri di pengelola file default\nbrowseInFinder=Menelusuri di pencari\ncopy=Menyalin\npaste=Tempel\ncopyLocation=Lokasi penyalinan\nabsolutePaths=Jalur absolut\nabsoluteLinkPaths=Jalur tautan absolut\nabsolutePathsQuoted=Jalur yang dikutip secara absolut\nfileNames=Nama file\nlinkFileNames=Menautkan nama file\nfileNamesQuoted=Nama file (Kutipan)\ndeleteFile=Menghapus $FILE$\neditWithEditor=Mengedit dengan $EDITOR$\nfollowLink=Ikuti tautan\ngoForward=Maju\nshowDetails=Tampilkan detail\nshowDetailsDescription=Menampilkan jejak tumpukan kesalahan\nopenFileWith=Buka dengan ...\nopenWithDefaultApplication=Membuka dengan aplikasi default\nrename=Mengganti nama\nrun=Jalankan\nopenInTerminal=Buka di terminal\nfile=File\ndirectory=Direktori\nsymbolicLink=Tautan simbolik\ndesktopEnvironment.displayName=Lingkungan desktop\ndesktopEnvironment.displayDescription=Membuat konfigurasi lingkungan desktop jarak jauh yang dapat digunakan kembali\ndesktopHost=Host desktop\ndesktopHostDescription=Sambungan desktop untuk digunakan sebagai basis\ndesktopShellDialect=Dialek cangkang\ndesktopShellDialectDescription=Dialek shell yang digunakan untuk menjalankan skrip dan aplikasi\ndesktopSnippets=Cuplikan naskah\ndesktopSnippetsDescription=Daftar cuplikan skrip yang dapat digunakan kembali untuk dijalankan terlebih dahulu\ndesktopInitScript=Skrip inisialisasi\ndesktopInitScriptDescription=Perintah init khusus untuk lingkungan ini\ndesktopTerminal=Aplikasi terminal\ndesktopTerminalDescription=Terminal yang digunakan pada desktop untuk memulai skrip di\ndesktopApplication.displayName=Aplikasi desktop\ndesktopApplication.displayDescription=Menjalankan aplikasi pada desktop jarak jauh\ndesktopBase=Desktop\ndesktopBaseDescription=Desktop untuk menjalankan aplikasi ini\ndesktopEnvironmentBase=Lingkungan desktop\ndesktopEnvironmentBaseDescription=Lingkungan desktop untuk menjalankan aplikasi ini\ndesktopApplicationPath=Jalur aplikasi\ndesktopApplicationPathDescription=Jalur eksekusi yang akan dijalankan\ndesktopApplicationArguments=Argumen\ndesktopApplicationArgumentsDescription=Argumen opsional yang akan diteruskan ke aplikasi\ndesktopCommand.displayName=Perintah desktop\ndesktopCommand.displayDescription=Menjalankan perintah di lingkungan desktop jarak jauh\ndesktopCommandScript=Perintah\ndesktopCommandScriptDescription=Perintah untuk dijalankan di lingkungan\nservice.displayName=Layanan\nservice.displayDescription=Meneruskan layanan jarak jauh ke mesin lokal Anda\nserviceLocalPort=Port lokal eksplisit\nserviceLocalPortDescription=Port lokal yang akan diteruskan, jika tidak, port acak yang digunakan\nserviceRemotePort=Port jarak jauh\nserviceRemotePortDescription=Port tempat layanan dijalankan\nserviceHost=Host layanan\nserviceHostDescription=Host tempat layanan dijalankan\nopenWebsite=Buka situs web\ncustomServiceGroup.displayName=Kelompok layanan\ncustomServiceGroup.displayDescription=Mengelompokkan beberapa layanan ke dalam satu kategori\ninitScript=Skrip init - Jalankan pada shell init\nshellScript=Skrip sesi shell - Membuat skrip tersedia untuk dijalankan selama sesi shell\nrunnableScript=Skrip yang dapat dijalankan - Memungkinkan skrip dijalankan langsung dari hub koneksi\nfileScript=Skrip file - Memungkinkan skrip dipanggil untuk file yang dipilih di peramban file\nrunScript=Menjalankan skrip\ncopyUrl=Salin URL\nfixedServiceGroup.displayName=Kelompok layanan\nfixedServiceGroup.displayDescription=Membuat daftar layanan yang tersedia pada sebuah sistem\nmappedService.displayName=Layanan\nmappedService.displayDescription=Berinteraksi dengan layanan yang diekspos oleh kontainer\ncustomService.displayName=Layanan\ncustomService.displayDescription=Secara otomatis membuka atau menyalurkan port layanan jarak jauh pada mesin lokal Anda\nfixedService.displayName=Layanan\nfixedService.displayDescription=Menggunakan layanan yang telah ditentukan sebelumnya\nnoServices=Tidak ada layanan yang tersedia\nhasServices=$COUNT$ layanan yang tersedia\nhasService=$COUNT$ layanan yang tersedia\nnoConnections=Tidak ada koneksi yang tersedia\nhasConnections=$COUNT$ koneksi yang tersedia\nhasConnection=$COUNT$ koneksi yang tersedia\nopenHttp=Membuka layanan HTTP\nopenHttps=Membuka layanan HTTPS\nnoScriptsAvailable=Tidak tersedia skrip yang diaktifkan dan kompatibel\nscriptsDisabled=Skrip dinonaktifkan\nchangeIcon=Ikon perubahan\ninit=Init\nshell=Shell\nhub=Hub\nscript=skrip\ngenericScript=Umum\ngradleTasks=Tugas-tugas gradle\nrunTask=Menjalankan tugas\narchiveName=Nama arsip\ncompress=Kompres\ncompressContents=Mengompres konten\nuntarHere=Untar di sini\nuntarDirectory=Untar ke $DIR$\nunzipDirectory=Buka ritsleting ke $DIR$\nunzipHere=Buka ritsleting di sini\nrequiresRestart=Memerlukan pengaktifan ulang untuk menerapkannya.\ndownload=Unduh\nservicePath=Jalur layanan\nservicePathDescription=Subpath opsional saat membuka URL di peramban\nactive=Aktif\ninactive=Tidak aktif\nstarting=Mulai\nremotePort=Port jarak jauh\nremotePortNumber=Port jarak jauh $PORT$\nuserIdentity=Identitas pribadi\nglobalIdentity=Identitas global\nidentityChoice=Identitas pengguna\nidentityChoiceDescription=Pilih identitas yang telah ditetapkan sebelumnya atau tentukan detail login hanya untuk sambungan ini\ndefineNewIdentityOrSelect=Masukkan yang baru atau pilih yang sudah ada\nlocalIdentity.displayName=Identitas lokal\nlocalIdentity.displayDescription=Membuat identitas yang dapat digunakan kembali untuk desktop lokal ini\nsyncedIdentity.displayName=Identitas yang disinkronkan\nsyncedIdentity.displayDescription=Membuat identitas yang dapat digunakan kembali yang disinkronkan di seluruh sistem\nlocalIdentity=Identitas lokal\nkeyNotSynced=File kunci belum disinkronkan ke repositori git. Gunakan tombol tambahkan ke git untuk menambahkan file kunci.\nusernameDescription=Nama pengguna untuk masuk sebagai\nidentity.displayName=Identitas\nidentity.displayDescription=Membuat identitas yang dapat digunakan kembali untuk koneksi\nlocal=Lokal\nshared=Global\nuserDescription=Nama pengguna atau identitas yang telah ditetapkan sebelumnya untuk masuk sebagai\nidentityAccessLevel=Tingkat akses\nidentityPerUser=Akses identitas pribadi\nidentityPerUserDescription=Membatasi akses ke identitas ini dan koneksi yang terkait hanya untuk pengguna brankas Anda\nidentityPerUserDisabled=Akses identitas pribadi (dinonaktifkan)\nidentityPerUserDisabledDescription=Membatasi akses ke identitas ini dan koneksi yang terkait hanya untuk pengguna brankas Anda (Membutuhkan tim untuk dikonfigurasi)\nidentityPerGroup=Akses identitas khusus grup\nidentityPerGroupDescription=Membatasi akses ke identitas ini dan koneksi terkait hanya untuk grup brankas ini\nlibrary=Perpustakaan\nlocation=Lokasi\nkeyAuthentication=Autentikasi berbasis kunci\nkeyAuthenticationDescription=Metode autentikasi yang digunakan jika autentikasi berbasis kunci diperlukan\nlocationDescription=Jalur file dari kunci pribadi Anda yang sesuai\nkeyFile=File kunci lokal\nkeyPassword=Frasa sandi\nkey=Kunci\nyubikeyPiv=Yubikey PIV\npageant=Kontes\ngpgAgent=Agen GPG\ncustomPkcs11Library=Perpustakaan PKCS # 11 khusus\nsshAgent=Agen OpenSSH\nnone=Tidak ada\nindex=Indeks ...\notherExternal=Agen eksternal lainnya\nsync=Sinkronisasi\nvaultSync=Sinkronisasi brankas\ncustomUsername=Nama pengguna\ncustomUsernameDescription=Pengguna alternatif opsional untuk masuk sebagai\ncustomUsernamePassword=Kata sandi\ncustomUsernamePasswordDescription=Kata sandi pengguna yang akan digunakan saat autentikasi sudo diperlukan\nshowInternalPods=Menampilkan pod internal\nshowAllNamespaces=Menampilkan semua ruang nama\nshowInternalContainers=Menampilkan wadah internal\nrefresh=Menyegarkan\nvmwareGui=Mulai GUI\nmonitorVm=Memantau VM\naddCluster=Menambahkan cluster ...\nshowNonRunningInstances=Menampilkan contoh yang tidak berjalan\nvmwareGuiDescription=Apakah akan memulai mesin virtual di latar belakang atau di jendela.\nvmwareEncryptionPassword=Kata sandi enkripsi\nvmwareEncryptionPasswordDescription=Kata sandi opsional yang digunakan untuk mengenkripsi VM.\nvmPasswordDescription=Kata sandi yang diperlukan untuk pengguna tamu.\nvmPassword=Kata sandi pengguna\nvmUser=Pengguna tamu\nrunTempContainer=Menjalankan wadah sementara\nvmUserDescription=Nama pengguna dari pengguna tamu utama Anda\ndockerTempRunAlertTitle=Menjalankan wadah sementara\ndockerTempRunAlertHeader=Ini akan menjalankan proses shell dalam wadah sementara yang akan dihapus secara otomatis setelah dihentikan.\nimageName=Nama gambar\nimageNameDescription=Pengidentifikasi gambar kontainer untuk digunakan\ncontainerName=Nama wadah\ncontainerNameDescription=Nama wadah khusus opsional\nvm=Mesin virtual\nvmDescription=File konfigurasi yang terkait.\nvmwareScan=Hypervisor desktop VMware\nvmwareMachine.displayName=Mesin Virtual VMware\nvmwareMachine.displayDescription=Menyambung ke mesin virtual melalui SSH\nvmwareInstallation.displayName=Instalasi hypervisor desktop VMware\nvmwareInstallation.displayDescription=Berinteraksi dengan VM yang terinstal melalui CLI-nya\nstart=Mulai\nstop=Berhenti\npause=Jeda\nrdpTunnelHost=Host target\nrdpTunnelHostDescription=Sambungan SSH untuk menyalurkan sambungan RDP ke\nrdpTunnelUsername=Nama pengguna\nrdpTunnelUsernameDescription=Pengguna khusus untuk masuk sebagai, menggunakan pengguna SSH jika dibiarkan kosong\nrdpFileLocation=Lokasi file\nrdpFileLocationDescription=Jalur file dari file .rdp\nrdpPasswordAuthentication=Otentikasi kata sandi\nrdpFiles=File RDP\nrdpPasswordAuthenticationDescription=Kata sandi untuk diisi atau disalin ke papan klip, tergantung pada dukungan klien\nrdpFile.displayName=File RDP\nrdpFile.displayDescription=Menghubungkan ke sistem melalui file .rdp yang sudah ada\nrequiredSshServerAlertTitle=Menyiapkan server SSH\nrequiredSshServerAlertHeader=Tidak dapat menemukan server SSH yang terinstal di VM.\nrequiredSshServerAlertContent=Untuk menyambung ke VM, XPipe mencari server SSH yang sedang berjalan, namun tidak ada server SSH yang terdeteksi untuk VM tersebut.\ncomputerName=Nama Komputer\npssComputerNameDescription=Nama komputer yang akan disambungkan\ncredentialUser=Pengguna Kredensial\ncredentialUserDescription=Pengguna yang akan masuk sebagai.\ncredentialPassword=Kata Sandi Kredensial\ncredentialPasswordDescription=Kata sandi pengguna.\nsshConfig=File konfigurasi SSH\nautostart=Tersambung secara otomatis saat pengaktifan XPipe\nacceptHostKey=Menerima kunci host\nmodifyHostKeyPermissions=Memodifikasi izin kunci host\nattachContainer=Melampirkan\ncontainerLogs=Menampilkan log\nopenSftpClient=Buka di klien SFTP eksternal\nopenTermius=Buka di Termius\nshowInternalInstances=Menampilkan contoh internal\neditPod=Edit pod\nacceptHostKeyDescription=Percayai kunci host baru dan lanjutkan\nmodifyHostKeyPermissionsDescription=Mencoba menghapus izin dari berkas asli agar OpenSSH senang\npsSession.displayName=Sesi Jarak Jauh PowerShell\npsSession.displayDescription=Menghubungkan melalui New-PSSession dan Enter-PSSession\nsshLocalTunnel.displayName=Terowongan SSH lokal\nsshLocalTunnel.displayDescription=Membuat terowongan SSH ke host jarak jauh\nsshRemoteTunnel.displayName=Terowongan SSH jarak jauh\nsshRemoteTunnel.displayDescription=Membuat terowongan SSH terbalik dari host jarak jauh\nsshDynamicTunnel.displayName=Terowongan SSH dinamis\nsshDynamicTunnel.displayDescription=Membuat proxy SOCKS melalui koneksi SSH\nshellEnvironmentGroup.displayName=Lingkungan shell\nshellEnvironmentGroup.displayDescription=Lingkungan shell\nshellEnvironment.displayName=Lingkungan shell\nshellEnvironment.displayDescription=Membuat lingkungan startup shell yang disesuaikan\nshellEnvironment.informationFormat=$TYPE$ lingkungan\nshellEnvironment.elevatedInformationFormat=$ELEVATION$ $TYPE$ lingkungan\nenvironmentConnectionDescription=Sambungan dasar untuk menciptakan lingkungan untuk\nenvironmentScriptDescription=Skrip init kustom opsional untuk dijalankan di shell\nenvironmentSnippets=Skrip shell\ncommandSnippetsDescription=Skrip shell opsional yang telah ditentukan sebelumnya untuk dijalankan terlebih dahulu\nenvironmentSnippetsDescription=Skrip shell opsional yang telah ditentukan sebelumnya untuk dijalankan saat inisialisasi\nshellTypeDescription=Jenis cangkang eksplisit untuk diluncurkan\noriginPort=Port asal\noriginAddress=Alamat asal\nremoteAddress=Alamat jarak jauh\nremoteSourceAddress=Alamat sumber jarak jauh\nremoteSourcePort=Port sumber jarak jauh\noriginDestinationPort=Port tujuan asal\noriginDestinationAddress=Alamat tujuan asal\norigin=Asal\nremoteHost=Host jarak jauh\naddress=Alamat\nproxmox.displayName=Proxmox\nproxmox.displayDescription=Menghubungkan ke sistem dalam Lingkungan Virtual Proxmox\nproxmoxVm.displayName=Proxmox VM\nproxmoxVm.displayDescription=Menghubungkan ke mesin virtual di Proxmox VE melalui SSH\nproxmoxContainer.displayName=Wadah Proxmox\nproxmoxContainer.displayDescription=Menghubungkan ke kontainer dalam Proxmox VE\nsshDynamicTunnel.hostDescription=Sistem yang akan digunakan sebagai proksi SOCKS\nsshDynamicTunnel.bindingDescription=Alamat apa yang akan digunakan untuk mengikat terowongan\nsshRemoteTunnel.hostDescription=Sistem untuk memulai terowongan jarak jauh ke titik asal\nsshRemoteTunnel.bindingDescription=Alamat apa yang akan digunakan untuk mengikat terowongan\nsshLocalTunnel.hostDescription=Sistem untuk membuka terowongan ke\nsshLocalTunnel.bindingDescription=Alamat apa yang akan digunakan untuk mengikat terowongan\nsshLocalTunnel.localAddressDescription=Alamat lokal yang akan diikat\nsshLocalTunnel.remoteAddressDescription=Alamat jarak jauh yang akan diikat\ncmd.displayName=Perintah\ncmd.displayDescription=Menjalankan perintah arbitrer pada sistem\nk8sPod.displayName=Kubernetes Pod\nk8sPod.displayDescription=Terhubung ke pod dan kontainernya melalui kubectl\nk8sContainer.displayName=Wadah Kubernetes\nk8sContainer.displayDescription=Membuka cangkang ke wadah\nk8sCluster.displayName=Kubernetes Cluster\nk8sCluster.displayDescription=Menghubungkan ke klaster dan podnya melalui kubectl\nsshTunnelGroup.displayName=Terowongan SSH\nsshTunnelGroup.displayCategory=Semua jenis terowongan SSH\nlocal.displayName=Mesin lokal\nlocal.displayDescription=Cangkang mesin lokal\ncygwin=Cygwin\nmsys2=MSYS2\ngitWindows=Git Untuk Windows\ngitForWindows.displayName=Git Untuk Windows\ngitForWindows.displayDescription=Mengakses lingkungan Git Untuk Windows lokal Anda\nmsys2.displayName=MSYS2\nmsys2.displayDescription=Mengakses cangkang lingkungan MSYS2 Anda\ncygwin.displayName=Cygwin\ncygwin.displayDescription=Mengakses cangkang lingkungan Cygwin Anda\nnamespace=Ruang nama\ngitVaultIdentityStrategy=Identitas Git SSH\ngitVaultIdentityStrategyDescription=Jika Anda memilih untuk menggunakan URL git SSH sebagai remote dan repositori jarak jauh Anda memerlukan identitas SSH, atur opsi ini.\\n\\nJika Anda memberikan url HTTP, Anda dapat mengabaikan opsi ini.\ndockerContainers=Wadah Docker\ndockerCmd.displayName=klien CLI docker\ndockerCmd.displayDescription=Mengakses kontainer Docker melalui klien CLI docker\nwslCmd.displayName=Instalasi WSL\nwslCmd.displayDescription=Mengakses instance WSL melalui klien CLI wsl\nk8sCmd.displayName=klien kubectl\nk8sCmd.displayDescription=Mengakses cluster Kubernetes melalui kubectl\nk8sClusters=Kluster Kubernetes\nshells=Cangkang yang tersedia\ninspectContainer=Memeriksa\ninspectContext=Memeriksa\nk8sClusterNameDescription=Nama konteks tempat cluster berada.\npod=Pod\npodName=Nama pod\nk8sClusterContext=Konteks\nk8sClusterContextDescription=Nama konteks tempat cluster berada\nk8sClusterNamespace=Ruang nama\nk8sClusterNamespaceDescription=Ruang nama khusus atau ruang nama default jika kosong\nk8sConfigLocation=File konfigurasi\nk8sConfigLocationDescription=File kubeconfig khusus atau file default jika dibiarkan kosong\ninspectPod=Memeriksa\nshowAllContainers=Menampilkan kontainer yang tidak berjalan\nshowAllPods=Menampilkan pod yang tidak berjalan\nk8sPodHostDescription=Host tempat pod berada\nk8sContainerDescription=Nama kontainer Kubernetes\nk8sPodDescription=Nama pod Kubernetes\npodDescription=Pod tempat wadah berada\nk8sClusterHostDescription=Host tempat cluster diakses. Harus memiliki kubectl yang terinstal dan dikonfigurasi untuk dapat mengakses cluster.\nconnection=Koneksi\nshellCommand.displayName=Perintah shell khusus\nshellCommand.displayDescription=Membuka shell standar melalui perintah khusus\nssh.displayName=Koneksi SSH\nssh.displayDescription=Menyambung ke sistem jarak jauh melalui klien baris perintah SSH\nsshConfig.displayName=File konfigurasi SSH\nsshConfig.displayDescription=Menyambung ke host yang ditentukan dalam file konfigurasi SSH\nsshConfigHost.displayName=Host file konfigurasi SSH\nsshConfigHost.displayDescription=Menyambung ke host yang ditentukan dalam file konfigurasi SSH\nsshConfigHost.password=Kata sandi\nsshConfigHost.passwordDescription=Berikan kata sandi opsional untuk login pengguna.\nsshConfigHost.identityPassphrase=Kata sandi kunci\nsshConfigHost.identityPassphraseDescription=Berikan frasa sandi opsional untuk kunci Anda.\nshellCommand.hostDescription=Host untuk menjalankan perintah pada\nshellCommand.commandDescription=Perintah yang akan membuka shell\ncommandType=Jenis perintah\ncommandTypeDescription=Cara menjalankan perintah\ncommandDescription=Perintah khusus untuk dijalankan pada host\ncommandHostDescription=Host untuk menjalankan perintah\ncommandDataFlowDescription=Bagaimana perintah ini menangani input dan output\ncommandElevationDescription=Jalankan perintah ini dengan izin yang lebih tinggi\ncommandShellTypeDescription=Cangkang yang digunakan untuk perintah ini\nlimitedSystem=Ini adalah sistem terbatas atau tertanam\nlimitedSystemDescription=Jangan mencoba mengidentifikasi jenis shell, yang diperlukan untuk sistem tertanam terbatas atau perangkat IOT\nsshForwardX11=Teruskan X11\nsshForwardX11Description=Mengaktifkan penerusan X11 untuk koneksi\ncustomAgent=Agen khusus\nidentityAgent=Agen identitas\nssh.proxyDescription=Host proxy opsional untuk digunakan saat membuat sambungan SSH. Harus memiliki klien ssh yang terinstal.\nusage=Penggunaan\nwslHostDescription=Host tempat instance WSL berada. Harus memiliki wsl yang terinstal.\nwslDistributionDescription=Nama instance WSL\nwslUsernameDescription=Nama pengguna eksplisit untuk masuk sebagai. Jika tidak ditentukan, nama pengguna default akan digunakan.\nwslPasswordDescription=Kata sandi pengguna yang dapat digunakan untuk perintah sudo.\ndockerHostDescription=Host tempat kontainer docker berada. Harus memiliki docker yang terinstal.\ndockerContainerDescription=Nama wadah docker\nlocalMachine=Mesin Lokal\nrootScan=Lingkungan cangkang Sudo\nloginEnvironmentScan=Lingkungan masuk khusus\nk8sScan=Kluster Kubernetes\noptions=Pilihan\ndockerRunningScan=Menjalankan kontainer docker\ndockerAllScan=Semua kontainer docker\nwslScan=Contoh WSL\nsshScan=Koneksi konfigurasi SSH\nrunAsUser=Jalankan sebagai pengguna\nrunAsUserDescription=Mulai lingkungan shell ini sebagai pengguna yang berbeda\ndefault=Default\nadministrator=Administrator\nwslHost=Host WSL\ntimeout=Batas waktu\ninstallLocation=Lokasi pemasangan\ninstallLocationDescription=Lokasi di mana lingkungan $NAME$ Anda diinstal\nwsl.displayName=Subsistem Windows untuk Linux\nwsl.displayDescription=Menghubungkan ke instance WSL yang berjalan di Windows\ndocker.displayName=Wadah Docker\ndocker.displayDescription=Menghubungkan ke kontainer docker\nport=Port\nuser=Pengguna\npassword=Kata sandi\nmethod=Metode\nuri=URL\nproxy=Proxy\ndistribution=Distribusi\nusername=Nama pengguna\nshellType=Jenis cangkang\nbrowseFile=Menelusuri file\nopenShell=Buka shell di terminal\nopenCommand=Menjalankan perintah di terminal\neditFile=Mengedit file\ndescription=Deskripsi\nfurtherCustomization=Kustomisasi lebih lanjut\nfurtherCustomizationDescription=Untuk opsi konfigurasi lainnya, gunakan file konfigurasi ssh\nbrowse=Menjelajah\nconfigHost=Tuan rumah\nconfigHostDescription=Host tempat konfigurasi berada\nconfigLocation=Lokasi konfigurasi\nconfigLocationDescription=Jalur file dari file konfigurasi\ngateway=Gerbang\ngatewayDescription=Gateway opsional untuk digunakan saat menyambung\nconnectionInformation=Informasi koneksi\nconnectionInformationDescription=Sistem mana yang akan disambungkan\npasswordAuthentication=Otentikasi kata sandi\npasswordAuthenticationDescription=Kata sandi opsional yang digunakan untuk mengautentikasi\nsshConfigString.displayName=Koneksi SSH berbasis konfigurasi\nsshConfigString.displayDescription=Membuat sambungan SSH yang sepenuhnya disesuaikan dalam format konfigurasi SSH\nsshConfigStringContent=Konfigurasi\nsshConfigStringContentDescription=Opsi SSH untuk koneksi dalam format konfigurasi OpenSSH\nvnc.displayName=Koneksi VNC melalui SSH\nvnc.displayDescription=Membuka sesi VNC melalui sambungan terowongan\nbinding=Mengikat\nvncPortDescription=Port yang didengarkan oleh server VNC\nrdpPortDescription=Port yang didengarkan oleh server RDP\nvncUsername=Nama pengguna\nvncUsernameDescription=Nama pengguna VNC opsional\nvncPassword=Kata sandi\nvncPasswordDescription=Kata sandi VNC\nx11WslInstance=Instance X11 Forward WSL\nx11WslInstanceDescription=Distribusi Windows Subsystem for Linux lokal untuk digunakan sebagai server X11 ketika menggunakan penerusan X11 dalam koneksi SSH. Distribusi ini harus merupakan distribusi WSL2.\nopenAsRoot=Buka sebagai root\nopenInWSL=Buka di WSL\nlaunch=Meluncurkan\nsshTrustKeyContent=Kunci host tidak diketahui, dan Anda telah mengaktifkan verifikasi kunci host secara manual. $CONTENT$\nsshTrustKeyTitle=Kunci host yang tidak dikenal\nrdpTunnel.displayName=Koneksi RDP melalui SSH\nrdpTunnel.displayDescription=Terhubung melalui RDP melalui koneksi terowongan\nrdpEnableDesktopIntegration=Mengaktifkan integrasi desktop\nrdpEnableDesktopIntegrationDescription=Jalankan aplikasi jarak jauh dengan asumsi bahwa daftar izin RDP mengizinkan\nrdpSetupAdminTitle=Diperlukan penyiapan RDP\nrdpSetupAllowTitle=Aplikasi jarak jauh RDP\nrdpSetupAllowContent=Memulai aplikasi jarak jauh secara langsung saat ini tidak diperbolehkan pada sistem ini. Apakah Anda ingin mengaktifkannya? Ini akan memungkinkan Anda untuk menjalankan aplikasi jarak jauh secara langsung dari XPipe dengan menonaktifkan daftar izin untuk aplikasi jarak jauh RDP.\nrdpServerEnableTitle=Server RDP\nrdpServerEnableContent=Server RDP dinonaktifkan pada sistem target. Apakah Anda ingin mengaktifkannya di registri untuk mengizinkan koneksi RDP jarak jauh?\nrdp=RDP\nrdpScan=Terowongan RDP melalui SSH\nwslX11SetupTitle=Pengaturan WSL X11\nwslX11SetupContent=XPipe dapat menggunakan distribusi WSL lokal Anda untuk bertindak sebagai server tampilan X11. Apakah Anda ingin menyiapkan X11 di $DIST$? Ini akan menginstal paket-paket dasar X11 pada distribusi WSL dan mungkin memerlukan waktu beberapa saat. Anda juga dapat mengubah distribusi mana yang digunakan pada menu pengaturan.\ncommand=Perintah\ncommandGroup=Grup perintah\nvncSystem=Sistem target VNC\nvncSystemDescription=Sistem yang sebenarnya untuk berinteraksi. Ini biasanya sama dengan host terowongan\nvncHost=Host VNC target\nvncHostDescription=Sistem tempat server VNC berjalan\nvncDirectHost=Tuan rumah\nvncDirectHostDescription=Entri host atau alamat manual server tempat server VNC berjalan\nrdpDirectHost=Tuan rumah\nrdpDirectHostDescription=Entri host atau alamat manual server tempat server RDP berjalan\ngitVaultTitle=Brankas git\ngitVaultForcePushContent=Apakah Anda ingin mendorong secara paksa ke repositori jarak jauh? Ini akan sepenuhnya menggantikan semua konten repositori jarak jauh dengan repositori lokal Anda, termasuk riwayatnya.\ngitVaultOverwriteLocalContent=Apakah Anda ingin menimpa perubahan brankas lokal Anda? Ini akan menerapkan semua perubahan jarak jauh ke repositori lokal Anda.\nrdpSimple.displayName=Koneksi RDP langsung\nrdpSimple.displayDescription=Menyambung ke host melalui RDP\nrdpUsername=Nama pengguna\nrdpUsernameDescription=Pengguna yang akan masuk sebagai. Dapat menyertakan awalan domain\naddressDescription=Tempat untuk menyambung ke\nrdpAdditionalOptions=Opsi RDP tambahan\nrdpAdditionalOptionsDescription=Opsi RDP mentah untuk disertakan, diformat sama seperti pada file .rdp\nproxmoxVncConfirmTitle=Akses VNC\nproxmoxVncConfirmContent=Apakah Anda ingin mengaktifkan akses VNC untuk VM? Ini akan mengaktifkan akses klien VNC langsung di file konfigurasi VM dan memulai ulang mesin virtual.\ndockerContext.displayName=Konteks Docker\ndockerContext.displayDescription=Berinteraksi dengan kontainer yang berada dalam konteks tertentu\nvmActions=Tindakan VM\ndockerContextActions=Tindakan konteks\nk8sPodActions=Tindakan pod\nopenVnc=Mengaktifkan akses VNC\naddVnc=Menambahkan koneksi VNC\ncommandGroup.displayName=Grup perintah\ncommandGroup.displayDescription=Mengelompokkan perintah yang tersedia untuk suatu sistem\nserial.displayName=Koneksi serial\nserial.displayDescription=Membuka sambungan serial di terminal\nserialPort=Port serial\nserialPortDescription=Port serial/perangkat yang akan disambungkan\nbaudRate=Baud rate\ndataBits=Bit data\nstopBits=Hentikan bit\nparity=Paritas\nflowControlWindow=Kontrol aliran\nserialImplementation=Implementasi serial\nserialImplementationDescription=Alat yang digunakan untuk menyambung ke port serial\nserialHost=Tuan rumah\nserialHostDescription=Sistem untuk mengakses port serial pada\nserialPortConfiguration=Konfigurasi port serial\nserialPortConfigurationDescription=Parameter konfigurasi perangkat serial yang tersambung\nserialInformation=Informasi serial\nopenXShell=Buka di XShell\ntsh.displayName=Teleportasi\ntsh.displayDescription=Menghubungkan ke node teleportasi Anda melalui tsh\ntshNode.displayName=Simpul teleportasi\ntshNode.displayDescription=Menghubungkan ke simpul teleportasi dalam sebuah cluster\nteleportCluster=Cluster\nteleportClusterDescription=Cluster tempat node berada\nteleportProxy=Proxy\nteleportProxyDescription=Server proxy yang digunakan untuk menyambung ke node\nteleportHost=Tuan rumah\nteleportHostDescription=Nama host dari simpul\nteleportUser=Pengguna\nteleportUserDescription=Pengguna untuk masuk sebagai\nlogin=Login\nhyperVInstall.displayName=Hyper-V\nhyperVInstall.displayDescription=Menyambungkan ke VM yang dikelola oleh Hyper-V\nhyperVVm.displayName=Hyper-V VM\nhyperVVm.displayDescription=Menyambungkan ke VM Hyper-V melalui SSH atau PSSession\ntrustHost=Tuan rumah kepercayaan\ntrustHostDescription=Menambahkan Nama Komputer ke daftar host tepercaya\ncopyIp=Salin IP\nvncDirect.displayName=Koneksi VNC langsung\nvncDirect.displayDescription=Menyambung ke sistem melalui VNC secara langsung\neditConfiguration=Mengedit konfigurasi\nviewInDashboard=Melihat di dasbor\nsetDefault=Menetapkan default\nremoveDefault=Menghapus default\nconnectAsOtherUser=Terhubung sebagai pengguna lain\nprovideUsername=Memberikan nama pengguna alternatif untuk masuk dengan\nvmIdentity=Identitas tamu\nvmIdentityDescription=Metode autentikasi identitas SSH yang digunakan untuk menyambung jika diperlukan\nvmPort=Port\nvmPortDescription=Port yang akan disambungkan melalui SSH\nforwardAgent=Agen penerus\nforwardAgentDescription=Membuat identitas agen SSH tersedia di sistem jarak jauh\nvirshUri=URI\nvirshUriDescription=URI hypervisor, alias juga didukung\nvirshDomain.displayName=domain libvirt\nvirshDomain.displayDescription=Menghubungkan ke domain libvirt\nvirshHypervisor.displayName=hypervisor libvirt\nvirshHypervisor.displayDescription=Menyambungkan ke driver hypervisor yang didukung libvirt\nvirshInstall.displayName=klien baris perintah libvirt\nvirshInstall.displayDescription=Hubungkan ke semua hypervisor libvirt yang tersedia melalui virsh\naddHypervisor=Menambahkan hypervisor\ninteractiveTerminal=Terminal interaktif\neditDomain=Mengedit domain\nlibvirt=domain libvirt\ncustomIp=IP khusus\ncustomIpDescription=Mengesampingkan deteksi IP VM lokal default jika Anda menggunakan jaringan tingkat lanjut\nautomaticallyDetect=Mendeteksi secara otomatis\nuserAddDialogTitle=Pembuatan pengguna\ngroupAddDialogTitle=Pembuatan grup\npassphrase=Frasa sandi\nrepeatPassphrase=Mengulangi frasa sandi\ngroupSecret=Rahasia kelompok\nrepeatGroupSecret=Mengulangi rahasia grup\nvaultGroup=Kelompok brankas\nloginAlertTitle=Diperlukan login\nloginAlertHeader=Membuka kunci brankas untuk mengakses koneksi pribadi Anda\nvaultUser=Pengguna brankas\nme=Saya\naddGroup=Menambahkan grup ...\naddGroupDescription=Membuat grup baru untuk brankas ini\naddUser=Menambahkan pengguna ...\naddUserDescription=Membuat pengguna baru untuk brankas ini\nskip=Lewati\nuserChangePasswordAlertTitle=Perubahan kata sandi\ngroupChangeSecretAlertTitle=Perubahan rahasia\ndocs=Dokumentasi\nlxd.displayName=Wadah LXD\nlxd.displayDescription=Terhubung ke kontainer LXD melalui lxc\nlxdCmd.displayName=Klien CLI LXD\nlxdCmd.displayDescription=Mengakses kontainer LXD melalui klien CLI lxc\npodman.displayName=Wadah Podman\npodman.displayDescription=Menghubungkan ke kontainer Podman\nincusInstall.displayName=Manajer mesin incus\nincusInstall.displayDescription=Mengakses kontainer incus melalui klien CLI incus\nincusContainer.displayName=Wadah inkus\nincusContainer.displayDescription=Menghubungkan ke wadah incus\npodmanCmd.displayName=Podman CLI\npodmanCmd.displayDescription=Mengakses kontainer Podman melalui klien CLI\nlxdHostDescription=Host tempat kontainer LXD berada. Harus memiliki lxc yang terinstal.\nlxdContainerDescription=Nama wadah LXD\npodmanContainers=Wadah podman\nlxdContainers=Wadah LXD\nincusContainers=Wadah inkus\ncontainer=Wadah\nhost=Tuan rumah\ncontainerActions=Tindakan kontainer\nserialConsole=Konsol serial\neditRunConfiguration=Mengedit konfigurasi yang dijalankan\ncommunityDescription=Alat bantu koneksi yang sempurna untuk kasus penggunaan pribadi Anda.\nupgradeDescription=Manajemen koneksi profesional untuk seluruh infrastruktur server Anda.\ndiscoverPlans=Temukan opsi peningkatan\nextendProfessional=Tingkatkan ke fitur profesional terbaru\ncommunityItem1=Sambungan tak terbatas ke sistem dan alat non-komersial\ncommunityItem2=Integrasi yang mulus dengan terminal dan editor yang terpasang\ncommunityItem3=Peramban file jarak jauh berfitur lengkap\ncommunityItem4=Sistem skrip yang kuat untuk semua shell\ncommunityItem5=Integrasi Git untuk sinkronisasi dan berbagi informasi koneksi\nupgradeItem1=Termasuk semua fitur edisi komunitas\nupgradeItem2=Paket Homelab mendukung hypervisor tak terbatas dan fitur SSH tingkat lanjut\nupgradeItem3=Paket Professional juga mendukung sistem operasi dan alat bantu perusahaan\nupgradeItem4=Paket Enterprise hadir dengan fleksibilitas penuh untuk kasus penggunaan individual Anda\nupgrade=Tingkatkan\nupgradeTitle=Paket yang tersedia\nstatus=Status\ntype=Ketik\nlicenseAlertTitle=Diperlukan lisensi\nuseCommunity=Lanjutkan dengan komunitas\npreviewDescription=Mencoba fitur baru selama beberapa minggu setelah rilis.\ntryPreview=Mengaktifkan pratinjau\npreviewItem1=Akses penuh ke fitur profesional yang baru dirilis selama 2 minggu setelah rilis\npreviewItem2=Mencoba fitur baru tanpa komitmen apa pun\nlicensedTo=Dilisensikan kepada\nemail=Alamat email\napply=Menerapkan\nclear=Hapus\nactivate=Mengaktifkan\nvalidUntil=Berlaku hingga\nlicenseActivated=Lisensi diaktifkan\nrestart=Mulai ulang\nlockVault=Kunci brankas\nrestartApp=Mulai ulang XPipe\nfree=Gratis\nupgradeInfo=Anda dapat menemukan informasi tentang peningkatan lisensi di bawah ini.\nupgradeInfoPreview=Anda dapat menemukan informasi tentang peningkatan lisensi di bawah ini atau mencoba pratinjau.\nenterLicenseKey=Masukkan kunci lisensi untuk meningkatkan\nisOnlySupported=hanya didukung dengan setidaknya lisensi $TYPE$\nareOnlySupported=hanya didukung dengan setidaknya lisensi $TYPE$\nlegacyLicense=Lisensi ini hanya menyertakan fitur Profesional baru yang dirilis dalam waktu satu tahun setelah pembelian.\npreviewExpiredLicense=Fitur ini baru-baru ini tersedia secara gratis dalam pratinjau, tetapi periode ini sekarang sudah berakhir.\nopenApiDocs=Dokumentasi API\nopenApiDocsDescription=Dokumentasi API HTTP tersedia secara online, termasuk spesifikasi OpenAPI .yaml. Anda dapat membukanya di browser web atau klien HTTP pilihan Anda.\nopenApiDocsButton=Buka dokumen\npythonApi=API Python\npersonalConnection=Sambungan ini dan semua anak sambungannya hanya tersedia untuk pengguna Anda karena bergantung pada identitas pribadi.\ndeveloperPrintInitFiles=Mencetak eksekusi file init\ndeveloperPrintInitFilesDescription=Mencetak semua skrip init shell yang dijalankan saat terminal diluncurkan.\ndeveloperShowSensitiveCommands=Mencatat perintah yang sensitif\ndeveloperShowSensitiveCommandsDescription=Menyertakan perintah sensitif dalam keluaran log untuk debugging.\ncheckingForUpdates=Memeriksa pembaruan\ncheckingForUpdatesDescription=Mengambil informasi rilis terbaru\ndownloadingUpdate=Mengambil rilis (Versi $VERSION$)\ndownloadingUpdateDescription=Mengunduh paket rilis\nupdateNag=Anda belum memperbarui XPipe dalam beberapa waktu. Anda mungkin melewatkan fitur-fitur baru dan perbaikan dari rilis yang lebih baru.\nupdateNagTitle=Pengingat pembaruan\nupdateNagButton=Lihat rilis\nrefreshServices=Menyegarkan layanan\nserviceProtocolType=Jenis protokol layanan\nserviceProtocolTypeDescription=Mengontrol cara membuka layanan\nserviceCommand=Perintah untuk dijalankan setelah layanan aktif\nserviceCommandDescription=Penampung $PORT akan diganti dengan port lokal yang sebenarnya\nvalue=Nilai\nshowAdvancedOptions=Menampilkan opsi lanjutan\nsshAdditionalConfigOptions=Opsi konfigurasi tambahan\nremoteFileManager=Manajer file jarak jauh\nclearUserData=Menghapus data pengguna\nclearUserDataDescription=Menghapus semua data konfigurasi pengguna, termasuk koneksi\nclearUserDataTitle=Penghapusan data pengguna\nclearUserDataContent=Ini akan menghapus semua data pengguna lokal untuk xpipe dan memulai ulang. Jika Anda peduli dengan koneksi Anda, pastikan untuk menyinkronkannya terlebih dahulu dengan repositori git.\nundefined=Tidak terdefinisi\ncopyAddress=Alamat salinan\nnetbirdDeviceScan=Koneksi Netbird\nnetbirdId=Kunci publik rekan\nnetbirdIdDescription=Id kunci publik netbird internal dari peer\ntailscaleDeviceScan=Koneksi skala ekor\ntailscaleInstall.displayName=Instalasi skala ekor\ntailscaleInstall.displayDescription=Hubungkan ke perangkat di tailnet Anda melalui SSH\ntailscaleDevice.displayName=Perangkat skala ekor\ntailscaleDevice.displayDescription=Hubungkan ke perangkat di tailnet Anda melalui SSH\ntailscaleId=ID Perangkat\ntailscaleIdDescription=ID perangkat skala ekor internal\ntailscaleHostName=Nama host\ntailscaleHostNameDescription=Nama host perangkat di tailnet\ntailscaleUsername=Nama pengguna\ntailscaleUsernameDescription=Pengguna untuk masuk sebagai\ntailscalePassword=Kata sandi\ntailscalePasswordDescription=Kata sandi pengguna opsional yang dapat digunakan untuk sudo\nscriptName=Nama skrip\nscriptNameDescription=Berikan nama khusus pada skrip ini\nscriptGroupName=Nama grup skrip\nscriptGroupNameDescription=Berikan nama khusus pada grup skrip ini\nidentityName=Nama identitas\nidentityNameDescription=Berikan identitas ini nama khusus\ntailscaleTailnet.displayName=Tailnet\ntailscaleTailnet.displayDescription=Terhubung ke tailnet tertentu dengan akun Anda\nputtyConnections=Koneksi PuTTY\nkittyConnections=Koneksi KiTTY\nicons=Ikon\ncustomIcons=Ikon khusus\niconSources=Sumber ikon\niconSourcesDescription=Anda dapat menambahkan sumber ikon Anda sendiri di sini. XPipe akan mengambil file .svg di lokasi yang ditambahkan dan menambahkannya ke kumpulan ikon yang tersedia.\\n\\nBaik direktori lokal maupun repositori git jarak jauh atau didukung sebagai lokasi ikon.\nrefreshSources=Menyegarkan ikon\nrefreshSourcesDescription=Memperbarui semua ikon dari sumber yang tersedia\naddDirectoryIconSource=Menambahkan sumber direktori ...\naddDirectoryIconSourceDescription=Menambahkan ikon dari direktori lokal\naddGitIconSource=Menambahkan sumber git ...\naddGitIconSourceDescription=Menambahkan ikon yang terletak di repositori git jarak jauh\nrepositoryUrl=URL Repositori Git\niconDirectory=Direktori ikon\naddUnsupportedKexMethod=Menambahkan metode pertukaran kunci yang tidak didukung\naddUnsupportedKexMethodDescription=Izinkan metode pertukaran kunci $VAL$ digunakan untuk koneksi ini\naddUnsupportedHostKeyType=Menambahkan jenis kunci host yang tidak didukung\naddUnsupportedHostKeyTypeDescription=Izinkan jenis kunci host $VAL$ digunakan untuk sambungan ini\naddUnsupportedMacType=Menambahkan jenis MAC yang tidak didukung\naddUnsupportedMacTypeDescription=Izinkan jenis MAC $VAL$ digunakan untuk koneksi ini\nrunSilent=diam-diam di latar belakang\nrunInFileBrowser=di peramban file\nrunInConnectionHub=di hub koneksi\ncommandOutput=Keluaran perintah\niconSourceDeletionTitle=Menghapus sumber ikon\niconSourceDeletionContent=Apakah Anda ingin menghapus sumber ikon ini dan semua ikon yang terkait dengannya?\nrefreshIcons=Menyegarkan ikon\nrefreshIconsDescription=Mengambil, merender, dan menyimpan semua 1000+ ikon yang tersedia dari sumber eksternal ke file .png. Ini mungkin memerlukan waktu beberapa saat...\nvaultUserLegacy=Pengguna lemari besi (Mode kompatibilitas lawas terbatas)\nupgradeInstructions=Petunjuk peningkatan\nexternalActionTitle=Permintaan tindakan eksternal\nexternalActionContent=Sebuah tindakan eksternal diminta. Apakah Anda ingin mengizinkan tindakan peluncuran dari luar XPipe?\nnoScriptStateAvailable=Menyegarkan untuk menentukan kompatibilitas skrip ...\ndocumentationDescription=Lihat dokumentasi\ncustomEditorCommandInTerminal=Menjalankan perintah khusus di terminal\ncustomEditorCommandInTerminalDescription=Jika editor Anda berbasis terminal, Anda dapat mengaktifkan opsi ini untuk secara otomatis membuka terminal dan menjalankan perintah dalam sesi terminal.\\n\\nAnda dapat menggunakan opsi ini untuk editor seperti vi, vim, nvim, dan lainnya.\ndisableHttpsTlsCheck=Menonaktifkan verifikasi sertifikat permintaan HTTPS\ndisableHttpsTlsCheckDescription=Jika organisasi Anda mendekripsi lalu lintas HTTPS di firewall menggunakan intersepsi SSL, pemeriksaan pembaruan atau pemeriksaan lisensi akan gagal karena sertifikat tidak cocok. Anda bisa memperbaikinya dengan mengaktifkan opsi ini dan menonaktifkan validasi sertifikat TLS.\nconnectionsSelected=$NUMBER$ koneksi yang dipilih\naddConnections=Menambahkan koneksi\nbrowseDirectory=Menelusuri direktori\nopenTerminal=Terminal terbuka\ndocumentation=Dokumentasi\nreport=Melaporkan kesalahan\nkeePassXcNotAssociated=Tautan KeePassXC\nkeePassXcNotAssociatedDescription=XPipe tidak terkait dengan basis data KeePassXC lokal anda. Klik di bawah ini untuk melakukan langkah satu kali mengaitkan XPipe dengan basis data KeePassXC agar XPipe dapat menanyakan kata sandi.\nkeePassXcAssociateMore=Menghubungkan lebih banyak basis data\nkeePassXcAssociateMoreDescription=Anda dapat tersambung ke beberapa basis data KeePassXC secara bersamaan\nkeePassXcAssociated=Tautan KeePassXC\nkeePassXcAssociatedDescription=XPipe tersambung ke basis data KeePassXC lokal berikut ini:\nkeePassXcNotAssociatedButton=Tautkan basis data\nidentifier=Pengenal\npasswordManagerCommand=Perintah khusus\npasswordManagerCommandDescription=Perintah khusus yang akan dijalankan untuk mengambil kata sandi. String penampung $KEY akan digantikan oleh kunci kata sandi yang dikutip saat dipanggil. Perintah ini akan memanggil CLI pengelola kata sandi Anda untuk mencetak kata sandi ke stdout, misalnya mypassmgr get $KEY.\nchooseTemplate=Memilih templat\nkeePassXcPlaceholder=URL entri KeePassXC\nterminalEnvironment=Lingkungan terminal\nterminalEnvironmentDescription=Jika Anda ingin menggunakan fitur lingkungan WSL berbasis Linux lokal untuk kustomisasi terminal, Anda dapat menggunakannya sebagai lingkungan terminal.\\n\\nPerintah init terminal khusus dan konfigurasi multiplexer terminal kemudian akan dijalankan dalam distribusi WSL ini.\nterminalInitScript=Skrip init terminal\nterminalInitScriptDescription=Perintah yang akan dijalankan di lingkungan terminal sebelum koneksi diluncurkan. Anda dapat menggunakan ini untuk mengonfigurasi lingkungan terminal saat pengaktifan.\nterminalMultiplexer=Multiplexer terminal\nterminalMultiplexerDescription=Multiplexer terminal untuk digunakan sebagai alternatif tab di terminal. Ini akan menggantikan karakteristik penanganan terminal tertentu, misalnya penanganan tab, dengan fungsi multiplekser.\\n\\nMembutuhkan eksekusi multiplexer yang bersangkutan untuk diinstal pada sistem.\nterminalMultiplexerWindowsDescription=Multiplexer terminal untuk digunakan sebagai alternatif tab di terminal. Ini akan menggantikan karakteristik penanganan terminal tertentu, misalnya penanganan tab, dengan fungsi multiplekser.\\n\\nMemerlukan penggunaan lingkungan terminal WSL pada Windows dan eksekusi multiplexer untuk diinstal pada sistem WSL.\nterminalAlwaysPauseOnExit=Selalu jeda saat keluar\nterminalAlwaysPauseOnExitDescription=Jika diaktifkan, keluar dari sesi terminal akan selalu meminta Anda untuk memulai ulang atau menutup sesi. Jika dinonaktifkan, XPipe hanya akan melakukan hal tersebut untuk koneksi yang gagal yang keluar dengan kesalahan.\nquerying=Mengajukan pertanyaan ...\nretrievedPassword=Diperoleh: $PASSWORD$\nrefreshOpenpubkey=Menyegarkan identitas openpubkey\nrefreshOpenpubkeyDescription=Jalankan penyegaran opkssh untuk membuat identitas openpubkey kembali valid\nall=Semua\nterminalPrompt=Perintah terminal\nterminalPromptDescription=Alat prompt terminal untuk digunakan di terminal jarak jauh. Mengaktifkan prompt terminal akan secara otomatis mengatur dan mengonfigurasi alat prompt pada sistem target saat membuka sesi terminal.\\n\\nHal ini tidak akan mengubah konfigurasi prompt atau file profil yang ada pada sistem. Hal ini akan meningkatkan waktu pemuatan terminal untuk pertama kalinya saat prompt sedang disiapkan pada sistem jarak jauh. Terminal Anda mungkin memerlukan font tambahan untuk menampilkan prompt dengan benar.\nterminalPromptConfiguration=Konfigurasi prompt terminal\nterminalPromptConfig=File konfigurasi\nterminalPromptConfigDescription=File konfigurasi khusus untuk diterapkan pada prompt. Konfigurasi ini akan secara otomatis diatur pada sistem target saat terminal diinisialisasi dan digunakan sebagai konfigurasi prompt default.\\n\\nJika Anda ingin menggunakan file konfigurasi default yang ada pada setiap sistem, Anda dapat mengosongkan bidang ini.\npasswordManagerKey=Kunci pengelola kata sandi\npasswordManagerKeyDescription=Pengidentifikasi pengelola kata sandi dari rahasia\npasswordManagerAgent=Agen pengelola kata sandi\ndockerComposeProject.displayName=Proyek penulisan Docker\ndockerComposeProject.displayDescription=Mengelompokkan wadah dari proyek penulisan bersama\nsshVerboseOutput=Mengaktifkan keluaran SSH yang bertele-tele\nsshVerboseOutputDescription=Ini akan mencetak banyak informasi debug saat menyambung melalui SSH. Berguna untuk memecahkan masalah pada koneksi SSH.\ndontUseGateway=Jangan gunakan gateway\ndontUseGatewayDescription=Jangan gunakan host hypervisor sebagai gateway dan menyambungkan langsung ke IP\ncategoryColor=Warna kategori\ncategoryColorDescription=Warna default yang digunakan untuk koneksi dalam kategori ini\ncategorySync=Sinkronisasi dengan repositori git\ncategorySyncDescription=Menyinkronkan semua koneksi secara otomatis dengan repositori git. Semua perubahan lokal pada koneksi akan didorong ke remote.\ncategorySyncSpecial=Sinkronisasi dengan repositori git\\n(Tidak dapat dikonfigurasi untuk kategori khusus \"$NAME$\")\ncategoryDontAllowScripts=Menonaktifkan semua modifikasi\ncategoryDontAllowScriptsDescription=Menonaktifkan eksekusi perintah dan operasi lain pada sistem dalam kategori ini untuk mencegah modifikasi. Ini akan menonaktifkan semua fungsionalitas skrip, perintah lingkungan shell, prompt, dan lainnya.\ncategoryConfirmAllModifications=Mengonfirmasi semua modifikasi\ncategoryConfirmAllModificationsDescription=Konfirmasikan terlebih dahulu segala jenis modifikasi untuk koneksi atau sistem file. Hal ini dapat mencegah operasi yang tidak disengaja pada sistem yang penting.\ncategoryDefaultIdentity=Identitas default\ncategoryDefaultIdentityDescription=Jika Anda sering menggunakan identitas tertentu pada banyak sistem dalam kategori ini, maka pengaturan identitas default akan memungkinkan Anda untuk memilihnya terlebih dahulu saat membuat sambungan baru.\ncategoryConfigTitle=$NAME$ konfigurasi\nconfigure=Mengkonfigurasi\naddConnection=Menambahkan koneksi\nnoCompatibleConnection=Tidak ditemukan koneksi yang kompatibel\nnoCompatibleIdentity=Tidak ditemukan identitas yang kompatibel\nnewCategory=Kategori baru\ndockerComposeRestricted=Proyek compose ini dibatasi oleh $NAME$ dan tidak dapat dimodifikasi secara eksternal. Silakan gunakan $NAME$ untuk mengelola proyek penulisan ini.\nrestricted=Dibatasi\ndisableSshPinCaching=Menonaktifkan penyimpanan PIN SSH\ndisableSshPinCachingDescription=XPipe akan secara otomatis menyimpan PIN yang dimasukkan sebagai kunci ketika menggunakan beberapa bentuk autentikasi berbasis perangkat keras.\\n\\nMenonaktifkan hal ini akan mengakibatkan Anda harus memasukkan kembali PIN pada setiap upaya koneksi.\ngitSyncPull=Tarik untuk menyinkronkan perubahan git jarak jauh\nenpassVaultFile=File brankas\nenpassVaultFileDescription=File brankas Enpass lokal.\nflat=Datar\nrecursive=Rekursif\nrdpAllowListBlocked=RemoteApp yang dipilih tampaknya tidak termasuk dalam daftar izin RDP untuk server.\npsonoServerUrl=URL server\npsonoServerUrlDescription=URL server backend psono\npsonoApiKey=Kunci API\npsonoApiKeyDescription=Kunci API yang akan digunakan, diformat sebagai uuid\npsonoApiSecretKey=Kunci rahasia API\npsonoApiSecretKeyDescription=Kunci rahasia API sebagai string heksa 64 byte\npassboltServerUrl=URL server\npassboltServerUrlDescription=URL dari server backend passbolt\npassboltPassphrase=Frasa sandi\npassboltPassphraseDescription=Frasa sandi untuk kunci pribadi brankas\npassboltPrivateKey=Kunci pribadi\npassboltPrivateKeyDescription=File kunci gpg pribadi untuk brankas\nfocusWindowOnNotifications=Jendela fokus pada pemberitahuan\nfocusWindowOnNotificationsDescription=Membawa XPipe ke latar depan saat pemberitahuan atau pesan kesalahan ditampilkan, misalnya saat koneksi atau terowongan terputus secara tidak terduga.\ngitUsername=Nama pengguna git khusus\ngitUsernameDescription=Pengguna khusus untuk mengautentikasi ke repositori jarak jauh git. Secara default, XPipe akan menggunakan kredensial yang saat ini dikonfigurasi dari git CLI.\\n\\nPengaturan ini akan mengganti kredensial default apa pun yang telah dikonfigurasi untuk klien CLI git lokal Anda.\ngitPassword=Kata sandi git khusus / token akses pribadi\ngitPasswordDescription=Kata sandi atau token akses pribadi yang digunakan untuk mengautentikasi. Apakah Anda memerlukan kata sandi atau token akses pribadi tergantung pada penyedia jarak jauh git. Pengaturan ini akan mengganti kredensial default yang sudah dikonfigurasi untuk klien CLI git lokal Anda.\nsetReadOnly=Mengatur hanya-baca\nunsetReadOnly=Tidak disetel hanya-baca\nreadOnlyStoreError=Konfigurasi entri ini dibekukan. Pilih nama yang berbeda untuk menyimpan perubahan Anda ke salinan baru.\ncategoryFreeze=Membekukan konfigurasi koneksi\ncategoryFreezeDescription=Menandai konfigurasi koneksi sebagai hanya-baca. Ini berarti tidak ada konfigurasi entri sambungan yang ada dalam kategori ini yang dapat dimodifikasi. Namun, koneksi baru dapat ditambahkan.\nupdateFail=Instalasi pembaruan tidak berhasil\nupdateFailAction=Menginstal pembaruan secara manual\nupdateFailActionDescription=Lihat rilis terbaru di GitHub\nonePasswordPlaceholder=Nama item atau op:// URL\ncomputeDirectorySizes=Menghitung ukuran direktori\ncomputeSize=Menghitung ukuran\ncustomSpiceCommand=Perintah khusus\ncustomSpiceCommandDescription=Perintah khusus yang akan dijalankan untuk meluncurkan sesi SPICE. String penampung $FILE akan diganti dengan jalur file yang dikutip ke file .vv saat dipanggil.\nvncClient=Klien VNC\nvncClientDescription=Klien VNC yang akan diluncurkan saat membuka koneksi VNC di XPipe.\\n\\nAnda memiliki opsi untuk menggunakan klien VNC terintegrasi dalam XPipe atau meluncurkan klien VNC eksternal yang terinstal secara lokal jika Anda menginginkan lebih banyak penyesuaian.\nintegratedXPipeVncClient=Klien VNC XPipe terintegrasi\ncustomVncCommand=Perintah khusus\ncustomVncCommandDescription=Perintah khusus yang akan dijalankan untuk meluncurkan sesi VNC. String penampung $ADDRESS akan diganti dengan alamat yang dikutip saat dipanggil.\nvncConnections=Koneksi VNC\npasswordManagerIdentity=Identitas pengelola kata sandi\npasswordManagerIdentity.displayName=Identitas pengelola kata sandi\npasswordManagerIdentity.displayDescription=Mengambil nama pengguna dan kata sandi identitas dari pengelola kata sandi Anda\npasswordCopied=Kata sandi sambungan disalin ke papan klip\nerrorOccurred=Terjadi kesalahan\nactionMacro.displayName=Makro tindakan\nactionMacro.displayDescription=Menjalankan aksi menggunakan pemicu yang disesuaikan\nmacroAdd=Menambahkan makro\nmacroName=Nama makro\nmacroNameDescription=Berikan nama khusus pada makro ini\nactionId=ID Tindakan\nactionIdDescription=Tindakan yang akan dijalankan dengan makro ini\nmacroRefs=Koneksi terkait\nmacroRefsDescription=Sambungan yang digunakan untuk menjalankan tindakan\nconnectionCopy=Salinan\nactionPickerTitle=Memilih tindakan\nactionPickerDescription=Klik pada sesuatu untuk melakukan suatu tindakan. Alih-alih menjalankan tindakan, Anda dapat membuat dan mengedit pintasan ke tindakan dalam mode pemilihan pintasan tindakan.\ncancelActionPicker=Membatalkan pilihan tindakan\nactionShortcut=Pintasan tindakan\nactionShortcuts=Pintasan tindakan\nactionStore=Penyimpanan tindakan\nactionStoreDescription=Entri toko untuk menjalankan tindakan pada\nactionStores=Penyimpanan tindakan\nactionStoresDescription=Entri penyimpanan untuk menjalankan tindakan\nactionDesktopShortcut=Pintasan desktop\nactionDesktopShortcutDescription=Membuat pintasan untuk tindakan ini di desktop Anda\nactionUrlShortcut=Pintasan URL\nactionUrlShortcutDescription=Menyalin URL yang dapat memicu tindakan ini saat dibuka\nactionUrlShortcutDisabled=Pintasan URL (Tidak tersedia)\nactionUrlShortcutDisabledDescription=Jenis penginstalan $TYPE$ tidak mendukung pembukaan URL\nactionApiCall=Permintaan API\nactionApiCallDescription=Panggil tindakan ini dari API HTTP\nactionMacro=Makro tindakan\nactionMacroDescription=Membuat makro dengan fungsionalitas lanjutan untuk tindakan ini\ncreateMacro=Membuat makro\nactionConfiguration=Parameter\nactionConfigurationDescription=Parameter yang akan diteruskan ke tindakan yang dieksekusi\nconfirmAction=Mengonfirmasi tindakan\nactionConnections=Koneksi tindakan\nactionConnectionsDescription=Sambungan untuk menjalankan tindakan\nactionConnection=Koneksi tindakan\nactionConnectionDescription=Sambungan untuk menjalankan tindakan pada\nappleContainerInstall.displayName=Wadah Apple\nappleContainerInstall.displayDescription=Mengakses contoh kontainer apel melalui CLI kontainer\nappleContainer.displayName=Wadah Apple\nappleContainer.displayDescription=Mengakses contoh kontainer apel melalui CLI kontainer\nappleContainerHostDescription=Host tempat wadah apel berada\nappleContainerDescription=Nama wadah apel\nappleContainers=Wadah Apple\nchangeOrderIndexTitle=Mengubah urutan\norderIndex=Indeks\norderIndexDescription=Indeks eksplisit untuk mengurutkan entri ini relatif terhadap entri lainnya. Indeks terendah ditampilkan di atas, tertinggi di bawah\nmoveToFirst=Pindah ke yang pertama\nmoveToLast=Pindah ke yang terakhir\ncategory=Kategori\nincludeRoot=Sertakan root\nexcludeRoot=Mengecualikan root\nfreezeConfiguration=Membekukan konfigurasi\nunfreezeConfiguration=Membekukan konfigurasi\nwaylandScalingTitle=Penskalaan Wayland\nactionApiUrl=$URL$ (Salin badan json)\ncopyBody=Badan permintaan salin\ngitRepoTerminalOpen=Membuka repositori di terminal\ngitRepoTerminalOpenDescription=Lihatlah repositori itu sendiri dengan baris perintah\ngitRepoOverwriteLocal=Menimpa repositori lokal\ngitRepoOverwriteLocalDescription=Mengganti semua perubahan lokal dengan perubahan dari remote\ngitRepoForcePush=Menimpa repositori jarak jauh\ngitRepoForcePushDescription=Gunakan git push --force untuk menerapkan perubahan lokal Anda ke remote\ngitRepoDontWarn=Jangan memperingatkan lagi\ngitRepoDontWarnDescription=Jika ini yang diharapkan, buatlah XPipe mengabaikan kesalahan ini di masa mendatang\ngitRepoTryAgain=Coba lagi\ngitRepoTryAgainDescription=Mencoba operasi yang sama lagi\ngitRepoEnablePlain=Menggunakan sinkronisasi direktori biasa\ngitRepoEnablePlainDescription=Jangan menginisialisasi repositori git untuk menyinkronkan perubahan ke direktori\ngitRepoCreateBare=Menggunakan sinkronisasi git\ngitRepoCreateBareDescription=Menginisialisasi repositori git kosong baru di direktori sinkronisasi\ngitRepoDisable=Nonaktifkan brankas git untuk saat ini\ngitRepoDisableDescription=Jangan melakukan perubahan apa pun selama sesi ini\ngitRepoPullRefresh=Menarik perubahan dan menyegarkan\ngitRepoPullRefreshDescription=Menggabungkan perubahan jarak jauh dan memuat ulang data\nbreakOutCategory=Kategori keluar\nmergeCategory=Gabungkan kategori\nopenWinScp=Buka di WinSCP\nuninstallApplication=Menghapus instalan\nuninstallApplicationDescription=Menjalankan skrip instalasi .pkg untuk menghapus instalasi XPipe sepenuhnya\nk8sEditPodTitle=Menerapkan perubahan\nk8sEditPodContent=Apakah Anda ingin menerapkan perubahan yang dibuat melalui perintah kubectl apply? Restart mungkin diperlukan agar perubahan dapat diterapkan.\nvirshEditDomainTitle=Menerapkan perubahan\nvirshEditDomainContent=Apakah Anda ingin menerapkan perubahan pada domain? Pengaktifan ulang mungkin diperlukan agar perubahan dapat diterapkan.\npkcs11Library=Perpustakaan PKCS # 11\npkcs11LibraryDescription=Jalur file pustaka yang ditautkan secara dinamis\nsshAgentSocket=Soket agen SSH khusus\nsshAgentSocketDescription=Soket khusus yang digunakan untuk berkomunikasi dengan agen SSH. Agen khusus ini dapat digunakan untuk koneksi dengan memilih opsi agen khusus untuknya.\npublicKey=Pengidentifikasi kunci publik\npublicKeyDescription=Kunci publik opsional untuk memaksa agen agar hanya menawarkan kunci privat yang cocok\nactions=Tindakan\nhcloudServer.displayName=Server awan Hetzner\nhcloudServer.displayDescription=Mengakses server yang dihosting di cloud Hetzner melalui SSH\nhcloudInstall.displayName=Hetzner Cloud CLI\nhcloudInstall.displayDescription=Mengakses server yang dihosting di cloud Hetzner melalui hcloud\nhcloudContext.displayName=konteks hcloud\nhcloudContext.displayDescription=Mengakses server dari konteks hcloud\nmetrics=Metrik\nopenInVsCode=Buka di VsCode\naddCloud=Awan ...\nhcloudToken=token hcloud\nhcloudTokenDescription=Token cloud Hetzner yang akan digunakan. Untuk informasi lebih lanjut, lihat dokumentasi\nhcloudLogin=Login cloud Hetzner\nclearHcloudToken=Hapus token hcloud\nclearHcloudTokenDescription=Menghapus token yang ada agar Anda dapat masuk kembali\nselectIdentity=Memilih identitas\nenableMcpServer=Mengaktifkan server MCP\nenableMcpServerDescription=Mengaktifkan server MCP XPipe, sehingga memungkinkan klien MCP eksternal mengirim permintaan ke server MCP. Lihat di bawah ini untuk rincian konfigurasi.\\n\\nPerhatikan bahwa API HTTP tidak harus diaktifkan untuk fungsionalitas MCP.\nenableMcpMutationTools=Mengaktifkan alat mutasi MCP\nenableMcpMutationToolsDescription=Secara default, hanya alat hanya-baca yang diaktifkan di server MCP. Hal ini untuk memastikan bahwa tidak ada operasi yang tidak disengaja yang berpotensi memodifikasi sistem.\\n\\nJika Anda berencana untuk membuat perubahan pada sistem melalui klien MCP, pastikan untuk memeriksa apakah klien MCP Anda dikonfigurasikan untuk mengonfirmasi tindakan yang berpotensi merusak sebelum mengaktifkan opsi ini. Memerlukan koneksi ulang klien MCP apa pun untuk diterapkan.\nmcpClientConfigurationDetails=Konfigurasi klien MCP\nmcpClientConfigurationDetailsDescription=Gunakan data konfigurasi ini untuk menyambung ke server MCP XPipe dari klien MCP pilihan Anda.\nswitchHostAddress=Mengubah alamat host\naddAnotherHostName=Menambahkan nama host lain\naddNetwork=Pemindaian jaringan ...\nnetworkScan=Pemindaian jaringan\nnetworkScanStore=Host target\nnetworkScanStoreDescription=Host yang akan dipindai jaringan lokal\nuseAsGateway=Menggunakan host sebagai gateway\nuseAsGatewayDescription=Apakah akan menggunakan host target sebagai gateway untuk koneksi yang dibuat\nnetworkScanPorts=Port yang akan dipindai\nnetworkScanPortsDescription=Daftar port yang dipisahkan dengan koma untuk disertakan dalam pemindaian\nnetworkScanType=Jenis koneksi\nnetworkScanTypeDescription=Jenis server yang harus dicari\nemptyDirectory=Direktori ini terlihat kosong\nhcloudConfigFile=file konfigurasi hcloud\nhcloudConfigFileDescription=Lokasi file konfigurasi .toml CLI hcloud CLI\npreferMonochromeIcons=Lebih menyukai ikon monokrom\npreferMonochromeIconsDescription=Bila diaktifkan, variabel ikon monokrom akan dipilih daripada versi ikon berwarna default, dengan asumsi bahwa varian ikon mode terang atau gelap yang terpisah tersedia untuk ikon dari sumber.\\n\\nMemerlukan penyegaran ikon untuk menerapkannya.\nalwaysShowSshMotd=Selalu tampilkan MOTD\nalwaysShowSshMotdDescription=Apakah akan menampilkan pesan hari ini yang dikonfigurasi pada sistem jarak jauh atau tidak saat masuk dalam sesi terminal baru. Perhatikan bahwa mengubah hal ini dapat mengubah perilaku inisialisasi koneksi SSH.\nmanageSubscription=Mengelola langganan\nnoListeningServer=Tidak ada server yang mendengarkan\nnetworkScanResults=Hasil pemindaian\nnetworkScanResultsDescription=Daftar sistem yang ditemukan dalam jaringan\nlocalShellDialect=Cangkang lokal\nlocalShellDialectDescription=Cangkang yang digunakan untuk operasi lokal. Jika shell default lokal normal dinonaktifkan atau rusak pada tingkat tertentu, opsi ini dapat digunakan untuk kembali ke alternatif lain.\\n\\nBeberapa konfigurasi seperti entri PATH khusus mungkin tidak berlaku dengan shell fallback jika belum dikonfigurasi dalam file profil shell yang bersangkutan.\nagentSocketNotFound=Tidak ditemukan soket agen aktif\nagentSocket=Lokasi soket\nagentSocketDescription=Jalur file soket agen\nagentSocketNotConfigured=Belum ada soket khusus yang dikonfigurasi\ndownloadInProgress=$NAME$ pengunduhan sedang berlangsung\nenableTerminalStartupBell=Mengaktifkan bel pengaktifan terminal\nenableTerminalStartupBellDescription=Memainkan perintah bip/lonceng dalam sesi terminal yang baru. Jika emulator terminal Anda mendukung lonceng, ini dapat digunakan untuk mempermudah identifikasi contoh terminal yang baru diluncurkan.\ninvalidSshGatewayChain=Konfigurasi rantai gateway campuran yang tidak valid dengan gateway lompat dan gateway non-lompat.\nsyncFileExists=File yang disinkronkan $FILE$ sudah ada\nreplaceFile=Mengganti file\nreplaceFileDescription=Mengganti file yang ada dengan file ini\nrenameFile=Mengganti nama file\nrenameFileDescription=Berikan nama yang berbeda pada file ini untuk disinkronkan\nnewFileName=Nama file baru\nparentHostDoesNotSupportTunneling=Host induk $NAME$ tidak mendukung tunneling\nconnectionNotesTemplate=Templat catatan\nconnectionNotesTemplateDescription=Templat penurunan harga yang harus digunakan saat menambahkan entri catatan baru ke sebuah sambungan.\nconnectionNotesButton=Mengedit catatan\nrdpSmartSizing=Mengaktifkan ukuran cerdas\nrdpSmartSizingDescription=Apabila diaktifkan, mstsc akan memperkecil ukuran desktop jika jendela terlalu kecil untuk menampilkannya dalam resolusi penuh. Rasio aspek desktop akan dipertahankan apabila diperkecil.\ndisableStartOnInit=Menonaktifkan pengaktifan otomatis\nenableStartOnInit=Mengaktifkan pengaktifan otomatis\nfileReadSudoTitle=Membaca file Sudo\nfileReadSudoContent=Berkas yang Anda coba baca tidak memberi Anda izin baca sebagai pengguna saat ini. Apakah Anda ingin membaca berkas ini sebagai pengguna root dengan sudo? Ini akan secara otomatis meningkatkannya menjadi root dengan kredensial yang ada atau melalui prompt.\nnetbirdInstall.displayName=Instalasi Netbird\nnetbirdInstall.displayDescription=Terhubung ke rekan-rekan di jaringan Netbird Anda\nnetbirdProfile.displayName=Profil Netbird\nnetbirdProfile.displayDescription=Membuat daftar rekan dalam profil tertentu\nnetbirdPeer.displayName=Rekan Netbird\nnetbirdPeer.displayDescription=Menyambung ke rekan melalui SSH\nnetbirdPublicKey=Kunci publik\nnetbirdPublicKeyDescription=Kunci publik internal dari rekan\nnetbirdHostName=Nama host\nnetbirdHostNameDescription=Nama host rekan dalam jaringan\nvncRefSystem=Sistem terkait\nvncRefSystemDescription=Entri koneksi untuk mengaitkan koneksi VNC ini. Biarkan kosong jika tidak ada\nabstractHost.displayName=Host abstrak\nabstractHost.displayDescription=Membuat entri untuk host yang tidak mendukung koneksi shell\nabstractHostAddress=Alamat host\nabstractHostAddressDescription=Alamat tuan rumah\nabstractHostGateway=Gerbang\nabstractHostGatewayDescription=Sistem gateway opsional yang dapat digunakan untuk menjangkau host ini\nabstractHostConvert=Mengonversi ke entri host abstrak\nhostNoConnections=Tidak ada koneksi yang tersedia\nhostHasConnections=$COUNT$ koneksi yang tersedia\nhostHasConnection=$COUNT$ koneksi yang tersedia\nlargeFileWarningTitle=Mengedit file besar\nlargeFileWarningContent=File yang ingin Anda edit cukup besar dengan $SIZE$. Apakah Anda benar-benar ingin membuka file ini di editor teks Anda?\nrdpAskpassUser=Nama pengguna RDP untuk host $HOST$\nrdpAskpassPassword=Kata sandi untuk pengguna $USER$\ninPlaceKey=Kunci\ninPlaceKeyText=Konten kunci pribadi\ninPlaceKeyTextDescription=Isi kunci pribadi\nnetbirdSelfhosted=Instance netbird yang dihosting sendiri\nnetbirdSelfhostedDescription=Menyediakan URL khusus alih-alih menggunakan versi yang dihosting di cloud\nnetbirdManagementUrl=URL manajemen Netbird\nnetbirdManagementUrlDescription=URL manajemen dari instans yang dihosting sendiri\nnetbirdSetupKey=Tombol pengaturan\nnetbirdSetupKeyDescription=Jika Anda menggunakan tombol pengaturan, Anda dapat menggunakannya untuk masuk\nnetbirdLogin=Login Netbird\naddProfile=Menambahkan profil\nnetbirdProfileNameAsktext=Nama profil netbird baru\nopenSftp=Buka dalam sesi SFTP\ncapslockWarning=Anda telah mengaktifkan capslock\ninherit=Mewarisi\nsshConfigStringSelected=Host target\nsshConfigStringSelectedDescription=Untuk beberapa host, host pertama digunakan sebagai target. Susun ulang host Anda untuk mengubah target\ntunnelToLocalhost=Terowongan ke host lokal\ntunnelToLocalhostDescription=Menyalurkan port jarak jauh secara otomatis ke host lokal\ntags=Tag\ntag=Tag\naddNewTag=Membuat tag baru\ncreateTag=Membuat tag ...\ninPlacePublicKey=Kunci publik\ninPlacePublicKeyDescription=Kunci publik terkait untuk kunci pribadi yang ditentukan\nsshKeygenTitle=Membuat kunci SSH baru\nsshKeygenAlgorithm=Algoritma\nsshKeygenAlgorithmDescription=Algoritme asimetris keygen yang digunakan untuk kunci\nrsaBits=Bit\nrsaBitsDescription=Jumlah bit dalam kunci yang dihasilkan\nsshKeygenComment=Komentar\nsshKeygenCommentDescription=Komentar opsional untuk kunci ini\nsshKeygenPassphrase=Frasa sandi\nsshKeygenPassphraseDescription=Frasa sandi opsional untuk kunci ini\ned25519SkResident=Membuat kunci residen\ned25519SkResidentDescription=Menyimpan kunci pribadi pada kunci keamanan perangkat keras\ned25519SkResidentKeyName=Label kunci residen\ned25519SkResidentKeyNameDescription=Memberi label pada kunci. Diperlukan saat menyimpan beberapa kunci pada kunci keamanan\ned25519SkPinRequired=Memerlukan PIN\ned25519SkPinRequiredDescription=Memerlukan entri PIN saat digunakan\ned25519SkUserPresenceRequired=Memerlukan kehadiran pengguna\ned25519SkUserPresenceRequiredDescription=Memerlukan sentuhan atau sejenisnya saat digunakan. Beberapa kunci keamanan mengharuskan hal ini diaktifkan\ncopyPublicKey=Salin kunci publik\ngeneratePublicKey=Menghasilkan kunci publik\npublicKeyGenerateNotice=Dapat dihasilkan dari kunci pribadi\nidentityApplyTargetHost=Target\nidentityApplyTargetHostDescription=Sistem untuk menerapkan identitas ke\nidentityApplyAuthorizedHost=Kunci SSH disahkan\nidentityApplyAuthorizedHostDescription=Kunci SSH ditambahkan ke file host yang diotorisasi\nidentityApplyAuthorizedHostButton=Menambahkan kunci ke file\napplyIdentityToHost=Menerapkan identitas ke host ...\nidentityApplyMissingPublicKeyTitle=Kunci publik yang hilang\nidentityApplyMissingPublicKeyContent=Kunci SSH identitas tidak memiliki kunci publik yang terkait dengannya. Lihat konfigurasi untuk detailnya.\nvalid=Valid\nnotValid=Tidak valid\nwarning=Peringatan\nidentityApplyTitle=Menerapkan identitas\nidentityApplyConfigPasswordEnabled=Penulisan kata sandi diaktifkan\nidentityApplyConfigPasswordEnabledDescription=Otentikasi kata sandi masih diaktifkan di konfigurasi sshd\nidentityApplyConfigPasswordDisabled=Penulisan kata sandi dinonaktifkan\nidentityApplyConfigPasswordDisabledDescription=Otentikasi kata sandi masih dinonaktifkan di konfigurasi sshd\nidentityApplyConfigKeyEnabled=Penulis kunci diaktifkan\nidentityApplyConfigKeyEnabledDescription=Autentikasi berbasis kunci masih diaktifkan dalam konfigurasi sshd\nidentityApplyConfigKeyDisabled=Penulis kunci dinonaktifkan\nidentityApplyConfigKeyDisabledDescription=Otentikasi berbasis kunci masih dinonaktifkan di konfigurasi sshd\nidentityApplyConfigRootDisabledWarning=Login root dinonaktifkan\nidentityApplyConfigRootDisabledWarningDescription=Login pengguna root tidak diaktifkan dalam konfigurasi sshd\nidentityApplyConfigAdminWarning=Kunci administrator yang dikonfigurasi\nidentityApplyConfigAdminWarningDescription=Kunci tersebut mungkin harus ditambahkan ke administrator_authorized_keys sebagai gantinya untuk pengguna admin\nidentityApplyEditConfig=Mengedit konfigurasi\nidentityApplyEditConfigDescription=Buka konfigurasi sshd di editor untuk memperbaiki masalah apa pun\nidentityApplyEditAuthorizedKeys=Mengedit kunci yang diotorisasi\nidentityApplyEditAuthorizedKeysDescription=Buka file authorized_keys di editor untuk mengedit atau menghapus kunci lain\nidentityApplyEditConfigButton=Buka sshd_config\nidentityApplyEditAuthorizedKeysButton=Buka kunci_otorisasi\nidentityApplySetStoreIdentity=Set identitas koneksi\nidentityApplySetStoreIdentityDescription=Identitas dikonfigurasikan untuk digunakan oleh koneksi\nidentityApplySetStoreIdentityButton=Menerapkan identitas\ngenerateKey=Menghasilkan kunci\ngroupSecretStrategy=Kontrol akses berbasis grup\ngroupSecretStrategyDescription=Cara mengambil rahasia grup yang digunakan untuk enkripsi dan dekripsi grup. Metode pengambilan yang Anda pilih akan dijalankan saat pengguna masuk ke brankas saat pengaktifan.\\n\\nPengaturan ini dikonfigurasikan berdasarkan per grup. Untuk mengubah pengaturan ini untuk grup yang berbeda selain grup yang sedang aktif, anda harus masuk ke vault sebagai anggota grup tersebut.\nfileSecret=Rahasia berbasis file\ncommandSecret=Perintah\nhttpRequestSecret=Respons HTTP\nfileSecretChoice=Lokasi file\nfileSecretChoiceDescription=Jalur ke file yang berisi rahasia enkripsi grup. Karena file ini dapat ditanyakan pada semua platform, Anda dapat menggunakan ~ pada jalur untuk merujuk ke direktori home. File ini harus tersedia di semua sistem tempat Anda membuka kunci brankas, jika tidak, login akan gagal.\ncommandSecretField=Skrip pengambilan\ncommandSecretFieldDescription=Perintah yang akan mengembalikan kunci enkripsi rahasia untuk grup saat ini. Perintah ini dijalankan di shell default sistem lokal dan kuncinya harus dicetak ke stdout.\nhttpRequestSecretField=Permintaan URI\nhttpRequestSecretFieldDescription=URI untuk mengirim permintaan HTTP. Rahasia grup diambil dari badan respons HTTP.\nvaultAuthentication=Otentikasi brankas\nvaultAuthenticationDescription=Cara mengautentikasi/membuka kunci data brankas. Ada beberapa cara yang berbeda untuk mengenkripsi dan membuka kunci data brankas, tergantung kepada siapa Anda ingin berbagi data brankas tersebut.\ngroupAuthFailed=Otentikasi rahasia gagal\nuserAuthFailed=Otentikasi kata sandi gagal\nsavingChanges=Menyimpan perubahan\nawsDeviceScan=AWS\naws=AWS\nawsCliInstallTitle=Diperlukan AWS CLI\nawsCliInstallContent=Integrasi AWS memerlukan AWS CLI untuk diinstal pada sistem lokal Anda\nawsProfileCreateTitle=Profil AWS baru\nawsProfileAccessKey=Kunci akses\nawsProfileName=Nama profil\nawsProfileNameDescription=Nama tampilan profil baru\nawsProfileRegion=Wilayah\nawsProfileRegionDescription=Wilayah AWS yang terkait dengan profil\nawsProfileAccessKeyId=ID kunci akses\nawsProfileAccessKeyIdDescription=ID kunci akses pengguna IAM\nawsProfileSecretAccessKey=Kunci akses rahasia\nawsProfileSecretAccessKeyDescription=Kunci akses rahasia yang terkait\nawsInstall.displayName=Instalasi AWS CLI\nawsInstall.displayDescription=Menghubungkan ke sistem AWS Anda melalui AWS CLI\nawsProfile.displayName=Profil CLI AWS\nawsProfile.displayDescription=Mengakses AWS melalui profil tertentu\nawsInstanceId=ID Instance\nawsInstanceIdDescription=ID internal dari instance ini\nawsInstanceUseSsm=Menghubungkan melalui SSM\nawsInstanceUseSsmDescription=Gunakan alat SSM untuk menyambung ke instans melalui SSH\nawsEc2Instance.displayName=Contoh AWS EC2\nawsEc2Instance.displayDescription=Menyambungkan ke instans EC2 melalui SSH\nawsS3Group.displayName=Ember S3\nawsS3Group.displayDescription=Mengakses bucket S3 dari profil AWS\nawsS3Bucket.displayName=Ember S3\nawsS3Bucket.displayDescription=Mengakses bucket S3 dari profil AWS\nawsEc2Group.displayName=Contoh EC2\nawsEc2Group.displayDescription=Mengakses contoh EC2 dari profil AWS\nawsEc2InstanceSsmTerminal=Membuka terminal SSM\ngenericS3Bucket.displayName=Ember S3 umum\ngenericS3Bucket.displayDescription=Mengakses bucket S3 generik melalui AWS CLI\naddFileSystem=Sistem file ...\ngenericS3BucketHost=Tuan rumah\ngenericS3BucketHostDescription=Entri host atau alamat manual dari server S3\ngenericS3BucketPortDescription=Port yang didengarkan server S3\ngenericS3BucketAccessKeyId=ID kunci akses\ngenericS3BucketAccessKeyIdDescription=ID kunci akses pengguna IAM\ngenericS3BucketSecretAccessKey=Kunci akses rahasia\ngenericS3BucketSecretAccessKeyDescription=Kunci akses rahasia yang terkait\ngenericS3BucketHttps=Mengaktifkan HTTPS\ngenericS3BucketHttpsDescription=Gunakan HTTPS untuk menyambung ke server. Beberapa penyedia mungkin memerlukan HTTPS\ntunnelled=Terowongan\nawsInstallSync=Sinkronisasi konfigurasi\nawsInstallSyncDescription=Menyinkronkan file konfigurasi AWS CLI ke brankas git\nawsInstallLocation=Lokasi data pengguna\nawsInstallLocationDescription=Jalur dari mana file konfigurasi AWS CLI bersumber\ninstanceActions=Tindakan contoh\nopenSplit=Buka di terminal terpisah\nterminalSplitStrategy=Membagi arah tampilan\nterminalSplitStrategyDescription=Mengontrol bagaimana tab terminal dibagi ketika menggunakan fungsi tampilan terbagi dalam mode batch untuk membuka beberapa sesi terminal di samping satu sama lain.\nterminalSplitStrategyDisabledDescription=Mengontrol bagaimana tab terminal dibagi ketika menggunakan fungsi tampilan terbagi dalam mode batch untuk membuka beberapa sesi terminal di samping satu sama lain.\\n\\nKonfigurasi terminal Anda saat ini tidak mendukung tampilan terpisah.\nhorizontal=Horisontal\nvertical=Vertikal\nbalanced=Seimbang\nclose=Tutup\nhelpButton=$TOPIC$ tautan dokumentasi\nquickAccess=Akses cepat\ntoggleEnabled=Beralih status\ncurrentPath=Jalur saat ini\ndirectoryContents=Isi direktori\ndirectoryOptions=Opsi direktori\nchooseConnectionType=Memilih jenis koneksi\nbatchMode=Mode batch\ntoggleButton=Tombol untuk berpindah\ntailscaleUseSsh=Gunakan autentikasi SSH skala ekor\ntailscaleUseSshDescription=Masuk melalui server SSH tailscale itu sendiri tanpa autentikasi SSH apa pun\nportDescription=Port yang digunakan untuk menjalankan server SSH\nloginAs=Masuk sebagai\nsshGatewayType=Jenis gateway\nsshGatewayTypeDescription=Apakah akan menyambung ke target melalui terowongan atau dengan opsi ProxyJump\ngatewayTunnel=Terowongan gateway\nproxyJump=Lompatan proxy\ncommandTypeAsyncBackground=Jalankan terpisah di latar belakang\ncommandTypeSyncBackground=Jalankan di latar belakang dan tunggu hingga selesai\ncommandTypeTerminalBackground=Buka di terminal\nasyncBackgroundCommand=Perintah latar belakang\nsyncBackgroundCommand=Memblokir perintah latar belakang\nterminalBackgroundCommand=Perintah terminal\ntestingConnection=Menguji koneksi ...\nopenManagementConsole=Konsol manajemen terbuka\nopenLxcTerminal=Membuka terminal LXC\nopenContainerConsole=Membuka konsol serial\nkeeper2fa=metode 2FA\nkeeper2faDescription=Metode autentikasi dua faktor utama yang dikonfigurasikan untuk akun Anda. Aktifkan ini jika akun Keeper Anda memerlukan autentikasi dua faktor untuk mengakses kata sandi.\nkeeperTotpDuration=Durasi kode 2FA khusus\nkeeperTotpDurationDescription=Mengganti durasi default tentang berapa lama kode 2FA berlaku. Hanya berlaku jika kebijakan organisasi Anda mengizinkan pengubahan durasi.\\n\\nNilai yang mungkin adalah: $VALUES$\nkeeperOtherAuth=Lainnya (RSA SecurID, Duo Security, Keeper DNA, dll.)\nextractReusableIdentities=Mengekstrak identitas yang dapat digunakan kembali\nidentitiesAdded=Identitas ditambahkan\nsyncMode=Mode sinkronisasi\nsyncModeDescription=Mengontrol bagaimana perubahan harus disinkronkan.\\n\\nMode instan akan mendorong dan menarik perubahan sesegera mungkin, mode pengaktifan dan keluar akan menyinkronkan semua perubahan yang dibuat selama sesi sekaligus, dan mode manual hanya akan menyinkronkan saat Anda memulainya.\ntoggleTerminalDock=Beralih dok terminal\nscriptDirectory=Lokasi direktori\nscriptDirectoryDescription=Direktori lokal yang berisi file skrip shell\nscriptSourceUrl=URL repositori\nscriptSourceUrlDescription=URL ke repositori git jarak jauh yang berisi file skrip shell\nscriptCollectionSourceType=Jenis sumber\nscriptCollectionSourceTypeDescription=Jenis sumber dari mana skrip shell harus dimuat\nscriptCollectionSourceEntry=Entri sumber\nscriptCollectionSourceEntryDescription=Sumber dari mana skrip shell harus dimuat\ngitRepository=Repositori git\nscriptCollectionSource.displayName=Sumber naskah\nscriptCollectionSource.displayDescription=Mengimpor skrip shell secara otomatis dari sumber yang ada\ndirectorySource=Sumber direktori\ngitRepositorySource=Sumber repositori git\nrefreshSource=Menyegarkan sumber\nscriptTextSourceUrl=URL skrip\nscriptTextSourceUrlDescription=URL untuk mengambil file skrip dari\nscriptSourceType=Sumber naskah\nscriptSourceTypeDescription=Dari mana mendapatkan sumber skrip\nscriptSourceTypeInPlace=Skrip di tempat\nscriptSourceTypeUrl=URL eksternal\nscriptSourceTypeSource=Sumber yang ada\nimportScripts=Mengimpor skrip\nscriptsContained=$NUMBER$ skrip\nscriptSourceCollectionImportTitle=Mengimpor skrip dari sumbernya ($SELECTED$/$COUNT$)\nnoScriptsFound=Tidak ada skrip yang ditemukan\ntunnel=Terowongan\nnotInitialized=Tidak diinisialisasi\nselectCategory=Pilih kategori ...\nscriptSourceName=Nama skrip\nscriptSourceNameDescription=Nama file skrip di dalam sumber\nworkspaceRestartTitle=Ruang kerja siap\nworkspaceRestartContent=Pintasan ke ruang kerja baru telah dibuat di $PATH$. Anda dapat menavigasi ke pintasan atau memulai ulang XPipe sekarang untuk membuka ruang kerja baru secara otomatis.\nbrowseShortcut=Menelusuri file\nsyncModeInstant=Menyinkronkan secara instan\nsyncModeSession=Sinkronisasi saat pengaktifan dan keluar\nsyncModeManual=Menyinkronkan secara manual\npushChanges=Mendorong perubahan\npullChanges=Tarik perubahan\nsourcedFrom=Bersumber dari $SOURCE$\ninPlaceScript=Skrip di tempat\ngeneric=Umum\nsyncToPlainDirectory=Menyinkronkan ke direktori biasa\nsyncToPlainDirectoryDescription=Saat menyinkronkan ke direktori lokal, Anda dapat memperlakukan direktori ini sebagai repositori git lain atau hanya sebagai direktori biasa. Jika pengaturan direktori biasa diaktifkan, direktori tidak diinisialisasi sebagai repositori git.\nopenSpiceSession=Membuka sesi SPICE\nterminalBehaviour=Perilaku terminal\nnoScanPossible=Tidak ditemukan koneksi yang didukung\nnetworkSwitchPorts=Port jaringan\nnswitchGroup.displayName=Port jaringan\nnswitchGroup.displayDescription=Membuat daftar port yang tersedia pada perangkat jaringan\nnswitchPort.displayName=Port jaringan\nnswitchPort.displayDescription=Mengontrol port individual pada perangkat sakelar jaringan\nenablePort=Mengaktifkan port\nshutdownPort=Mematikan port\nresetPort=Atur ulang port\nuseSystemDefault=Menggunakan default sistem\nportStatus=Status port\nclearCounters=Hapus penghitung\nshowStatus=Menampilkan status\nshowAllPorts=Menampilkan semua port\nactiveLicense=Lisensi\nactiveLicenseDescription=Mengaktifkan kunci lisensi XPipe\nauthenticatorApp=Aplikasi autentikator\nsecurityKey=Kunci keamanan\nmcpAdditionalContext=Konteks MCP tambahan\nmcpAdditionalContextDescription=Instruksi tambahan untuk diteruskan ke klien MCP. Gunakan ini untuk mengontrol perilaku agen dan memberikan konteks tambahan untuk pengaturan individual Anda.\nmcpAdditionalContextSample=- Jangan memulai ulang layanan dan daemon apa pun secara otomatis tanpa mengonfirmasi terlebih dahulu\\n- Saat mengonfigurasi antarmuka jaringan, selalu gunakan 192.168.1.1/24 sebagai gateway\nprefsRestartTitle=Diperlukan restart\nprefsRestartContent=Beberapa pilihan yang Anda ubah memerlukan aplikasi dimulai ulang untuk menerapkannya. Apakah Anda ingin memulai ulang XPipe sekarang?\nbashShell=Cangkang bash\n"
  },
  {
    "path": "lang/strings/translations_it.properties",
    "content": "delete=Eliminare\nproperties=Proprietà\nusedDate=Utilizzato $DATE$\nopenDir=Elenco aperto\nsortLastUsed=Ordina per data di ultimo utilizzo\nsortAlphabetical=Ordinamento alfabetico per nome\nsortIndexed=Ordinamento per indice d'ordine\nrestartDescription=Un riavvio può spesso essere una soluzione rapida\nreportIssue=Segnala un problema\nreportIssueDescription=Apri il segnalatore di problemi integrato\nusefulActions=Azioni utili\nstored=Salvato\ntroubleshootingOptions=Strumenti per la risoluzione dei problemi\ntroubleshoot=Risoluzione dei problemi\nremote=File remoto\naddShellStore=Aggiungi Shell ...\naddShellTitle=Aggiungi connessione alla shell\nsavedConnections=Connessioni salvate\nsave=Salva\nclean=Pulire\nmoveTo=Passare a ...\naddDatabase=Database ...\nbrowseInternalStorage=Sfogliare la memoria interna\naddTunnel=Tunnel ...\naddService=Servizio ...\naddScript=Script ...\naddHost=Host remoto ...\naddShell=Ambiente Shell ...\naddCommand=Comando ...\naddAutomatically=Aggiungi automaticamente ...\naddOther=Aggiungi altro ...\nconnectionAdd=Aggiungi connessione\nscriptAdd=Aggiungi script\nscriptGroupAdd=Aggiungi gruppo di script\nidentityAdd=Aggiungi identità\nnew=Nuovo\nselectType=Seleziona il tipo\nselectTypeDescription=Seleziona il tipo di connessione\nselectShellType=Tipo di shell\nselectShellTypeDescription=Seleziona il tipo di connessione della shell\nname=Nome\nstoreIntroHeader=Hub di connessione\nstoreIntroContent=Qui puoi gestire tutte le tue connessioni shell locali e remote in un unico posto. Per iniziare, puoi rilevare rapidamente le connessioni disponibili in modo automatico e scegliere quali aggiungere.\nstoreIntroButton=Ricerca di connessioni ...\ndragAndDropFilesHere=Oppure trascina e rilascia un file qui\nconfirmDsCreationAbortTitle=Conferma l'interruzione\nconfirmDsCreationAbortHeader=Vuoi interrompere la creazione dell'origine dati?\nconfirmDsCreationAbortContent=Qualsiasi progresso nella creazione di un'origine dati andrà perso.\nconfirmInvalidStoreTitle=Convalida del salto\nconfirmInvalidStoreContent=Vuoi saltare la convalida della connessione? Puoi aggiungere questa connessione anche se non è stata convalidata e risolvere i problemi di connessione in un secondo momento.\nexpand=Espandi\naccessSubConnections=Connessioni secondarie di accesso\ncommon=Comune\ncolor=Colore\nalwaysConfirmElevation=Conferma sempre l'elevazione dei permessi\nalwaysConfirmElevationDescription=Controlla come gestire i casi in cui sono richiesti permessi elevati per eseguire un comando su un sistema, ad esempio con sudo.\\n\\nPer impostazione predefinita, le credenziali sudo vengono memorizzate nella cache durante una sessione e fornite automaticamente quando necessario. Se l'opzione è attivata, ti chiederà di confermare l'accesso all'elevazione ogni volta.\nallow=Consenti\nask=Chiedi\ndeny=Rifiuta\nshare=Aggiungi al repository git\nunshare=Rimuovi dal repository git\nremove=Rimuovere\ncreateNewCategory=Nuova sottocategoria\nprompt=Prompt\ncustomCommand=Comando personalizzato\nother=Altro\nsetLock=Imposta blocco\nselectConnection=Seleziona la connessione\nselectEntry=Seleziona la voce\ncreateLock=Creare una passphrase\nchangeLock=Modifica della passphrase\ntest=Test\nfinish=Terminare\nerror=Si è verificato un errore\ndownloadStageDescription=Sposta i file scaricati nella directory dei download del sistema e li apre.\nok=Ok\nsearch=Ricerca\nrepeatPassword=Ripeti la password\naskpassAlertTitle=Askpass\nunsupportedOperation=Operazione non supportata: $MSG$\nfileConflictAlertTitle=Risolvere un conflitto\nfileConflictAlertContent=È stato riscontrato un conflitto. Il file $FILE$ esiste già sul sistema di destinazione.\\n\\nCome vuoi procedere?\nfileConflictAlertContentMultiple=Si è verificato un conflitto. Il file $FILE$ esiste già.\\n\\nCome vuoi procedere? Potrebbero esserci altri conflitti che puoi risolvere automaticamente scegliendo un'opzione valida per tutti.\nmoveAlertTitle=Conferma la mossa\nmoveAlertHeader=Vuoi spostare gli elementi ($COUNT$) selezionati in $TARGET$?\ndeleteAlertTitle=Conferma l'eliminazione\ndeleteAlertHeader=Vuoi cancellare gli elementi ($COUNT$) selezionati?\nselectedElements=Elementi selezionati:\nmustNotBeEmpty=$VALUE$ non deve essere vuoto\nvalueMustNotBeEmpty=Il valore non deve essere vuoto\ntransferDescription=Trascina i file qui per scaricarli\ndragLocalFiles=Trascina i download da qui\nnull=$VALUE$ deve essere non nullo\nroots=Radici\nscripts=Script\nsearchFilter=Ricerca ...\nrecent=Recente\nshortcut=Scorciatoia\nbrowserWelcomeEmptyHeader=Browser di file\nbrowserWelcomeEmptyContent=A sinistra puoi scegliere quali sistemi aprire nel browser dei file. XPipe ricorderà i sistemi e le directory a cui hai avuto accesso in precedenza e li mostrerà in futuro in un menu di accesso rapido.\nbrowserWelcomeEmptyButton=Aprire un browser di file locale\nbrowserWelcomeSystems=Recentemente sei stato collegato ai seguenti sistemi:\nbrowserWelcomeDocsHeader=Documentazione\nbrowserWelcomeDocsContent=Se preferisci un approccio più guidato per familiarizzare con XPipe, consulta il sito web della documentazione.\nbrowserWelcomeDocsButton=Documentazione aperta\nhostFeatureUnsupported=$FEATURE$ non è installato sull'host\nmissingStore=$NAME$ non esiste\nconnectionName=Nome della connessione\nconnectionNameDescription=Assegna a questa connessione un nome personalizzato\nopenFileTitle=Aprire un file\nunknown=Sconosciuto\nscanAlertTitle=Aggiungi connessioni\nscanAlertChoiceHeader=Obiettivo\nscanAlertChoiceHeaderDescription=Scegli dove cercare le connessioni. In questo modo verranno cercate prima tutte le connessioni disponibili.\nscanAlertHeader=Tipi di connessione\nscanAlertHeaderDescription=Seleziona i tipi di connessioni che vuoi aggiungere automaticamente al sistema.\nnoInformationAvailable=Nessuna informazione disponibile\nyes=Sì\nno=No\nerrorOccured=Si è verificato un errore\nterminalErrorOccured=Si è verificato un errore del terminale\nerrorTypeOccured=È stata lanciata un'eccezione del tipo $TYPE$\npermissionsAlertTitle=Permessi richiesti\npermissionsAlertHeader=Per eseguire questa operazione sono necessari ulteriori permessi.\npermissionsAlertContent=Segui il pop-up per dare a XPipe i permessi richiesti nel menu delle impostazioni.\nerrorDetails=Dettagli dell'errore\nupdateReadyAlertTitle=Aggiornamento pronto\nupdateReadyAlertHeader=L'aggiornamento alla versione $VERSION$ è pronto per essere installato\nupdateReadyAlertContent=Questo installerà la nuova versione e riavvierà XPipe al termine dell'installazione.\nerrorNoDetail=Non sono disponibili dettagli sull'errore\nerrorNoExceptionMessage=È stato lanciato un errore del tipo $TYPE$\nupdateAvailableTitle=Aggiornamento disponibile\nupdateAvailableContent=L'aggiornamento di XPipe alla versione $VERSION$ è disponibile per l'installazione. Anche se non è stato possibile avviare XPipe, puoi provare a installare l'aggiornamento per risolvere il problema.\nclipboardActionDetectedTitle=Azione Appunti rilevata\nclipboardActionDetectedContent=XPipe ha rilevato un contenuto negli appunti che può essere aperto. Vuoi aprirlo ora? Vuoi importare il contenuto degli appunti?\ninstall=Installare ...\nignore=Ignorare\npossibleActions=Azioni disponibili\nreportError=Segnala un errore\nreportOnGithub=Crea una segnalazione di problema su GitHub\nreportOnGithubDescription=Aprire un nuovo problema nel repository GitHub\nreportErrorDescription=Inviare un rapporto di errore con un feedback opzionale dell'utente e informazioni diagnostiche\nignoreError=Ignora errore\nignoreErrorDescription=Ignora questo errore e continua come se niente fosse\nprovideEmail=Come possiamo contattarti (facoltativo, solo se vuoi ricevere una risposta). La tua segnalazione è anonima per impostazione predefinita, quindi puoi fornire informazioni di contatto come un indirizzo e-mail.\nadditionalErrorInfo=Fornisci informazioni aggiuntive (facoltative)\nadditionalErrorAttachments=Seleziona gli allegati (opzionale)\ndataHandlingPolicies=Politica sulla privacy\nsendReport=Inviare un rapporto\nerrorHandler=Gestore degli errori\nevents=Eventi\nvalidate=Convalidare\nstackTrace=Traccia dello stack\npreviousStep=< Precedente\nnextStep=Avanti >\nfinishStep=Terminare\nselect=Seleziona\nbrowseInternal=Sfogliare interno\ncheckOutUpdate=Aggiornamento del check out\nquit=Abbandono\nnoTerminalSet=Nessuna applicazione terminale è stata impostata automaticamente. Puoi farlo manualmente nel menu delle impostazioni.\nconnections=Connessioni\nconnectionHub=Hub di connessione\nsettings=Impostazioni\nexplorePlans=Licenza\nhelp=Aiuto\nabout=Informazioni su\ndeveloper=Sviluppatore\nbrowseFileTitle=Sfogliare un file\nbrowser=Browser di file\nselectFileFromComputer=Seleziona un file da questo computer\nlinks=Collegamenti\nwebsite=Sito web\ndiscordDescription=Unisciti al server Discord\nredditDescription=Unisciti al subreddit XPipe\nsecurity=La sicurezza\nsecurityPolicy=Informazioni sulla sicurezza\nsecurityPolicyDescription=Leggi la politica di sicurezza dettagliata\nprivacy=Politica sulla privacy\nprivacyDescription=Leggi l'informativa sulla privacy dell'applicazione XPipe\nslackDescription=Partecipa allo spazio di lavoro Slack\nsupport=Supporto\ngithubDescription=Dai un'occhiata al repository di GitHub\nopenSourceNotices=Avvisi Open Source\ncheckForUpdates=Controlla gli aggiornamenti\ncheckForUpdatesDescription=Scaricare un aggiornamento, se presente\nlastChecked=Ultimo controllo\nversion=Versione\nbuild=Versione Build\nruntimeVersion=Versione runtime\nvirtualMachine=Macchina virtuale\nupdateReady=Installare l'aggiornamento\nupdateReadyPortable=Aggiornamento del check out\nupdateReadyDescription=Un aggiornamento è stato scaricato ed è pronto per essere installato\nupdateReadyDescriptionPortable=Un aggiornamento è disponibile per il download\nupdateRestart=Riavviare per aggiornare\nnever=Mai\nupdateAvailableTooltip=Aggiornamento disponibile\nptbAvailableTooltip=Disponibile la versione di prova pubblica\nvisitGithubRepository=Visita il repository GitHub\nupdateAvailable=Aggiornamento disponibile: $VERSION$\ndownloadUpdate=Scarica l'aggiornamento\nlegalAccept=Accetto il Contratto di licenza con l'utente finale\nconfirm=Confermare\nprint=Stampa\nwhatsNew=Cosa c'è di nuovo nella versione $VERSION$ ($DATE$)\nantivirusNoticeTitle=Una nota sui programmi antivirus\nupdateChangelogAlertTitle=Changelog\ngreetingsAlertTitle=Benvenuto in XPipe\neula=Contratto di licenza con l'utente finale\nnews=Notizie\nintroduction=Introduzione\nprivacyPolicy=Politica sulla privacy\nagree=Accettare\ndisagree=Non sono d'accordo\ndirectories=Elenchi\nlogFile=File di log\nlogFiles=File di log\nlogFilesAttachment=File di log\nissueReporter=Segnalatore di problemi\nopenCurrentLogFile=File di log\nopenCurrentLogFileDescription=Aprire il file di log della sessione corrente\nopenLogsDirectory=Aprire la directory dei log\ninstallationFiles=File di installazione\nopenInstallationDirectory=File di installazione\nopenInstallationDirectoryDescription=Aprire la directory di installazione di XPipe\nlaunchDebugMode=Modalità di debug\nlaunchDebugModeDescription=Riavviare XPipe in modalità debug\nextensionInstallTitle=Scarica\nextensionInstallDescription=Questa azione richiede librerie aggiuntive di terze parti che non sono distribuite da XPipe. Puoi installarle automaticamente qui. I componenti vengono poi scaricati dal sito web del fornitore:\nextensionInstallLicenseNote=Effettuando il download e l'installazione automatica accetti i termini delle licenze di terze parti:\nlicense=Licenza\ninstallRequired=Installazione richiesta\nrestore=Ripristino\nrestoreAllSessions=Ripristina tutte le sessioni\nlimitedTouchscreenMode=Modalità touchscreen limitata\nlimitedTouchscreenModeDescription=Quando si utilizza questa applicazione su un'interfaccia touchscreen più esotica, come lo schermo di un telefono, alcuni menu potrebbero non funzionare correttamente. Quando questa opzione è abilitata, l'implementazione del menu utilizza una funzionalità più limitata per lavorare con eventi mouse/touch inviati in modo limitato.\nappearance=Aspetto\ndisplay=Visualizza\npersonalization=Personalizzazione\ndisplayOptions=Opzioni di visualizzazione\ntheme=Tema\nrdpConfiguration=Configurazione del desktop remoto\nrdpClient=Client RDP\nrdpClientDescription=Il programma client RDP da richiamare quando si avviano le connessioni RDP.\\n\\nSi noti che i vari client hanno diversi gradi di abilità e integrazioni. Alcuni client non supportano il passaggio automatico delle password, per cui è necessario inserirle all'avvio.\nlocalShell=Guscio locale\nthemeDescription=Il tema di visualizzazione che preferisci.\ndontAutomaticallyStartVmSshServer=Non avviare automaticamente il server SSH per le macchine virtuali quando necessario\ndontAutomaticallyStartVmSshServerDescription=Qualsiasi connessione shell a una macchina virtuale in esecuzione in un hypervisor viene effettuata tramite SSH. XPipe può avviare automaticamente il server SSH installato quando necessario. Se non vuoi che questo avvenga per motivi di sicurezza, puoi disabilitare questo comportamento con questa opzione.\nconfirmGitShareTitle=Sincronizzazione Git\nconfirmGitShareContent=Vuoi aggiungere il file selezionato al tuo repository git vault? In questo modo copierai una versione criptata del file nel tuo vault git e farai il commit delle tue modifiche. In questo modo avrai accesso al file su tutti i desktop sincronizzati.\ngitShareFileTooltip=Aggiungi un file alla directory dei dati di git vault in modo che venga sincronizzato automaticamente.\\n\\nQuesta azione può essere utilizzata solo quando il git vault è abilitato nelle impostazioni.\nperformanceMode=Modalità di prestazione\nperformanceModeDescription=Disattiva tutti gli effetti visivi non necessari per migliorare le prestazioni dell'applicazione.\ndontAcceptNewHostKeys=Non accettare automaticamente le nuove chiavi host SSH\ndontAcceptNewHostKeysDescription=XPipe accetta automaticamente le chiavi host per impostazione predefinita dai sistemi in cui il tuo client SSH non ha una chiave host nota già salvata. Tuttavia, se la chiave host conosciuta è cambiata, si rifiuterà di connettersi a meno che tu non accetti quella nuova.\\n\\nDisabilitare questo comportamento ti permette di controllare tutte le chiavi host, anche se inizialmente non c'è alcun conflitto.\nuiScale=Scala UI\nuiScaleDescription=Un valore di scala personalizzato che può essere impostato indipendentemente dalla scala di visualizzazione del sistema. I valori sono espressi in percentuale, quindi, ad esempio, un valore di 150 si tradurrà in una scala dell'interfaccia utente del 150%.\neditorProgram=Programma Editor\neditorProgramDescription=L'editor di testo predefinito da utilizzare per modificare qualsiasi tipo di dato testuale.\nwindowOpacity=Opacità della finestra\nwindowOpacityDescription=Cambia l'opacità della finestra per tenere traccia di ciò che accade in background.\nuseSystemFont=Usa il font di sistema\nopenDataDir=Directory di dati del caveau\nopenDataDirButton=Elenco di dati aperti\nopenDataDirDescription=Se vuoi sincronizzare altri file, come le chiavi SSH, tra i vari sistemi con il tuo repository git, puoi inserirli nella directory dei dati di archiviazione. Tutti i file che vi fanno riferimento avranno il loro percorso adattato automaticamente su ogni sistema sincronizzato.\nupdates=Aggiornamenti\nselectAll=Seleziona tutti\nadvanced=Avanzato\nthirdParty=Avvisi open source\neulaDescription=Leggi l'Accordo di licenza con l'utente finale per l'applicazione XPipe\nthirdPartyDescription=Visualizza le licenze open source delle librerie di terze parti\nworkspaceLock=Passphrase principale\nenableGitStorage=Abilita la sincronizzazione\nsharing=Condivisione\ngitSync=Sincronizzazione Git\nenableGitStorageDescription=Se abilitato, XPipe inizializzerà un repository git per il vault locale e vi effettuerà il commit di tutte le modifiche. Si noti che questo richiede l'installazione di git e potrebbe rallentare le operazioni di caricamento e salvataggio.\\n\\nTutte le categorie che devono essere sincronizzate devono essere contrassegnate esplicitamente come sincronizzate.\nstorageGitRemote=URL di sincronizzazione remota\nstorageGitRemoteDescription=Se impostato, XPipe preleverà automaticamente tutte le modifiche al momento del caricamento e spingerà le modifiche al repository remoto al momento del salvataggio.\\n\\nQuesto ti permette di condividere il tuo vault tra più installazioni di XPipe. Supporta gli URL HTTP e SSH, oltre alle directory locali.\nvault=Volta\nworkspaceLockDescription=Imposta una password personalizzata per criptare le informazioni sensibili memorizzate in XPipe.\\n\\nQuesto aumenta la sicurezza in quanto fornisce un ulteriore livello di crittografia per le informazioni sensibili memorizzate. All'avvio di XPipe ti verrà richiesto di inserire la password.\nuseSystemFontDescription=Controlla se utilizzare il font di sistema predefinito o il font Inter, incluso in XPipe.\ntooltipDelay=Ritardo del tooltip\ntooltipDelayDescription=La quantità di millisecondi da attendere prima che venga visualizzato un tooltip.\nfontSize=Dimensione del carattere\nwindowOptions=Opzioni della finestra\nsaveWindowLocation=Posizione della finestra di salvataggio\nsaveWindowLocationDescription=Controlla se le coordinate della finestra devono essere salvate e ripristinate al riavvio.\nstartupShutdown=Avvio / Spegnimento\nshowChildrenConnectionsInParentCategory=Mostra le categorie figlio nella categoria padre\nshowChildrenConnectionsInParentCategoryDescription=Se includere o meno tutte le connessioni situate nelle sottocategorie quando viene selezionata una determinata categoria madre.\\n\\nSe questa opzione è disattivata, le categorie si comportano come le classiche cartelle che mostrano solo il loro contenuto diretto senza includere le sottocartelle.\ncondenseConnectionDisplay=Visualizzazione condensata della connessione\ncondenseConnectionDisplayDescription=Fai in modo che ogni connessione di primo livello occupi meno spazio in verticale per consentire un elenco di connessioni più sintetico.\nopenConnectionSearchWindowOnConnectionCreation=Aprire la finestra di ricerca delle connessioni alla creazione della connessione\nopenConnectionSearchWindowOnConnectionCreationDescription=Se aprire o meno automaticamente la finestra per la ricerca delle sottoconnessioni disponibili quando si aggiunge una nuova connessione shell.\nworkflow=Flusso di lavoro\nsystem=Il sistema\napplication=Applicazione\nstorage=Conservazione\nrunOnStartup=Esegui all'avvio\ncloseBehaviour=Comportamento di uscita\ncloseBehaviourDescription=Controlla come XPipe deve procedere alla chiusura della sua finestra principale.\nlanguage=La lingua\nlanguageDescription=La lingua di visualizzazione da utilizzare. Le traduzioni vengono migliorate grazie ai contributi della comunità. Puoi aiutare il lavoro di traduzione inviando le correzioni di traduzione su GitHub.\nlightTheme=Tema luminoso\ndarkTheme=Tema scuro\nexit=Esci da XPipe\ncontinueInBackground=Continua in background\nminimizeToTray=Ridurre a icona la barra delle applicazioni\ncloseBehaviourAlertTitle=Imposta il comportamento di chiusura\ncloseBehaviourAlertTitleHeader=Seleziona cosa deve accadere alla chiusura della finestra. Tutte le connessioni attive verranno chiuse quando l'applicazione verrà chiusa.\nstartupBehaviour=Comportamento all'avvio\nstartupBehaviourDescription=Controlla il comportamento predefinito dell'applicazione desktop all'avvio di XPipe.\nclearCachesAlertTitle=Pulire la cache\nclearCachesAlertContent=Vuoi pulire tutte le cache di XPipe? In questo modo verranno eliminati tutti i dati della cache che vengono memorizzati per migliorare l'esperienza dell'utente.\nstartGui=Avvio dell'interfaccia grafica\nstartInTray=Avvia in tray\nstartInBackground=Avvio in background\nclearCaches=Cancella le cache ...\nclearCachesDescription=Cancellare tutti i dati della cache\ncancel=Annullamento\nnotAnAbsolutePath=Non è un percorso assoluto\nnotADirectory=Non è una directory\nnotAnEmptyDirectory=Non una directory vuota\nautomaticallyCheckForUpdates=Controlla gli aggiornamenti\nautomaticallyCheckForUpdatesDescription=Se abilitato, le informazioni sulle nuove versioni vengono recuperate automaticamente durante l'esecuzione di XPipe dopo un po' di tempo. Devi comunque confermare esplicitamente l'installazione di ogni aggiornamento.\nsendAnonymousErrorReports=Invia segnalazioni di errore anonime\nsendUsageStatistics=Inviare statistiche d'uso anonime\nstorageDirectory=Directory di archiviazione\nstorageDirectoryDescription=La posizione in cui XPipe deve memorizzare tutte le informazioni sulla connessione. Quando si cambia questa posizione, i dati presenti nella vecchia directory non vengono copiati in quella nuova.\nlogLevel=Livello di log\nappBehaviour=Comportamento dell'applicazione\nlogLevelDescription=Il livello di log da utilizzare per la scrittura dei file di log.\ndeveloperMode=Modalità sviluppatore\ndeveloperModeDescription=Una volta abilitato, avrai accesso a una serie di opzioni aggiuntive utili per lo sviluppo.\neditor=Editore\ncustom=Personalizzato\npasswordManager=Gestore di password\nexternalPasswordManager=Gestore di password esterno\npasswordManagerDescription=Il gestore di password installato localmente con cui integrarsi.\\n\\nSe hai installato un gestore di password, puoi configurare XPipe per recuperare le password da esso, in modo che XPipe non debba memorizzarle da solo. Una volta abilitato, ogni campo password di una connessione può essere configurato per utilizzare il gestore di password.\npasswordManagerCommandTest=Test del gestore di password\npasswordManagerCommandTestDescription=Puoi verificare se l'output è corretto se hai impostato un gestore di password.\npreferTerminalTabs=Preferisce aprire nuove schede\npreferTerminalTabsDescription=Controlla se XPipe cercherà di aprire nuove schede nel terminale scelto anziché nuove finestre. Non tutti i terminali supportano le schede.\ncustomRdpClientCommand=Comando personalizzato\ncustomRdpClientCommandDescription=Il comando da eseguire per avviare il client RDP personalizzato.\\n\\nLa stringa segnaposto $FILE sarà sostituita dal nome assoluto del file .rdp quotato quando verrà richiamata. Ricordati di citare il percorso dell'eseguibile se contiene spazi.\ncustomEditorCommand=Comando editor personalizzato\ncustomEditorCommandDescription=Il comando da eseguire per avviare l'editor personalizzato.\\n\\nLa stringa segnaposto $FILE sarà sostituita dal nome assoluto del file quotato quando verrà richiamata. Ricordati di citare il percorso dell'editor eseguibile se contiene spazi.\neditorReloadTimeout=Timeout di ricarica dell'editor\neditorReloadTimeoutDescription=Il numero di millisecondi da attendere prima di leggere un file dopo che è stato aggiornato. In questo modo si evitano problemi nei casi in cui l'editor è lento a scrivere o a rilasciare i blocchi dei file.\nencryptAllVaultData=Crittografa tutti i dati della cassaforte\nencryptAllVaultDataDescription=Se abilitata, ogni parte dei dati di connessione al vault sarà crittografata con la chiave di crittografia del vault dell'utente e non solo i segreti contenuti in quei dati. Questo aggiunge un ulteriore livello di sicurezza per altri parametri come nomi utente, nomi di host e così via, che non sono crittografati di default nel vault.\\n\\nQuesta opzione renderà inutile la cronologia e i diff del tuo vault git, in quanto non potrai più vedere le modifiche originali, ma solo quelle binarie.\nvaultSecurity=Sicurezza del caveau\ndeveloperDisableUpdateVersionCheck=Disabilita il controllo della versione dell'aggiornamento\ndeveloperDisableUpdateVersionCheckDescription=Controlla se il verificatore di aggiornamenti ignora il numero di versione quando cerca un aggiornamento.\ndeveloperDisableGuiRestrictions=Disabilita le restrizioni della GUI\ndeveloperDisableGuiRestrictionsDescription=Controlla se alcune azioni disabilitate possono essere eseguite dall'interfaccia utente.\ndeveloperShowHiddenEntries=Mostra voci nascoste\ndeveloperShowHiddenEntriesDescription=Se abilitato, le fonti di dati nascoste e interne verranno mostrate.\ndeveloperShowHiddenProviders=Mostra fornitori nascosti\ndeveloperShowHiddenProvidersDescription=Controlla se i provider di connessione e di origine dati nascosti e interni saranno mostrati nella finestra di dialogo di creazione.\ndeveloperDisableConnectorInstallationVersionCheck=Disabilita il controllo della versione del connettore\ndeveloperDisableConnectorInstallationVersionCheckDescription=Controlla se il verificatore di aggiornamenti ignora il numero di versione quando controlla la versione di un connettore XPipe installato su un computer remoto.\nshellCommandTest=Test dei comandi di shell\nshellCommandTestDescription=Esegui un comando nella sessione di shell utilizzata internamente da XPipe.\nterminal=Terminale\nterminalType=Emulatore di terminale\nterminalConfiguration=Configurazione del terminale\nterminalCustomization=Personalizzazione del terminale\neditorConfiguration=Configurazione dell'editor\ndefaultApplication=Applicazione predefinita\ninitialSetup=Configurazione iniziale\nterminalTypeDescription=Il terminale predefinito da utilizzare per aprire le connessioni shell.\\n\\nIl livello di supporto delle funzioni varia a seconda del terminale e ognuno di essi è contrassegnato come consigliato o non consigliato. La tua esperienza d'uso sarà migliore quando userai un terminale consigliato.\nprogram=Programma\ncustomTerminalCommand=Comando di terminale personalizzato\ncustomTerminalCommandDescription=Il comando da eseguire per aprire il terminale personalizzato con un determinato comando.\\n\\nXPipe creerà uno script di lancio temporaneo da eseguire sul tuo terminale. La stringa segnaposto $CMD nel comando fornito verrà sostituita dallo script di avvio effettivo quando verrà richiamato. Ricordati di citare il percorso dell'eseguibile del tuo terminale se contiene spazi.\nclearTerminalOnInit=Cancella il terminale all'avvio\nclearTerminalOnInitDescription=Se abilitato, XPipe esegue un comando di cancellazione dopo l'avvio di una nuova sessione di terminale per rimuovere qualsiasi output non necessario stampato all'avvio della sessione di terminale.\ndontCachePasswords=Non memorizzare nella cache le password richieste\ndontCachePasswordsDescription=Controlla se le password interrogate devono essere memorizzate nella cache interna di XPipe in modo da non doverle inserire nuovamente nella sessione corrente.\\n\\nSe questo comportamento è disattivato, dovrai reinserire le credenziali richieste ogni volta che il sistema le richiederà.\ndenyTempScriptCreation=Rifiuta la creazione di script temporanei\ndenyTempScriptCreationDescription=Per realizzare alcune delle sue funzionalità, XPipe a volte crea degli script di shell temporanei sul sistema di destinazione per consentire una facile esecuzione di semplici comandi. Questi non contengono informazioni sensibili e vengono creati solo a scopo di implementazione.\\n\\nSe questo comportamento è disattivato, XPipe non creerà alcun file temporaneo su un sistema remoto. Questa opzione è utile in contesti ad alta sicurezza in cui ogni modifica del file system viene monitorata. Se questa opzione è disattivata, alcune funzionalità, ad esempio gli ambienti di shell e gli script, non funzioneranno come previsto.\ndisableCertutilUse=Disabilitare l'uso di certutil su Windows\nuseLocalFallbackShell=Usa la shell di fallback locale\nuseLocalFallbackShellDescription=Passa all'utilizzo di un'altra shell locale per gestire le operazioni locali. Si tratta di PowerShell su Windows e di bourne shell su altri sistemi.\\n\\nQuesta opzione può essere utilizzata nel caso in cui la normale shell locale predefinita sia disabilitata o in qualche modo danneggiata. Alcune funzioni potrebbero però non funzionare come previsto quando questa opzione è abilitata.\ndisableCertutilUseDescription=A causa di diverse carenze e bug di cmd.exe, gli script di shell temporanei vengono creati con certutil utilizzandolo per decodificare l'input base64, poiché cmd.exe si interrompe con input non ASCII. XPipe può anche utilizzare PowerShell per questo scopo, ma sarà più lento.\\n\\nQuesto disabilita l'uso di certutil sui sistemi Windows per realizzare alcune funzionalità e si ripiega su PowerShell. Questo potrebbe far piacere ad alcuni AV che bloccano l'uso di certutil.\ndisableTerminalRemotePasswordPreparation=Disabilita la preparazione della password remota del terminale\ndisableTerminalRemotePasswordPreparationDescription=In situazioni in cui è necessario stabilire nel terminale una connessione shell remota che attraversa più sistemi intermedi, potrebbe essere necessario preparare le password richieste su uno dei sistemi intermedi per consentire la compilazione automatica di eventuali richieste.\\n\\nSe non vuoi che le password vengano mai trasferite a un sistema intermedio, puoi disabilitare questo comportamento. Le password intermedie richieste verranno quindi richieste nel terminale stesso all'apertura.\nmore=Di più\ntranslate=Traduzioni\nallConnections=Tutte le connessioni\nallScripts=Tutti gli script\nallIdentities=Tutte le identità\nsynced=Sincronizzato\npredefined=Predefinito\nsamples=Campioni\ngoodMorning=Buongiorno\ngoodAfternoon=Buon pomeriggio\ngoodEvening=Buona sera\naddVisual=Visual ...\naddDesktop=Desktop ...\nssh=SSH\nsshConfiguration=Configurazione SSH\nsize=Dimensione\nattributes=Attributi\nmodified=Modificato\nowner=Proprietario\nupdateReadyTitle=Aggiornamento a $VERSION$ pronto\ntemplates=Modelli\nretry=Riprova\nretryAll=Riprova tutti\nreplace=Sostituire\nreplaceAll=Sostituisci tutto\nhibernateBehaviour=Comportamento di ibernazione\nhibernateBehaviourDescription=Controlla il comportamento dell'applicazione quando il sistema viene messo in ibernazione o a riposo.\noverview=Panoramica\nhistory=La storia\nskipAll=Salta tutto\nnotes=Note\naddNotes=Aggiungi note\norder=Riordinare\nkeepFirst=Mantieni per primo\nkeepLast=Mantieni l'ultimo\npinToTop=Spillo in alto\nunpinFromTop=Stacca dall'alto\norderAheadOf=Ordina prima di ...\nclearIndex=Indice di reset\nhttpServer=Server HTTP\nmcpServer=Server MCP\napiKey=Chiave API\napiKeyDescription=La chiave API per autenticare le richieste API del demone XPipe. Per ulteriori informazioni sulle modalità di autenticazione, consulta la documentazione generale dell'API.\ndisableApiAuthentication=Disabilita l'autenticazione API\ndisableApiAuthenticationDescription=Disabilita tutti i metodi di autenticazione richiesti in modo che qualsiasi richiesta non autenticata venga gestita.\\n\\nL'autenticazione dovrebbe essere disabilitata solo per scopi di sviluppo.\napi=API\nstoreIntroImportContent=Stai già usando XPipe su un altro sistema? Sincronizza le connessioni esistenti su più sistemi attraverso un repository git remoto. Puoi anche effettuare la sincronizzazione in un secondo momento, in qualsiasi momento, se non è ancora stata impostata.\nstoreIntroImportButton=Sincronizzazione delle connessioni ...\nstoreIntroImportHeader=Importazione di connessioni\nshowNonRunningChildren=Mostra i bambini non in esecuzione\nhttpApi=API HTTP\nisOnlySupportedLimit=è supportato solo con una licenza professionale quando ci sono più di $COUNT$ connessioni\nareOnlySupportedLimit=sono supportati solo con una licenza professionale quando ci sono più di $COUNT$ connessioni\nenabled=Abilitato\nenableGitStoragePtbDisabled=La sincronizzazione Git è disabilitata per le build di test pubbliche per evitare l'uso con i repository git di rilascio regolari e per scoraggiare l'uso di una build PTB come guida quotidiana.\ncopyId=Copia dell'ID API\nrequireDoubleClickForConnections=Richiede un doppio clic per le connessioni\nrequireDoubleClickForConnectionsDescription=Se abilitato, devi fare doppio clic sulle connessioni per avviarle. Questo è utile se sei abituato a fare doppio clic.\nclearTransferDescription=Cancella la selezione\nselectTab=Seleziona scheda\ncloseTab=Chiudi scheda\ncloseOtherTabs=Chiudere altre schede\ncloseAllTabs=Chiudi tutte le schede\ncloseLeftTabs=Chiudere le schede a sinistra\ncloseRightTabs=Chiudere le schede a destra\naddSerial=Seriale ...\nconnect=Collegare\nworkspaces=Spazi di lavoro\nmanageWorkspaces=Gestire gli spazi di lavoro\naddWorkspace=Aggiungi spazio di lavoro ...\nworkspaceAdd=Aggiungere un nuovo spazio di lavoro\nworkspaceAddDescription=Gli spazi di lavoro sono configurazioni distinte per l'esecuzione di XPipe. Ogni workspace ha una directory di dati in cui vengono memorizzati tutti i dati a livello locale. Questi includono i dati di connessione, le impostazioni e altro ancora.\\n\\nSe utilizzi la funzione di sincronizzazione, puoi anche scegliere di sincronizzare ogni workspace con un repository git diverso.\nworkspaceName=Nome dello spazio di lavoro\nworkspaceNameDescription=Il nome di visualizzazione dell'area di lavoro\nworkspacePath=Percorso dello spazio di lavoro\nworkspacePathDescription=La posizione della directory dei dati dell'area di lavoro\nworkspaceCreationAlertTitle=Creazione di uno spazio di lavoro\ndeveloperForceSshTty=Forza SSH TTY\ndeveloperForceSshTtyDescription=Fai in modo che tutte le connessioni SSH allocino una pty per testare il supporto di una stderr e di una pty mancanti.\ndeveloperDisableSshTunnelGateways=Disabilita il tunneling del gateway SSH\ndeveloperDisableSshTunnelGatewaysDescription=Non utilizzare le sessioni di tunnel per i gateway e connettiti direttamente al sistema.\nttyWarning=La connessione ha allocato forzatamente una pty/tty e non fornisce un flusso stderr separato.\\n\\nQuesto potrebbe causare alcuni problemi.\\n\\nSe puoi, cerca di fare in modo che il comando di connessione non allarghi una pty.\nxshellSetup=Configurazione di Xshell\ntermiusSetup=Configurazione di Termius\ntryPtbDescription=Prova subito le nuove funzionalità nelle build per sviluppatori di XPipe\nconfirmVaultUnencryptTitle=Conferma la decodifica del caveau\nconfirmVaultUnencryptContent=Vuoi davvero disattivare la crittografia avanzata del caveau? Questo rimuoverà la crittografia aggiuntiva per i dati memorizzati e sovrascriverà i dati esistenti.\nenableHttpApi=Abilita l'API HTTP\nenableHttpApiDescription=Abilita l'API, consentendo ai programmi esterni di chiamare il demone XPipe per eseguire azioni con le connessioni gestite.\nchooseCustomIcon=Scegli un'icona personalizzata\ngitVault=Cassaforte Git\nfileBrowser=Browser di file\nconfirmAllDeletions=Conferma tutte le eliminazioni\nconfirmAllDeletionsDescription=Se mostrare una finestra di conferma per tutte le operazioni di cancellazione. Per impostazione predefinita, solo le directory richiedono una conferma.\nyesterday=Ieri\ngreen=Verde\nyellow=Giallo\nblue=Blu\nred=Rosso\ncyan=Ciano\npurple=Viola\nasktextAlertTitle=Prompt\nfileWriteSudoTitle=Scrittura di file Sudo\nfileWriteSudoContent=Il file che stai cercando di scrivere non concede i permessi di scrittura al tuo utente. Vuoi scrivere questo file come root con sudo? In questo modo, l'utente verrà automaticamente elevato a root con le credenziali esistenti o tramite un prompt.\ndontAllowTerminalRestart=Non consentire il riavvio del terminale\ndontAllowTerminalRestartDescription=Per impostazione predefinita, le sessioni del terminale possono essere riavviate dopo la loro conclusione dall'interno del terminale stesso. Per consentire ciò, XPipe accetterà le seguenti richieste esterne dal terminale per avviare nuovamente la sessione\\n\\nXPipe non ha alcun controllo sul terminale e sulla provenienza di questa chiamata, quindi anche le applicazioni locali malintenzionate possono utilizzare questa funzionalità per avviare connessioni attraverso XPipe. Disabilitando questa funzionalità si evita questo scenario.\nopenDocumentation=Aprire la documentazione\nopenDocumentationDescription=Visita la pagina dei documenti di XPipe per questo problema\nrenameAll=Rinomina tutti\nlogging=Registrazione\nenableTerminalLogging=Abilita la registrazione del terminale\nenableTerminalLoggingDescription=Abilita la registrazione lato client per tutte le sessioni del terminale. Tutti gli input e gli output della sessione del terminale vengono scritti in un file di log della sessione. Le informazioni sensibili, come le richieste di password, non vengono registrate.\nterminalLoggingDirectory=Registri di sessione del terminale\nterminalLoggingDirectoryDescription=Tutti i registri vengono memorizzati nella directory dei dati di XPipe sul tuo sistema locale.\nopenSessionLogs=Registri di sessione aperti\nsessionLogging=Registrazione del terminale\nsessionActive=Per questa connessione è in corso una sessione in background.\\n\\nPer interrompere manualmente questa sessione, clicca sull'indicatore di stato.\nskipValidation=Convalida del salto\nscriptsIntroHeader=Informazioni sugli script\nscriptsIntroContent=Puoi eseguire gli script all'avvio della shell, nel browser dei file e su richiesta. Puoi creare script all'interno di XPipe o importarne di esistenti dal tuo sistema locale o da un repository git remoto.\nscriptsIntroBottomHeader=Utilizzo di script\nscriptsIntroBottomContent=Ci sono diversi esempi di script per iniziare. Puoi cliccare sul pulsante di modifica dei singoli script per vedere come sono stati implementati. Gli script devono innanzitutto essere abilitati all'esecuzione e alla visualizzazione nei menu; per questo c'è una levetta su ogni script.\nscriptsIntroBottomButton=Iniziare\nscriptSourcesIntroHeader=Fonti di script\nscriptSourcesIntroContent=Puoi aggiungere fonti di script personalizzate per avere accesso immediato a un'intera collezione di script di shell. Sono supportate sia le fonti locali che i repository git remoti. Tutti gli script rilevati dalla sorgente saranno disponibili automaticamente.\nscriptSourcesIntroButton=Aggiungi fonte ...\ncheckForSecurityUpdates=Controlla gli aggiornamenti di sicurezza\ncheckForSecurityUpdatesDescription=XPipe può verificare la presenza di potenziali aggiornamenti di sicurezza separatamente dai normali aggiornamenti delle funzioni. Se questa opzione è attivata, l'installazione degli aggiornamenti di sicurezza più importanti viene consigliata anche se il normale controllo degli aggiornamenti è disattivato.\\n\\nDisattivando questa impostazione, non verrà eseguita alcuna richiesta di versione esterna e non riceverai alcuna notifica sugli aggiornamenti di sicurezza.\nclickToDock=Clicca per agganciare il terminale\nterminalStarting=In attesa dell'avvio del terminale ...\npinTab=Scheda Pin\nunpinTab=Scheda da staccare\npinned=Appuntato\nenableConnectionHubTerminalDocking=Abilitazione del collegamento hub terminale docking\nenableConnectionHubTerminalDockingDescription=Puoi agganciare le finestre del terminale alla finestra dell'applicazione XPipe nell'hub di connessione per simulare un terminale integrato. Le finestre del terminale vengono gestite da XPipe in modo da essere sempre inserite nel dock.\nenableFileBrowserTerminalDocking=Abilita il docking del terminale del browser di file\nenableFileBrowserTerminalDockingDescription=Puoi agganciare le finestre del terminale alla finestra dell'applicazione XPipe nel browser dei file per simulare un terminale integrato. Le finestre del terminale vengono poi gestite da XPipe in modo da essere sempre inserite nel dock.\ndownloadsDirectory=Directory di download personalizzata\ndownloadsDirectoryDescription=La directory personalizzata in cui inserire i file scaricati quando si fa clic sul pulsante sposta nei download. Per impostazione predefinita, XPipe utilizzerà la directory dei download dell'utente.\npinLocalMachineOnStartup=Blocca la scheda della macchina locale all'avvio\npinLocalMachineOnStartupDescription=Apre automaticamente una scheda del computer locale e la blocca. Questa funzione è utile se utilizzi spesso un browser di file suddiviso con il computer locale e il file system remoto aperti.\nterminalErrorDescription=Questo errore è un terminale e XPipe non può continuare senza risolvere il problema.\ngroupName=Nome del gruppo\nchmodPermissions=Nuovi permessi\neditFilesWithDoubleClick=Modifica i file con un doppio clic\neditFilesWithDoubleClickDescription=Se abilitato, facendo doppio clic sui file, questi verranno aperti direttamente nell'editor di testo invece di mostrare il menu contestuale.\ncensorMode=Modalità censura\ncensorModeDescription=Sfuma qualsiasi informazione come nomi di host, nomi di utenti, nomi di connessioni e altro.\\n\\nÈ utile se intendi screenshottare o condividere XPipe e non vuoi perdere alcuna informazione.\naddIdentity=Identità ...\nidentities=Identità\naddMacro=Azione ...\nidentitiesIntroHeader=Informazioni sulle identità\nidentitiesIntroContent=Se riutilizzi combinazioni comuni di nomi utente, password e chiavi, potrebbe essere utile creare identità riutilizzabili. In questo modo potrai fare rapidamente riferimento ad esse quando aggiungi nuove connessioni.\nidentitiesIntroBottomHeader=Condivisione di identità\nidentitiesIntroBottomContent=Puoi aggiungere le identità localmente o anche sincronizzarle nel repository git quando questo è abilitato. Questo permette di condividere selettivamente le identità su più sistemi e con altri membri del team.\nidentitiesIntroBottomButton=Configurazione della sincronizzazione\nidentitiesIntroButton=Crea identità\nuserName=Nome utente\nuserAuth=Autenticazione con password basata sull'utente\ngroupAuth=Autenticazione segreta di gruppo\nteam=Squadra\nteamSettings=Impostazioni del team\nteamVaults=I caveau del team\nvaultTypeNameDefault=Cassaforte predefinita\nvaultTypeNameLegacy=Una cassaforte personale ereditata\nvaultTypeNamePersonal=Cassaforte personale\nvaultTypeNameTeam=Cassaforte del team\nteamVaultsDescription=I vault di gruppo consentono a più utenti e gruppi di avere un accesso sicuro a un vault condiviso. Puoi configurare le connessioni e le identità in modo che siano condivise da tutti gli utenti o che siano disponibili solo per singoli utenti e gruppi, criptandole con la loro chiave. Gli altri utenti del vault non possono accedere alle connessioni e alle identità personali e di gruppo se non hanno accesso alla chiave.\nvaultTypeContentDefault=Attualmente stai utilizzando un vault predefinito senza utente e con una passphrase personalizzata. I segreti sono crittografati con la chiave del vault locale. Puoi passare a un vault personale creando un account utente del vault. In questo modo potrai criptare i segreti del vault con una passphrase personale che dovrai inserire a ogni accesso per sbloccare il vault.\nvaultTypeContentLegacy=Attualmente stai utilizzando una cassaforte personale per il tuo utente. I segreti sono criptati con la tua passphrase personale. Questa compatibilità legacy ha funzioni limitate e non può essere aggiornata con un vault di squadra.\nvaultTypeContentPersonal=Attualmente stai utilizzando una cassaforte personale per il tuo utente. I segreti sono criptati con la tua passphrase personale. Puoi passare a un vault di gruppo aggiungendo altri utenti del vault o aggiungendo una configurazione di accesso basata su gruppi.\nvaultTypeContentTeam=Attualmente stai utilizzando un vault di gruppo, che consente a più utenti di avere accesso sicuro a un vault condiviso. Puoi configurare le connessioni e le identità in modo che siano condivise da tutti gli utenti oppure che siano disponibili solo per il tuo utente personale o per il tuo gruppo, criptandole con la tua chiave personale o di gruppo. Gli altri utenti del vault non possono accedere alle tue connessioni e identità personali e di gruppo se non hanno accesso alla chiave.\ngroupManagement=Gestione dei gruppi\ngroupManagementEmpty=Gestione dei gruppi\ngroupManagementDescription=Gestisci i gruppi di vault esistenti o creane di nuovi. Ogni gruppo di vault ha la sua chiave segreta individuale che viene utilizzata per criptare le connessioni e le identità che devono essere disponibili solo al gruppo e non ad altri.\ngroupManagementEmptyDescription=Gestisci i gruppi di vault esistenti o creane di nuovi. Ogni gruppo di vault ha la sua chiave segreta individuale che viene utilizzata per criptare le connessioni e le identità che devono essere disponibili solo al gruppo e non ad altri.\\n\\nGli account di gruppo per un team sono supportati nel piano professionale.\nuserManagement=Gestione degli utenti\nuserManagementEmpty=Gestione degli utenti\nuserManagementDescription=Gestisci gli utenti del vault esistenti o creane di nuovi. Ogni utente del vault ha una password individuale che viene utilizzata per criptare le connessioni e le identità che devono essere disponibili solo all'utente e non ad altri.\nuserManagementEmptyDescription=Gestisci gli utenti del vault esistenti o creane di nuovi. Ogni utente del vault ha una password individuale che viene utilizzata per crittografare le connessioni e le identità che devono essere disponibili solo all'utente e non ad altri. Crea un utente per te stesso per poter criptare le connessioni e le identità con la tua chiave personale.\\n\\nNell'edizione community è supportato un singolo account utente. Nel piano professionale sono supportati più account utente per un team.\nuserIntroHeader=Gestione degli utenti\nuserIntroContent=Crea il primo account utente per te stesso per iniziare. Questo ti permette di bloccare l'area di lavoro con una password.\naddReusableIdentity=Aggiungi un'identità riutilizzabile\nusers=Utenti\nsyncVault=Sincronizzazione del caveau\nsyncVaultDescription=Per sincronizzare il tuo vault con più sistemi o con più membri del team, attiva la sincronizzazione git per questo vault.\nenableGitSync=Abilita la sincronizzazione git\nbrowseVault=Dati in cassaforte\nbrowseVaultDescription=Puoi dare un'occhiata alla directory del vault con il tuo file manager nativo. Si noti che le modifiche esterne non sono consigliate e possono causare una serie di problemi.\nbrowseVaultButton=Sfogliare il caveau\nvaultUsers=Utenti del caveau\ncreateHeapDump=Creare un heap dump\ncreateHeapDumpDescription=Dump del contenuto della memoria su file per risolvere i problemi di utilizzo della memoria\ninitializingApp=Caricamento delle connessioni\ncheckingLicense=Verifica della licenza\nloadingGit=Sincronizzazione con git repo\nloadingGpg=Avviare il demone GnuPG per git\nloadingSettings=Impostazioni di caricamento\nloadingConnections=Caricamento delle connessioni\nunlockingVault=Sblocco della cassaforte\nloadingUserInterface=Interfaccia utente di caricamento\nptbNotice=Avviso per la build di prova pubblica\nuserDeletionTitle=Eliminazione di un utente\nuserDeletionContent=Vuoi eliminare questo utente del vault? In questo modo tutte le tue identità personali e i segreti di connessione verranno crittografati nuovamente utilizzando la chiave del vault disponibile per tutti gli utenti. L'operazione richiederà un po' di tempo e XPipe si riavvierà per applicare le modifiche agli utenti.\ngroupDeletionTitle=Eliminazione di un gruppo\ngroupDeletionContent=Vuoi eliminare questo gruppo di vault? In questo modo tutte le identità e i segreti di connessione riservati al gruppo verranno crittografati nuovamente utilizzando la chiave del vault disponibile per tutti gli utenti. L'operazione richiederà un po' di tempo e XPipe si riavvierà per applicare le modifiche al gruppo.\nkillTransfer=Trasferimento di uccisioni\ndestination=Destinazione\nconfiguration=Configurazione\nnewFile=Nuovo file\nnewLink=Nuovo link\nlinkName=Nome del link\nscanConnections=Trova le connessioni disponibili ...\nobserve=Iniziare a osservare\nstopObserve=Smetti di osservare\ncreateShortcut=Creare un collegamento al desktop\nbrowseFiles=Sfogliare i file\nclone=Clone\ntargetPath=Percorso di destinazione\nnewDirectory=Nuova directory\ncopyShareLink=Copia link\nselectStore=Seleziona il negozio\nsaveSource=Salva per dopo\nexecute=Eseguire\ndeleteChildren=Rimuovi tutti i bambini\nscriptGroupDescriptionDescription=Dai a questo gruppo una descrizione opzionale\nabstractHostDescriptionDescription=Assegna a questo host una descrizione opzionale\nselectSource=Seleziona la fonte\ncommandLineRead=Aggiornamento\ncommandLineWrite=Scrivi\nadditionalOptions=Opzioni aggiuntive\ninput=Ingresso\nmachine=Macchina\nopen=Aprire\nedit=Modifica\nscriptContents=Contenuti dello script\nscriptContentsDescription=I comandi di script da eseguire\nsnippets=Dipendenze degli script\nsnippetsDescription=Altri script da eseguire prima\nsnippetsDependenciesDescription=Tutti i possibili script che devono essere eseguiti, se applicabile\nisDefault=Eseguito su init in tutte le shell compatibili\nbringToShells=Porta a tutte le shell compatibili\nisDefaultGroup=Esegui tutti gli script di gruppo all'avvio della shell\nexecutionType=Tipo di esecuzione\nexecutionTypeDescription=In quali contesti utilizzare questo script\nminimumShellDialect=Tipo di shell\nminimumShellDialectDescription=Il tipo di shell in cui eseguire questo script\ndumbOnly=Muto\nterminalOnly=Terminale\nboth=Entrambi\nshouldElevate=Dovrebbe elevare\nshouldElevateDescription=Se eseguire questo script con permessi elevati o meno\nscript.displayName=Script di shell\nscript.displayDescription=Crea uno script di shell riutilizzabile\nscriptGroup.displayName=Gruppo di script\nscriptGroup.displayDescription=Raggruppa gli script e li organizza all'interno di\nscriptGroup=Gruppo\nscriptGroupDescription=Il gruppo a cui assegnare questo script\nscriptGroupGroupDescription=Il gruppo padre opzionale a cui assegnare questo gruppo di script\nopenInNewTab=Aprire una nuova scheda\nexecuteInBackground=in background\nexecuteInTerminal=in $TERM$\nback=Torna indietro\nbrowseInWindowsExplorer=Sfogliare in Windows explorer\nbrowseInDefaultFileManager=Sfoglia nel file manager predefinito\nbrowseInFinder=Sfoglia in finder\ncopy=Copia\npaste=Incolla\ncopyLocation=Posizione di copia\nabsolutePaths=Percorsi assoluti\nabsoluteLinkPaths=Percorsi di collegamento assoluti\nabsolutePathsQuoted=Percorsi quotati assoluti\nfileNames=Nomi di file\nlinkFileNames=Nomi di file di collegamento\nfileNamesQuoted=Nomi di file (citati)\ndeleteFile=Eliminare $FILE$\neditWithEditor=Modifica con $EDITOR$\nfollowLink=Segui il link\ngoForward=Vai avanti\nshowDetails=Mostra i dettagli\nshowDetailsDescription=Mostra la traccia dello stack dell'errore\nopenFileWith=Apri con ...\nopenWithDefaultApplication=Apri con l'applicazione predefinita\nrename=Rinominare\nrun=Esegui\nopenInTerminal=Aprire nel terminale\nfile=File\ndirectory=Elenco\nsymbolicLink=Collegamento simbolico\ndesktopEnvironment.displayName=Ambiente desktop\ndesktopEnvironment.displayDescription=Crea una configurazione riutilizzabile dell'ambiente desktop remoto\ndesktopHost=Host desktop\ndesktopHostDescription=La connessione al desktop da utilizzare come base\ndesktopShellDialect=Dialetto della shell\ndesktopShellDialectDescription=Il dialetto della shell da utilizzare per eseguire script e applicazioni\ndesktopSnippets=Snippet di script\ndesktopSnippetsDescription=Elenco di snippet di script riutilizzabili da eseguire per primi\ndesktopInitScript=Script di avvio\ndesktopInitScriptDescription=Comandi di avvio specifici per questo ambiente\ndesktopTerminal=Applicazione terminale\ndesktopTerminalDescription=Il terminale da usare sul desktop per avviare gli script\ndesktopApplication.displayName=Applicazione desktop\ndesktopApplication.displayDescription=Eseguire un'applicazione su un desktop remoto\ndesktopBase=Desktop\ndesktopBaseDescription=Il desktop su cui eseguire questa applicazione\ndesktopEnvironmentBase=Ambiente desktop\ndesktopEnvironmentBaseDescription=L'ambiente desktop su cui eseguire questa applicazione\ndesktopApplicationPath=Percorso dell'applicazione\ndesktopApplicationPathDescription=Il percorso dell'eseguibile da eseguire\ndesktopApplicationArguments=Argomenti\ndesktopApplicationArgumentsDescription=Gli argomenti opzionali da passare all'applicazione\ndesktopCommand.displayName=Comando del desktop\ndesktopCommand.displayDescription=Eseguire un comando in un ambiente desktop remoto\ndesktopCommandScript=Comandi\ndesktopCommandScriptDescription=I comandi da eseguire nell'ambiente\nservice.displayName=Servizio\nservice.displayDescription=Inoltrare un servizio remoto al computer locale\nserviceLocalPort=Porta locale esplicita\nserviceLocalPortDescription=La porta locale a cui inoltrare, altrimenti ne viene utilizzata una a caso\nserviceRemotePort=Porta remota\nserviceRemotePortDescription=La porta su cui è in esecuzione il servizio\nserviceHost=Servizio host\nserviceHostDescription=L'host su cui è in esecuzione il servizio\nopenWebsite=Sito web aperto\ncustomServiceGroup.displayName=Gruppo di servizio\ncustomServiceGroup.displayDescription=Raggruppa più servizi in un'unica categoria\ninitScript=Script di avvio - Eseguito all'avvio della shell\nshellScript=Script di sessione di shell - Rendere disponibile uno script da eseguire durante una sessione di shell\nrunnableScript=Script eseguibile - Consente l'esecuzione di uno script direttamente dall'hub di connessione\nfileScript=File script - Consente di richiamare uno script per i file selezionati nel browser dei file\nrunScript=Esegui script\ncopyUrl=Copia URL\nfixedServiceGroup.displayName=Gruppo di servizio\nfixedServiceGroup.displayDescription=Elenco dei servizi disponibili su un sistema\nmappedService.displayName=Servizio\nmappedService.displayDescription=Interagire con un servizio esposto da un contenitore\ncustomService.displayName=Servizio\ncustomService.displayDescription=Aprire o chiudere automaticamente una porta di servizio remota sulla macchina locale\nfixedService.displayName=Servizio\nfixedService.displayDescription=Utilizzare un servizio predefinito\nnoServices=Nessun servizio disponibile\nhasServices=$COUNT$ servizi disponibili\nhasService=$COUNT$ servizio disponibile\nnoConnections=Nessuna connessione disponibile\nhasConnections=$COUNT$ connessioni disponibili\nhasConnection=$COUNT$ connessione disponibile\nopenHttp=Servizio HTTP aperto\nopenHttps=Servizio HTTPS aperto\nnoScriptsAvailable=Non sono disponibili script abilitati e compatibili\nscriptsDisabled=Script disabilitati\nchangeIcon=Cambia icona\ninit=Init\nshell=Shell\nhub=Hub\nscript=script\ngenericScript=Generico\ngradleTasks=Attività di Gradle\nrunTask=Esegui attività\narchiveName=Nome dell'archivio\ncompress=Comprimere\ncompressContents=Comprimere i contenuti\nuntarHere=Non scrivere qui\nuntarDirectory=Untar a $DIR$\nunzipDirectory=Decomprimere in $DIR$\nunzipHere=Decomprimi qui\nrequiresRestart=Richiede un riavvio per essere applicato.\ndownload=Scarica\nservicePath=Percorso del servizio\nservicePathDescription=Il sottopercorso opzionale quando si apre l'URL in un browser\nactive=Attivo\ninactive=Inattivo\nstarting=Avvio\nremotePort=Porta remota\nremotePortNumber=Porta remota $PORT$\nuserIdentity=Identità personale\nglobalIdentity=Identità globale\nidentityChoice=Identità dell'utente\nidentityChoiceDescription=Scegliere un'identità predefinita o specificare i dettagli di login solo per questa connessione\ndefineNewIdentityOrSelect=Inserisci un nuovo testo o scegli quello esistente\nlocalIdentity.displayName=Identità locale\nlocalIdentity.displayDescription=Creare un'identità riutilizzabile per questo desktop locale\nsyncedIdentity.displayName=Identità sincronizzata\nsyncedIdentity.displayDescription=Creare un'identità riutilizzabile e sincronizzata tra i vari sistemi\nlocalIdentity=Identità locale\nkeyNotSynced=Il file chiave non è ancora sincronizzato con il repository git. Usa il pulsante Aggiungi a git per aggiungere il file chiave.\nusernameDescription=Il nome utente con cui accedere\nidentity.displayName=Identità\nidentity.displayDescription=Creare un'identità riutilizzabile per le connessioni\nlocal=Locale\nshared=Globale\nuserDescription=Il nome utente o l'identità predefinita con cui effettuare il login\nidentityAccessLevel=Livello di accesso\nidentityPerUser=Accesso all'identità personale\nidentityPerUserDescription=Limita l'accesso a questa identità e alle connessioni ad essa associate solo all'utente del tuo vault\nidentityPerUserDisabled=Accesso all'identità personale (disabilitato)\nidentityPerUserDisabledDescription=Limita l'accesso a questa identità e alle connessioni ad essa associate solo all'utente del vault (Richiede la configurazione del team)\nidentityPerGroup=Accesso all'identità solo per gruppi\nidentityPerGroupDescription=Limita l'accesso a questa identità e alle connessioni ad essa associate solo a questo gruppo di vault\nlibrary=Biblioteca\nlocation=Posizione\nkeyAuthentication=Autenticazione basata su chiavi\nkeyAuthenticationDescription=Il metodo di autenticazione da utilizzare se è richiesta l'autenticazione a chiave\nlocationDescription=Il percorso del file della chiave privata corrispondente\nkeyFile=File chiave locale\nkeyPassword=Passphrase\nkey=Chiave di lettura\nyubikeyPiv=Yubikey PIV\npageant=Pagina\ngpgAgent=Agente GPG\ncustomPkcs11Library=Libreria PKCS#11 personalizzata\nsshAgent=Agente OpenSSH\nnone=Non c'è nulla di selezionato\nindex=Indice ...\notherExternal=Altro agente esterno\nsync=Sincronizzazione\nvaultSync=Sincronizzazione del caveau\ncustomUsername=Nome utente\ncustomUsernameDescription=L'utente alternativo opzionale come cui accedere\ncustomUsernamePassword=Password\ncustomUsernamePasswordDescription=La password dell'utente da utilizzare quando è richiesta l'autenticazione sudo\nshowInternalPods=Mostra i pod interni\nshowAllNamespaces=Mostra tutti gli spazi dei nomi\nshowInternalContainers=Mostra i contenitori interni\nrefresh=Aggiornare\nvmwareGui=Avvio dell'interfaccia grafica\nmonitorVm=Monitoraggio VM\naddCluster=Aggiungi cluster ...\nshowNonRunningInstances=Mostra le istanze non in esecuzione\nvmwareGuiDescription=Se avviare una macchina virtuale in background o in una finestra.\nvmwareEncryptionPassword=Password di crittografia\nvmwareEncryptionPasswordDescription=La password opzionale utilizzata per criptare la VM.\nvmPasswordDescription=La password richiesta per l'utente ospite.\nvmPassword=Password utente\nvmUser=Utente ospite\nrunTempContainer=Eseguire un contenitore temporaneo\nvmUserDescription=Il nome utente dell'utente ospite principale\ndockerTempRunAlertTitle=Eseguire un contenitore temporaneo\ndockerTempRunAlertHeader=Esegue un processo di shell in un contenitore temporaneo che verrà rimosso automaticamente una volta arrestato.\nimageName=Nome dell'immagine\nimageNameDescription=L'identificatore dell'immagine del contenitore da utilizzare\ncontainerName=Nome del contenitore\ncontainerNameDescription=Il nome del contenitore personalizzato opzionale\nvm=Macchina virtuale\nvmDescription=Il file di configurazione associato.\nvmwareScan=Ipervisori desktop VMware\nvmwareMachine.displayName=Macchina virtuale VMware\nvmwareMachine.displayDescription=Connettersi a una macchina virtuale tramite SSH\nvmwareInstallation.displayName=Installazione dell'hypervisor desktop VMware\nvmwareInstallation.displayDescription=Interagire con le macchine virtuali installate tramite la sua CLI\nstart=Iniziare\nstop=Fermati\npause=Pausa\nrdpTunnelHost=Host di destinazione\nrdpTunnelHostDescription=La connessione SSH su cui eseguire il tunnel della connessione RDP\nrdpTunnelUsername=Nome utente\nrdpTunnelUsernameDescription=L'utente personalizzato con cui accedere, utilizza l'utente SSH se lasciato vuoto\nrdpFileLocation=Posizione dei file\nrdpFileLocationDescription=Il percorso del file .rdp\nrdpPasswordAuthentication=Autenticazione tramite password\nrdpFiles=File RDP\nrdpPasswordAuthenticationDescription=La password da compilare o copiare negli appunti, a seconda del supporto del client\nrdpFile.displayName=File RDP\nrdpFile.displayDescription=Connettersi a un sistema tramite un file .rdp esistente\nrequiredSshServerAlertTitle=Configurazione del server SSH\nrequiredSshServerAlertHeader=Impossibile trovare un server SSH installato nella macchina virtuale.\nrequiredSshServerAlertContent=Per connettersi alla macchina virtuale, XPipe cerca un server SSH funzionante ma non è stato rilevato alcun server SSH disponibile per la macchina virtuale.\ncomputerName=Nome del computer\npssComputerNameDescription=Il nome del computer a cui connettersi\ncredentialUser=Credenziale utente\ncredentialUserDescription=L'utente con cui accedere.\ncredentialPassword=Password della credenziale\ncredentialPasswordDescription=La password dell'utente.\nsshConfig=File di configurazione SSH\nautostart=Connessione automatica all'avvio di XPipe\nacceptHostKey=Accetta la chiave host\nmodifyHostKeyPermissions=Modificare i permessi della chiave host\nattachContainer=Allegare\ncontainerLogs=Mostra i log\nopenSftpClient=Aprire in un client SFTP esterno\nopenTermius=Apri in Termius\nshowInternalInstances=Mostra le istanze interne\neditPod=Modifica pod\nacceptHostKeyDescription=Fidati della nuova chiave host e continua\nmodifyHostKeyPermissionsDescription=Tentativo di rimuovere i permessi del file originale in modo che OpenSSH sia soddisfatto\npsSession.displayName=Sessione remota PowerShell\npsSession.displayDescription=Connettersi tramite New-PSSession e Enter-PSSession\nsshLocalTunnel.displayName=Tunnel SSH locale\nsshLocalTunnel.displayDescription=Stabilire un tunnel SSH verso un host remoto\nsshRemoteTunnel.displayName=Tunnel SSH remoto\nsshRemoteTunnel.displayDescription=Stabilire un tunnel SSH inverso da un host remoto\nsshDynamicTunnel.displayName=Tunnel SSH dinamico\nsshDynamicTunnel.displayDescription=Stabilire un proxy SOCKS attraverso una connessione SSH\nshellEnvironmentGroup.displayName=Ambienti di shell\nshellEnvironmentGroup.displayDescription=Ambienti di shell\nshellEnvironment.displayName=Ambiente di shell\nshellEnvironment.displayDescription=Creare un ambiente di avvio della shell personalizzato\nshellEnvironment.informationFormat=$TYPE$ ambiente\nshellEnvironment.elevatedInformationFormat=$ELEVATION$ $TYPE$ ambiente\nenvironmentConnectionDescription=La connessione di base per creare un ambiente per\nenvironmentScriptDescription=Lo script di init personalizzato opzionale da eseguire nella shell\nenvironmentSnippets=Script di shell\ncommandSnippetsDescription=Gli script di shell predefiniti opzionali da eseguire per primi\nenvironmentSnippetsDescription=Gli script di shell predefiniti opzionali da eseguire all'inizializzazione\nshellTypeDescription=Il tipo di shell esplicita da lanciare\noriginPort=Porta di origine\noriginAddress=Indirizzo di origine\nremoteAddress=Indirizzo remoto\nremoteSourceAddress=Indirizzo sorgente remoto\nremoteSourcePort=Porta sorgente remota\noriginDestinationPort=Porta di origine e destinazione\noriginDestinationAddress=Indirizzo di origine e destinazione\norigin=Origine\nremoteHost=Host remoto\naddress=Indirizzo\nproxmox.displayName=Proxmox\nproxmox.displayDescription=Connettersi ai sistemi in un ambiente virtuale Proxmox\nproxmoxVm.displayName=Proxmox VM\nproxmoxVm.displayDescription=Connettersi a una macchina virtuale in un VE Proxmox tramite SSH\nproxmoxContainer.displayName=Contenitore Proxmox\nproxmoxContainer.displayDescription=Connettersi a un contenitore in un VE Proxmox\nsshDynamicTunnel.hostDescription=Il sistema da utilizzare come proxy SOCKS\nsshDynamicTunnel.bindingDescription=Quali sono gli indirizzi a cui associare il tunnel\nsshRemoteTunnel.hostDescription=Il sistema da cui avviare il tunnel remoto verso l'origine\nsshRemoteTunnel.bindingDescription=Quali sono gli indirizzi a cui associare il tunnel\nsshLocalTunnel.hostDescription=Il sistema per aprire il tunnel a\nsshLocalTunnel.bindingDescription=Quali sono gli indirizzi a cui associare il tunnel\nsshLocalTunnel.localAddressDescription=L'indirizzo locale a cui fare il bind\nsshLocalTunnel.remoteAddressDescription=L'indirizzo remoto a cui fare il bind\ncmd.displayName=Comando\ncmd.displayDescription=Eseguire un comando arbitrario su un sistema\nk8sPod.displayName=Pod Kubernetes\nk8sPod.displayDescription=Connettersi a un pod e ai suoi container tramite kubectl\nk8sContainer.displayName=Contenitore Kubernetes\nk8sContainer.displayDescription=Aprire una shell in un contenitore\nk8sCluster.displayName=Cluster Kubernetes\nk8sCluster.displayDescription=Connettersi a un cluster e ai suoi pod tramite kubectl\nsshTunnelGroup.displayName=Tunnel SSH\nsshTunnelGroup.displayCategory=Tutti i tipi di tunnel SSH\nlocal.displayName=Macchina locale\nlocal.displayDescription=La shell della macchina locale\ncygwin=Cygwin\nmsys2=MSYS2\ngitWindows=Git per Windows\ngitForWindows.displayName=Git per Windows\ngitForWindows.displayDescription=Accedere all'ambiente locale di Git For Windows\nmsys2.displayName=MSYS2\nmsys2.displayDescription=Gusci di accesso del tuo ambiente MSYS2\ncygwin.displayName=Cygwin\ncygwin.displayDescription=Accesso alle shell dell'ambiente Cygwin\nnamespace=Spazio dei nomi\ngitVaultIdentityStrategy=Identità Git SSH\ngitVaultIdentityStrategyDescription=Se hai scelto di utilizzare un URL git SSH come remoto e il tuo repository remoto richiede un'identità SSH, imposta questa opzione.\\n\\nSe hai fornito un URL HTTP, puoi ignorare questa opzione.\ndockerContainers=Contenitori Docker\ndockerCmd.displayName=client docker CLI\ndockerCmd.displayDescription=Accesso ai container Docker tramite il client docker CLI\nwslCmd.displayName=Installazione WSL\nwslCmd.displayDescription=Accesso alle istanze WSL tramite il client CLI wsl\nk8sCmd.displayName=client kubectl\nk8sCmd.displayDescription=Accedere ai cluster Kubernetes tramite kubectl\nk8sClusters=Cluster Kubernetes\nshells=Gusci disponibili\ninspectContainer=Ispezione\ninspectContext=Ispezione\nk8sClusterNameDescription=Il nome del contesto in cui si trova il cluster.\npod=Pod\npodName=Nome del pod\nk8sClusterContext=Il contesto\nk8sClusterContextDescription=Il nome del contesto in cui si trova il cluster\nk8sClusterNamespace=Spazio dei nomi\nk8sClusterNamespaceDescription=Lo spazio dei nomi personalizzato o quello predefinito se vuoto\nk8sConfigLocation=File di configurazione\nk8sConfigLocationDescription=Il file kubeconfig personalizzato o quello predefinito se lasciato vuoto\ninspectPod=Ispezione\nshowAllContainers=Mostra i container non in esecuzione\nshowAllPods=Mostra i pod non in esecuzione\nk8sPodHostDescription=L'host su cui si trova il pod\nk8sContainerDescription=Il nome del contenitore Kubernetes\nk8sPodDescription=Il nome del pod Kubernetes\npodDescription=Il pod su cui si trova il contenitore\nk8sClusterHostDescription=L'host attraverso il quale si deve accedere al cluster. Deve avere kubectl installato e configurato per poter accedere al cluster.\nconnection=Connessione\nshellCommand.displayName=Comando shell personalizzato\nshellCommand.displayDescription=Aprire una shell standard attraverso un comando personalizzato\nssh.displayName=Connessione SSH\nssh.displayDescription=Connettersi a un sistema remoto tramite il client a riga di comando SSH\nsshConfig.displayName=File di configurazione SSH\nsshConfig.displayDescription=Connettersi agli host definiti in un file di configurazione SSH\nsshConfigHost.displayName=File di configurazione SSH host\nsshConfigHost.displayDescription=Connettersi a un host definito in un file di configurazione SSH\nsshConfigHost.password=Password\nsshConfigHost.passwordDescription=Fornisce la password opzionale per il login dell'utente.\nsshConfigHost.identityPassphrase=Passphrase chiave\nsshConfigHost.identityPassphraseDescription=Fornisci la passphrase opzionale per la tua chiave.\nshellCommand.hostDescription=L'host su cui eseguire il comando\nshellCommand.commandDescription=Il comando che apre una shell\ncommandType=Tipo di comando\ncommandTypeDescription=Come eseguire il comando\ncommandDescription=I comandi personalizzati da eseguire sull'host\ncommandHostDescription=L'host su cui eseguire il comando\ncommandDataFlowDescription=Come questo comando gestisce l'input e l'output\ncommandElevationDescription=Esegui questo comando con permessi elevati\ncommandShellTypeDescription=La shell da utilizzare per questo comando\nlimitedSystem=Si tratta di un sistema limitato o incorporato\nlimitedSystemDescription=Non cercare di identificare il tipo di shell, necessario per i sistemi embedded limitati o per i dispositivi IOT\nsshForwardX11=Avanti X11\nsshForwardX11Description=Abilita l'inoltro X11 per la connessione\ncustomAgent=Agente personalizzato\nidentityAgent=Agente di identità\nssh.proxyDescription=L'host proxy opzionale da utilizzare per stabilire la connessione SSH. Deve essere installato un client ssh.\nusage=Utilizzo\nwslHostDescription=L'host su cui si trova l'istanza WSL. Deve essere installato wsl.\nwslDistributionDescription=Il nome dell'istanza WSL\nwslUsernameDescription=Il nome utente esplicito con cui effettuare il login. Se non viene specificato, verrà utilizzato il nome utente predefinito.\nwslPasswordDescription=La password dell'utente che può essere utilizzata per i comandi sudo.\ndockerHostDescription=L'host su cui si trova il contenitore docker. Deve essere installato docker.\ndockerContainerDescription=Il nome del contenitore docker\nlocalMachine=Macchina locale\nrootScan=Ambiente di shell Sudo\nloginEnvironmentScan=Ambiente di login personalizzato\nk8sScan=Cluster Kubernetes\noptions=Opzioni\ndockerRunningScan=Esecuzione di container docker\ndockerAllScan=Tutti i contenitori docker\nwslScan=Istanze WSL\nsshScan=Connessioni di configurazione SSH\nrunAsUser=Esegui come utente\nrunAsUserDescription=Avvia questo ambiente di shell come utente diverso\ndefault=Predefinito\nadministrator=Amministratore\nwslHost=Host WSL\ntimeout=Timeout\ninstallLocation=Posizione di installazione\ninstallLocationDescription=La posizione in cui è installato l'ambiente $NAME$\nwsl.displayName=Sottosistema Windows per Linux\nwsl.displayDescription=Connettersi a un'istanza WSL in esecuzione su Windows\ndocker.displayName=Contenitore Docker\ndocker.displayDescription=Connettersi a un contenitore docker\nport=Porta\nuser=Utente\npassword=Password\nmethod=Metodo\nuri=URL\nproxy=Proxy\ndistribution=Distribuzione\nusername=Nome utente\nshellType=Tipo di shell\nbrowseFile=Sfogliare un file\nopenShell=Aprire una shell nel terminale\nopenCommand=Eseguire un comando nel terminale\neditFile=Modifica file\ndescription=Descrizione\nfurtherCustomization=Ulteriore personalizzazione\nfurtherCustomizationDescription=Per ulteriori opzioni di configurazione, usa i file di configurazione di ssh\nbrowse=Sfoglia\nconfigHost=Ospite\nconfigHostDescription=L'host su cui si trova la configurazione\nconfigLocation=Posizione di configurazione\nconfigLocationDescription=Il percorso del file di configurazione\ngateway=Gateway\ngatewayDescription=Il gateway opzionale da utilizzare per la connessione\nconnectionInformation=Informazioni sulla connessione\nconnectionInformationDescription=A quale sistema connettersi\npasswordAuthentication=Autenticazione tramite password\npasswordAuthenticationDescription=La password opzionale da utilizzare per l'autenticazione\nsshConfigString.displayName=Connessione SSH basata sulla configurazione\nsshConfigString.displayDescription=Creare una connessione SSH completamente personalizzata nel formato SSH config\nsshConfigStringContent=Configurazione\nsshConfigStringContentDescription=Opzioni SSH per la connessione nel formato OpenSSH config\nvnc.displayName=Connessione VNC su SSH\nvnc.displayDescription=Aprire una sessione VNC su una connessione con tunnel\nbinding=Rilegatura\nvncPortDescription=La porta su cui è in ascolto il server VNC\nrdpPortDescription=La porta su cui è in ascolto il server RDP\nvncUsername=Nome utente\nvncUsernameDescription=Il nome utente VNC opzionale\nvncPassword=Password\nvncPasswordDescription=La password di VNC\nx11WslInstance=Istanza X11 Forward WSL\nx11WslInstanceDescription=La distribuzione locale di Windows Subsystem for Linux da utilizzare come server X11 quando si utilizza l'inoltro X11 in una connessione SSH. Questa distribuzione deve essere una distribuzione WSL2.\nopenAsRoot=Apri come root\nopenInWSL=Aprire in WSL\nlaunch=Lancio\nsshTrustKeyContent=La chiave host non è nota e hai attivato la verifica manuale della chiave host. $CONTENT$\nsshTrustKeyTitle=Chiave host sconosciuta\nrdpTunnel.displayName=Connessione RDP tramite SSH\nrdpTunnel.displayDescription=Connettersi tramite RDP su una connessione con tunnel\nrdpEnableDesktopIntegration=Abilita l'integrazione del desktop\nrdpEnableDesktopIntegrationDescription=Eseguire applicazioni remote supponendo che l'elenco dei permessi di RDP consenta di farlo\nrdpSetupAdminTitle=È necessaria l'impostazione di RDP\nrdpSetupAllowTitle=Applicazione remota RDP\nrdpSetupAllowContent=L'avvio diretto di applicazioni remote non è attualmente consentito su questo sistema. Vuoi abilitarlo? In questo modo potrai eseguire le tue applicazioni remote direttamente da XPipe, disabilitando l'elenco dei permessi per le applicazioni remote RDP.\nrdpServerEnableTitle=Server RDP\nrdpServerEnableContent=Il server RDP è disabilitato sul sistema di destinazione. Vuoi abilitarlo nel registro di sistema per consentire le connessioni RDP remote?\nrdp=RDP\nrdpScan=Tunnel RDP su SSH\nwslX11SetupTitle=Configurazione WSL X11\nwslX11SetupContent=XPipe può utilizzare la tua distribuzione WSL locale per agire come server di visualizzazione X11. Vuoi configurare X11 su $DIST$? Questa operazione installerà i pacchetti X11 di base sulla distribuzione WSL e potrebbe richiedere un po' di tempo. Puoi anche cambiare la distribuzione utilizzata nel menu delle impostazioni.\ncommand=Comando\ncommandGroup=Gruppo di comando\nvncSystem=Sistema di destinazione VNC\nvncSystemDescription=Il sistema effettivo con cui interagire. Di solito coincide con l'host del tunnel\nvncHost=Host VNC di destinazione\nvncHostDescription=Il sistema su cui viene eseguito il server VNC\nvncDirectHost=Ospite\nvncDirectHostDescription=La voce host o l'indirizzo manuale del server su cui il server VNC è in esecuzione\nrdpDirectHost=Ospite\nrdpDirectHostDescription=La voce host o l'indirizzo manuale del server su cui è in esecuzione il server RDP\ngitVaultTitle=Cassaforte Git\ngitVaultForcePushContent=Vuoi forzare il push al repository remoto? Questo sostituirà completamente tutti i contenuti del repository remoto con quello locale, compresa la cronologia.\ngitVaultOverwriteLocalContent=Vuoi sovrascrivere le modifiche del tuo vault locale? In questo modo tutte le modifiche remote verranno applicate al tuo repository locale.\nrdpSimple.displayName=Connessione diretta RDP\nrdpSimple.displayDescription=Connettersi a un host tramite RDP\nrdpUsername=Nome utente\nrdpUsernameDescription=L'utente con cui accedere. Può includere un prefisso di dominio\naddressDescription=Dove connettersi\nrdpAdditionalOptions=Opzioni RDP aggiuntive\nrdpAdditionalOptionsDescription=Opzioni RDP grezze da includere, formattate come nei file .rdp\nproxmoxVncConfirmTitle=Accesso VNC\nproxmoxVncConfirmContent=Vuoi abilitare l'accesso VNC per la macchina virtuale? In questo modo si abilita l'accesso diretto al client VNC nel file di configurazione della VM e si riavvia la macchina virtuale.\ndockerContext.displayName=Contesto Docker\ndockerContext.displayDescription=Interagire con i contenitori situati in un contesto specifico\nvmActions=Azioni della VM\ndockerContextActions=Azioni contestuali\nk8sPodActions=Azioni del pod\nopenVnc=Abilita l'accesso a VNC\naddVnc=Aggiungi una connessione VNC\ncommandGroup.displayName=Gruppo di comando\ncommandGroup.displayDescription=Gruppo di comandi disponibili per un sistema\nserial.displayName=Connessione seriale\nserial.displayDescription=Aprire una connessione seriale in un terminale\nserialPort=Porta seriale\nserialPortDescription=La porta seriale/dispositivo a cui connettersi\nbaudRate=Velocità di trasmissione\ndataBits=Bit di dati\nstopBits=Bit di stop\nparity=Parità\nflowControlWindow=Controllo del flusso\nserialImplementation=Implementazione seriale\nserialImplementationDescription=Lo strumento da utilizzare per collegarsi alla porta seriale\nserialHost=Ospite\nserialHostDescription=Il sistema per accedere alla porta seriale su\nserialPortConfiguration=Configurazione della porta seriale\nserialPortConfigurationDescription=Parametri di configurazione del dispositivo seriale collegato\nserialInformation=Informazioni di serie\nopenXShell=Apri in XShell\ntsh.displayName=Teletrasporto\ntsh.displayDescription=Connettiti ai tuoi nodi di teletrasporto via tsh\ntshNode.displayName=Nodo di teletrasporto\ntshNode.displayDescription=Connettersi a un nodo teleport in un cluster\nteleportCluster=Cluster\nteleportClusterDescription=Il cluster in cui si trova il nodo\nteleportProxy=Proxy\nteleportProxyDescription=Il server proxy utilizzato per connettersi al nodo\nteleportHost=Ospite\nteleportHostDescription=Il nome host del nodo\nteleportUser=Utente\nteleportUserDescription=L'utente con cui accedere\nlogin=Accesso\nhyperVInstall.displayName=Hyper-V\nhyperVInstall.displayDescription=Connettersi alle macchine virtuali gestite da Hyper-V\nhyperVVm.displayName=VM Hyper-V\nhyperVVm.displayDescription=Connettersi a una macchina virtuale Hyper-V tramite SSH o PSSession\ntrustHost=Host fiduciario\ntrustHostDescription=Aggiungi NomeComputer all'elenco degli host attendibili\ncopyIp=Copia IP\nvncDirect.displayName=Connessione diretta VNC\nvncDirect.displayDescription=Connettersi direttamente a un sistema tramite VNC\neditConfiguration=Modifica la configurazione\nviewInDashboard=Vista nel cruscotto\nsetDefault=Imposta predefinito\nremoveDefault=Rimuovi l'impostazione predefinita\nconnectAsOtherUser=Connettersi come altro utente\nprovideUsername=Fornisce un nome utente alternativo con cui accedere\nvmIdentity=Identità dell'ospite\nvmIdentityDescription=Il metodo di autenticazione dell'identità SSH da utilizzare per la connessione, se necessario\nvmPort=Porta\nvmPortDescription=La porta a cui connettersi tramite SSH\nforwardAgent=Agente di inoltro\nforwardAgentDescription=Rendere disponibili le identità dell'agente SSH sul sistema remoto\nvirshUri=URI\nvirshUriDescription=L'URI dell'hypervisor, sono supportati anche gli alias\nvirshDomain.displayName=dominio libvirt\nvirshDomain.displayDescription=Connettersi a un dominio libvirt\nvirshHypervisor.displayName=hypervisor libvirt\nvirshHypervisor.displayDescription=Connettersi a un driver di hypervisor supportato da libvirt\nvirshInstall.displayName=client a riga di comando libvirt\nvirshInstall.displayDescription=Connettersi a tutti gli hypervisor libvirt disponibili tramite virsh\naddHypervisor=Aggiungi un hypervisor\ninteractiveTerminal=Terminale interattivo\neditDomain=Modifica dominio\nlibvirt=domini libvirt\ncustomIp=IP personalizzato\ncustomIpDescription=Sovrascrive il rilevamento dell'IP locale predefinito della VM se utilizzi una rete avanzata\nautomaticallyDetect=Rileva automaticamente\nuserAddDialogTitle=Creazione di un utente\ngroupAddDialogTitle=Creazione di un gruppo\npassphrase=Passphrase\nrepeatPassphrase=Ripeti la passphrase\ngroupSecret=Segreto di gruppo\nrepeatGroupSecret=Ripetizione del segreto di gruppo\nvaultGroup=Gruppo di casseforti\nloginAlertTitle=Accesso richiesto\nloginAlertHeader=Sblocca il caveau per accedere alle tue connessioni personali\nvaultUser=Utente del caveau\nme=Io\naddGroup=Aggiungi gruppo ...\naddGroupDescription=Crea un nuovo gruppo per questa cassaforte\naddUser=Aggiungi utente ...\naddUserDescription=Crea un nuovo utente per questa cassaforte\nskip=Salto\nuserChangePasswordAlertTitle=Modifica della password\ngroupChangeSecretAlertTitle=Cambiamento segreto\ndocs=Documentazione\nlxd.displayName=Contenitore LXD\nlxd.displayDescription=Connettersi a un contenitore LXD tramite lxc\nlxdCmd.displayName=Client CLI LXD\nlxdCmd.displayDescription=Accesso ai contenitori LXD tramite il client lxc CLI\npodman.displayName=Contenitore Podman\npodman.displayDescription=Connettersi a un contenitore Podman\nincusInstall.displayName=Gestore di macchine Incus\nincusInstall.displayDescription=Accesso ai contenitori incus tramite il client CLI incus\nincusContainer.displayName=Contenitore Incus\nincusContainer.displayDescription=Connettersi a un contenitore incus\npodmanCmd.displayName=Podman CLI\npodmanCmd.displayDescription=Accesso ai contenitori Podman tramite il client CLI\nlxdHostDescription=L'host su cui si trova il contenitore LXD. Deve avere installato lxc.\nlxdContainerDescription=Il nome del contenitore LXD\npodmanContainers=Contenitori Podman\nlxdContainers=Contenitori LXD\nincusContainers=Contenitori Incus\ncontainer=Contenitore\nhost=Ospite\ncontainerActions=Azioni del contenitore\nserialConsole=Console seriale\neditRunConfiguration=Modifica la configurazione dell'esecuzione\ncommunityDescription=Uno strumento di connessione perfetto per i tuoi casi d'uso personali.\nupgradeDescription=Gestione professionale delle connessioni per l'intera infrastruttura server.\ndiscoverPlans=Scoprire le opzioni di aggiornamento\nextendProfessional=Aggiornamento alle ultime funzionalità professionali\ncommunityItem1=Connessioni illimitate a sistemi e strumenti non commerciali\ncommunityItem2=Integrazione perfetta con i terminali e gli editor installati\ncommunityItem3=Browser di file remoto completo\ncommunityItem4=Potente sistema di scripting per tutte le shell\ncommunityItem5=Integrazione Git per la sincronizzazione e la condivisione delle informazioni di connessione\nupgradeItem1=Include tutte le funzioni della community edition\nupgradeItem2=Il piano Homelab supporta un numero illimitato di hypervisor e funzioni SSH avanzate\nupgradeItem3=Il piano Professional supporta inoltre i sistemi operativi e gli strumenti aziendali\nupgradeItem4=Il piano Enterprise offre la massima flessibilità per i tuoi casi d'uso individuali\nupgrade=Aggiornamento\nupgradeTitle=Piani disponibili\nstatus=Stato\ntype=Tipo\nlicenseAlertTitle=Licenza richiesta\nuseCommunity=Continua con la comunità\npreviewDescription=Prova le nuove funzionalità per un paio di settimane dopo il rilascio.\ntryPreview=Attiva l'anteprima\npreviewItem1=Accesso completo alle nuove funzionalità professionali per 2 settimane dal rilascio\npreviewItem2=Prova nuove funzionalità senza alcun impegno\nlicensedTo=Con licenza di\nemail=Indirizzo e-mail\napply=Applica\nclear=Cancella\nactivate=Attivare\nvalidUntil=Valido fino a\nlicenseActivated=Licenza attivata\nrestart=Riavvio\nlockVault=Cassaforte con serratura\nrestartApp=Riavviare XPipe\nfree=Gratuito\nupgradeInfo=Qui di seguito puoi trovare informazioni sull'aggiornamento della licenza.\nupgradeInfoPreview=Puoi trovare informazioni sull'aggiornamento della licenza qui sotto o provare l'anteprima.\nenterLicenseKey=Inserisci la chiave di licenza per l'aggiornamento\nisOnlySupported=è supportato solo con almeno una licenza $TYPE$\nareOnlySupported=sono supportati solo con una licenza di almeno $TYPE$\nlegacyLicense=Questa licenza include solo le nuove funzionalità di Professional rilasciate entro un anno dall'acquisto.\npreviewExpiredLicense=Questa funzione è stata recentemente disponibile gratuitamente in anteprima, ma il periodo è ora scaduto.\nopenApiDocs=Documentazione API\nopenApiDocsDescription=La documentazione dell'API HTTP è disponibile online, compresa una specifica OpenAPI .yaml. Puoi aprirla nel tuo browser web o nel tuo client HTTP preferito.\nopenApiDocsButton=Documenti aperti\npythonApi=API Python\npersonalConnection=Questa connessione e tutti i suoi figli sono disponibili solo per il tuo utente in quanto dipendono da un'identità personale.\ndeveloperPrintInitFiles=Stampa dell'esecuzione del file init\ndeveloperPrintInitFilesDescription=Stampa tutti gli script di avvio della shell che vengono eseguiti quando viene lanciato un terminale.\ndeveloperShowSensitiveCommands=Log dei comandi sensibili\ndeveloperShowSensitiveCommandsDescription=Includere comandi sensibili nell'output di log per il debug.\ncheckingForUpdates=Controllo degli aggiornamenti\ncheckingForUpdatesDescription=Recuperare le informazioni sull'ultima release\ndownloadingUpdate=Recupero della release (Versione $VERSION$)\ndownloadingUpdateDescription=Download di un pacchetto di rilascio\nupdateNag=È da un po' che non aggiorni XPipe. Potresti perdere le nuove funzionalità e le correzioni delle versioni più recenti.\nupdateNagTitle=Promemoria di aggiornamento\nupdateNagButton=Vedere i comunicati\nrefreshServices=Aggiorna i servizi\nserviceProtocolType=Tipo di protocollo di servizio\nserviceProtocolTypeDescription=Controlla come aprire il servizio\nserviceCommand=Il comando da eseguire una volta che il servizio è attivo\nserviceCommandDescription=Il segnaposto $PORT verrà sostituito con la porta locale effettiva del tunnel\nvalue=Valore\nshowAdvancedOptions=Mostra opzioni avanzate\nsshAdditionalConfigOptions=Opzioni di configurazione aggiuntive\nremoteFileManager=Gestore di file remoto\nclearUserData=Cancellare i dati dell'utente\nclearUserDataDescription=Elimina tutti i dati di configurazione dell'utente, comprese le connessioni\nclearUserDataTitle=Cancellazione dei dati dell'utente\nclearUserDataContent=In questo modo verranno cancellati tutti i dati degli utenti locali di xpipe e verrà riavviato. Se tieni alle tue connessioni, assicurati di sincronizzarle prima con un repository git.\nundefined=Non definito\ncopyAddress=Indirizzo di copia\nnetbirdDeviceScan=Connessioni Netbird\nnetbirdId=Chiave pubblica del peer\nnetbirdIdDescription=L'id della chiave pubblica interna netbird del peer\ntailscaleDeviceScan=Connessioni Tailscale\ntailscaleInstall.displayName=Installazione di Tailscale\ntailscaleInstall.displayDescription=Connettiti ai dispositivi della tua tailnet via SSH\ntailscaleDevice.displayName=Dispositivo Tailscale\ntailscaleDevice.displayDescription=Connettersi a un dispositivo della tua tailnet tramite SSH\ntailscaleId=ID dispositivo\ntailscaleIdDescription=L'ID interno del dispositivo tailscale\ntailscaleHostName=Nome dell'host\ntailscaleHostNameDescription=Il nome dell'host del dispositivo nella tailnet\ntailscaleUsername=Nome utente\ntailscaleUsernameDescription=L'utente con cui accedere\ntailscalePassword=Password\ntailscalePasswordDescription=La password utente opzionale che può essere utilizzata per sudo\nscriptName=Nome dello script\nscriptNameDescription=Assegna a questo script un nome personalizzato\nscriptGroupName=Nome del gruppo di script\nscriptGroupNameDescription=Assegna a questo gruppo di script un nome personalizzato\nidentityName=Nome dell'identità\nidentityNameDescription=Assegna a questa identità un nome personalizzato\ntailscaleTailnet.displayName=Tailnet\ntailscaleTailnet.displayDescription=Connettiti a una tailnet specifica con il tuo account\nputtyConnections=Connessioni PuTTY\nkittyConnections=Connessioni KiTTY\nicons=Icone\ncustomIcons=Icone personalizzate\niconSources=Fonti di icone\niconSourcesDescription=Puoi aggiungere qui le tue fonti di icone. XPipe raccoglierà tutti i file .svg presenti nel percorso aggiunto e li aggiungerà al set di icone disponibili.\\n\\nSono supportate sia le directory locali che i repository git remoti come posizioni per le icone.\nrefreshSources=Icone di aggiornamento\nrefreshSourcesDescription=Aggiorna tutte le icone dalle fonti disponibili\naddDirectoryIconSource=Aggiungi una fonte di directory ...\naddDirectoryIconSourceDescription=Aggiungere icone da una directory locale\naddGitIconSource=Aggiungi il sorgente git ...\naddGitIconSourceDescription=Aggiungi le icone che si trovano in un repository git remoto\nrepositoryUrl=URL del repository Git\niconDirectory=Elenco di icone\naddUnsupportedKexMethod=Aggiungi un metodo di scambio di chiavi non supportato\naddUnsupportedKexMethodDescription=Consenti l'utilizzo del metodo di scambio di chiavi $VAL$ per questa connessione\naddUnsupportedHostKeyType=Aggiungi un tipo di chiave host non supportata\naddUnsupportedHostKeyTypeDescription=Consenti l'utilizzo della chiave host di tipo $VAL$ per questa connessione\naddUnsupportedMacType=Aggiungi un tipo di MAC non supportato\naddUnsupportedMacTypeDescription=Consenti l'utilizzo del tipo MAC $VAL$ per questa connessione\nrunSilent=silenziosamente in background\nrunInFileBrowser=nel browser di file\nrunInConnectionHub=in un hub di connessione\ncommandOutput=Output del comando\niconSourceDeletionTitle=Elimina l'icona sorgente\niconSourceDeletionContent=Vuoi eliminare questa fonte di icone e tutte le icone ad essa associate?\nrefreshIcons=Icone di aggiornamento\nrefreshIconsDescription=Recuperare, renderizzare e memorizzare nella cache tutte le oltre 1000 icone disponibili da fonti esterne in file .png. Questo potrebbe richiedere un po' di tempo ...\nvaultUserLegacy=Utente del Vault (modalità di compatibilità legacy limitata)\nupgradeInstructions=Istruzioni per l'aggiornamento\nexternalActionTitle=Richiesta di azione esterna\nexternalActionContent=È stata richiesta un'azione esterna. Vuoi consentire il lancio di azioni dall'esterno di XPipe?\nnoScriptStateAvailable=Aggiorna per determinare la compatibilità dello script ...\ndocumentationDescription=Controlla la documentazione\ncustomEditorCommandInTerminal=Eseguire un comando personalizzato in un terminale\ncustomEditorCommandInTerminalDescription=Se il tuo editor è basato su un terminale, puoi attivare questa opzione per aprire automaticamente un terminale ed eseguire il comando nella sessione del terminale.\\n\\nPuoi utilizzare questa opzione per editor come vi, vim, nvim e altri.\ndisableHttpsTlsCheck=Disabilita la verifica dei certificati delle richieste HTTPS\ndisableHttpsTlsCheckDescription=Se la tua azienda decifra il traffico HTTPS nei firewall utilizzando l'intercettazione SSL, qualsiasi controllo degli aggiornamenti o delle licenze fallirà a causa della mancata corrispondenza dei certificati. Puoi risolvere il problema abilitando questa opzione e disabilitando la convalida dei certificati TLS.\nconnectionsSelected=$NUMBER$ connessioni selezionate\naddConnections=Aggiungi connessioni\nbrowseDirectory=Sfogliare la directory\nopenTerminal=Terminale aperto\ndocumentation=Documentazione\nreport=Segnala un errore\nkeePassXcNotAssociated=Collegamento a KeePassXC\nkeePassXcNotAssociatedDescription=XPipe non è associato al database locale di KeePassXC. Fai clic qui sotto per eseguire l'operazione una tantum di associazione di XPipe al database KeePassXC in modo che XPipe possa interrogare le password.\nkeePassXcAssociateMore=Collegare più database\nkeePassXcAssociateMoreDescription=Puoi essere connesso a più database di KeePassXC allo stesso tempo\nkeePassXcAssociated=Link a KeePassXC\nkeePassXcAssociatedDescription=XPipe è collegato ai seguenti database locali di KeePassXC:\nkeePassXcNotAssociatedButton=Database di collegamento\nidentifier=Identificatore\npasswordManagerCommand=Comando personalizzato\npasswordManagerCommandDescription=Il comando personalizzato da eseguire per recuperare le password. La stringa segnaposto $KEY sarà sostituita dalla chiave della password citata quando verrà chiamata. Questo comando dovrebbe richiamare la CLI del tuo gestore di password per stampare la password su stdout, ad esempio mypassmgr get $KEY.\nchooseTemplate=Scegli il modello\nkeePassXcPlaceholder=URL della voce KeePassXC\nterminalEnvironment=Ambiente terminale\nterminalEnvironmentDescription=Se vuoi utilizzare le caratteristiche di un ambiente WSL locale basato su Linux per la personalizzazione del tuo terminale, puoi utilizzarle come ambiente terminale.\\n\\nTutti i comandi di avvio del terminale e la configurazione del multiplexer del terminale verranno eseguiti in questa distribuzione WSL.\nterminalInitScript=Script di avvio del terminale\nterminalInitScriptDescription=Comandi da eseguire nell'ambiente del terminale prima dell'avvio della connessione. Puoi usarli per configurare l'ambiente del terminale all'avvio.\nterminalMultiplexer=Multiplexer terminale\nterminalMultiplexerDescription=Il multiplexer del terminale da utilizzare come alternativa alle schede in un terminale. Questo sostituirà alcune caratteristiche di gestione del terminale, ad esempio la gestione delle schede, con la funzionalità del multiplexer.\\n\\nRichiede che l'eseguibile del multiplexer sia installato sul sistema.\nterminalMultiplexerWindowsDescription=Il multiplexer del terminale da utilizzare come alternativa alle schede in un terminale. Questo sostituirà alcune caratteristiche di gestione del terminale, ad esempio la gestione delle schede, con la funzionalità del multiplexer.\\n\\nRichiede l'utilizzo di un ambiente terminale WSL su Windows e l'installazione dell'eseguibile del multiplexer sul sistema WSL.\nterminalAlwaysPauseOnExit=Metti sempre in pausa all'uscita\nterminalAlwaysPauseOnExitDescription=Se abilitato, l'uscita da una sessione di terminale ti chiederà sempre di riavviare o chiudere la sessione. Se è disattivato, XPipe lo farà solo per le connessioni fallite che escono con un errore.\nquerying=Interrogare ...\nretrievedPassword=Ottenuto: $PASSWORD$\nrefreshOpenpubkey=Aggiorna l'identità openpubkey\nrefreshOpenpubkeyDescription=Esegui opkssh refresh per rendere di nuovo valida l'identità di openpubkey\nall=Tutti\nterminalPrompt=Prompt del terminale\nterminalPromptDescription=Lo strumento di prompt del terminale da utilizzare nei terminali remoti. Abilitando un prompt del terminale, lo strumento di prompt verrà automaticamente impostato e configurato sul sistema di destinazione all'apertura di una sessione di terminale.\\n\\nQuesto non modifica le configurazioni del prompt o i file di profilo esistenti sul sistema. Questo aumenterà il tempo di caricamento del terminale per la prima volta mentre il prompt viene configurato sul sistema remoto. Il tuo terminale potrebbe aver bisogno di font aggiuntivi per visualizzare correttamente il prompt.\nterminalPromptConfiguration=Configurazione del prompt del terminale\nterminalPromptConfig=File di configurazione\nterminalPromptConfigDescription=Il file di configurazione personalizzato da applicare al prompt. Questa configurazione verrà impostata automaticamente sul sistema di destinazione quando il terminale viene inizializzato e verrà utilizzata come configurazione predefinita del prompt.\\n\\nSe vuoi utilizzare il file di configurazione predefinito esistente su ogni sistema, puoi lasciare questo campo vuoto.\npasswordManagerKey=Chiave del gestore di password\npasswordManagerKeyDescription=L'identificatore del gestore di password del segreto\npasswordManagerAgent=Agente per la gestione delle password\ndockerComposeProject.displayName=Progetto Docker compose\ndockerComposeProject.displayDescription=Raggruppa i contenitori di un progetto di composizione\nsshVerboseOutput=Abilita l'output verboso di SSH\nsshVerboseOutputDescription=Stampa molte informazioni di debug quando ci si connette tramite SSH. È utile per risolvere i problemi delle connessioni SSH.\ndontUseGateway=Non usare il gateway\ndontUseGatewayDescription=Non utilizzare l'host dell'hypervisor come gateway e connettersi direttamente all'IP\ncategoryColor=Categoria colore\ncategoryColorDescription=Il colore predefinito da utilizzare per le connessioni di questa categoria\ncategorySync=Sincronizzazione con il repository git\ncategorySyncDescription=Sincronizza automaticamente tutte le connessioni con il repository git. Tutte le modifiche locali alle connessioni saranno inviate al repository remoto.\ncategorySyncSpecial=Sincronizzazione con il repository git\\n(Non configurabile per la categoria speciale \"$NAME$\")\ncategoryDontAllowScripts=Disabilita tutte le modifiche\ncategoryDontAllowScriptsDescription=Disabilita l'esecuzione di comandi e altre operazioni sui sistemi appartenenti a questa categoria per impedire qualsiasi modifica. In questo modo verranno disabilitate tutte le funzionalità di scripting, i comandi dell'ambiente shell, i prompt e altro ancora.\ncategoryConfirmAllModifications=Conferma tutte le modifiche\ncategoryConfirmAllModificationsDescription=Conferma prima qualsiasi tipo di modifica di una connessione o di un file system. In questo modo si possono evitare operazioni accidentali su sistemi importanti.\ncategoryDefaultIdentity=Identità predefinita\ncategoryDefaultIdentityDescription=Se utilizzi spesso una determinata identità su molti dei sistemi di questa categoria, allora l'impostazione di un'identità predefinita ti permetterà di preselezionarla quando crei nuove connessioni.\ncategoryConfigTitle=$NAME$ configurazione\nconfigure=Configurare\naddConnection=Aggiungi connessione\nnoCompatibleConnection=Nessuna connessione compatibile trovata\nnoCompatibleIdentity=Non è stata trovata un'identità compatibile\nnewCategory=Nuova categoria\ndockerComposeRestricted=Il progetto compose è limitato da $NAME$ e non può essere modificato dall'esterno. Utilizza $NAME$ per gestire questo progetto di composizione.\nrestricted=Limitato\ndisableSshPinCaching=Disabilita la cache del PIN SSH\ndisableSshPinCachingDescription=XPipe memorizza automaticamente i PIN inseriti per una chiave quando si utilizza una forma di autenticazione basata sull'hardware.\\n\\nDisabilitando questa funzione, sarà necessario reinserire il PIN a ogni tentativo di connessione.\ngitSyncPull=Pull per sincronizzare le modifiche di git in remoto\nenpassVaultFile=File caveau\nenpassVaultFileDescription=Il file del caveau Enpass locale.\nflat=Piatto\nrecursive=Ricorsivo\nrdpAllowListBlocked=La RemoteApp selezionata non sembra essere inclusa nell'elenco dei permessi RDP del server.\npsonoServerUrl=URL del server\npsonoServerUrlDescription=URL del server backend di psono\npsonoApiKey=Chiave API\npsonoApiKeyDescription=La chiave API da utilizzare, formattata come un uuid\npsonoApiSecretKey=Chiave segreta API\npsonoApiSecretKeyDescription=La chiave segreta API come stringa esadecimale di 64 byte\npassboltServerUrl=URL del server\npassboltServerUrlDescription=URL del server backend passbolt\npassboltPassphrase=Passphrase\npassboltPassphraseDescription=La passphrase per la chiave privata del vault\npassboltPrivateKey=Chiave privata\npassboltPrivateKeyDescription=Il file della chiave gpg privata per il caveau\nfocusWindowOnNotifications=Finestra di messa a fuoco sulle notifiche\nfocusWindowOnNotificationsDescription=Porta XPipe in primo piano quando viene visualizzato un messaggio di notifica o di errore, ad esempio quando una connessione o un tunnel termina inaspettatamente.\ngitUsername=Nome utente git personalizzato\ngitUsernameDescription=L'utente personalizzato per l'autenticazione al repository remoto git. Per impostazione predefinita, XPipe utilizzerà le credenziali attualmente configurate della CLI di git.\\n\\nQuesta impostazione sovrascrive le credenziali predefinite già configurate per il tuo client git CLI locale.\ngitPassword=Password git personalizzata / token di accesso personale\ngitPasswordDescription=La password o il token di accesso personale da utilizzare per l'autenticazione. La necessità di una password o di un token di accesso personale dipende dal provider remoto di git. Questa impostazione sovrascrive le credenziali predefinite già configurate per il tuo client git CLI locale.\nsetReadOnly=Imposta la sola lettura\nunsetReadOnly=Unset di sola lettura\nreadOnlyStoreError=La configurazione di questa voce è congelata. Scegli un nome diverso per salvare le modifiche in una nuova copia.\ncategoryFreeze=Congelare le configurazioni di connessione\ncategoryFreezeDescription=Contrassegna le configurazioni di connessione come di sola lettura. Ciò significa che nessuna configurazione di connessione esistente in questa categoria può essere modificata. Tuttavia, è possibile aggiungere nuove connessioni.\nupdateFail=L'installazione dell'aggiornamento non è andata a buon fine\nupdateFailAction=Installare l'aggiornamento manualmente\nupdateFailActionDescription=Scopri le ultime versioni su GitHub\nonePasswordPlaceholder=Nome dell'oggetto o URL op://\ncomputeDirectorySizes=Calcolo delle dimensioni delle directory\ncomputeSize=Dimensione di calcolo\ncustomSpiceCommand=Comando personalizzato\ncustomSpiceCommandDescription=Il comando personalizzato da eseguire per lanciare le sessioni SPICE. La stringa segnaposto $FILE sarà sostituita dal percorso quotato del file .vv quando viene richiamata.\nvncClient=Client VNC\nvncClientDescription=Il client VNC da lanciare quando si aprono le connessioni VNC in XPipe.\\n\\nHai la possibilità di utilizzare il client VNC integrato in XPipe o, in alternativa, di lanciare un client VNC esterno installato localmente se desideri una maggiore personalizzazione.\nintegratedXPipeVncClient=Client VNC XPipe integrato\ncustomVncCommand=Comando personalizzato\ncustomVncCommandDescription=Il comando personalizzato da eseguire per lanciare le sessioni VNC. La stringa segnaposto $ADDRESS sarà sostituita dall'indirizzo citato quando verrà richiamata.\nvncConnections=Connessioni VNC\npasswordManagerIdentity=Identità del gestore di password\npasswordManagerIdentity.displayName=Identità del gestore di password\npasswordManagerIdentity.displayDescription=Recupera il nome utente e la password di un'identità dal tuo gestore di password\npasswordCopied=Password di connessione copiata negli appunti\nerrorOccurred=Si è verificato un errore\nactionMacro.displayName=Macro azione\nactionMacro.displayDescription=Eseguire in azione utilizzando trigger personalizzati\nmacroAdd=Aggiungi macro\nmacroName=Nome della macro\nmacroNameDescription=Assegna a questa macro un nome personalizzato\nactionId=ID azione\nactionIdDescription=L'azione da eseguire con questa macro\nmacroRefs=Connessioni associate\nmacroRefsDescription=Le connessioni con cui eseguire l'azione\nconnectionCopy=Copia\nactionPickerTitle=Azione di prelievo\nactionPickerDescription=Clicca su qualcosa per eseguire un'azione. Invece di eseguire l'azione, puoi creare e modificare i collegamenti all'azione nella modalità di selezione dei collegamenti all'azione.\ncancelActionPicker=Annullamento di un'azione\nactionShortcut=Scorciatoia d'azione\nactionShortcuts=Scorciatoie d'azione\nactionStore=Negozio di azioni\nactionStoreDescription=La voce del negozio su cui eseguire l'azione\nactionStores=Memorizza l'azione\nactionStoresDescription=Le voci del negozio su cui eseguire l'azione\nactionDesktopShortcut=Collegamento al desktop\nactionDesktopShortcutDescription=Crea un collegamento per questa azione sul desktop\nactionUrlShortcut=Scorciatoia URL\nactionUrlShortcutDescription=Copia un URL che può attivare queste azioni quando viene aperto\nactionUrlShortcutDisabled=Collegamenti URL (non disponibile)\nactionUrlShortcutDisabledDescription=Il tipo di installazione $TYPE$ non supporta l'apertura di URL\nactionApiCall=Richiesta API\nactionApiCallDescription=Chiama questa azione dall'API HTTP\nactionMacro=Macro azione\nactionMacroDescription=Crea una macro con funzionalità avanzate per questa azione\ncreateMacro=Creare una macro\nactionConfiguration=Parametri\nactionConfigurationDescription=I parametri da passare all'azione eseguita\nconfirmAction=Conferma l'azione\nactionConnections=Connessioni di azione\nactionConnectionsDescription=Le connessioni su cui eseguire l'azione\nactionConnection=Azione di connessione\nactionConnectionDescription=La connessione su cui eseguire l'azione\nappleContainerInstall.displayName=Contenitori Apple\nappleContainerInstall.displayDescription=Accesso alle istanze del container apple tramite la CLI del container\nappleContainer.displayName=Contenitore Apple\nappleContainer.displayDescription=Accesso alle istanze del container apple tramite la CLI del container\nappleContainerHostDescription=L'host su cui si trova il contenitore apple\nappleContainerDescription=Il nome del contenitore apple\nappleContainers=Contenitori Apple\nchangeOrderIndexTitle=Cambiare ordine\norderIndex=Indice\norderIndexDescription=Indice esplicito per ordinare questa voce rispetto alle altre. Gli indici più bassi sono mostrati in alto, quelli più alti in basso\nmoveToFirst=Sposta al primo posto\nmoveToLast=Sposta all'ultimo posto\ncategory=Categoria\nincludeRoot=Include la radice\nexcludeRoot=Escludere la radice\nfreezeConfiguration=Congelamento della configurazione\nunfreezeConfiguration=Scongelare la configurazione\nwaylandScalingTitle=Scala di Wayland\nactionApiUrl=$URL$ (Copia corpo json)\ncopyBody=Copia del corpo della richiesta\ngitRepoTerminalOpen=Aprire il repository nel terminale\ngitRepoTerminalOpenDescription=Dai un'occhiata al repository con la riga di comando\ngitRepoOverwriteLocal=Sovrascrive il repository locale\ngitRepoOverwriteLocalDescription=Sostituire tutte le modifiche locali con le modifiche provenienti dal computer remoto\ngitRepoForcePush=Sovrascrive il repository remoto\ngitRepoForcePushDescription=Usa git push --force per applicare le tue modifiche locali a quelle remote\ngitRepoDontWarn=Non avvertire più\ngitRepoDontWarnDescription=Se questo è previsto, fai in modo che XPipe ignori questo errore in futuro\ngitRepoTryAgain=Riprova\ngitRepoTryAgainDescription=Tentare di nuovo la stessa operazione\ngitRepoEnablePlain=Usa la sincronizzazione di directory semplici\ngitRepoEnablePlainDescription=Non inizializzare un repository git per sincronizzare le modifiche alla directory\ngitRepoCreateBare=Usa git sync\ngitRepoCreateBareDescription=Inizializza un nuovo repository git nudo nella directory di sincronizzazione\ngitRepoDisable=Disabilita git vault per ora\ngitRepoDisableDescription=Non apportare modifiche durante questa sessione\ngitRepoPullRefresh=Estrarre le modifiche e aggiornarle\ngitRepoPullRefreshDescription=Unire le modifiche remote e ricaricare i dati\nbreakOutCategory=Categoria break out\nmergeCategory=Categoria Merge\nopenWinScp=Aprire in WinSCP\nuninstallApplication=Disinstallare\nuninstallApplicationDescription=Esegue lo script di installazione .pkg per disinstallare completamente XPipe\nk8sEditPodTitle=Applica le modifiche\nk8sEditPodContent=Vuoi applicare le modifiche apportate con il comando kubectl apply? È probabile che sia necessario un riavvio affinché le modifiche vengano applicate.\nvirshEditDomainTitle=Applica le modifiche\nvirshEditDomainContent=Vuoi applicare le modifiche al dominio? È probabile che sia necessario un riavvio per applicare le modifiche.\npkcs11Library=Libreria PKCS#11\npkcs11LibraryDescription=Il percorso del file della libreria a collegamento dinamico\nsshAgentSocket=Un socket personalizzato per l'agente SSH\nsshAgentSocketDescription=Il socket personalizzato da utilizzare per comunicare con l'agente SSH. Questo agente personalizzato può essere utilizzato per una connessione selezionando l'opzione agente personalizzato.\npublicKey=Identificatore di chiave pubblica\npublicKeyDescription=La chiave pubblica opzionale per costringere l'agente a offrire solo la chiave privata corrispondente\nactions=Azioni\nhcloudServer.displayName=Server cloud Hetzner\nhcloudServer.displayDescription=Accedere a un server ospitato sul cloud Hetzner tramite SSH\nhcloudInstall.displayName=Hetzner Cloud CLI\nhcloudInstall.displayDescription=Accesso ai server ospitati sul cloud Hetzner tramite hcloud\nhcloudContext.displayName=contesto hcloud\nhcloudContext.displayDescription=Server di accesso di un contesto hcloud\nmetrics=Metriche\nopenInVsCode=Apri in VsCode\naddCloud=Cloud ...\nhcloudToken=token hcloud\nhcloudTokenDescription=Il token cloud Hetzner da utilizzare. Per maggiori informazioni, consulta la documentazione\nhcloudLogin=Accesso al cloud Hetzner\nclearHcloudToken=Cancella il token hcloud\nclearHcloudTokenDescription=Elimina il token esistente in modo da poter accedere di nuovo\nselectIdentity=Seleziona l'identità\nenableMcpServer=Abilita il server MCP\nenableMcpServerDescription=Abilita il server MCP XPipe, consentendo ai client MCP esterni di inviare richieste al server MCP. Vedi sotto per i dettagli della configurazione.\\n\\nNota che l'API HTTP non deve essere abilitata per la funzionalità MCP.\nenableMcpMutationTools=Abilita gli strumenti di mutazione MCP\nenableMcpMutationToolsDescription=Per impostazione predefinita, nel server MCP sono abilitati solo strumenti di sola lettura. Questo per garantire che non vengano effettuate operazioni accidentali che potrebbero modificare il sistema.\\n\\nSe intendi apportare modifiche ai sistemi tramite i client MCP, assicurati di verificare che il tuo client MCP sia configurato per confermare qualsiasi azione potenzialmente distruttiva prima di abilitare questa opzione. Richiede la riconnessione di tutti i client MCP per essere applicata.\nmcpClientConfigurationDetails=Configurazione del client MCP\nmcpClientConfigurationDetailsDescription=Utilizza questi dati di configurazione per connetterti al server XPipe MCP dal tuo client MCP preferito.\nswitchHostAddress=Cambiare l'indirizzo dell'host\naddAnotherHostName=Aggiungi un altro nome di host\naddNetwork=Scansione di rete ...\nnetworkScan=Scansione di rete\nnetworkScanStore=Host di destinazione\nnetworkScanStoreDescription=L'host per il quale eseguire la scansione della rete locale\nuseAsGateway=Usa l'host come gateway\nuseAsGatewayDescription=Se utilizzare o meno l'host di destinazione come gateway per le connessioni create\nnetworkScanPorts=Porte da scansionare\nnetworkScanPortsDescription=L'elenco separato da virgole delle porte da includere nella scansione\nnetworkScanType=Tipo di connessione\nnetworkScanTypeDescription=Il tipo di server da cercare\nemptyDirectory=Questa directory sembra essere vuota\nhcloudConfigFile=file di configurazione hcloud\nhcloudConfigFileDescription=La posizione del file di configurazione .toml di hcloud CLI\npreferMonochromeIcons=Preferisci le icone monocromatiche\npreferMonochromeIconsDescription=Quando è abilitata, le variabili monocromatiche delle icone saranno scelte rispetto alle versioni colorate predefinite di un'icona, a condizione che sia disponibile una variante separata di icona in modalità chiara o scura per un'icona da una fonte.\\n\\nRichiede un aggiornamento delle icone da applicare.\nalwaysShowSshMotd=Mostra sempre MOTD\nalwaysShowSshMotdDescription=Se mostrare o meno il messaggio del giorno configurato su un sistema remoto al momento del login in una nuova sessione di terminale. Si noti che la modifica di questa opzione potrebbe alterare il comportamento di inizializzazione delle connessioni SSH.\nmanageSubscription=Gestire l'abbonamento\nnoListeningServer=Nessun server in ascolto\nnetworkScanResults=Risultati della scansione\nnetworkScanResultsDescription=L'elenco dei sistemi trovati nella rete\nlocalShellDialect=Guscio locale\nlocalShellDialectDescription=La shell utilizzata per le operazioni locali. Nel caso in cui la normale shell locale predefinita sia disabilitata o in qualche modo danneggiata, questa opzione può essere utilizzata per tornare a un'altra alternativa.\\n\\nAlcune configurazioni come le voci PATH personalizzate potrebbero non essere applicate alla shell di ripiego se non sono ancora configurate nei rispettivi file di profilo della shell.\nagentSocketNotFound=Non è stato trovato alcun socket attivo per l'agente\nagentSocket=Posizione del socket\nagentSocketDescription=Il percorso del file socket dell'agente\nagentSocketNotConfigured=Non è stato ancora configurato alcun socket personalizzato\ndownloadInProgress=$NAME$ download in corso\nenableTerminalStartupBell=Abilita la campana di avvio del terminale\nenableTerminalStartupBellDescription=Riproduce un segnale acustico/campanellino in una nuova sessione di terminale. Se il tuo emulatore di terminale supporta i campanelli, questo può essere utilizzato per facilitare l'identificazione delle istanze di terminale appena avviate.\ninvalidSshGatewayChain=Configurazione mista non valida della catena di gateway con gateway di salto e gateway non di salto.\nsyncFileExists=Il file sincronizzato $FILE$ esiste già\nreplaceFile=Sostituire un file\nreplaceFileDescription=Ho sostituito il file esistente con questo\nrenameFile=Rinominare un file\nrenameFileDescription=Dai a questo file un nome diverso per sincronizzarlo\nnewFileName=Nuovo nome di file\nparentHostDoesNotSupportTunneling=L'host principale $NAME$ non supporta il tunneling\nconnectionNotesTemplate=Modello di nota\nconnectionNotesTemplateDescription=Il modello markdown da utilizzare quando si aggiunge una nuova voce di note a una connessione.\nconnectionNotesButton=Modifica le note\nrdpSmartSizing=Abilita il dimensionamento intelligente\nrdpSmartSizingDescription=Quando è abilitato, mstsc ridimensiona le dimensioni del desktop se la finestra è troppo piccola per essere visualizzata alla massima risoluzione. Il rapporto d'aspetto del desktop viene mantenuto quando viene ridimensionato.\ndisableStartOnInit=Disabilita l'avvio automatico\nenableStartOnInit=Abilita l'avvio automatico\nfileReadSudoTitle=Lettura di file Sudo\nfileReadSudoContent=Il file che stai cercando di leggere non ti concede i permessi di lettura come utente corrente. Vuoi leggere questo file come utente root con sudo? In questo modo l'utente verrà automaticamente elevato a root con le credenziali esistenti o tramite un prompt.\nnetbirdInstall.displayName=Installazione di Netbird\nnetbirdInstall.displayDescription=Connettersi ai peer della rete Netbird\nnetbirdProfile.displayName=Profilo Netbird\nnetbirdProfile.displayDescription=Elenco dei peer in un profilo specifico\nnetbirdPeer.displayName=Netbird peer\nnetbirdPeer.displayDescription=Connettersi a un peer tramite SSH\nnetbirdPublicKey=Chiave pubblica\nnetbirdPublicKeyDescription=La chiave pubblica interna del peer\nnetbirdHostName=Nome dell'host\nnetbirdHostNameDescription=Il nome dell'host del peer nella rete\nvncRefSystem=Sistema associato\nvncRefSystemDescription=La voce di connessione a cui associare questa connessione VNC. Lasciare vuoto se non esiste\nabstractHost.displayName=Ospite astratto\nabstractHost.displayDescription=Creare una voce per un host che non supporta le connessioni shell\nabstractHostAddress=Indirizzo dell'host\nabstractHostAddressDescription=L'indirizzo dell'host\nabstractHostGateway=Gateway\nabstractHostGatewayDescription=Il sistema di gateway opzionale attraverso il quale raggiungere questo host\nabstractHostConvert=Convertire in voce host astratta\nhostNoConnections=Nessuna connessione disponibile\nhostHasConnections=$COUNT$ connessioni disponibili\nhostHasConnection=$COUNT$ connessione disponibile\nlargeFileWarningTitle=Modifica di un file di grandi dimensioni\nlargeFileWarningContent=Il file che vuoi modificare è piuttosto grande con $SIZE$. Vuoi davvero aprire questo file con il tuo editor di testo?\nrdpAskpassUser=Nome utente RDP per l'host $HOST$\nrdpAskpassPassword=Password per l'utente $USER$\ninPlaceKey=Chiave di lettura\ninPlaceKeyText=Contenuto della chiave privata\ninPlaceKeyTextDescription=Il contenuto della chiave privata\nnetbirdSelfhosted=Istanza netbird autogestita\nnetbirdSelfhostedDescription=Fornire un URL personalizzato invece di utilizzare la versione ospitata nel cloud\nnetbirdManagementUrl=URL di gestione Netbird\nnetbirdManagementUrlDescription=L'URL di gestione della tua istanza self-hosted\nnetbirdSetupKey=Tasto di impostazione\nnetbirdSetupKeyDescription=Se utilizzi i tasti di configurazione, puoi usarne uno per il login\nnetbirdLogin=Accesso a Netbird\naddProfile=Aggiungi profilo\nnetbirdProfileNameAsktext=Nome del nuovo profilo Netbird\nopenSftp=Aprire una sessione SFTP\ncapslockWarning=Hai attivato il capslock\ninherit=Eredita\nsshConfigStringSelected=Host di destinazione\nsshConfigStringSelectedDescription=Nel caso di più host, il primo viene utilizzato come destinazione. Riordina gli host per cambiare la destinazione\ntunnelToLocalhost=Tunnel verso localhost\ntunnelToLocalhostDescription=Effettua automaticamente il tunnel della porta remota verso localhost\ntags=Tag\ntag=Tag\naddNewTag=Crea un nuovo tag\ncreateTag=Crea un tag ...\ninPlacePublicKey=Chiave pubblica\ninPlacePublicKeyDescription=La chiave pubblica associata alla chiave privata specificata\nsshKeygenTitle=Generare una nuova chiave SSH\nsshKeygenAlgorithm=Algoritmo\nsshKeygenAlgorithmDescription=L'algoritmo di keygen asimmetrico da utilizzare per la chiave\nrsaBits=Bit\nrsaBitsDescription=Numero di bit nella chiave generata\nsshKeygenComment=Commento\nsshKeygenCommentDescription=Il commento opzionale per questa chiave\nsshKeygenPassphrase=Passphrase\nsshKeygenPassphraseDescription=La passphrase opzionale per questa chiave\ned25519SkResident=Creare una chiave residente\ned25519SkResidentDescription=Memorizza la chiave privata sulla chiave di sicurezza hardware\ned25519SkResidentKeyName=Etichetta della chiave residente\ned25519SkResidentKeyNameDescription=Assegna un'etichetta alla chiave. Necessario quando si memorizzano più chiavi sulla chiave di sicurezza\ned25519SkPinRequired=Richiedi il PIN\ned25519SkPinRequiredDescription=Richiede l'inserimento del PIN al momento dell'utilizzo\ned25519SkUserPresenceRequired=Richiede la presenza dell'utente\ned25519SkUserPresenceRequiredDescription=Richiede l'uso del touch o simili. Alcune chiavi di sicurezza richiedono che questo sia abilitato\ncopyPublicKey=Copia della chiave pubblica\ngeneratePublicKey=Generare una chiave pubblica\npublicKeyGenerateNotice=Può essere generato dalla chiave privata\nidentityApplyTargetHost=Obiettivo\nidentityApplyTargetHostDescription=Il sistema per applicare l'identità a\nidentityApplyAuthorizedHost=Chiave SSH autorizzata\nidentityApplyAuthorizedHostDescription=La chiave SSH viene aggiunta al file hosts autorizzato\nidentityApplyAuthorizedHostButton=Aggiungi la chiave al file\napplyIdentityToHost=Applicare l'identità all'host ...\nidentityApplyMissingPublicKeyTitle=Chiave pubblica mancante\nidentityApplyMissingPublicKeyContent=La chiave SSH dell'identità non ha una chiave pubblica associata. Controlla la configurazione per maggiori dettagli.\nvalid=Valido\nnotValid=Non valido\nwarning=Avviso\nidentityApplyTitle=Applicare l'identità\nidentityApplyConfigPasswordEnabled=Autenticazione con password abilitata\nidentityApplyConfigPasswordEnabledDescription=L'autenticazione tramite password è ancora abilitata nella configurazione di sshd\nidentityApplyConfigPasswordDisabled=Autenticazione della password disabilitata\nidentityApplyConfigPasswordDisabledDescription=L'autenticazione con password è ancora disabilitata nella configurazione di sshd\nidentityApplyConfigKeyEnabled=Autenticazione delle chiavi abilitata\nidentityApplyConfigKeyEnabledDescription=L'autenticazione basata su chiavi è ancora abilitata nella configurazione di sshd\nidentityApplyConfigKeyDisabled=Autenticazione delle chiavi disabilitata\nidentityApplyConfigKeyDisabledDescription=L'autenticazione basata su chiavi è ancora disabilitata nella configurazione di sshd\nidentityApplyConfigRootDisabledWarning=Accesso di root disabilitato\nidentityApplyConfigRootDisabledWarningDescription=Il login dell'utente root non è abilitato nella configurazione di sshd\nidentityApplyConfigAdminWarning=Tasti amministratore configurati\nidentityApplyConfigAdminWarningDescription=La chiave potrebbe dover essere aggiunta a administrators_authorized_keys per gli utenti admin\nidentityApplyEditConfig=Modifica configurazione\nidentityApplyEditConfigDescription=Aprire la configurazione di sshd nell'editor per correggere eventuali problemi\nidentityApplyEditAuthorizedKeys=Modifica delle chiavi autorizzate\nidentityApplyEditAuthorizedKeysDescription=Apri il file authorized_keys nell'editor per modificare o rimuovere altre chiavi\nidentityApplyEditConfigButton=Aprire sshd_config\nidentityApplyEditAuthorizedKeysButton=Aprire chiavi_autorizzate\nidentityApplySetStoreIdentity=Set di identità di connessione\nidentityApplySetStoreIdentityDescription=L'identità è configurata per essere utilizzata dalla connessione\nidentityApplySetStoreIdentityButton=Applicare l'identità\ngenerateKey=Generare una chiave\ngroupSecretStrategy=Controllo dell'accesso basato su gruppi\ngroupSecretStrategyDescription=Come recuperare il segreto di gruppo utilizzato per la crittografia e la decrittografia del gruppo. Il metodo di recupero scelto verrà eseguito quando un utente accede al vault all'avvio.\\n\\nQuesta impostazione viene configurata per ogni gruppo. Per modificare questa impostazione per un gruppo diverso da quello attualmente attivo, dovrai accedere al vault come membro di quel gruppo.\nfileSecret=Segreto basato su file\ncommandSecret=Comando\nhttpRequestSecret=Risposta HTTP\nfileSecretChoice=Posizione dei file\nfileSecretChoiceDescription=Il percorso del file contenente il segreto di crittografia del gruppo. Poiché questo file può essere interrogato su tutte le piattaforme, puoi usare ~ nel percorso per fare riferimento alla directory home. Il file deve essere disponibile su tutti i sistemi da cui sbloccherai il vault, altrimenti il login fallirà.\ncommandSecretField=Script di recupero\ncommandSecretFieldDescription=Il comando che restituisce la chiave di crittografia segreta del gruppo corrente. Il comando viene eseguito nella shell predefinita del sistema locale e la chiave deve essere stampata su stdout.\nhttpRequestSecretField=URI di richiesta\nhttpRequestSecretFieldDescription=L'URI a cui inviare una richiesta HTTP. Il segreto del gruppo viene preso dal corpo della risposta HTTP.\nvaultAuthentication=Autenticazione del caveau\nvaultAuthenticationDescription=Come autenticare/sbloccare i dati del caveau. Esistono diversi modi per crittografare e sbloccare i dati del caveau, a seconda di chi vuole condividere i dati del caveau.\ngroupAuthFailed=Autenticazione segreta fallita\nuserAuthFailed=Autenticazione della password fallita\nsavingChanges=Salvataggio delle modifiche\nawsDeviceScan=AWS\naws=AWS\nawsCliInstallTitle=AWS CLI richiesto\nawsCliInstallContent=L'integrazione con AWS richiede l'installazione di AWS CLI sul tuo sistema locale\nawsProfileCreateTitle=Nuovo profilo AWS\nawsProfileAccessKey=Chiave di accesso\nawsProfileName=Nome del profilo\nawsProfileNameDescription=Il nome visualizzato del nuovo profilo\nawsProfileRegion=Regione\nawsProfileRegionDescription=La regione AWS associata al profilo\nawsProfileAccessKeyId=ID della chiave di accesso\nawsProfileAccessKeyIdDescription=L'ID della chiave di accesso dell'utente IAM\nawsProfileSecretAccessKey=Chiave di accesso segreta\nawsProfileSecretAccessKeyDescription=La chiave di accesso segreta associata\nawsInstall.displayName=Installazione di AWS CLI\nawsInstall.displayDescription=Connettersi ai sistemi AWS tramite la CLI di AWS\nawsProfile.displayName=Profilo AWS CLI\nawsProfile.displayDescription=Accesso ad AWS attraverso un profilo specifico\nawsInstanceId=ID istanza\nawsInstanceIdDescription=L'ID interno di questa istanza\nawsInstanceUseSsm=Connettersi tramite SSM\nawsInstanceUseSsmDescription=Usa lo strumento SSM per connetterti all'istanza tramite SSH\nawsEc2Instance.displayName=Istanza AWS EC2\nawsEc2Instance.displayDescription=Connettersi a un'istanza EC2 tramite SSH\nawsS3Group.displayName=Secchi S3\nawsS3Group.displayDescription=Accedere ai bucket S3 di un profilo AWS\nawsS3Bucket.displayName=Secchio S3\nawsS3Bucket.displayDescription=Accedere a un bucket S3 di un profilo AWS\nawsEc2Group.displayName=Istanze EC2\nawsEc2Group.displayDescription=Accesso alle istanze EC2 di un profilo AWS\nawsEc2InstanceSsmTerminal=Aprire il terminale SSM\ngenericS3Bucket.displayName=Secchio S3 generico\ngenericS3Bucket.displayDescription=Accedere a un generico bucket S3 tramite la CLI di AWS\naddFileSystem=File system ...\ngenericS3BucketHost=Ospite\ngenericS3BucketHostDescription=La voce host o l'indirizzo manuale del server S3\ngenericS3BucketPortDescription=La porta su cui è in ascolto il server S3\ngenericS3BucketAccessKeyId=ID della chiave di accesso\ngenericS3BucketAccessKeyIdDescription=L'ID della chiave di accesso dell'utente IAM\ngenericS3BucketSecretAccessKey=Chiave di accesso segreta\ngenericS3BucketSecretAccessKeyDescription=La chiave di accesso segreta associata\ngenericS3BucketHttps=Abilita HTTPS\ngenericS3BucketHttpsDescription=Usa HTTPS per connetterti al server. Alcuni provider potrebbero richiedere l'HTTPS\ntunnelled=Tunneled\nawsInstallSync=Sincronizzazione della configurazione\nawsInstallSyncDescription=Sincronizzare i file di configurazione di AWS CLI con il vault git\nawsInstallLocation=Posizione dei dati dell'utente\nawsInstallLocationDescription=Il percorso da cui provengono i file di configurazione della AWS CLI\ninstanceActions=Azioni dell'istanza\nopenSplit=Aprire in un terminale diviso\nterminalSplitStrategy=Direzione della vista divisa\nterminalSplitStrategyDescription=Controlla il modo in cui vengono suddivise le schede del terminale quando si utilizza la funzionalità di visualizzazione suddivisa in modalità batch per aprire più sessioni di terminale una accanto all'altra.\nterminalSplitStrategyDisabledDescription=Controlla il modo in cui vengono suddivise le schede del terminale quando si utilizza la funzionalità di visualizzazione suddivisa in modalità batch per aprire più sessioni di terminale una accanto all'altra.\\n\\nLa configurazione attuale del terminale non supporta le visualizzazioni divise.\nhorizontal=Orizzontale\nvertical=Verticale\nbalanced=Equilibrato\nclose=Chiudere\nhelpButton=$TOPIC$ link alla documentazione\nquickAccess=Accesso rapido\ntoggleEnabled=Stato di commutazione\ncurrentPath=Percorso attuale\ndirectoryContents=Contenuti della directory\ndirectoryOptions=Opzioni della directory\nchooseConnectionType=Scegliere il tipo di connessione\nbatchMode=Modalità batch\ntoggleButton=Pulsante Toggle\ntailscaleUseSsh=Usa tailscale SSH auth\ntailscaleUseSshDescription=Accedi tramite il server SSH di tailscale stesso senza alcuna autenticazione SSH\nportDescription=La porta su cui gira il server SSH\nloginAs=Accedi come\nsshGatewayType=Tipo di gateway\nsshGatewayTypeDescription=Se connettersi alla destinazione tramite un tunnel o con l'opzione ProxyJump\ngatewayTunnel=Tunnel gateway\nproxyJump=Salto proxy\ncommandTypeAsyncBackground=Eseguire in background\ncommandTypeSyncBackground=Eseguire in background e attendere il completamento\ncommandTypeTerminalBackground=Aprire nel terminale\nasyncBackgroundCommand=Comando di sfondo\nsyncBackgroundCommand=Comando di blocco in background\nterminalBackgroundCommand=Comando del terminale\ntestingConnection=Verifica della connessione ...\nopenManagementConsole=Console di gestione aperta\nopenLxcTerminal=Aprire il terminale LXC\nopenContainerConsole=Aprire la console seriale\nkeeper2fa=metodo 2FA\nkeeper2faDescription=Il metodo principale di autenticazione a due fattori configurato per il tuo account. Abilita questa opzione se il tuo account Keeper richiede l'autenticazione a due fattori per accedere alle password.\nkeeperTotpDuration=Durata del codice 2FA personalizzato\nkeeperTotpDurationDescription=Annulla la durata predefinita della validità di un codice 2FA. Si applica solo se la politica dell'organizzazione consente di modificare la durata.\\n\\nI valori possibili sono: $VALUES$\nkeeperOtherAuth=Altro (RSA SecurID, Duo Security, Keeper DNA, ecc.)\nextractReusableIdentities=Estrarre identità riutilizzabili\nidentitiesAdded=Identità aggiunte\nsyncMode=Modalità di sincronizzazione\nsyncModeDescription=Controlla la modalità di sincronizzazione delle modifiche.\\n\\nLa modalità istantanea spinge e tira le modifiche il prima possibile, la modalità di avvio e di uscita sincronizza tutte le modifiche apportate durante una sessione in una sola volta, mentre la modalità manuale sincronizza solo quando sei tu ad avviarla.\ntoggleTerminalDock=Dock del terminale\nscriptDirectory=Posizione della directory\nscriptDirectoryDescription=La directory locale contenente i file di script di shell\nscriptSourceUrl=URL del repository\nscriptSourceUrlDescription=L'URL di un repository git remoto contenente file di script di shell\nscriptCollectionSourceType=Tipo di fonte\nscriptCollectionSourceTypeDescription=Il tipo di sorgente da cui devono essere caricati gli script di shell\nscriptCollectionSourceEntry=Fonte\nscriptCollectionSourceEntryDescription=La fonte da cui caricare gli script di shell\ngitRepository=Repository Git\nscriptCollectionSource.displayName=Fonte dello script\nscriptCollectionSource.displayDescription=Importa automaticamente gli script di shell da una fonte esistente\ndirectorySource=Fonte della directory\ngitRepositorySource=Fonte del repository Git\nrefreshSource=Aggiorna la fonte\nscriptTextSourceUrl=URL dello script\nscriptTextSourceUrlDescription=L'URL da cui recuperare il file di script\nscriptSourceType=Fonte dello script\nscriptSourceTypeDescription=Da dove prendere lo script\nscriptSourceTypeInPlace=Script in-place\nscriptSourceTypeUrl=URL esterno\nscriptSourceTypeSource=Fonte esistente\nimportScripts=Importazione di script\nscriptsContained=$NUMBER$ script\nscriptSourceCollectionImportTitle=Importazione di script dalla sorgente ($SELECTED$/$COUNT$)\nnoScriptsFound=Nessuno script trovato\ntunnel=Tunnel\nnotInitialized=Non inizializzato\nselectCategory=Seleziona la categoria ...\nscriptSourceName=Nome dello script\nscriptSourceNameDescription=Il nome del file dello script nel sorgente\nworkspaceRestartTitle=Spazio di lavoro pronto\nworkspaceRestartContent=All'indirizzo $PATH$ è stato creato un collegamento al nuovo spazio di lavoro. Puoi navigare verso il collegamento o riavviare XPipe per aprire automaticamente il nuovo spazio di lavoro.\nbrowseShortcut=Sfogliare un file\nsyncModeInstant=Sincronizzazione istantanea\nsyncModeSession=Sincronizzazione all'avvio e all'uscita\nsyncModeManual=Sincronizzazione manuale\npushChanges=Modifiche push\npullChanges=Cambiamenti di tipo \"pull\nsourcedFrom=Proveniente da $SOURCE$\ninPlaceScript=Script in-place\ngeneric=Generico\nsyncToPlainDirectory=Sincronizzazione con la directory normale\nsyncToPlainDirectoryDescription=Quando sincronizzi una directory locale, puoi trattare questa directory come un altro repository git oppure come una semplice directory. Se l'impostazione directory semplice è attivata, la directory non viene inizializzata come repository git.\nopenSpiceSession=Aprire una sessione SPICE\nterminalBehaviour=Comportamento del terminale\nnoScanPossible=Non sono state trovate connessioni supportate\nnetworkSwitchPorts=Porte di rete\nnswitchGroup.displayName=Porte di rete\nnswitchGroup.displayDescription=Elenco delle porte disponibili su un dispositivo di rete\nnswitchPort.displayName=Porta di rete\nnswitchPort.displayDescription=Controllare una singola porta di un dispositivo switch di rete\nenablePort=Abilita la porta\nshutdownPort=Arresto della porta\nresetPort=Porta di reset\nuseSystemDefault=Usa l'impostazione predefinita del sistema\nportStatus=Stato della porta\nclearCounters=Cancella i contatori\nshowStatus=Mostra stato\nshowAllPorts=Mostra tutte le porte\nactiveLicense=Licenza\nactiveLicenseDescription=Attivare una chiave di licenza XPipe\nauthenticatorApp=App Autenticatore\nsecurityKey=Chiave di sicurezza\nmcpAdditionalContext=Contesto MCP aggiuntivo\nmcpAdditionalContextDescription=Istruzioni aggiuntive da passare al client MCP. Utilizzale per controllare il comportamento dell'agente e fornire un contesto aggiuntivo per la tua configurazione individuale.\nmcpAdditionalContextSample=- Non riavviare automaticamente i servizi e i demoni senza averne prima avuto conferma\\n- Quando configuri un'interfaccia di rete, usa sempre 192.168.1.1/24 come gateway\nprefsRestartTitle=Riavvio necessario\nprefsRestartContent=Alcune opzioni che hai modificato richiedono il riavvio dell'applicazione per essere applicate. Vuoi riavviare XPipe adesso?\nbashShell=Guscio Bash\n"
  },
  {
    "path": "lang/strings/translations_ja.properties",
    "content": "delete=削除する\nproperties=プロパティ\nusedDate=中古$DATE$\nopenDir=オープンディレクトリ\nsortLastUsed=最終使用日でソートする\nsortAlphabetical=アルファベット順に並べる\nsortIndexed=順序インデックスでソートする\nrestartDescription=再起動はしばしば迅速な解決策となる\nreportIssue=問題を報告する\nreportIssueDescription=統合された問題レポーターを開く\nusefulActions=便利なアクション\nstored=保存された\ntroubleshootingOptions=トラブルシューティングツール\ntroubleshoot=トラブルシューティング\nremote=リモートファイル\naddShellStore=シェルを追加する\naddShellTitle=シェル接続を追加する\nsavedConnections=保存された接続\nsave=保存\nclean=クリーン\nmoveTo=移動する\naddDatabase=データベース ...\nbrowseInternalStorage=内部ストレージをブラウズする\naddTunnel=トンネル ...\naddService=サービス ...\naddScript=スクリプト ...\naddHost=リモートホスト ...\naddShell=シェル環境 ...\naddCommand=コマンド ...\naddAutomatically=自動的に追加される\naddOther=その他を追加する\nconnectionAdd=接続を追加する\nscriptAdd=スクリプトを追加する\nscriptGroupAdd=スクリプトグループを追加する\nidentityAdd=IDを追加する\nnew=新しい\nselectType=タイプを選択する\nselectTypeDescription=接続タイプを選択する\nselectShellType=シェルタイプ\nselectShellTypeDescription=シェル接続のタイプを選択する\nname=名前\nstoreIntroHeader=接続ハブ\nstoreIntroContent=ローカルとリモートのシェル接続を一元管理できる。まず始めに、利用可能な接続を自動的に素早く検出し、追加する接続を選択することができる。\nstoreIntroButton=接続を検索する\ndragAndDropFilesHere=または、ここにファイルをドラッグ・アンド・ドロップする\nconfirmDsCreationAbortTitle=中止を確認する\nconfirmDsCreationAbortHeader=データソースの作成を中止するか？\nconfirmDsCreationAbortContent=データソース作成の進行状況はすべて失われる。\nconfirmInvalidStoreTitle=検証をスキップする\nconfirmInvalidStoreContent=接続の検証をスキップするか？接続が検証されなかった場合でも、この接続を追加し、後で接続の問題を修正することができる。\nexpand=拡大する\naccessSubConnections=アクセスサブ接続\ncommon=一般的な\ncolor=カラー\nalwaysConfirmElevation=許可の昇格を常に確認する\nalwaysConfirmElevationDescription=sudoなど、システム上でコマンドを実行するために昇格パーミッションが必要な場合の処理方法を制御する。\\n\\nデフォルトでは、sudo 認証情報はセッション中にキャッシュされ、 必要なときに自動的に提供される。このオプションを有効にすると、毎回昇格アクセスの確認を求められる。\nallow=許可する\nask=尋ねる\ndeny=拒否する\nshare=gitリポジトリに追加する\nunshare=gitリポジトリから削除する\nremove=削除する\ncreateNewCategory=新しいサブカテゴリー\nprompt=プロンプト\ncustomCommand=カスタムコマンド\nother=その他\nsetLock=ロックを設定する\nselectConnection=接続を選択する\nselectEntry=エントリーを選択する\ncreateLock=パスフレーズを作成する\nchangeLock=パスフレーズを変更する\ntest=テスト\nfinish=終了する\nerror=エラーが発生した\ndownloadStageDescription=ダウンロードしたファイルをシステムのダウンロード・ディレクトリに移動し、開く。\nok=OK\nsearch=検索\nrepeatPassword=リピートパスワード\naskpassAlertTitle=アスクパス\nunsupportedOperation=サポートされていない操作：$MSG$\nfileConflictAlertTitle=競合を解決する\nfileConflictAlertContent=競合が発生した。ファイル$FILE$ はターゲット・システムに既に存在する。\\n\\nどうする？\nfileConflictAlertContentMultiple=競合が発生した。ファイル$FILE$ はすでに存在する。\\n\\nどうする？すべてに適用されるオプションを選択することで、自動的に解決できる競合がもっとあるかもしれない。\nmoveAlertTitle=移動を確認する\nmoveAlertHeader=($COUNT$) で選択した要素を$TARGET$ に移動するか？\ndeleteAlertTitle=削除を確認する\ndeleteAlertHeader=選択した ($COUNT$) 要素を削除するか？\nselectedElements=選択された要素：\nmustNotBeEmpty=$VALUE$ は空であってはならない\nvalueMustNotBeEmpty=値は空であってはならない\ntransferDescription=ここにファイルをドラッグしてダウンロードする\ndragLocalFiles=ここからダウンロードをドラッグする\nnull=$VALUE$ はnullであってはならない。\nroots=ルーツ\nscripts=スクリプト\nsearchFilter=検索 ...\nrecent=最近の\nshortcut=ショートカット\nbrowserWelcomeEmptyHeader=ファイルブラウザ\nbrowserWelcomeEmptyContent=ファイルブラウザで開くシステムを左側で選択できる。XPipeは、以前にアクセスしたシステムやディレクトリを記憶し、今後クイックアクセスメニューに表示する。\nbrowserWelcomeEmptyButton=ローカルのファイルブラウザを開く\nbrowserWelcomeSystems=あなたは最近、以下のシステムに接続した：\nbrowserWelcomeDocsHeader=ドキュメンテーション\nbrowserWelcomeDocsContent=XPipeを使いこなすために、よりガイド的なアプローチがお望みなら、ドキュメントのウェブサイトをチェックしよう。\nbrowserWelcomeDocsButton=ドキュメントを開く\nhostFeatureUnsupported=$FEATURE$ がホストにインストールされていない\nmissingStore=$NAME$ 存在しない\nconnectionName=接続名\nconnectionNameDescription=この接続にカスタム名を付ける\nopenFileTitle=ファイルを開く\nunknown=不明\nscanAlertTitle=接続を追加する\nscanAlertChoiceHeader=ターゲット\nscanAlertChoiceHeaderDescription=接続を検索する場所を選択する。これは、利用可能なすべての接続を最初に検索する。\nscanAlertHeader=接続タイプ\nscanAlertHeaderDescription=システムに自動的に追加する接続のタイプを選択する。\nnoInformationAvailable=情報がない\nyes=はい\nno=いいえ\nerrorOccured=エラーが発生した\nterminalErrorOccured=端末エラーが発生した\nerrorTypeOccured=$TYPE$ 型の例外が発生した。\npermissionsAlertTitle=必要なパーミッション\npermissionsAlertHeader=この操作を行うには、追加のパーミッションが必要である。\npermissionsAlertContent=ポップアップに従い、XPipeの設定メニューで必要な権限を与える。\nerrorDetails=エラーの詳細\nupdateReadyAlertTitle=更新準備完了\nupdateReadyAlertHeader=バージョン$VERSION$ へのアップデートをインストールする準備ができた。\nupdateReadyAlertContent=これで新しいバージョンがインストールされ、インストールが終了したらXPipeを再起動する。\nerrorNoDetail=エラーの詳細がわからない\nerrorNoExceptionMessage=$TYPE$ 型のエラーが発生した。\nupdateAvailableTitle=更新可能\nupdateAvailableContent=XPipeの更新プログラム（バージョン$VERSION$ ）をインストールできる。XPipeが起動できなくても、更新プログラムをインストールすることで問題を解決できる可能性がある。\nclipboardActionDetectedTitle=クリップボードアクションが検出された\nclipboardActionDetectedContent=XPipeがクリップボードのコンテンツを検出した。今すぐ開くか？クリップボードの内容をインポートするか？\ninstall=インストールする\nignore=無視する\npossibleActions=利用可能なアクション\nreportError=エラーを報告する\nreportOnGithub=GitHub で課題レポートを作成する\nreportOnGithubDescription=GitHub リポジトリに新しい課題をオープンする\nreportErrorDescription=任意のユーザーフィードバックと診断情報を含むエラーレポートを送信する\nignoreError=エラーを無視する\nignoreErrorDescription=このエラーを無視して、何事もなかったかのように続ける\nprovideEmail=連絡方法（任意、返信を希望する場合のみ）。あなたの報告はデフォルトでは匿名である。\nadditionalErrorInfo=追加情報を提供する（オプション）\nadditionalErrorAttachments=添付ファイルを選択する（オプション）\ndataHandlingPolicies=プライバシーポリシー\nsendReport=レポートを送信する\nerrorHandler=エラーハンドラ\nevents=イベント\nvalidate=検証する\nstackTrace=スタックトレース\npreviousStep=< 前へ\nnextStep=次のページ\nfinishStep=完了する\nselect=選択する\nbrowseInternal=内部をブラウズする\ncheckOutUpdate=チェックアウト更新\nquit=終了する\nnoTerminalSet=端末アプリケーションが自動設定されていない。設定メニューで手動で設定できる。\nconnections=接続\nconnectionHub=接続ハブ\nsettings=設定\nexplorePlans=ライセンス\nhelp=ヘルプ\nabout=について\ndeveloper=開発者\nbrowseFileTitle=ファイルをブラウズする\nbrowser=ファイルブラウザ\nselectFileFromComputer=このコンピューターからファイルを選択する\nlinks=リンク\nwebsite=ウェブサイト\ndiscordDescription=Discordサーバーに参加する\nredditDescription=XPipeサブレディットに参加する\nsecurity=セキュリティ\nsecurityPolicy=セキュリティ情報\nsecurityPolicyDescription=詳細なセキュリティポリシーを読む\nprivacy=プライバシーポリシー\nprivacyDescription=XPipeアプリケーションのプライバシーポリシーを読む\nslackDescription=Slackワークスペースに参加する\nsupport=サポート\ngithubDescription=GitHubリポジトリをチェックする\nopenSourceNotices=オープンソースのお知らせ\ncheckForUpdates=アップデートを確認する\ncheckForUpdatesDescription=アップデートがあればダウンロードする\nlastChecked=最終チェック\nversion=バージョン\nbuild=ビルドバージョン\nruntimeVersion=ランタイムバージョン\nvirtualMachine=仮想マシン\nupdateReady=アップデートをインストールする\nupdateReadyPortable=チェックアウト更新\nupdateReadyDescription=アップデートがダウンロードされ、インストールする準備ができた。\nupdateReadyDescriptionPortable=アップデートがダウンロードできる\nupdateRestart=再起動して更新する\nnever=決して\nupdateAvailableTooltip=更新可能\nptbAvailableTooltip=パブリックテストビルド\nvisitGithubRepository=GitHubリポジトリにアクセスする\nupdateAvailable=アップデート可能：$VERSION$\ndownloadUpdate=ダウンロード更新\nlegalAccept=エンドユーザーライセンス契約に同意する\nconfirm=確認する\nprint=印刷する\nwhatsNew=バージョン$VERSION$ ($DATE$) の新機能\nantivirusNoticeTitle=アンチウイルスプログラムについて\nupdateChangelogAlertTitle=変更履歴\ngreetingsAlertTitle=XPipeへようこそ\neula=エンドユーザー使用許諾契約書\nnews=ニュース\nintroduction=はじめに\nprivacyPolicy=プライバシーポリシー\nagree=同意する\ndisagree=同意しない\ndirectories=ディレクトリ\nlogFile=ログファイル\nlogFiles=ログファイル\nlogFilesAttachment=ログファイル\nissueReporter=問題レポーター\nopenCurrentLogFile=ログファイル\nopenCurrentLogFileDescription=現在のセッションのログファイルを開く\nopenLogsDirectory=ログディレクトリを開く\ninstallationFiles=インストールファイル\nopenInstallationDirectory=インストールファイル\nopenInstallationDirectoryDescription=XPipeのインストールディレクトリを開く\nlaunchDebugMode=デバッグモード\nlaunchDebugModeDescription=XPipeをデバッグモードで再起動する\nextensionInstallTitle=ダウンロード\nextensionInstallDescription=このアクションには、XPipeが配布していない追加のサードパーティライブラリが必要である。ここで自動的にインストールできる。コンポーネントはベンダーのウェブサイトからダウンロードする：\nextensionInstallLicenseNote=ダウンロードおよび自動インストールを実行することにより、サードパーティライセンスの条項に同意したものとみなされる：\nlicense=ライセンス\ninstallRequired=インストールが必要\nrestore=リストア\nrestoreAllSessions=すべてのセッションを復元する\nlimitedTouchscreenMode=タッチスクリーン限定モード\nlimitedTouchscreenModeDescription=電話画面のような、よりエキゾチックなタッチスクリーンインターフェースでこのアプリケーションを使用する場合、一部のメニューが正しく動作しないことがある。このオプションを有効にすると、メニューの実装は、マウス/タッチ・イベントがまばらに送信されても動作するように、より制限された機能を使用する。\nappearance=外観\ndisplay=表示\npersonalization=パーソナライゼーション\ndisplayOptions=表示オプション\ntheme=テーマ\nrdpConfiguration=リモートデスクトップの設定\nrdpClient=RDPクライアント\nrdpClientDescription=RDP接続を開始するときに呼び出すRDPクライアントプログラム。\\n\\n様々なクライアントの能力や統合の程度が異なることに注意。クライアントの中には、パスワードの自動受け渡しに対応していないものもあるので、起動時にパスワードを入力する必要がある。\nlocalShell=ローカルシェル\nthemeDescription=お好みの表示テーマ。\ndontAutomaticallyStartVmSshServer=必要なときにVM用のSSHサーバーを自動的に起動しない\ndontAutomaticallyStartVmSshServerDescription=ハイパーバイザーで稼働しているVMへのシェル接続は、SSHを介して行われる。XPipeは、必要に応じてインストールされたSSHサーバを自動的に起動することができる。セキュリティ上の理由でこれを望まない場合は、このオプションでこの動作を無効にすることができる。\nconfirmGitShareTitle=Git同期\nconfirmGitShareContent=選択したファイルを git vault リポジトリに追加するか。これにより、暗号化されたバージョンのファイルがあなたの git vault にコピーされ、変更がコミットされる。すると、同期しているすべてのデスクトップでそのファイルにアクセスできるようになる。\ngitShareFileTooltip=git vaultのデータディレクトリにファイルを追加し、自動的に同期されるようにする。\\n\\nこのアクションは、設定でgit vaultが有効になっている場合にのみ使用できる。\nperformanceMode=パフォーマンスモード\nperformanceModeDescription=アプリケーションのパフォーマンスを向上させるために不要な視覚効果をすべて無効にする。\ndontAcceptNewHostKeys=新しいSSHホスト鍵を自動的に受け取らない\ndontAcceptNewHostKeysDescription=XPipeは、SSHクライアントに既知のホスト鍵が保存されていない場合、デフォルトで自動的にホスト鍵を受け付ける。しかし、既知のホスト鍵が変更されている場合は、新しいものを受け入れない限り接続を拒否する。\\n\\nこの動作を無効にすると、最初は衝突がなくても、すべてのホスト鍵をチェックできるようになる。\nuiScale=UIスケール\nuiScaleDescription=システム全体の表示スケールとは別に設定できるカスタムスケーリング値。値の単位はパーセントで、例えば150を指定するとUIのスケールは150%になる。\neditorProgram=エディタープログラム\neditorProgramDescription=あらゆる種類のテキストデータを編集するときに使用するデフォルトのテキストエディタ。\nwindowOpacity=ウィンドウの不透明度\nwindowOpacityDescription=ウィンドウの不透明度を変更し、バックグラウンドで何が起こっているかを追跡する。\nuseSystemFont=システムフォントを使用する\nopenDataDir=保管庫のデータディレクトリ\nopenDataDirButton=オープンデータディレクトリ\nopenDataDirDescription=SSH キーなどの追加ファイルを git リポジトリとシステム間で同期させたい場合は、storage data ディレクトリに置くことができる。そこで参照されるファイルは、同期されたシステム上で自動的にファイルパスが適応される。\nupdates=更新情報\nselectAll=すべて選択する\nadvanced=高度な\nthirdParty=オープンソースのお知らせ\neulaDescription=XPipeアプリケーションのエンドユーザーライセンス契約を読む\nthirdPartyDescription=サードパーティーライブラリのオープンソースライセンスを見る\nworkspaceLock=マスター・パスフレーズ\nenableGitStorage=同期を有効にする\nsharing=共有\ngitSync=Git同期\nenableGitStorageDescription=有効にすると、XPipeはローカル保管庫のgitリポジトリを初期化し、変更があればコミットする。これにはgitがインストールされている必要があり、読み込みや保存の動作が遅くなる可能性があることに注意。\\n\\n同期すべきカテゴリは、明示的に同期済みとマークする必要がある。\nstorageGitRemote=リモート同期URL\nstorageGitRemoteDescription=設定すると、XPipe は読み込み時に変更点を自動的にプルし、保存時に変更点をリモートリポジトリにプッシュする。\\n\\nこれにより、複数のXPipeインストール間で金庫を共有することができる。HTTPとSSHのURLに加え、ローカルディレクトリにも対応している。\nvault=金庫\nworkspaceLockDescription=XPipeに保存されている機密情報を暗号化するためのカスタムパスワードを設定する。\\n\\nこれにより、保存された機密情報の暗号化レイヤーが追加され、セキュリティが向上する。XPipe起動時にパスワードの入力を求められる。\nuseSystemFontDescription=デフォルトのシステムフォントを使用するか、XPipeに含まれているInterフォントを使用するかを制御する。\ntooltipDelay=ツールチップの遅延\ntooltipDelayDescription=ツールチップが表示されるまでの待ち時間（ミリ秒）。\nfontSize=文字サイズ\nwindowOptions=ウィンドウオプション\nsaveWindowLocation=ウィンドウの保存場所\nsaveWindowLocationDescription=ウィンドウ座標を保存し、再起動時に復元するかどうかを制御する。\nstartupShutdown=スタートアップ/シャットダウン\nshowChildrenConnectionsInParentCategory=親カテゴリに子カテゴリを表示する\nshowChildrenConnectionsInParentCategoryDescription=特定の親カテゴリが選択されたときに、サブカテゴリにあるすべての接続を含めるかどうか。\\n\\nこれを無効にすると、カテゴリはサブフォルダを含めずに直接の内容だけを表示する古典的なフォルダのように動作する。\ncondenseConnectionDisplay=接続表示を凝縮する\ncondenseConnectionDisplayDescription=すべてのトップレベル接続の縦のスペースを少なくして、接続リストをより凝縮できるようにする。\nopenConnectionSearchWindowOnConnectionCreation=接続作成時に接続検索ウィンドウを開く\nopenConnectionSearchWindowOnConnectionCreationDescription=新しいシェル接続を追加するときに、利用可能なサブ接続を検索するウィンドウを自動的に開くかどうか。\nworkflow=ワークフロー\nsystem=システム\napplication=アプリケーション\nstorage=ストレージ\nrunOnStartup=起動時に実行する\ncloseBehaviour=終了時の動作\ncloseBehaviourDescription=XPipeのメインウィンドウを閉じたときの処理を制御する。\nlanguage=言語\nlanguageDescription=使用する表示言語。翻訳はコミュニティの貢献によって改善されている。GitHubで翻訳の修正を投稿することで、翻訳作業を手伝うことができる。\nlightTheme=ライトテーマ\ndarkTheme=ダークテーマ\nexit=XPipeを終了する\ncontinueInBackground=バックグラウンドで続ける\nminimizeToTray=トレイに最小化する\ncloseBehaviourAlertTitle=閉じる動作を設定する\ncloseBehaviourAlertTitleHeader=ウィンドウを閉じるときの動作を選択する。アプリケーションがシャットダウンされると、アクティブな接続はすべて閉じられる。\nstartupBehaviour=起動時の動作\nstartupBehaviourDescription=XPipe起動時のデスクトップアプリケーションのデフォルト動作を制御する。\nclearCachesAlertTitle=クリーンキャッシュ\nclearCachesAlertContent=XPipeのキャッシュをすべて削除したい？これは、ユーザーエクスペリエンスを向上させるために保存されているすべてのキャッシュデータを削除する。\nstartGui=GUIを起動する\nstartInTray=トレイで起動する\nstartInBackground=バックグラウンドで起動する\nclearCaches=キャッシュをクリアする\nclearCachesDescription=すべてのキャッシュデータを削除する\ncancel=キャンセル\nnotAnAbsolutePath=絶対パスではない\nnotADirectory=ディレクトリではない\nnotAnEmptyDirectory=空のディレクトリではない\nautomaticallyCheckForUpdates=アップデートを確認する\nautomaticallyCheckForUpdatesDescription=有効にすると、XPipeの実行中に、しばらくすると新しいリリース情報が自動的に取得される。それでも、アップデートのインストールを明示的に確認する必要がある。\nsendAnonymousErrorReports=匿名でエラー報告を送信する\nsendUsageStatistics=匿名で利用統計を送信する\nstorageDirectory=ストレージディレクトリ\nstorageDirectoryDescription=XPipeがすべての接続情報を保存する場所。これを変更すると、古いディレクトリのデータは新しいディレクトリにコピーされない。\nlogLevel=ログレベル\nappBehaviour=アプリケーションの動作\nlogLevelDescription=ログファイルを書くときに使用するログレベル。\ndeveloperMode=開発者モード\ndeveloperModeDescription=有効にすると、開発に役立つさまざまな追加オプションにアクセスできるようになる。\neditor=エディター\ncustom=カスタム\npasswordManager=パスワードマネージャー\nexternalPasswordManager=外部パスワードマネージャー\npasswordManagerDescription=ローカルにインストールされているパスワードマネージャーと統合する。\\n\\nパスワードマネージャーがインストールされている場合、XPipeがパスワードを保存する必要がないように、XPipeがパスワードマネージャーからパスワードを取得するように設定することができる。有効にすると、接続のパスワードフィールドは、パスワードマネージャーを使用するように設定できる。\npasswordManagerCommandTest=テストパスワードマネージャー\npasswordManagerCommandTestDescription=パスワード・マネージャーを設定した場合、出力が正しく見えるかどうかをここでテストできる。\npreferTerminalTabs=新しいタブを開くことを好む\npreferTerminalTabsDescription=XPipeが新しいウィンドウではなく、選択したターミナルで新しいタブを開こうとするかどうかを制御する。すべてのターミナルがタブをサポートしているわけではない。\ncustomRdpClientCommand=カスタムコマンド\ncustomRdpClientCommandDescription=カスタムRDPクライアントを起動するために実行するコマンド。\\n\\nプレースホルダ文字列$FILEは、呼び出されたときに引用符で囲まれた絶対.rdpファイル名に置き換えられる。実行パスにスペースが含まれている場合は、引用符で囲むことを忘れないこと。\ncustomEditorCommand=カスタムエディターコマンド\ncustomEditorCommandDescription=カスタムエディタを起動するために実行するコマンド。\\n\\nプレースホルダー文字列$FILEは、呼び出されると引用符で囲まれた絶対ファイル名に置き換えられる。エディタの実行パスにスペースが含まれている場合は、引用符で囲むことを忘れないこと。\neditorReloadTimeout=エディタのリロードタイムアウト\neditorReloadTimeoutDescription=ファイルが更新された後、そのファイルを読み込む前に待つミリ秒数。これにより、エディターがファイルロックの書き込みや解放に時間がかかる場合の問題を避けることができる。\nencryptAllVaultData=すべての金庫データを暗号化する\nencryptAllVaultDataDescription=この機能を有効にすると、データ保管庫の接続データのあらゆる部分が、そのデータ内の秘密情報だけでなく、 ユーザのデータ保管庫の暗号化キーで暗号化されるようになる。これは、ユーザー名やホスト名など、デフォルトではデータ保管庫で暗号化されていない他のパラメータに対して、もう 1 つのセキュリティ層を追加するものである。\\n\\nこのオプションを指定すると、git vault の履歴と diff が無意味になり、元の変更を見ることができなくなる。\nvaultSecurity=金庫のセキュリティ\ndeveloperDisableUpdateVersionCheck=アップデートのバージョンチェックを無効にする\ndeveloperDisableUpdateVersionCheckDescription=アップデートチェッカーがアップデートを探すときにバージョン番号を無視するかどうかを制御する。\ndeveloperDisableGuiRestrictions=GUIの制限を無効にする\ndeveloperDisableGuiRestrictionsDescription=無効にしたアクションをユーザーインターフェースから実行できるかどうかを制御する。\ndeveloperShowHiddenEntries=隠しエントリーを表示する\ndeveloperShowHiddenEntriesDescription=有効にすると、非表示の内部データソースが表示される。\ndeveloperShowHiddenProviders=非表示のプロバイダーを表示する\ndeveloperShowHiddenProvidersDescription=非表示の内部接続プロバイダとデータソースプロバイダを作成ダイアログに表示するかどうかを制御する。\ndeveloperDisableConnectorInstallationVersionCheck=コネクタのバージョンチェックを無効にする\ndeveloperDisableConnectorInstallationVersionCheckDescription=リモートマシンにインストールされたXPipeコネクタのバージョンを検査するときに、アップデートチェッカがバージョン番号を無視するかどうかを制御する。\nshellCommandTest=シェルコマンドテスト\nshellCommandTestDescription=XPipeが内部的に使用するシェルセッションでコマンドを実行する。\nterminal=ターミナル\nterminalType=ターミナルエミュレータ\nterminalConfiguration=端末設定\nterminalCustomization=端末のカスタマイズ\neditorConfiguration=エディターの設定\ndefaultApplication=デフォルトのアプリケーション\ninitialSetup=初期設定\nterminalTypeDescription=シェル接続を開くときに使うデフォルトの端末。\\n\\nサポートする機能のレベルは端末によって異なり、それぞれに推奨または非推奨のマークがついている。推奨端末を使うと、ユーザーエクスペリエンスが最高になる。\nprogram=プログラム\ncustomTerminalCommand=カスタム端末コマンド\ncustomTerminalCommandDescription=指定されたコマンドでカスタムターミナルを開くために実行するコマンド。\\n\\nXPipeは、端末が実行するための一時的なランチャー・シェル・スクリプトを作成する。指定したコマンドのプレースホルダ文字列$CMDは、呼び出されたときに実際のランチャー・スクリプトに置き換えられる。ターミナルの実行パスにスペースが含まれている場合は、引用符で囲むことを忘れないこと。\nclearTerminalOnInit=開始時にターミナルをクリアする\nclearTerminalOnInitDescription=有効にすると、XPipeは新しいターミナル・セッションの起動後にclearコマンドを実行し、ターミナル・セッションの起動時に出力された不要な出力を削除する。\ndontCachePasswords=プロンプトのパスワードをキャッシュしない\ndontCachePasswordsDescription=現在のセッションで再度入力する必要がないように、照会されたパスワードをXPipeが内部的にキャッシュするかどうかを制御する。\\n\\nこの動作を無効にすると、システムから要求されるたびに、プロンプトされた認証情報を再入力する必要がある。\ndenyTempScriptCreation=一時的なスクリプトの作成を拒否する\ndenyTempScriptCreationDescription=XPipeは、いくつかの機能を実現するために、ターゲットシステム上に一時的なシェルスクリプトを作成し、簡単なコマンドを簡単に実行できるようにすることがある。これには機密情報は含まれておらず、単に実装のために作成される。\\n\\nこの動作を無効にすると、XPipeはリモートシステム上に一時ファイルを作成しない。このオプションは、ファイルシステムの変更がすべて監視されるようなセキュリティの高い状況で有用である。このオプションを無効にすると、シェル環境やスクリプトなど、一部の機能が意図したとおりに動作しなくなる。\ndisableCertutilUse=Windowsでcertutilの使用を無効にする\nuseLocalFallbackShell=ローカルのフォールバックシェルを使う\nuseLocalFallbackShellDescription=ローカル操作を処理するために、別のローカルシェルを使うように切り替える。WindowsではPowerShell、その他のシステムではボーンシェルがこれにあたる。\\n\\nこのオプションは、通常のローカルデフォルトのシェルが無効になっているか、ある程度壊れている場合に使用できる。このオプションが有効になっていると、いくつかの機能は期待通りに動作しないかもしれない。\ndisableCertutilUseDescription=cmd.exeにはいくつかの欠点やバグがあるため、cmd.exeは非ASCII入力で壊れてしまうため、certutilを使ってbase64入力をデコードし、一時的なシェルスクリプトを作成する。XPipeはPowerShellを使用することもできるが、その場合は動作が遅くなる。\\n\\nこれにより、Windowsシステムでcertutilを使用して一部の機能を実現することができなくなり、代わりにPowerShellにフォールバックする。AVの中にはcertutilの使用をブロックするものもあるので、これは喜ぶかもしれない。\ndisableTerminalRemotePasswordPreparation=端末のリモートパスワード準備を無効にする\ndisableTerminalRemotePasswordPreparationDescription=複数の中間システムを経由するリモートシェル接続をターミナルで確立しなければならない状況では、プロンプトを自動的に埋めることができるように、中間システムの1つに必要なパスワードを準備する必要があるかもしれない。\\n\\n中間システムにパスワードを転送したくない場合は、この動作を無効にすることができる。中間システムで必要なパスワードは、ターミナルを開いたときに照会される。\nmore=詳細\ntranslate=翻訳\nallConnections=すべての接続\nallScripts=すべてのスクリプト\nallIdentities=すべてのID\nsynced=同期\npredefined=定義済み\nsamples=サンプル\ngoodMorning=おはよう\ngoodAfternoon=こんにちは\ngoodEvening=こんばんは\naddVisual=ビジュアル ...\naddDesktop=デスクトップ ...\nssh=SSH\nsshConfiguration=SSHの設定\nsize=サイズ\nattributes=属性\nmodified=変更された\nowner=所有者\nupdateReadyTitle=$VERSION$ に更新\ntemplates=テンプレート\nretry=リトライ\nretryAll=すべて再試行する\nreplace=置き換える\nreplaceAll=すべて置き換える\nhibernateBehaviour=ハイバネーションの動作\nhibernateBehaviourDescription=システムがハイバネーション/スリープ状態になったときのアプリケーションの動作を制御する。\noverview=概要\nhistory=歴史\nskipAll=すべてスキップする\nnotes=備考\naddNotes=メモを追加する\norder=並び替え\nkeepFirst=最初に保持する\nkeepLast=最後に保つ\npinToTop=トップに戻る\nunpinFromTop=上からピンを外す\norderAheadOf=先に注文する\nclearIndex=インデックスをリセットする\nhttpServer=HTTPサーバー\nmcpServer=MCPサーバー\napiKey=APIキー\napiKeyDescription=XPipeデーモンAPIリクエストを認証するためのAPIキー。認証方法の詳細については、一般的なAPIドキュメントを参照のこと。\ndisableApiAuthentication=API認証を無効にする\ndisableApiAuthenticationDescription=認証されていないリクエストが処理されるように、必要な認証方法をすべて無効にする。\\n\\n認証は開発目的でのみ無効にすべきである。\napi=API\nstoreIntroImportContent=すでに他のシステムでXPipeを使っている？リモートgitリポジトリを通して、複数のシステム間で既存の接続を同期する。まだ設定されていない場合は、いつでも後で同期することもできる。\nstoreIntroImportButton=同期接続 ...\nstoreIntroImportHeader=コネクションのインポート\nshowNonRunningChildren=実行されていない子供を表示する\nhttpApi=HTTP API\nisOnlySupportedLimit=は、$COUNT$ を超える接続がある場合、プロフェッショナルライセンスでのみサポートされる。\nareOnlySupportedLimit=$COUNT$ 以上の接続がある場合、プロフェッショナルライセンスでのみサポートされる。\nenabled=有効にする\nenableGitStoragePtbDisabled=Gitの同期をパブリックテストビルドでは無効にしているのは、通常のリリース用gitリポジトリとの使い回しを防ぎ、PTBビルドをデイリードライバーとして使わないようにするためだ。\ncopyId=コピーAPI ID\nrequireDoubleClickForConnections=接続にはダブルクリックが必要\nrequireDoubleClickForConnectionsDescription=有効にすると、接続をダブルクリックしないと起動しなくなる。ダブルクリックに慣れている人には便利な機能だ。\nclearTransferDescription=選択範囲をクリアする\nselectTab=タブを選択する\ncloseTab=閉じるタブ\ncloseOtherTabs=他のタブを閉じる\ncloseAllTabs=すべてのタブを閉じる\ncloseLeftTabs=タブを左に閉じる\ncloseRightTabs=タブを右に閉じる\naddSerial=シリアル ...\nconnect=接続する\nworkspaces=ワークスペース\nmanageWorkspaces=ワークスペースを管理する\naddWorkspace=ワークスペースを追加する\nworkspaceAdd=新しいワークスペースを追加する\nworkspaceAddDescription=ワークスペースは、XPipeを実行するための個別の設定である。すべてのワークスペースには、すべてのデータがローカルに保存されるデータ・ディレクトリがある。これには、接続データや設定などが含まれる。\\n\\n同期機能を使えば、ワークスペースごとに異なるgitリポジトリと同期させることもできる。\nworkspaceName=ワークスペース名\nworkspaceNameDescription=ワークスペースの表示名\nworkspacePath=ワークスペースのパス\nworkspacePathDescription=ワークスペースのデータディレクトリの場所\nworkspaceCreationAlertTitle=ワークスペースの作成\ndeveloperForceSshTty=強制SSH TTY\ndeveloperForceSshTtyDescription=すべてのSSHコネクションにptyを割り当て、stderrとptyがない場合のサポートをテストする。\ndeveloperDisableSshTunnelGateways=SSHゲートウェイトンネリングを無効にする\ndeveloperDisableSshTunnelGatewaysDescription=ゲートウェイにトンネルセッションを使わず、システムに直接接続する。\nttyWarning=接続が強制的にpty/ttyを割り当て、個別のstderrストリームを提供しない。\\n\\nこれはいくつかの問題を引き起こす可能性がある。\\n\\n可能であれば、接続コマンドで pty を割り当てないようにすることを検討してほしい。\nxshellSetup=Xshellのセットアップ\ntermiusSetup=テルミウスのセットアップ\ntryPtbDescription=XPipe開発者ビルドで新機能をいち早く試す\nconfirmVaultUnencryptTitle=金庫の暗号化解除を確認する\nconfirmVaultUnencryptContent=本当に高度な金庫の暗号化を無効にしたいのか？これにより、保存データの暗号化が解除され、既存のデータが上書きされる。\nenableHttpApi=HTTP APIを有効にする\nenableHttpApiDescription=APIを有効にし、外部プログラムからXPipeデーモンを呼び出して、管理されている接続に対してアクションを実行できるようにする。\nchooseCustomIcon=カスタムアイコンを選ぶ\ngitVault=Git保管庫\nfileBrowser=ファイルブラウザ\nconfirmAllDeletions=すべての削除を確認する\nconfirmAllDeletionsDescription=すべての削除操作に対して確認ダイアログを表示するかどうか。デフォルトでは、確認が必要なのはディレクトリだけである。\nyesterday=昨日\ngreen=グリーン\nyellow=黄色\nblue=ブルー\nred=赤い\ncyan=シアン\npurple=パープル\nasktextAlertTitle=プロンプト\nfileWriteSudoTitle=須藤ファイル書き込み\nfileWriteSudoContent=あなたが書き込もうとしているファイルは、あなたのユーザーに書き込み権限を与えていない。このファイルをrootとしてsudoで書き込むか？これにより、既存の認証情報またはプロンプト経由で自動的にrootに昇格する。\ndontAllowTerminalRestart=端末の再起動を許可しない\ndontAllowTerminalRestartDescription=デフォルトでは、ターミナル・セッションはターミナル内から終了後に再開することができる。これを可能にするため、XPipeはターミナルからセッションを再度起動するための以下の外部リクエストを受け付ける。\\n\\nXPipeはターミナルとこの呼び出しの発信元を制御できないため、悪意のあるローカルアプリケーションはこの機能を使用してXPipe経由で接続を開始することができる。この機能を無効にすることで、このシナリオを防ぐことができる。\nopenDocumentation=ドキュメントを開く\nopenDocumentationDescription=この問題のXPipeドキュメントページを見る\nrenameAll=すべての名前を変更する\nlogging=ロギング\nenableTerminalLogging=ターミナルロギングを有効にする\nenableTerminalLoggingDescription=すべての端末セッションのクライアント側ログを有効にする。端末セッションのすべての入力と出力がセッションログファイルに書き込まれる。パスワードプロンプトのような機密情報は記録されないことに注意。\nterminalLoggingDirectory=端末のセッションログ\nterminalLoggingDirectoryDescription=すべてのログは、ローカルシステムのXPipeデータディレクトリに保存される。\nopenSessionLogs=セッションログを開く\nsessionLogging=ターミナルロギング\nsessionActive=この接続ではバックグラウンドセッションが実行されている。\\n\\nこのセッションを手動で停止するには、ステータスインジケータをクリックする。\nskipValidation=検証をスキップする\nscriptsIntroHeader=スクリプトについて\nscriptsIntroContent=スクリプトは、シェルinit、ファイルブラウザ、オンデマンドで実行できる。XPipe内で自分でスクリプトを作成したり、ローカルシステムやリモートのgitリポジトリから既存のスクリプトをインポートしたりできる。\nscriptsIntroBottomHeader=スクリプトを使用する\nscriptsIntroBottomContent=スクリプトには様々なサンプルが用意されている。各スクリプトの編集ボタンをクリックして、どのように実装されているかを確認することができる。スクリプトを実行し、メニューに表示するには、まずスクリプトを有効にする必要がある。\nscriptsIntroBottomButton=始める\nscriptSourcesIntroHeader=スクリプトソース\nscriptSourcesIntroContent=カスタムスクリプトソースを追加して、シェルスクリプトのコレクション全体に即座にアクセスすることができる。ローカルソースとリモートgitリポジトリの両方がソースとしてサポートされている。ソースから検出されたスクリプトはすべて自動的に利用可能になる。\nscriptSourcesIntroButton=ソースを追加する\ncheckForSecurityUpdates=セキュリティアップデートを確認する\ncheckForSecurityUpdatesDescription=XPipeは、通常の機能アップデートとは別に、潜在的なセキュリティアップデートをチェックすることができる。これを有効にすると、通常のアップデートチェックが無効になっている場合でも、少なくとも重要なセキュリティアップデートのインストールが推奨される。\\n\\nこの設定を無効にすると、外部バージョン要求が実行されなくなり、セキュリティアップデートが通知されなくなる。\nclickToDock=クリックして端末をドッキングする\nterminalStarting=端末の起動を待つ\npinTab=ピンタブ\nunpinTab=タブを外す\npinned=ピン留め\nenableConnectionHubTerminalDocking=接続ハブ端末のドッキングを有効にする\nenableConnectionHubTerminalDockingDescription=ターミナルウィンドウを接続ハブのXPipeアプリケーションウィンドウにドッキングすることで、ある程度統合されたターミナルをシミュレートすることができる。ターミナルウィンドウは、XPipeによって常にドックに収まるように管理される。\nenableFileBrowserTerminalDocking=ファイルブラウザ端末のドッキングを有効にする\nenableFileBrowserTerminalDockingDescription=ターミナルウィンドウをファイルブラウザのXPipeアプリケーションウィンドウにドッキングすることで、ある程度統合されたターミナルをシミュレートすることができる。ターミナルウィンドウは、XPipeによって常にドックに収まるように管理される。\ndownloadsDirectory=カスタムダウンロードディレクトリ\ndownloadsDirectoryDescription=ダウンロードに移動ボタンをクリックしたときに、ダウンロードしたファイルを置くカスタムディレクトリ。デフォルトでは、XPipeはユーザーダウンロードディレクトリを使用する。\npinLocalMachineOnStartup=起動時にローカルマシンのタブをピン留めする\npinLocalMachineOnStartupDescription=ローカルマシンのタブを自動的に開き、ピン留めする。これは、ローカルマシンとリモートファイルシステムを開いた状態で、分割ファイルブラウザを頻繁に使用する場合に便利である。\nterminalErrorDescription=このエラーは末期的なもので、XPipeはこのエラーを修正しなければ続行できない。\ngroupName=グループ名\nchmodPermissions=新しいパーミッション\neditFilesWithDoubleClick=ダブルクリックでファイルを編集する\neditFilesWithDoubleClickDescription=有効にすると、ファイルをダブルクリックしてもコンテキストメニューが表示されず、そのままテキストエディタで開くことができる。\ncensorMode=検閲モード\ncensorModeDescription=ホスト名、ユーザー名、接続名などのあらゆる情報をぼかす。\\n\\nXPipeをスクリーンショットやスクリーンシェアするつもりで、情報を漏らしたくない場合に役立つ。\naddIdentity=アイデンティティ ...\nidentities=アイデンティティ\naddMacro=アクション ...\nidentitiesIntroHeader=IDについて\nidentitiesIntroContent=ユーザー名、パスワード、キーの一般的な組み合わせを再利用する場合、再利用可能なIDを作成することは理にかなっているかもしれない。こうすることで、新しい接続を追加するときに素早く参照できるようになる。\nidentitiesIntroBottomHeader=アイデンティティの共有\nidentitiesIntroBottomContent=ローカルにアイデンティティを追加することもできるし、gitリポジトリに同期することもできる。これにより、複数のシステムや他のチームメンバーとIDを選択的に共有することができる。\nidentitiesIntroBottomButton=同期を設定する\nidentitiesIntroButton=IDを作成する\nuserName=ユーザー名\nuserAuth=ユーザーベースのパスワード認証\ngroupAuth=グループベースの秘密認証\nteam=チーム\nteamSettings=チーム設定\nteamVaults=チーム金庫\nvaultTypeNameDefault=デフォルトの金庫\nvaultTypeNameLegacy=レガシー個人金庫\nvaultTypeNamePersonal=個人の金庫\nvaultTypeNameTeam=チーム金庫\nteamVaultsDescription=チーム保管庫では、複数のユーザーやグループが共有保管庫に安全にアクセスできる。接続と ID は、すべてのユーザーで共有することも、個々のユーザーやグループ独自の鍵で暗号化することで、そのユーザーやグループだけが利用できるように構成することもできる。他のデータ保管庫ユーザーは、その鍵にアクセスできなければ、個人ベースおよびグループ ベースの接続と ID にアクセスできない。\nvaultTypeContentDefault=あなたは現在、ユーザーとカスタムパスフレーズが設定されていないデフォルトのデータ保管庫を使用している。秘密はローカルのデータ保管庫キーで暗号化される。デー タ 保管庫のユーザア カ ウ ン ト を作成す る こ と で、 個人用デー タ 保管庫へア ッ プグ レー ド で き る 。これにより、データ保管庫の秘密を自分専用のパスフレーズで暗号化することができ、データ保管庫のロックを解除するためにログインのたびに入力する必要がある。\nvaultTypeContentLegacy=あなたは現在、レガシーな個人用金庫をユーザー用に使っている。秘密は個人のパスフレーズで暗号化される。このレガシーな互換性には制限があり、その場でチーム金庫にアップグレードすることはできない。\nvaultTypeContentPersonal=あなたは現在、ユーザー用の個人用金庫を使っている。秘密は個人のパスフレーズで暗号化される。保管庫ユーザーを追加するか、グループベースのアクセス設定を追加することで、チーム保管庫にアップグレードできる。\nvaultTypeContentTeam=あなたは現在、複数のユーザが共有金庫に安全にアクセスできるチーム金庫を使用している。接続と ID は、すべてのユーザーで共有するか、または個人キーまたはグループ キーで暗号化することによって、個人ユーザーまたはグループでのみ使用できるように構成できる。他のデータ保管庫ユーザーは、鍵にアクセスできなければ、個人およびグループベースの接続と ID にアクセスできない。\ngroupManagement=グループ管理\ngroupManagementEmpty=グループ管理\ngroupManagementDescription=既存のデータ保管庫グループを管理したり、新しいデータ保管庫グループを作成する。各データ保管庫グループには、そのグループだけが利用でき、他には知られてはならない接続と ID を暗号化するために使用される、個別の秘密鍵がある。\ngroupManagementEmptyDescription=既存のデータ保管庫グループを管理したり、新しいデータ保管庫グループを作成する。各データ保管庫グループには、そのグループだけが利用でき、他には知られてはならない接続と ID を暗号化するために使用される、個別の秘密鍵がある。\\n\\nチームのためのグループベースのアカウントは、プロフェッショナルプランでサポートされている。\nuserManagement=ユーザー管理\nuserManagementEmpty=ユーザー管理\nuserManagementDescription=既存の保管庫ユーザーを管理したり、新しい保管庫ユーザーを作成したりする。各保管庫ユーザは、そのユーザだけが利用でき、他のユーザは利用できないはずの接続と ID を暗号化するために使用される、個別のパスワードを持つ。\nuserManagementEmptyDescription=既存の保管庫ユーザーを管理したり、新しい保管庫ユーザーを作成したりする。各データ保管庫ユーザは、そのユーザだけが利用でき、他のユーザは利用できない接続と ID を暗号化するために使用される、個別のパスワードを持つ。自分用のユーザーを作成し、個人鍵で接続やIDを暗号化できるようにする。\\n\\nコミュニティ・エディションでは、単一のユーザー・アカウントがサポートされている。プロフェッショナル・プランでは、チーム用の複数のユーザー・アカウントがサポートされている。\nuserIntroHeader=ユーザー管理\nuserIntroContent=最初のユーザーアカウントを作成する。これにより、このワークスペースをパスワードでロックすることができる。\naddReusableIdentity=再利用可能なIDを追加する\nusers=ユーザー\nsyncVault=金庫の同期\nsyncVaultDescription=複数のシステム間または複数のチームメンバーと保管庫を同期するには、この保管庫の git 同期を有効にする。\nenableGitSync=git同期を有効にする\nbrowseVault=データ保管庫\nbrowseVaultDescription=保管庫ディレクトリは、ネイティブのファイルマネージャで自分で見ることができる。外部からの編集は推奨されず、さまざまな問題を引き起こす可能性があることに注意。\nbrowseVaultButton=金庫を閲覧する\nvaultUsers=金庫のユーザー\ncreateHeapDump=ヒープダンプを作成する\ncreateHeapDumpDescription=メモリの使用状況をトラブルシューティングするために、メモリの内容をファイルにダンプする\ninitializingApp=接続を読み込む\ncheckingLicense=ライセンスの確認\nloadingGit=gitリポジトリと同期する\nloadingGpg=git用のGnuPGデーモンを起動する\nloadingSettings=設定の読み込み\nloadingConnections=接続をロードする\nunlockingVault=金庫の鍵を開ける\nloadingUserInterface=ユーザーインターフェースの読み込み\nptbNotice=公開テストビルドのお知らせ\nuserDeletionTitle=ユーザー削除\nuserDeletionContent=このデータ保管庫ユーザーを削除するか？これにより、すべてのユーザが利用可能な保管庫キーを使用して、すべての個人IDと接続の秘密が再暗号化される。これにはしばらく時間がかかり、XPipeはユーザーの変更を適用するために再起動する。\ngroupDeletionTitle=グループ削除\ngroupDeletionContent=このデータ保管庫グループを削除するか？これにより、すべてのユーザが利用可能なデータ保管庫キーを使用して、すべてのグループ専用 ID と接続秘密が再暗号化される。これにはしばらく時間がかかり、XPipeはグループの変更を適用するために再起動する。\nkillTransfer=キルトランスファー\ndestination=行き先\nconfiguration=構成\nnewFile=新規ファイル\nnewLink=新しいリンク\nlinkName=リンク名\nscanConnections=利用可能な接続を検索する\nobserve=観察を開始する\nstopObserve=観察をやめる\ncreateShortcut=デスクトップのショートカットを作成する\nbrowseFiles=ファイルをブラウズする\nclone=クローン\ntargetPath=ターゲットパス\nnewDirectory=新しいディレクトリ\ncopyShareLink=リンクをコピーする\nselectStore=ストアを選択する\nsaveSource=後で保存する\nexecute=実行する\ndeleteChildren=すべての子供を削除する\nscriptGroupDescriptionDescription=このグループに任意の説明を与える\nabstractHostDescriptionDescription=このホストに任意の説明を与える\nselectSource=ソースを選択する\ncommandLineRead=更新\ncommandLineWrite=書く\nadditionalOptions=追加オプション\ninput=入力\nmachine=マシン\nopen=開く\nedit=編集する\nscriptContents=スクリプトの内容\nscriptContentsDescription=実行するスクリプトコマンド\nsnippets=スクリプトの依存関係\nsnippetsDescription=最初に実行する他のスクリプト\nsnippetsDependenciesDescription=該当する場合、実行可能なすべてのスクリプト\nisDefault=すべての互換シェルでinit時に実行される\nbringToShells=互換性のあるすべてのシェルに適用する\nisDefaultGroup=シェルinitですべてのグループスクリプトを実行する\nexecutionType=実行タイプ\nexecutionTypeDescription=このスクリプトをどのような文脈で使うか\nminimumShellDialect=シェルタイプ\nminimumShellDialectDescription=このスクリプトを実行するシェルタイプ\ndumbOnly=ダム\nterminalOnly=ターミナル\nboth=どちらも\nshouldElevate=高めるべきである\nshouldElevateDescription=このスクリプトを昇格権限で実行するかどうか\nscript.displayName=シェルスクリプト\nscript.displayDescription=再利用可能なシェルスクリプトを作成する\nscriptGroup.displayName=スクリプトグループ\nscriptGroup.displayDescription=スクリプトをグループ化し\nscriptGroup=グループ\nscriptGroupDescription=このスクリプトを割り当てるグループ\nscriptGroupGroupDescription=このスクリプトグループを割り当てる親グループ（オプション）。\nopenInNewTab=新しいタブで開く\nexecuteInBackground=背景\nexecuteInTerminal=で$TERM$\nback=戻る\nbrowseInWindowsExplorer=Windowsエクスプローラでブラウズする\nbrowseInDefaultFileManager=デフォルトのファイルマネージャーでブラウズする\nbrowseInFinder=ファインダーでブラウズする\ncopy=コピー\npaste=貼り付け\ncopyLocation=コピー位置\nabsolutePaths=絶対パス\nabsoluteLinkPaths=絶対リンクパス\nabsolutePathsQuoted=絶対引用パス\nfileNames=ファイル名\nlinkFileNames=リンクファイル名\nfileNamesQuoted=ファイル名（引用）\ndeleteFile=削除する$FILE$\neditWithEditor=で編集する。$EDITOR$\nfollowLink=リンクをたどる\ngoForward=進む\nshowDetails=詳細を表示する\nshowDetailsDescription=エラーのスタックトレースを表示する\nopenFileWith=... で開く\nopenWithDefaultApplication=デフォルトのアプリケーションで開く\nrename=名前を変更する\nrun=実行する\nopenInTerminal=ターミナルで開く\nfile=ファイル\ndirectory=ディレクトリ\nsymbolicLink=シンボリックリンク\ndesktopEnvironment.displayName=デスクトップ環境\ndesktopEnvironment.displayDescription=再利用可能なリモートデスクトップ環境設定を作成する\ndesktopHost=デスクトップホスト\ndesktopHostDescription=ベースとなるデスクトップ接続\ndesktopShellDialect=シェル方言\ndesktopShellDialectDescription=スクリプトやアプリケーションの実行に使用するシェル方言\ndesktopSnippets=スクリプトスニペット\ndesktopSnippetsDescription=最初に実行する再利用可能なスクリプトスニペットのリスト\ndesktopInitScript=イニシャルスクリプト\ndesktopInitScriptDescription=この環境に特有の初期化コマンド\ndesktopTerminal=端末アプリケーション\ndesktopTerminalDescription=デスクトップでスクリプトを起動する際に使用するターミナル\ndesktopApplication.displayName=デスクトップアプリケーション\ndesktopApplication.displayDescription=リモートデスクトップでアプリケーションを実行する\ndesktopBase=デスクトップ\ndesktopBaseDescription=このアプリケーションを実行するデスクトップ\ndesktopEnvironmentBase=デスクトップ環境\ndesktopEnvironmentBaseDescription=このアプリケーションを実行するデスクトップ環境\ndesktopApplicationPath=アプリケーションパス\ndesktopApplicationPathDescription=実行ファイルのパス\ndesktopApplicationArguments=引数\ndesktopApplicationArgumentsDescription=アプリケーションに渡すオプションの引数\ndesktopCommand.displayName=デスクトップコマンド\ndesktopCommand.displayDescription=リモートデスクトップ環境でコマンドを実行する\ndesktopCommandScript=コマンド\ndesktopCommandScriptDescription=環境で実行するコマンド\nservice.displayName=サービス\nservice.displayDescription=リモートサービスをローカルマシンに転送する\nserviceLocalPort=明示的なローカルポート\nserviceLocalPortDescription=転送先のローカルポート。そうでない場合はランダムなポートが使われる。\nserviceRemotePort=リモートポート\nserviceRemotePortDescription=サービスが実行されているポート\nserviceHost=サービスホスト\nserviceHostDescription=サービスが稼働しているホスト\nopenWebsite=オープンウェブサイト\ncustomServiceGroup.displayName=サービスグループ\ncustomServiceGroup.displayDescription=複数のサービスを1つのカテゴリーにまとめる\ninitScript=initスクリプト - シェルのinit時に実行する\nshellScript=シェルセッションスクリプト - シェルセッション中に実行可能なスクリプトを作成する。\nrunnableScript=実行可能なスクリプト - 接続ハブからスクリプトを直接実行できるようにする\nfileScript=ファイルスクリプト - ファイルブラウザで選択したファイルに対してスクリプトを呼び出せるようにする\nrunScript=スクリプトを実行する\ncopyUrl=URLをコピーする\nfixedServiceGroup.displayName=サービスグループ\nfixedServiceGroup.displayDescription=システムで利用可能なサービスをリストアップする\nmappedService.displayName=サービス\nmappedService.displayDescription=コンテナによって公開されたサービスと相互作用する\ncustomService.displayName=サービス\ncustomService.displayDescription=ローカルマシンでリモートサービスのポートを自動的にオープンまたはトンネルする\nfixedService.displayName=サービス\nfixedService.displayDescription=定義済みのサービスを使う\nnoServices=利用可能なサービスはない\nhasServices=$COUNT$ 利用可能なサービス\nhasService=$COUNT$ 利用可能なサービス\nnoConnections=利用可能な接続はない\nhasConnections=$COUNT$ 利用可能な接続\nhasConnection=$COUNT$ 利用可能な接続\nopenHttp=オープンHTTPサービス\nopenHttps=HTTPSサービスを開く\nnoScriptsAvailable=使用可能なスクリプトと互換性のあるスクリプトがない\nscriptsDisabled=スクリプトを無効にする\nchangeIcon=アイコンの変更\ninit=イニシャル\nshell=シェル\nhub=ハブ\nscript=スクリプト\ngenericScript=一般的な\ngradleTasks=Gradleタスク\nrunTask=タスクを実行する\narchiveName=アーカイブ名\ncompress=圧縮する\ncompressContents=コンテンツを圧縮する\nuntarHere=ここをクリック\nuntarDirectory=未対応$DIR$\nunzipDirectory=解凍先$DIR$\nunzipHere=ここで解凍する\nrequiresRestart=適用には再起動が必要である。\ndownload=ダウンロード\nservicePath=サービスパス\nservicePathDescription=ブラウザでURLを開く際のオプションのサブパス\nactive=アクティブ\ninactive=非アクティブ\nstarting=開始\nremotePort=リモートポート\nremotePortNumber=リモートポート$PORT$\nuserIdentity=個人ID\nglobalIdentity=グローバルID\nidentityChoice=ユーザーID\nidentityChoiceDescription=事前に定義されたIDを選択するか、この接続のためだけにログインの詳細を指定する\ndefineNewIdentityOrSelect=新規に入力するか、既存のものを選択する\nlocalIdentity.displayName=ローカルID\nlocalIdentity.displayDescription=このローカルデスクトップ用に再利用可能なIDを作成する\nsyncedIdentity.displayName=同期されたID\nsyncedIdentity.displayDescription=システム間で同期される、再利用可能なIDを作成する。\nlocalIdentity=ローカルID\nkeyNotSynced=キーファイルはまだgitリポジトリに同期されていない。キーファイルを追加するには、gitに追加ボタンを使用する。\nusernameDescription=ログインするユーザー名\nidentity.displayName=アイデンティティ\nidentity.displayDescription=接続用の再利用可能なIDを作成する\nlocal=ローカル\nshared=グローバル\nuserDescription=ログインするためのユーザー名または事前定義されたID\nidentityAccessLevel=アクセスレベル\nidentityPerUser=個人IDアクセス\nidentityPerUserDescription=このIDおよび関連する接続へのアクセスを、自分のVaultユーザーのみに制限する。\nidentityPerUserDisabled=個人IDアクセス（無効）\nidentityPerUserDisabledDescription=このIDおよび関連する接続へのアクセスを、自分のVaultユーザーのみに制限する（チームの設定が必要）\nidentityPerGroup=グループのみのIDアクセス\nidentityPerGroupDescription=このIDおよび関連する接続へのアクセスを、この保管庫グループのみに制限する\nlibrary=ライブラリ\nlocation=場所\nkeyAuthentication=鍵ベースの認証\nkeyAuthenticationDescription=鍵ベースの認証が必要な場合に使用する認証方法。\nlocationDescription=対応する秘密鍵のファイルパス\nkeyFile=ローカルキーファイル\nkeyPassword=パスフレーズ\nkey=キー\nyubikeyPiv=ユビキーPIV\npageant=ページェント\ngpgAgent=GPGエージェント\ncustomPkcs11Library=カスタムPKCS#11ライブラリ\nsshAgent=OpenSSHエージェント\nnone=なし\nindex=インデックス ...\notherExternal=その他の外部エージェント\nsync=同期\nvaultSync=金庫の同期\ncustomUsername=ユーザー名\ncustomUsernameDescription=ログインする代替ユーザー\ncustomUsernamePassword=パスワード\ncustomUsernamePasswordDescription=sudo認証が必要なときに使うユーザーのパスワード\nshowInternalPods=内部ポッドを表示する\nshowAllNamespaces=すべての名前空間を表示する\nshowInternalContainers=内部コンテナを表示する\nrefresh=リフレッシュする\nvmwareGui=GUIを起動する\nmonitorVm=モニターVM\naddCluster=クラスタを追加する\nshowNonRunningInstances=実行されていないインスタンスを表示する\nvmwareGuiDescription=仮想マシンをバックグラウンドで起動するか、ウィンドウで起動するか。\nvmwareEncryptionPassword=暗号化パスワード\nvmwareEncryptionPasswordDescription=VMを暗号化するためのオプションのパスワード。\nvmPasswordDescription=ゲストユーザーに必要なパスワード。\nvmPassword=ユーザーパスワード\nvmUser=ゲストユーザー\nrunTempContainer=一時コンテナを実行する\nvmUserDescription=プライマリゲストユーザーのユーザー名\ndockerTempRunAlertTitle=一時コンテナを実行する\ndockerTempRunAlertHeader=一時的なコンテナ内でシェルプロセスを実行し、停止すると自動的に削除される。\nimageName=画像名\nimageNameDescription=使用するコンテナイメージ識別子\ncontainerName=コンテナ名\ncontainerNameDescription=オプションのカスタムコンテナ名\nvm=仮想マシン\nvmDescription=関連する設定ファイル。\nvmwareScan=VMwareデスクトップハイパーバイザー\nvmwareMachine.displayName=VMware仮想マシン\nvmwareMachine.displayDescription=SSH経由で仮想マシンに接続する\nvmwareInstallation.displayName=VMwareデスクトップハイパーバイザーのインストール\nvmwareInstallation.displayDescription=CLI経由でインストールされたVMと対話する\nstart=スタート\nstop=停止する\npause=一時停止\nrdpTunnelHost=対象ホスト\nrdpTunnelHostDescription=RDP接続をトンネルするSSH接続先\nrdpTunnelUsername=ユーザー名\nrdpTunnelUsernameDescription=空白の場合はSSHユーザーを使用する。\nrdpFileLocation=ファイルの場所\nrdpFileLocationDescription=.rdpファイルのファイルパス\nrdpPasswordAuthentication=パスワード認証\nrdpFiles=RDPファイル\nrdpPasswordAuthenticationDescription=クライアントのサポートに応じて、パスワードを入力するか、クリップボードにコピーする。\nrdpFile.displayName=RDPファイル\nrdpFile.displayDescription=既存の.rdpファイルを介してシステムに接続する\nrequiredSshServerAlertTitle=SSHサーバーをセットアップする\nrequiredSshServerAlertHeader=VMにインストールされたSSHサーバーが見つからない。\nrequiredSshServerAlertContent=VMに接続するため、XPipeは稼働中のSSHサーバーを探しているが、VMで利用可能なSSHサーバーが検出されなかった。\ncomputerName=コンピューター名\npssComputerNameDescription=接続先のコンピューター名\ncredentialUser=クレデンシャル・ユーザー\ncredentialUserDescription=ユーザーとしてログインする。\ncredentialPassword=クレデンシャルパスワード\ncredentialPasswordDescription=ユーザーのパスワード。\nsshConfig=SSH設定ファイル\nautostart=XPipe起動時に自動的に接続する\nacceptHostKey=ホスト・キーを受け付ける\nmodifyHostKeyPermissions=ホストキーのパーミッションを変更する\nattachContainer=添付する\ncontainerLogs=ログを表示する\nopenSftpClient=外部のSFTPクライアントで開く\nopenTermius=テルミウスで開く\nshowInternalInstances=内部インスタンスを表示する\neditPod=ポッドを編集する\nacceptHostKeyDescription=新しいホスト鍵を信頼して続行する\nmodifyHostKeyPermissionsDescription=OpenSSHが満足するように、オリジナルファイルのパーミッションの削除を試みる。\npsSession.displayName=PowerShellリモートセッション\npsSession.displayDescription=New-PSSessionとEnter-PSSessionで接続する。\nsshLocalTunnel.displayName=ローカルSSHトンネル\nsshLocalTunnel.displayDescription=リモートホストへのSSHトンネルを確立する\nsshRemoteTunnel.displayName=リモートSSHトンネル\nsshRemoteTunnel.displayDescription=リモートホストから逆SSHトンネルを確立する\nsshDynamicTunnel.displayName=動的SSHトンネル\nsshDynamicTunnel.displayDescription=SSH接続でSOCKSプロキシを確立する\nshellEnvironmentGroup.displayName=シェル環境\nshellEnvironmentGroup.displayDescription=シェル環境\nshellEnvironment.displayName=シェル環境\nshellEnvironment.displayDescription=カスタマイズされたシェル起動環境を作成する\nshellEnvironment.informationFormat=$TYPE$ 環境\nshellEnvironment.elevatedInformationFormat=$ELEVATION$ $TYPE$ 環境\nenvironmentConnectionDescription=の環境を構築するためのベースとなる接続\nenvironmentScriptDescription=シェルで実行するカスタムinitスクリプト（オプション\nenvironmentSnippets=シェルスクリプト\ncommandSnippetsDescription=最初に実行する定義済みシェルスクリプト（オプション\nenvironmentSnippetsDescription=初期化時に実行する定義済みシェルスクリプト（オプション\nshellTypeDescription=起動する明示的なシェルタイプ\noriginPort=オリジンポート\noriginAddress=オリジンアドレス\nremoteAddress=リモートアドレス\nremoteSourceAddress=リモート発信元アドレス\nremoteSourcePort=リモート・ソース・ポート\noriginDestinationPort=オリジン宛先ポート\noriginDestinationAddress=送信元アドレス\norigin=由来\nremoteHost=リモートホスト\naddress=アドレス\nproxmox.displayName=プロックスモックス\nproxmox.displayDescription=Proxmox仮想環境のシステムに接続する\nproxmoxVm.displayName=Proxmox VM\nproxmoxVm.displayDescription=SSH 経由で Proxmox VE の仮想マシンに接続する\nproxmoxContainer.displayName=Proxmoxコンテナ\nproxmoxContainer.displayDescription=Proxmox VEでコンテナに接続する\nsshDynamicTunnel.hostDescription=SOCKSプロキシとして使用するシステム\nsshDynamicTunnel.bindingDescription=トンネルをどのアドレスにバインドするか\nsshRemoteTunnel.hostDescription=オリジンへのリモートトンネルを開始するシステム\nsshRemoteTunnel.bindingDescription=トンネルをどのアドレスにバインドするか\nsshLocalTunnel.hostDescription=トンネルを開くシステム\nsshLocalTunnel.bindingDescription=トンネルをどのアドレスにバインドするか\nsshLocalTunnel.localAddressDescription=バインドするローカルアドレス\nsshLocalTunnel.remoteAddressDescription=バインドするリモートアドレス\ncmd.displayName=コマンド\ncmd.displayDescription=システム上で任意のコマンドを実行する\nk8sPod.displayName=Kubernetesポッド\nk8sPod.displayDescription=kubectl経由でポッドとそのコンテナに接続する\nk8sContainer.displayName=Kubernetesコンテナ\nk8sContainer.displayDescription=コンテナにシェルを開く\nk8sCluster.displayName=Kubernetesクラスタ\nk8sCluster.displayDescription=クラスタとそのポッドにkubectlで接続する\nsshTunnelGroup.displayName=SSHトンネル\nsshTunnelGroup.displayCategory=すべてのタイプのSSHトンネル\nlocal.displayName=ローカルマシン\nlocal.displayDescription=ローカルマシンのシェル\ncygwin=サイグウィン\nmsys2=MSYS2\ngitWindows=Windows用Git\ngitForWindows.displayName=Windows用Git\ngitForWindows.displayDescription=ローカルの Git For Windows 環境にアクセスする\nmsys2.displayName=MSYS2\nmsys2.displayDescription=MSYS2環境のシェルにアクセスする\ncygwin.displayName=サイグウィン\ncygwin.displayDescription=Cygwin環境のシェルにアクセスする\nnamespace=名前空間\ngitVaultIdentityStrategy=Git SSH ID\ngitVaultIdentityStrategyDescription=リモートとして SSH git URL を使うことにしていて、リモートリポジトリが SSH ID を必要とする場合は、このオプションを設定する。\\n\\nHTTP URL を指定した場合は、このオプションを無視してもよい。\ndockerContainers=Dockerコンテナ\ndockerCmd.displayName=docker CLI クライアント\ndockerCmd.displayDescription=docker CLIクライアントを使ってDockerコンテナにアクセスする。\nwslCmd.displayName=WSLインストール\nwslCmd.displayDescription=wsl CLI クライアント経由で WSL インスタンスにアクセスする\nk8sCmd.displayName=kubectlクライアント\nk8sCmd.displayDescription=kubectl経由でKubernetesクラスタにアクセスする\nk8sClusters=Kubernetesクラスタ\nshells=利用可能なシェル\ninspectContainer=検査する\ninspectContext=検査する\nk8sClusterNameDescription=クラスタが属するコンテキストの名前。\npod=ポッド\npodName=ポッド名\nk8sClusterContext=コンテキスト\nk8sClusterContextDescription=クラスタが存在するコンテキストの名前\nk8sClusterNamespace=名前空間\nk8sClusterNamespaceDescription=カスタム名前空間、または空の場合はデフォルトの名前空間。\nk8sConfigLocation=コンフィグファイル\nk8sConfigLocationDescription=カスタムkubeconfigファイル、または空の場合はデフォルトのkubeconfigファイル\ninspectPod=検査する\nshowAllContainers=実行されていないコンテナを表示する\nshowAllPods=実行されていないポッドを表示する\nk8sPodHostDescription=ポッドが置かれているホスト\nk8sContainerDescription=Kubernetesコンテナの名前\nk8sPodDescription=Kubernetesポッドの名前\npodDescription=コンテナが置かれているポッド\nk8sClusterHostDescription=クラスタにアクセスするホスト。クラスタにアクセスするにはkubectlがインストールされ、設定されている必要がある。\nconnection=接続\nshellCommand.displayName=カスタムシェルコマンド\nshellCommand.displayDescription=カスタムコマンドで標準シェルを開く\nssh.displayName=SSH接続\nssh.displayDescription=SSHコマンドラインクライアントを使用してリモートシステムに接続する\nsshConfig.displayName=SSH設定ファイル\nsshConfig.displayDescription=SSH設定ファイルで定義されたホストに接続する\nsshConfigHost.displayName=SSH設定ファイルホスト\nsshConfigHost.displayDescription=SSHコンフィグファイルで定義されたホストに接続する\nsshConfigHost.password=パスワード\nsshConfigHost.passwordDescription=ユーザーログイン用の任意のパスワードを入力する。\nsshConfigHost.identityPassphrase=キーのパスフレーズ\nsshConfigHost.identityPassphraseDescription=鍵のパスフレーズ（オプション）を入力する。\nshellCommand.hostDescription=コマンドを実行するホスト\nshellCommand.commandDescription=シェルを開くコマンド\ncommandType=コマンドタイプ\ncommandTypeDescription=コマンドの実行方法\ncommandDescription=ホスト上で実行するカスタムコマンド\ncommandHostDescription=コマンドを実行するホスト\ncommandDataFlowDescription=このコマンドはどのように入出力を処理するか\ncommandElevationDescription=このコマンドを昇格した権限で実行する\ncommandShellTypeDescription=このコマンドに使用するシェル\nlimitedSystem=これは限定された、あるいは組み込まれたシステムである\nlimitedSystemDescription=シェルの種類を特定しようとしないこと。限られた組み込みシステムやIoTデバイスに必要である。\nsshForwardX11=フォワードX11\nsshForwardX11Description=接続のX11転送を有効にする\ncustomAgent=カスタムエージェント\nidentityAgent=アイデンティティ・エージェント\nssh.proxyDescription=SSH接続を確立するときに使用するオプションのプロキシホスト。sshクライアントがインストールされている必要がある。\nusage=使用方法\nwslHostDescription=WSL インスタンスが置かれているホスト。wslがインストールされている必要がある。\nwslDistributionDescription=WSL インスタンスの名前\nwslUsernameDescription=ログインするための明示的なユーザー名。指定しない場合は、デフォルトのユーザー名が使われる。\nwslPasswordDescription=sudoコマンドで使用できるユーザーのパスワード。\ndockerHostDescription=dockerコンテナが置かれているホスト。docker がインストールされている必要がある。\ndockerContainerDescription=dockerコンテナの名前。\nlocalMachine=ローカルマシン\nrootScan=須藤シェル環境\nloginEnvironmentScan=カスタムログイン環境\nk8sScan=Kubernetesクラスタ\noptions=オプション\ndockerRunningScan=dockerコンテナを実行する\ndockerAllScan=すべてのdockerコンテナ\nwslScan=WSLインスタンス\nsshScan=SSHコンフィグ接続\nrunAsUser=ユーザーとして実行する\nrunAsUserDescription=このシェル環境を別のユーザーとして起動する\ndefault=デフォルト\nadministrator=管理者\nwslHost=WSLホスト\ntimeout=タイムアウト\ninstallLocation=インストール場所\ninstallLocationDescription=$NAME$ 環境がインストールされている場所\nwsl.displayName=Linux用Windowsサブシステム\nwsl.displayDescription=Windows上で動作するWSLインスタンスに接続する\ndocker.displayName=Dockerコンテナ\ndocker.displayDescription=ドッカーコンテナに接続する\nport=ポート\nuser=ユーザー\npassword=パスワード\nmethod=方法\nuri=URL\nproxy=プロキシ\ndistribution=ディストリビューション\nusername=ユーザー名\nshellType=シェルタイプ\nbrowseFile=ファイルをブラウズする\nopenShell=ターミナルでシェルを開く\nopenCommand=ターミナルでコマンドを実行する\neditFile=ファイルを編集する\ndescription=説明\nfurtherCustomization=さらなるカスタマイズ\nfurtherCustomizationDescription=より詳細な設定オプションについては、ssh設定ファイルを使用する。\nbrowse=閲覧する\nconfigHost=ホスト\nconfigHostDescription=コンフィグが置かれているホスト\nconfigLocation=コンフィグの場所\nconfigLocationDescription=コンフィグファイルのファイルパス\ngateway=ゲートウェイ\ngatewayDescription=接続時に使用するオプションのゲートウェイ\nconnectionInformation=接続情報\nconnectionInformationDescription=どのシステムに接続するか\npasswordAuthentication=パスワード認証\npasswordAuthenticationDescription=認証に使用する任意のパスワード\nsshConfigString.displayName=コンフィグベースのSSH接続\nsshConfigString.displayDescription=SSH configフォーマットで完全にカスタマイズされたSSH接続を作成する\nsshConfigStringContent=構成\nsshConfigStringContentDescription=OpenSSHコンフィグフォーマットでの接続のためのSSHオプション\nvnc.displayName=SSH経由でのVNC接続\nvnc.displayDescription=トンネル接続でVNCセッションを開く\nbinding=バインディング\nvncPortDescription=VNCサーバーがリッスンしているポート\nrdpPortDescription=RDPサーバーがリッスンしているポート\nvncUsername=ユーザー名\nvncUsernameDescription=オプションのVNCユーザー名\nvncPassword=パスワード\nvncPasswordDescription=VNCパスワード\nx11WslInstance=X11フォワードWSLインスタンス\nx11WslInstanceDescription=SSH接続でX11転送を使うときにX11サーバーとして使うローカルのWindows Subsystem for Linuxディストリビューション。このディストリビューションはWSL2ディストリビューションでなければならない。\nopenAsRoot=ルートとして開く\nopenInWSL=WSLで開く\nlaunch=起動\nsshTrustKeyContent=ホスト鍵が不明で、手動ホスト鍵検証を有効にしている。$CONTENT$\nsshTrustKeyTitle=不明なホストキー\nrdpTunnel.displayName=SSH経由のRDP接続\nrdpTunnel.displayDescription=トンネル接続を介してRDPで接続する\nrdpEnableDesktopIntegration=デスクトップ統合を有効にする\nrdpEnableDesktopIntegrationDescription=RDPの許可リストが許可していると仮定して、リモートアプリケーションを実行する。\nrdpSetupAdminTitle=RDPのセットアップが必要\nrdpSetupAllowTitle=RDPリモートアプリケーション\nrdpSetupAllowContent=現在このシステムでは、リモートアプリケーションを直接起動することは許可されていない。有効にするか？RDPリモートアプリケーションの許可リストを無効にすることで、XPipeからリモートアプリケーションを直接実行できるようになる。\nrdpServerEnableTitle=RDPサーバー\nrdpServerEnableContent=ターゲットシステムでRDPサーバーが無効になっている。リモートRDP接続を許可するために、レジストリで有効にするか。\nrdp=RDP\nrdpScan=SSH経由のRDPトンネル\nwslX11SetupTitle=WSL X11のセットアップ\nwslX11SetupContent=XPipeは、ローカルのWSLディストリビューションをX11ディスプレイサーバーとして使用することができる。$DIST$ 、X11をセットアップする。WSLディストリビューションに基本的なX11パッケージをインストールするので、時間がかかるかもしれない。どのディストリビューションを使用するかは、設定メニューで変更することもできる。\ncommand=コマンド\ncommandGroup=コマンドグループ\nvncSystem=VNCターゲットシステム\nvncSystemDescription=実際にやりとりするシステム。これは通常トンネルホストと同じである。\nvncHost=ターゲットVNCホスト\nvncHostDescription=VNCサーバーが動作しているシステム\nvncDirectHost=ホスト\nvncDirectHostDescription=VNCサーバーが動作しているサーバーのホストエントリまたは手動アドレス\nrdpDirectHost=ホスト\nrdpDirectHostDescription=RDPサーバーが動作しているサーバーのホストエントリまたは手動アドレス\ngitVaultTitle=Git保管庫\ngitVaultForcePushContent=リモートリポジトリに強制プッシュするか？これにより、履歴も含めてすべてのリモートリポジトリの内容がローカルのものに完全に置き換わる。\ngitVaultOverwriteLocalContent=ローカルの保管庫の変更を上書きするか？これは、すべてのリモートの変更をローカルリポジトリに適用する。\nrdpSimple.displayName=直接RDP接続\nrdpSimple.displayDescription=RDPでホストに接続する\nrdpUsername=ユーザー名\nrdpUsernameDescription=ログインするユーザー。ドメインプレフィックスを含むことができる。\naddressDescription=接続先\nrdpAdditionalOptions=RDPの追加オプション\nrdpAdditionalOptionsDescription=.rdpファイルと同じ書式で、RDPの生オプションを含める。\nproxmoxVncConfirmTitle=VNCアクセス\nproxmoxVncConfirmContent=VMのVNCアクセスを有効にするか。VMのコンフィグファイルで直接VNCクライアントアクセスを有効にし、仮想マシンを再起動する。\ndockerContext.displayName=Dockerコンテキスト\ndockerContext.displayDescription=特定のコンテキストに配置されたコンテナと対話する\nvmActions=VMアクション\ndockerContextActions=コンテキストアクション\nk8sPodActions=ポッドアクション\nopenVnc=VNCアクセスを有効にする\naddVnc=VNC接続を追加する\ncommandGroup.displayName=コマンドグループ\ncommandGroup.displayDescription=システムで使用可能なコマンドをグループ化する\nserial.displayName=シリアル接続\nserial.displayDescription=ターミナルでシリアル接続を開く\nserialPort=シリアルポート\nserialPortDescription=接続するシリアルポート/デバイス\nbaudRate=ボーレート\ndataBits=データビット\nstopBits=ストップビット\nparity=パリティ\nflowControlWindow=フロー制御\nserialImplementation=シリアルの実装\nserialImplementationDescription=シリアルポートに接続するために使用するツール\nserialHost=ホスト\nserialHostDescription=のシリアルポートにアクセスするシステム\nserialPortConfiguration=シリアルポートの設定\nserialPortConfigurationDescription=接続されたシリアル・デバイスの設定パラメーター\nserialInformation=シリアル情報\nopenXShell=XShellで開く\ntsh.displayName=テレポート\ntsh.displayDescription=tsh経由でテレポートノードに接続する\ntshNode.displayName=テレポートノード\ntshNode.displayDescription=クラスタ内のテレポートノードに接続する\nteleportCluster=クラスター\nteleportClusterDescription=ノードが属するクラスタ\nteleportProxy=プロキシ\nteleportProxyDescription=ノードへの接続に使用されるプロキシサーバー\nteleportHost=ホスト\nteleportHostDescription=ノードのホスト名\nteleportUser=ユーザー\nteleportUserDescription=ログインするユーザー\nlogin=ログイン\nhyperVInstall.displayName=ハイパーV\nhyperVInstall.displayDescription=Hyper-Vが管理するVMに接続する\nhyperVVm.displayName=Hyper-V VM\nhyperVVm.displayDescription=SSHまたはPSSession経由でHyper-V VMに接続する\ntrustHost=トラストホスト\ntrustHostDescription=ComputerNameを信頼済みホストリストに追加する\ncopyIp=コピーIP\nvncDirect.displayName=直接VNC接続\nvncDirect.displayDescription=VNC経由でシステムに直接接続する\neditConfiguration=設定を編集する\nviewInDashboard=ダッシュボードで見る\nsetDefault=デフォルトを設定する\nremoveDefault=デフォルトを削除する\nconnectAsOtherUser=他のユーザーとして接続する\nprovideUsername=別のユーザー名でログインする\nvmIdentity=ゲストID\nvmIdentityDescription=必要に応じて接続に使用するSSH ID認証方法\nvmPort=ポート\nvmPortDescription=SSHで接続するポート\nforwardAgent=フォワードエージェント\nforwardAgentDescription=SSHエージェントのIDをリモートシステムで利用可能にする\nvirshUri=URI\nvirshUriDescription=ハイパーバイザーのURI。エイリアスもサポートされている。\nvirshDomain.displayName=libvirt ドメイン\nvirshDomain.displayDescription=libvirt ドメインに接続する\nvirshHypervisor.displayName=libvirt ハイパーバイザー\nvirshHypervisor.displayDescription=libvirt がサポートするハイパーバイザドライバに接続する\nvirshInstall.displayName=libvirt コマンドラインクライアント\nvirshInstall.displayDescription=virsh 経由で利用可能なすべての libvirt ハイパーバイザーに接続する\naddHypervisor=ハイパーバイザーを追加する\ninteractiveTerminal=インタラクティブ端末\neditDomain=ドメインを編集する\nlibvirt=libvirt ドメイン\ncustomIp=カスタムIP\ncustomIpDescription=アドバンスドネットワーキングを使用している場合、デフォルトのローカルVM IP検出をオーバーライドする。\nautomaticallyDetect=自動的に検出する\nuserAddDialogTitle=ユーザー作成\ngroupAddDialogTitle=グループ作成\npassphrase=パスフレーズ\nrepeatPassphrase=パスフレーズを繰り返す\ngroupSecret=グループシークレット\nrepeatGroupSecret=グループシークレットを繰り返す\nvaultGroup=金庫グループ\nloginAlertTitle=ログインが必要\nloginAlertHeader=個人的なつながりにアクセスするために金庫の鍵を開ける\nvaultUser=金庫のユーザー\nme=私\naddGroup=グループを追加する\naddGroupDescription=この金庫のために新しいグループを作成する\naddUser=ユーザーを追加する\naddUserDescription=この金庫の新しいユーザーを作成する\nskip=スキップする\nuserChangePasswordAlertTitle=パスワードの変更\ngroupChangeSecretAlertTitle=秘密の変更\ndocs=ドキュメンテーション\nlxd.displayName=LXDコンテナ\nlxd.displayDescription=lxc経由でLXDコンテナに接続する。\nlxdCmd.displayName=LXD CLIクライアント\nlxdCmd.displayDescription=lxc CLIクライアントを使用してLXDコンテナにアクセスする。\npodman.displayName=ポッドマンコンテナ\npodman.displayDescription=Podmanコンテナに接続する\nincusInstall.displayName=インカスマシンマネージャー\nincusInstall.displayDescription=incus CLI クライアント経由で incus コンテナにアクセスする。\nincusContainer.displayName=インカスコンテナ\nincusContainer.displayDescription=インカス・コンテナに接続する\npodmanCmd.displayName=ポッドマンCLI\npodmanCmd.displayDescription=CLIクライアント経由でPodmanコンテナにアクセスする\nlxdHostDescription=LXDコンテナが置かれているホスト。lxc がインストールされている必要がある。\nlxdContainerDescription=LXDコンテナの名前。\npodmanContainers=ポッドマンコンテナ\nlxdContainers=LXDコンテナ\nincusContainers=インカスのコンテナ\ncontainer=コンテナ\nhost=ホスト\ncontainerActions=コンテナ・アクション\nserialConsole=シリアルコンソール\neditRunConfiguration=実行設定を編集する\ncommunityDescription=個人的なユースケースに最適な接続パワーツール。\nupgradeDescription=サーバーインフラ全体のプロフェッショナルな接続管理\ndiscoverPlans=アップグレードオプションを発見する\nextendProfessional=最新のプロフェッショナル機能にアップグレードする\ncommunityItem1=非商用システムやツールに無制限に接続できる\ncommunityItem2=インストールされている端末やエディターとのシームレスな統合\ncommunityItem3=フル機能のリモートファイルブラウザ\ncommunityItem4=すべてのシェル用の強力なスクリプトシステム\ncommunityItem5=接続情報の同期と共有のためのGitの統合\nupgradeItem1=コミュニティ版の全機能を含む\nupgradeItem2=ホームラボプランは無制限のハイパーバイザーと高度なSSH機能をサポートする\nupgradeItem3=プロフェッショナル・プランでは、エンタープライズ・オペレーティング・システムとツールもサポートする。\nupgradeItem4=エンタープライズプランには、個々のユースケースに対応する柔軟性が備わっている。\nupgrade=アップグレード\nupgradeTitle=利用可能なプラン\nstatus=ステータス\ntype=タイプ\nlicenseAlertTitle=ライセンスが必要\nuseCommunity=コミュニティに続く\npreviewDescription=リリース後数週間は新機能を試す。\ntryPreview=プレビューを有効にする\npreviewItem1=リリース後2週間、新しくリリースされたプロフェッショナル機能にフルアクセスできる\npreviewItem2=コミットメントなしで新機能を試す\nlicensedTo=ライセンス対象\nemail=電子メールアドレス\napply=適用する\nclear=クリア\nactivate=アクティブにする\nvalidUntil=有効期限\nlicenseActivated=ライセンスの有効化\nrestart=再起動\nlockVault=鍵の金庫\nrestartApp=XPipeを再起動する\nfree=無料\nupgradeInfo=ライセンスへのアップグレードに関する情報は以下を参照のこと。\nupgradeInfoPreview=ライセンスへのアップグレードに関する情報は下記をご覧いただくか、プレビューをお試しいただきたい。\nenterLicenseKey=ライセンスキーを入力してアップグレードする\nisOnlySupported=は、少なくとも$TYPE$ ライセンスでのみサポートされる。\nareOnlySupported=は、少なくとも$TYPE$ ライセンスでのみサポートされる。\nlegacyLicense=このライセンスには、購入後1年以内にリリースされたプロフェッショナルの新機能のみが含まれる。\npreviewExpiredLicense=この機能は最近、プレビューで無料で利用できたが、この期間はすでに終了している。\nopenApiDocs=APIドキュメント\nopenApiDocsDescription=HTTP APIドキュメントは、OpenAPI .yaml仕様を含めてオンラインで入手できる。ウェブブラウザやお好みのHTTPクライアントで開くことができる。\nopenApiDocsButton=ドキュメントを開く\npythonApi=Python API\npersonalConnection=この接続とそのすべての子は、個人のIDに依存するため、あなたのユーザーのみが利用できる。\ndeveloperPrintInitFiles=印刷開始ファイルの実行\ndeveloperPrintInitFilesDescription=ターミナル起動時に実行されるすべてのシェルinitスクリプトを表示する。\ndeveloperShowSensitiveCommands=機密コマンドのログ\ndeveloperShowSensitiveCommandsDescription=デバッグ用のログ出力に機密コマンドを含める。\ncheckingForUpdates=アップデートをチェックする\ncheckingForUpdatesDescription=最新リリース情報を取得する\ndownloadingUpdate=リリースの取得 (バージョン$VERSION$)\ndownloadingUpdateDescription=リリースパッケージをダウンロードする\nupdateNag=XPipeをしばらくアップデートしていない。新しいリリースの新機能や修正を見逃しているかもしれない。\nupdateNagTitle=更新リマインダー\nupdateNagButton=リリースを見る\nrefreshServices=リフレッシュ・サービス\nserviceProtocolType=サービスプロトコルタイプ\nserviceProtocolTypeDescription=サービスを開く方法を制御する\nserviceCommand=サービスがアクティブになったら実行するコマンド\nserviceCommandDescription=プレースホルダー$PORTは、実際にトンネルされたローカルポートに置き換えられる。\nvalue=価値\nshowAdvancedOptions=詳細オプションを表示する\nsshAdditionalConfigOptions=追加設定オプション\nremoteFileManager=リモートファイルマネージャー\nclearUserData=ユーザーデータを削除する\nclearUserDataDescription=接続を含むすべてのユーザー設定データを削除する\nclearUserDataTitle=ユーザーデータの削除\nclearUserDataContent=これでxpipeのローカルユーザーデータがすべて削除され、再起動する。接続を気にするのであれば、まずgitリポジトリと同期させること。\nundefined=未定義\ncopyAddress=コピーアドレス\nnetbirdDeviceScan=ネットバード接続\nnetbirdId=ピア公開鍵\nnetbirdIdDescription=相手の内部 netbird 公開鍵 ID。\ntailscaleDeviceScan=テールスケール接続\ntailscaleInstall.displayName=Tailscaleのインストール\ntailscaleInstall.displayDescription=SSH経由でテールネット内のデバイスに接続する\ntailscaleDevice.displayName=テールスケール・デバイス\ntailscaleDevice.displayDescription=SSH経由でテールネット内のデバイスに接続する\ntailscaleId=デバイスID\ntailscaleIdDescription=内部のtailscaleデバイスID\ntailscaleHostName=ホスト名\ntailscaleHostNameDescription=テールネット内のデバイスのホスト名\ntailscaleUsername=ユーザー名\ntailscaleUsernameDescription=ログインするユーザー\ntailscalePassword=パスワード\ntailscalePasswordDescription=sudoで使用できるオプションのユーザーパスワード。\nscriptName=スクリプト名\nscriptNameDescription=このスクリプトにカスタム名を付ける\nscriptGroupName=スクリプトグループ名\nscriptGroupNameDescription=このスクリプトグループにカスタム名を付ける\nidentityName=アイデンティティ名\nidentityNameDescription=このIDにカスタム名を付ける\ntailscaleTailnet.displayName=テールネット\ntailscaleTailnet.displayDescription=アカウントを使って特定のテールネットに接続する\nputtyConnections=PuTTY接続\nkittyConnections=KiTTY接続\nicons=アイコン\ncustomIcons=カスタムアイコン\niconSources=アイコンソース\niconSourcesDescription=アイコンのソースをここに追加できる。XPipeは、追加された場所にあるすべての.svgファイルをピックアップし、利用可能なアイコンセットに追加する。\\n\\nアイコンの保存場所としては、ローカルディレクトリとリモートのgitリポジトリの両方がサポートされている。\nrefreshSources=リフレッシュアイコン\nrefreshSourcesDescription=利用可能なソースからすべてのアイコンを更新する\naddDirectoryIconSource=ディレクトリソースを追加する\naddDirectoryIconSourceDescription=ローカルディレクトリからアイコンを追加する\naddGitIconSource=gitソースを追加する\naddGitIconSourceDescription=リモートのgitリポジトリにあるアイコンを追加する\nrepositoryUrl=GitリポジトリのURL\niconDirectory=アイコンディレクトリ\naddUnsupportedKexMethod=サポートされていない鍵交換方法を追加する\naddUnsupportedKexMethodDescription=この接続で鍵交換方式$VAL$ の使用を許可する。\naddUnsupportedHostKeyType=サポートされていないホスト鍵タイプを追加する\naddUnsupportedHostKeyTypeDescription=この接続にホスト鍵タイプ$VAL$ を使用することを許可する。\naddUnsupportedMacType=サポートされていないMACタイプを追加する\naddUnsupportedMacTypeDescription=この接続にMACタイプ$VAL$ を使用することを許可する。\nrunSilent=バックグラウンドで静かに\nrunInFileBrowser=ファイルブラウザ\nrunInConnectionHub=コネクションハブ\ncommandOutput=コマンド出力\niconSourceDeletionTitle=アイコンのソースを削除する\niconSourceDeletionContent=このアイコンソースとそれに関連するすべてのアイコンを削除するか？\nrefreshIcons=リフレッシュアイコン\nrefreshIconsDescription=外部ソースから利用可能な1000以上のアイコンをすべて.pngファイルに取得し、レンダリングし、キャッシュする。これには時間がかかるかもしれない．\nvaultUserLegacy=Vaultユーザー（限定レガシー互換モード）\nupgradeInstructions=アップグレード手順\nexternalActionTitle=外部アクション要求\nexternalActionContent=外部アクションが要求された。XPipeの外部からのアクションの起動を許可するか。\nnoScriptStateAvailable=スクリプトの互換性を判断するためにリフレッシュする...\ndocumentationDescription=ドキュメントをチェックする\ncustomEditorCommandInTerminal=ターミナルでカスタムコマンドを実行する\ncustomEditorCommandInTerminalDescription=エディターがターミナルベースの場合、このオプションを有効にすると、自動的にターミナルが開き、代わりにターミナルセッションでコマンドが実行される。\\n\\nこのオプションは、vi、vim、nvimなどのエディタに使用できる。\ndisableHttpsTlsCheck=HTTPSリクエストの証明書検証を無効にする\ndisableHttpsTlsCheckDescription=SSLインターセプトを使用してファイアウォールでHTTPSトラフィックを復号化している場合、証明書が一致しないために更新チェックやライセンスチェックが失敗する。このオプションを有効にし、TLS証明書の検証を無効にすることで、この問題を解決できる。\nconnectionsSelected=$NUMBER$ 選択されたコネクション\naddConnections=接続を追加する\nbrowseDirectory=ディレクトリをブラウズする\nopenTerminal=端末を開く\ndocumentation=ドキュメンテーション\nreport=エラーを報告する\nkeePassXcNotAssociated=KeePassXCリンク\nkeePassXcNotAssociatedDescription=XPipeがローカルのKeePassXCデータベースに関連付けられていない。以下をクリックして、XPipeとKeePassXCデータベースを関連付け、XPipeがパスワードを照会できるようにする1回限りのステップを実行する。\nkeePassXcAssociateMore=より多くのデータベースに接続する\nkeePassXcAssociateMoreDescription=同時に複数のKeePassXCデータベースに接続できる\nkeePassXcAssociated=KeePassXCのリンク\nkeePassXcAssociatedDescription=XPipeは以下のローカルKeePassXCデータベースに接続されている：\nkeePassXcNotAssociatedButton=リンクデータベース\nidentifier=識別子\npasswordManagerCommand=カスタムコマンド\npasswordManagerCommandDescription=パスワードを取得するために実行するカスタムコマンド。プレースホルダ文字列$KEYは、呼び出されたときに引用符で囲まれたパスワードキーに置き換えられる。これは、パスワードを標準出力に出力するために、パスワードマネージャCLIを呼び出す必要がある。\nchooseTemplate=テンプレートを選ぶ\nkeePassXcPlaceholder=KeePassXCエントリーURL\nterminalEnvironment=端末環境\nterminalEnvironmentDescription=端末のカスタマイズに、ローカルのLinuxベースのWSL環境の機能を使いたい場合、それらを端末環境として使うことができる。\\n\\nカスタム端末のinitコマンドや端末マルチプレクサの設定は、このWSLディストリビューションで実行される。\nterminalInitScript=ターミナルinitスクリプト\nterminalInitScriptDescription=接続が開始される前にターミナル環境で実行するコマンド。起動時にターミナル環境を設定するために使用できる。\nterminalMultiplexer=端末マルチプレクサ\nterminalMultiplexerDescription=端末のタブの代替として使用する端末マルチプレクサ。これにより、特定の端末操作特性、例えばタブ操作がマルチプレクサ機能で置き換えられる。\\n\\nそれぞれのマルチプレクサ実行ファイルがシステムにインストールされている必要がある。\nterminalMultiplexerWindowsDescription=端末のタブの代替として使用する端末マルチプレクサ。これにより、特定の端末操作特性、例えばタブ操作がマルチプレクサ機能で置き換えられる。\\n\\nWindows上のWSLターミナル環境と、WSLシステムにインストールされるマルチプレクサの実行ファイルが必要である。\nterminalAlwaysPauseOnExit=終了時に常に一時停止する\nterminalAlwaysPauseOnExitDescription=有効にした場合、ターミナルセッションを終了すると、常にセッションの再起動または終了を促すプロンプトが表示される。無効にすると、XPipe はエラーで終了する失敗した接続に対してのみ、この処理を行う。\nquerying=クエリ ...\nretrievedPassword=取得した：$PASSWORD$\nrefreshOpenpubkey=openpubkeyのIDをリフレッシュする\nrefreshOpenpubkeyDescription=opkssh refreshを実行し、openpubkeyのIDを再度有効にする。\nall=すべて\nterminalPrompt=ターミナルプロンプト\nterminalPromptDescription=リモート端末で使用する端末プロンプトツール。ターミナルプロンプトを有効にすると、ターミナルセッションを開くときに、ターゲットシステム上でプロンプトツールが自動的にセットアップされ、設定される。\\n\\nこれは、システム上の既存のプロンプト設定やプロファイルファイルを変更しない。このため、リモートシステム上でプロンプトが設定されている間、 最初の端末のロード時間が長くなる。プロンプトを正しく表示するには、端末に追加のフォントが必要になる。\nterminalPromptConfiguration=ターミナルプロンプトの設定\nterminalPromptConfig=コンフィグファイル\nterminalPromptConfigDescription=プロンプトに適用するカスタムconfigファイル。このコンフィグは、端末が初期化されたときにターゲットシステム上で 自動的に設定され、デフォルトのプロンプトコンフィグとして使われる。\\n\\n各システムで既存のデフォルトコンフィグファイルを使いたい場合は、 このフィールドを空にしておくことができる。\npasswordManagerKey=パスワードマネージャーキー\npasswordManagerKeyDescription=秘密のパスワードマネージャー識別子\npasswordManagerAgent=パスワードマネージャーエージェント\ndockerComposeProject.displayName=Docker composeプロジェクト\ndockerComposeProject.displayDescription=composeプロジェクトのコンテナをグループ化する\nsshVerboseOutput=冗長なSSH出力を有効にする\nsshVerboseOutputDescription=SSHで接続するときに、多くのデバッグ情報を表示する。SSH接続のトラブルシューティングに役立つ。\ndontUseGateway=ゲートウェイを使用しない\ndontUseGatewayDescription=ハイパーバイザーホストをゲートウェイとして使用せず、IPに直接接続する。\ncategoryColor=カテゴリーカラー\ncategoryColorDescription=このカテゴリ内の接続に使用するデフォルトの色\ncategorySync=gitリポジトリと同期する\ncategorySyncDescription=すべての接続をgitリポジトリと自動的に同期する。接続に対するローカルの変更はすべてリモートにプッシュされる。\ncategorySyncSpecial=git リポジトリと同期する\\n(特別なカテゴリ \"$NAME$\" では設定できない)\ncategoryDontAllowScripts=すべての変更を無効にする\ncategoryDontAllowScriptsDescription=このカテゴリ内のシステムでコマンドの実行やその他の操作を無効にし、改ざんを防ぐ。これにより、すべてのスクリプト機能、シェル環境コマンド、プロンプトなどが無効になる。\ncategoryConfirmAllModifications=すべての変更を確認する\ncategoryConfirmAllModificationsDescription=接続やファイルシステムに対するいかなる変更も、最初に確認すること。これにより、重要なシステムに対する誤操作を防ぐことができる。\ncategoryDefaultIdentity=デフォルトID\ncategoryDefaultIdentityDescription=このカテゴリの多くのシステムで特定のIDを頻繁に使用する場合、デフォルトのIDを設定することで、新しい接続を作成するときにそのIDを事前に選択できるようになる。\ncategoryConfigTitle=$NAME$ 構成\nconfigure=設定する\naddConnection=接続を追加する\nnoCompatibleConnection=互換性のある接続が見つからない\nnoCompatibleIdentity=互換性のあるIDが見つからない\nnewCategory=新しいカテゴリー\ndockerComposeRestricted=composeプロジェクトは$NAME$ 。このcomposeプロジェクトの管理には$NAME$ 。\nrestricted=制限あり\ndisableSshPinCaching=SSH PINキャッシュを無効にする\ndisableSshPinCachingDescription=XPipeは、ハードウェア・ベースの認証を使用している場合、キーに入力されたPINを自動的にキャッシュする。\\n\\nこれを無効にすると、接続のたびにPINを再入力しなければならなくなる。\ngitSyncPull=プルしてリモートの git の変更を同期する\nenpassVaultFile=保管庫ファイル\nenpassVaultFileDescription=ローカルのEnpass保管庫ファイル。\nflat=フラット\nrecursive=再帰的\nrdpAllowListBlocked=選択したRemoteAppが、サーバーのRDP許可リストに含まれていない。\npsonoServerUrl=サーバーURL\npsonoServerUrlDescription=psonoバックエンドサーバのURL\npsonoApiKey=APIキー\npsonoApiKeyDescription=uuidとしてフォーマットされた、使用するAPIキー。\npsonoApiSecretKey=APIシークレットキー\npsonoApiSecretKeyDescription=64バイトの16進文字列としてのAPI秘密鍵\npassboltServerUrl=サーバーURL\npassboltServerUrlDescription=passboltバックエンドサーバーのURL\npassboltPassphrase=パスフレーズ\npassboltPassphraseDescription=金庫の秘密鍵のパスフレーズ\npassboltPrivateKey=秘密鍵\npassboltPrivateKeyDescription=保管庫のプライベートgpgキーファイル\nfocusWindowOnNotifications=通知ウィンドウをフォーカスする\nfocusWindowOnNotificationsDescription=接続やトンネルが予期せず終了した場合など、通知やエラーメッセージが表示されたときにXPipeをフォアグラウンドにする。\ngitUsername=カスタム git ユーザー名\ngitUsernameDescription=git リモートリポジトリを認証するためのカスタムユーザー。デフォルトでは、XPipe は現在設定されている git CLI の認証情報を使用する。\\n\\nこの設定は、ローカルの git CLI クライアントに設定されているデフォルトの認証情報を上書きする。\ngitPassword=カスタムgitパスワード/個人アクセストークン\ngitPasswordDescription=認証に使用するパスワードまたは個人アクセストークン。パスワードや個人アクセストークンが必要かどうかは、git リモートプロバイダに依存する。この設定は、ローカルの git CLI クライアントに設定されているデフォルトの認証情報を上書きする。\nsetReadOnly=読み取り専用に設定する\nunsetReadOnly=未設定の読み取り専用\nreadOnlyStoreError=このエントリーの設定はフリーズしている。別の名前を選択して、新しいコピーに変更を保存する。\ncategoryFreeze=接続設定をフリーズする\ncategoryFreezeDescription=接続設定を読み取り専用としてマークする。これは、このカテゴリの既存の接続エントリ構成を変更できないことを意味する。新しい接続を追加することはできる。\nupdateFail=アップデートのインストールが成功しない\nupdateFailAction=手動でアップデートをインストールする\nupdateFailActionDescription=GitHubで最新リリースをチェックする\nonePasswordPlaceholder=項目名または op:// URL\ncomputeDirectorySizes=ディレクトリサイズを計算する\ncomputeSize=サイズを計算する\ncustomSpiceCommand=カスタムコマンド\ncustomSpiceCommandDescription=SPICEセッションを起動するために実行するカスタムコマンド。プレースホルダ文字列$FILEは、呼び出されたときに.vvファイルへの引用符で囲まれたファイルパスに置き換えられる。\nvncClient=VNCクライアント\nvncClientDescription=XPipeでVNC接続を開くときに起動するVNCクライアント。\\n\\nXPipeに統合されたVNCクライアントを使用するか、よりカスタマイズしたい場合は、ローカルにインストールされた外部VNCクライアントを起動する。\nintegratedXPipeVncClient=統合XPipe VNCクライアント\ncustomVncCommand=カスタムコマンド\ncustomVncCommandDescription=VNCセッションを起動するために実行するカスタムコマンド。プレースホルダ文字列$ADDRESSは、呼び出されたときに引用符で囲まれたアドレスに置き換えられる。\nvncConnections=VNC接続\npasswordManagerIdentity=パスワードマネージャーのID\npasswordManagerIdentity.displayName=パスワードマネージャーのID\npasswordManagerIdentity.displayDescription=パスワードマネージャーからIDのユーザー名とパスワードを取得する\npasswordCopied=クリップボードにコピーされた接続パスワード\nerrorOccurred=エラーが発生した\nactionMacro.displayName=アクションマクロ\nactionMacro.displayDescription=カスタマイズしたトリガーを使って実行する\nmacroAdd=マクロを追加する\nmacroName=マクロ名\nmacroNameDescription=このマクロにカスタム名を付ける\nactionId=アクションID\nactionIdDescription=このマクロで実行するアクション\nmacroRefs=関連する接続\nmacroRefsDescription=アクションを実行するコネクション\nconnectionCopy=コピー\nactionPickerTitle=ピックアクション\nactionPickerDescription=何かをクリックしてアクションを実行する。アクションを実行する代わりに、アクションショートカットピックモードでアクションへのショートカットを作成・編集することができる。\ncancelActionPicker=アクションピックをキャンセルする\nactionShortcut=アクションショートカット\nactionShortcuts=アクションショートカット\nactionStore=アクションストア\nactionStoreDescription=アクションを実行するストアエントリー\nactionStores=アクションストア\nactionStoresDescription=アクションを実行するストアエントリ\nactionDesktopShortcut=デスクトップのショートカット\nactionDesktopShortcutDescription=このアクションのショートカットをデスクトップに作成する\nactionUrlShortcut=URLショートカット\nactionUrlShortcutDescription=開いたときにこのアクションをトリガーできるURLをコピーする\nactionUrlShortcutDisabled=URLショートカット（使用不可）\nactionUrlShortcutDisabledDescription=$TYPE$ のインストールタイプは、URLを開くことをサポートしていない。\nactionApiCall=APIリクエスト\nactionApiCallDescription=HTTP APIからこのアクションを呼び出す\nactionMacro=アクションマクロ\nactionMacroDescription=このアクションのための高度な機能を持つマクロを作成する\ncreateMacro=マクロを作成する\nactionConfiguration=パラメータ\nactionConfigurationDescription=実行されるアクションに渡すパラメータ\nconfirmAction=アクションの確認\nactionConnections=アクション接続\nactionConnectionsDescription=アクションを実行する接続\nactionConnection=アクション接続\nactionConnectionDescription=アクションを実行する接続\nappleContainerInstall.displayName=アップルのコンテナ\nappleContainerInstall.displayDescription=コンテナCLI経由でappleコンテナインスタンスにアクセスする。\nappleContainer.displayName=アップルのコンテナ\nappleContainer.displayDescription=コンテナCLI経由でappleコンテナインスタンスにアクセスする。\nappleContainerHostDescription=appleコンテナが置かれているホスト。\nappleContainerDescription=appleのコンテナの名前。\nappleContainers=アップルのコンテナ\nchangeOrderIndexTitle=順序を変更する\norderIndex=索引\norderIndexDescription=このエントリーを他のエントリーと相対的に並べるための明示的なインデックス。最も低いインデックスは上に、最も高いインデックスは下に表示される。\nmoveToFirst=先頭に移動する\nmoveToLast=最後に移動する\ncategory=カテゴリー\nincludeRoot=ルートを含む\nexcludeRoot=ルートを除外する\nfreezeConfiguration=フリーズ設定\nunfreezeConfiguration=設定の凍結を解除する\nwaylandScalingTitle=ウェイランドのスケーリング\nactionApiUrl=$URL$ (json本文をコピーする)\ncopyBody=リクエストボディのコピー\ngitRepoTerminalOpen=ターミナルでリポジトリを開く\ngitRepoTerminalOpenDescription=コマンドラインでリポジトリを見てみる\ngitRepoOverwriteLocal=ローカルリポジトリを上書きする\ngitRepoOverwriteLocalDescription=ローカルの変更をリモートの変更に置き換える\ngitRepoForcePush=リモートリポジトリを上書きする\ngitRepoForcePushDescription=git push --force を使って、ローカルの変更をリモートに適用する\ngitRepoDontWarn=もう警告しない\ngitRepoDontWarnDescription=これが予想される場合、XPipeが今後このエラーを無視するようにする。\ngitRepoTryAgain=もう一度試す\ngitRepoTryAgainDescription=もう一度同じ操作を試みる\ngitRepoEnablePlain=プレーンなディレクトリ同期を使用する\ngitRepoEnablePlainDescription=ディレクトリへの変更を同期するためにgitリポジトリを初期化しない\ngitRepoCreateBare=git syncを使う\ngitRepoCreateBareDescription=sync ディレクトリに新しいベア git リポジトリを初期化する\ngitRepoDisable=とりあえずgit vaultを無効にする\ngitRepoDisableDescription=このセッション中に変更をコミットしてはいけない\ngitRepoPullRefresh=変更をプルしてリフレッシュする\ngitRepoPullRefreshDescription=リモートの変更をマージし、データを再ロードする\nbreakOutCategory=ブレイクアウトカテゴリー\nmergeCategory=マージカテゴリー\nopenWinScp=WinSCPで開く\nuninstallApplication=アンインストールする\nuninstallApplicationDescription=.pkg インストールスクリプトを実行し、XPipe を完全にアンインストールする。\nk8sEditPodTitle=変更を適用する\nk8sEditPodContent=kubectl applyコマンドによる変更を適用するか？変更を適用するには再起動が必要と思われる。\nvirshEditDomainTitle=変更を適用する\nvirshEditDomainContent=ドメインに変更を適用するか?変更を適用するには再起動が必要である。\npkcs11Library=PKCS#11ライブラリ\npkcs11LibraryDescription=ダイナミックリンクされたライブラリファイルのパス\nsshAgentSocket=カスタムSSHエージェントソケット\nsshAgentSocketDescription=SSHエージェントとの通信に使用するカスタムソケット。このカスタムエージェントは、カスタムエージェントオプションを選択することで接続に使用できる。\npublicKey=公開鍵識別子\npublicKeyDescription=オプションの公開鍵は、エージェントが一致する秘密鍵のみを提供することを強制する。\nactions=アクション\nhcloudServer.displayName=ヘッツナークラウドサーバー\nhcloudServer.displayDescription=ヘッツナークラウド上のサーバーにSSHでアクセスする\nhcloudInstall.displayName=ヘッツナークラウドCLI\nhcloudInstall.displayDescription=hcloud経由でヘッツナークラウド上のサーバーにアクセスする\nhcloudContext.displayName=hcloudコンテキスト\nhcloudContext.displayDescription=hcloudコンテキストのアクセスサーバー\nmetrics=メトリクス\nopenInVsCode=VsCodeで開く\naddCloud=クラウド ...\nhcloudToken=hcloudトークン\nhcloudTokenDescription=使用するヘッツナー・クラウド・トークン。詳細はドキュメントを参照のこと\nhcloudLogin=ヘッツナークラウドログイン\nclearHcloudToken=hcloudトークンをクリアする\nclearHcloudTokenDescription=既存のトークンを削除して再ログインできるようにする\nselectIdentity=IDを選択する\nenableMcpServer=MCPサーバーを有効にする\nenableMcpServerDescription=XPipeのMCPサーバーを有効にし、外部のMCPクライアントがMCPサーバーにリクエストを送信できるようにする。設定の詳細については、以下を参照。\\n\\nMCP機能を使用するためにHTTP APIを有効にする必要はない。\nenableMcpMutationTools=MCP変異ツールを有効にする\nenableMcpMutationToolsDescription=デフォルトでは、MCPサーバーでは読み取り専用ツールだけが有効になっている。これは、システムを変更する可能性のある偶発的な操作が行われないようにするためである。\\n\\nMCPクライアントを介してシステムに変更を加える予定がある場合は、このオプショ ンを有効にする前に、破壊的な可能性のある操作を確認するようにMCPクライアントが構成さ れていることを確認する。適用するには、MCPクライアントの再接続が必要である。\nmcpClientConfigurationDetails=MCPクライアントの設定\nmcpClientConfigurationDetailsDescription=この設定データを使用して、MCPクライアントからXPipe MCPサーバーに接続する。\nswitchHostAddress=ホストアドレスを変更する\naddAnotherHostName=別のホスト名を追加する\naddNetwork=ネットワークスキャン ...\nnetworkScan=ネットワークスキャン\nnetworkScanStore=対象ホスト\nnetworkScanStoreDescription=ローカルネットワークをスキャンするホスト\nuseAsGateway=ホストをゲートウェイとして使う\nuseAsGatewayDescription=作成された接続のゲートウェイとしてターゲットホストを使用するかどうか\nnetworkScanPorts=スキャンするポート\nnetworkScanPortsDescription=スキャンに含めるポートのカンマ区切りリスト\nnetworkScanType=接続タイプ\nnetworkScanTypeDescription=探すべきサーバーのタイプ\nemptyDirectory=このディレクトリは空のようだ\nhcloudConfigFile=hcloud設定ファイル\nhcloudConfigFileDescription=hcloud CLI .toml 設定ファイルの場所\npreferMonochromeIcons=モノクロのアイコンを好む\npreferMonochromeIconsDescription=有効にすると、アイコンのデフォルトのカラーバージョンよりも、モノクロのアイコンバリアントが選択される。\\n\\n適用するにはアイコンのリフレッシュが必要である。\nalwaysShowSshMotd=常にMOTDを表示する\nalwaysShowSshMotdDescription=新しい端末セッションのログイン時に、リモートシステムで設定されている日替わりメッセージを表示するかどうか。これを変更すると、SSH 接続の初期化の動作が変わるかもしれないことに注意。\nmanageSubscription=サブスクリプションを管理する\nnoListeningServer=リスニングサーバーがない\nnetworkScanResults=スキャン結果\nnetworkScanResultsDescription=ネットワークで見つかったシステムのリスト\nlocalShellDialect=ローカルシェル\nlocalShellDialectDescription=ローカル操作に使用するシェル。通常のローカルデフォルトのシェルが無効になっているか、ある程度壊れている場合、このオプションを使って別の代替シェルにフォールバックすることができる。\\n\\nカスタムPATHエントリーのようないくつかの設定は、それぞれのシェルプロファイルファイルでまだ設定されていない場合、フォールバックシェルでは適用されないかもしれない。\nagentSocketNotFound=アクティブなエージェントソケットが見つからなかった\nagentSocket=ソケットの位置\nagentSocketDescription=エージェントソケットファイルのパス\nagentSocketNotConfigured=カスタムソケットはまだ設定されていない\ndownloadInProgress=$NAME$ ダウンロード中\nenableTerminalStartupBell=端末の起動ベルを有効にする\nenableTerminalStartupBellDescription=新しいターミナルセッションでビープ音/ベルコマンドを鳴らす。端末エミュレータがベルをサポートしている場合、新しく起動した端末インスタンスを識別しやすくするために使用できる。\ninvalidSshGatewayChain=ジャンプゲートウェイと非ジャンプゲートウェイが混在した無効なゲートウェイチェーン構成。\nsyncFileExists=同期ファイル$FILE$ は既に存在する\nreplaceFile=置換ファイル\nreplaceFileDescription=既存のファイルをこのファイルに置き換えた\nrenameFile=ファイル名を変更する\nrenameFileDescription=このファイルに別の名前を付けて同期する\nnewFileName=新しいファイル名\nparentHostDoesNotSupportTunneling=親ホスト$NAME$ はトンネリングをサポートしていない\nconnectionNotesTemplate=ノートテンプレート\nconnectionNotesTemplateDescription=接続に新しいノートエントリーを追加するときに使用するマークダウンテンプレート。\nconnectionNotesButton=メモを編集する\nrdpSmartSizing=スマートサイジングを有効にする\nrdpSmartSizingDescription=有効にすると、ウィンドウが小さすぎてフル解像度で表示できない場合、mstscはデスクトップサイズを縮小する。縮小してもデスクトップの縦横比は維持される。\ndisableStartOnInit=自動スタートアップを無効にする\nenableStartOnInit=自動スタートアップを有効にする\nfileReadSudoTitle=須藤ファイル読み込み\nfileReadSudoContent=読み込もうとしているファイルには、現在のユーザーに読み取り権限が与えられていない。このファイルをrootユーザーとしてsudoで読むか？これにより、既存の認証情報またはプロンプトを介して、自動的にrootに昇格する。\nnetbirdInstall.displayName=ネットバードのインストール\nnetbirdInstall.displayDescription=Netbirdネットワーク内のピアに接続する\nnetbirdProfile.displayName=ネットバードプロファイル\nnetbirdProfile.displayDescription=特定のプロファイルのピアリスト\nnetbirdPeer.displayName=ネットバードのピア\nnetbirdPeer.displayDescription=SSH経由でピアに接続する\nnetbirdPublicKey=公開鍵\nnetbirdPublicKeyDescription=相手の内部公開鍵\nnetbirdHostName=ホスト名\nnetbirdHostNameDescription=ネットワーク上のピアのホスト名\nvncRefSystem=関連システム\nvncRefSystemDescription=このVNC接続を関連付ける接続エントリ。ない場合は空のままにする\nabstractHost.displayName=ホストの概要\nabstractHost.displayDescription=シェル接続をサポートしていないホストのエントリを作成する\nabstractHostAddress=ホストアドレス\nabstractHostAddressDescription=ホストのアドレス\nabstractHostGateway=ゲートウェイ\nabstractHostGatewayDescription=このホストに到達するためのオプションのゲートウェイシステム\nabstractHostConvert=抽象ホストエントリーに変換する\nhostNoConnections=利用可能な接続はない\nhostHasConnections=$COUNT$ 利用可能な接続\nhostHasConnection=$COUNT$ 利用可能な接続\nlargeFileWarningTitle=大きなファイルの編集\nlargeFileWarningContent=編集したいファイルは$SIZE$ とかなり大きい。このファイルを本当にテキストエディタで開きたいのか？\nrdpAskpassUser=ホストのRDPユーザー名$HOST$\nrdpAskpassPassword=ユーザーのパスワード$USER$\ninPlaceKey=キー\ninPlaceKeyText=秘密鍵の内容\ninPlaceKeyTextDescription=秘密鍵の内容\nnetbirdSelfhosted=セルフホストネットバードインスタンス\nnetbirdSelfhostedDescription=クラウドホスト版の代わりにカスタムURLを提供する\nnetbirdManagementUrl=ネットバードの管理URL\nnetbirdManagementUrlDescription=セルフホスト・インスタンスの管理URL\nnetbirdSetupKey=設定キー\nnetbirdSetupKeyDescription=セットアップキーを使用している場合、ログインに1つ使用できる\nnetbirdLogin=ネットバードログイン\naddProfile=プロファイルを追加する\nnetbirdProfileNameAsktext=新しいネットバードプロファイルの名前\nopenSftp=SFTPセッションで開く\ncapslockWarning=キャップロックを有効にしている\ninherit=継承する\nsshConfigStringSelected=対象ホスト\nsshConfigStringSelectedDescription=複数のホストの場合、最初のホストがターゲットとして使われる。ホストの順番を変えてターゲットを変更する\ntunnelToLocalhost=ローカルホストにトンネルする\ntunnelToLocalhostDescription=リモートポートを自動的にlocalhostにトンネルする\ntags=タグ\ntag=タグ\naddNewTag=新しいタグを作成する\ncreateTag=タグを作成する\ninPlacePublicKey=公開鍵\ninPlacePublicKeyDescription=指定された秘密鍵に関連する公開鍵\nsshKeygenTitle=新しいSSHキーを生成する\nsshKeygenAlgorithm=アルゴリズム\nsshKeygenAlgorithmDescription=鍵に使用する非対称鍵生成アルゴリズム。\nrsaBits=ビット\nrsaBitsDescription=生成された鍵のビット数\nsshKeygenComment=コメント\nsshKeygenCommentDescription=このキーのオプションのコメント\nsshKeygenPassphrase=パスフレーズ\nsshKeygenPassphraseDescription=この鍵のパスフレーズ（オプション\ned25519SkResident=常駐キーを作成する\ned25519SkResidentDescription=ハードウェア・セキュリティ・キーに秘密鍵を保存する\ned25519SkResidentKeyName=常駐キーラベル\ned25519SkResidentKeyNameDescription=鍵にラベルを付ける。複数の鍵をセキュリティ・キーに格納する際に必要となる。\ned25519SkPinRequired=暗証番号を要求する\ned25519SkPinRequiredDescription=使用時にPIN入力を要求する\ned25519SkUserPresenceRequired=ユーザーの存在を要求する\ned25519SkUserPresenceRequiredDescription=使用時にタッチなどを要求する。セキュリティキーによっては、これを有効にする必要がある\ncopyPublicKey=公開鍵をコピーする\ngeneratePublicKey=公開鍵を生成する\npublicKeyGenerateNotice=秘密鍵から生成できる\nidentityApplyTargetHost=ターゲット\nidentityApplyTargetHostDescription=IDを適用するシステム\nidentityApplyAuthorizedHost=SSH鍵の認証\nidentityApplyAuthorizedHostDescription=SSH鍵が認可されたhostsファイルに追加される\nidentityApplyAuthorizedHostButton=ファイルにキーを追加する\napplyIdentityToHost=ホストにIDを適用する\nidentityApplyMissingPublicKeyTitle=公開鍵の紛失\nidentityApplyMissingPublicKeyContent=IDのSSH鍵に公開鍵が関連付けられていない。詳細は設定を確認してほしい。\nvalid=有効\nnotValid=有効ではない\nwarning=警告\nidentityApplyTitle=IDを適用する\nidentityApplyConfigPasswordEnabled=パスワード認証を有効にする\nidentityApplyConfigPasswordEnabledDescription=sshdの設定でパスワード認証が有効になっている\nidentityApplyConfigPasswordDisabled=パスワード認証を無効にする\nidentityApplyConfigPasswordDisabledDescription=sshdコンフィグでパスワード認証がまだ無効になっている\nidentityApplyConfigKeyEnabled=キー認証を有効にする\nidentityApplyConfigKeyEnabledDescription=sshdのコンフィグでキーベース認証が有効になっている。\nidentityApplyConfigKeyDisabled=キー認証は無効である\nidentityApplyConfigKeyDisabledDescription=sshdのコンフィグでキーベース認証が無効のままになっている\nidentityApplyConfigRootDisabledWarning=ルートログインを無効にする\nidentityApplyConfigRootDisabledWarningDescription=sshdのコンフィグでルートユーザーログインが有効になっていない\nidentityApplyConfigAdminWarning=管理者キーの設定\nidentityApplyConfigAdminWarningDescription=adminユーザーには、代わりにadministrators_authorized_keysにキーを追加する必要があるかもしれない\nidentityApplyEditConfig=コンフィグを編集する\nidentityApplyEditConfigDescription=エディターでsshdの設定を開き、問題を修正する\nidentityApplyEditAuthorizedKeys=許可されたキーを編集する\nidentityApplyEditAuthorizedKeysDescription=authorized_keysファイルをエディタで開き、他のキーを編集または削除する。\nidentityApplyEditConfigButton=sshd_config を開く\nidentityApplyEditAuthorizedKeysButton=開く authorized_keys\nidentityApplySetStoreIdentity=接続IDセット\nidentityApplySetStoreIdentityDescription=IDは、接続によって使用されるように設定される。\nidentityApplySetStoreIdentityButton=IDを適用する\ngenerateKey=キーを生成する\ngroupSecretStrategy=グループベースのアクセス制御\ngroupSecretStrategyDescription=グループの暗号化および復号化に使用されるグループシークレットの取得方法。選択した取得方法は、ユーザが起動時に保管庫にログインしたときに実行される。\\n\\nこの設定はグループごとに構成される。現在アクティブなグループ以外の別のグループに対してこの設定を変更するには、そのグループのメンバとしてデータ保管庫にログインする必要がある。\nfileSecret=ファイルベースの秘密\ncommandSecret=コマンド\nhttpRequestSecret=HTTPレスポンス\nfileSecretChoice=ファイルの場所\nfileSecretChoiceDescription=グループ暗号化秘密情報を含むファイルへのパス。このファイルはすべてのプラットフォームで照会できるため、ホームディレクトリを参照するためにパスに ~ を使用することができる。このファイルは、データ保管庫のロックを解除するすべてのシステムで利用可能でなければならず、そうでない場合はログインに失敗する。\ncommandSecretField=検索スクリプト\ncommandSecretFieldDescription=現在のグループの秘密の暗号化キーを返すコマンド。このコマンドはローカルシステムのデフォルトシェルで実行され、キーは標準出力に出力される。\nhttpRequestSecretField=リクエストURI\nhttpRequestSecretFieldDescription=HTTPリクエストを送るURI。グループシークレットはHTTPレスポンスボディから取得される。\nvaultAuthentication=金庫の認証\nvaultAuthenticationDescription=保管庫データの認証/ロック解除方法。保管庫データを誰と共有したいかによって、保管庫データの暗号化とロック解除には複数の異なる方法がある。\ngroupAuthFailed=秘密認証に失敗した\nuserAuthFailed=パスワード認証に失敗した\nsavingChanges=変更を保存する\nawsDeviceScan=AWS\naws=AWS\nawsCliInstallTitle=AWS CLIが必要\nawsCliInstallContent=AWSとの統合には、ローカルシステムにAWS CLIがインストールされている必要がある\nawsProfileCreateTitle=新しいAWSプロファイル\nawsProfileAccessKey=アクセスキー\nawsProfileName=プロファイル名\nawsProfileNameDescription=新しいプロファイルの表示名\nawsProfileRegion=地域\nawsProfileRegionDescription=プロファイルに関連付けられたAWSリージョン\nawsProfileAccessKeyId=アクセスキーID\nawsProfileAccessKeyIdDescription=IAMユーザーのアクセスキーID\nawsProfileSecretAccessKey=シークレットアクセスキー\nawsProfileSecretAccessKeyDescription=関連する秘密のアクセスキー\nawsInstall.displayName=AWS CLIのインストール\nawsInstall.displayDescription=AWS CLI経由でAWSシステムに接続する\nawsProfile.displayName=AWS CLIプロファイル\nawsProfile.displayDescription=特定のプロファイルを通してAWSにアクセスする\nawsInstanceId=インスタンスID\nawsInstanceIdDescription=このインスタンスの内部ID\nawsInstanceUseSsm=SSM経由で接続する\nawsInstanceUseSsmDescription=SSMツールを使ってSSH経由でインスタンスに接続する\nawsEc2Instance.displayName=AWS EC2インスタンス\nawsEc2Instance.displayDescription=SSH経由でEC2インスタンスに接続する\nawsS3Group.displayName=S3バケット\nawsS3Group.displayDescription=AWSプロファイルのS3バケットにアクセスする\nawsS3Bucket.displayName=S3バケット\nawsS3Bucket.displayDescription=AWSプロファイルのS3バケットにアクセスする\nawsEc2Group.displayName=EC2インスタンス\nawsEc2Group.displayDescription=AWSプロファイルのEC2インスタンスにアクセスする\nawsEc2InstanceSsmTerminal=SSM端末を開く\ngenericS3Bucket.displayName=一般的なS3バケット\ngenericS3Bucket.displayDescription=AWS CLI経由で一般的なS3バケットにアクセスする\naddFileSystem=ファイルシステム ...\ngenericS3BucketHost=ホスト\ngenericS3BucketHostDescription=S3サーバーのホストエントリまたは手動アドレス\ngenericS3BucketPortDescription=S3サーバーがリッスンしているポート\ngenericS3BucketAccessKeyId=アクセスキーID\ngenericS3BucketAccessKeyIdDescription=IAMユーザーのアクセスキーID\ngenericS3BucketSecretAccessKey=シークレットアクセスキー\ngenericS3BucketSecretAccessKeyDescription=関連する秘密のアクセスキー\ngenericS3BucketHttps=HTTPSを有効にする\ngenericS3BucketHttpsDescription=HTTPSを使用してサーバーに接続する。プロバイダーによってはHTTPSを要求する場合もある。\ntunnelled=トンネル型\nawsInstallSync=コンフィギュレーション同期\nawsInstallSyncDescription=AWS CLIの設定ファイルをgit vaultに同期する\nawsInstallLocation=ユーザーデータの場所\nawsInstallLocationDescription=AWS CLIの設定ファイルのソースとなるパス\ninstanceActions=インスタンスアクション\nopenSplit=分割ターミナルで開く\nterminalSplitStrategy=分割表示方向\nterminalSplitStrategyDescription=バッチモードでスプリットビュー機能を使用して複数のターミナルセッションを隣り合わせに開いたときのターミナルタブの分割方法を制御する。\nterminalSplitStrategyDisabledDescription=バッチモードでスプリットビュー機能を使用して複数のターミナルセッションを隣り合わせに開いたときのターミナルタブの分割方法を制御する。\\n\\n現在のターミナル構成では、分割ビューはサポートされていない。\nhorizontal=水平\nvertical=縦書き\nbalanced=バランス\nclose=閉じる\nhelpButton=$TOPIC$ ドキュメントリンク\nquickAccess=クイックアクセス\ntoggleEnabled=トグル状態\ncurrentPath=現在のパス\ndirectoryContents=ディレクトリの内容\ndirectoryOptions=ディレクトリオプション\nchooseConnectionType=接続タイプを選択する\nbatchMode=バッチモード\ntoggleButton=トグルボタン\ntailscaleUseSsh=tailscale SSH認証を使用する\ntailscaleUseSshDescription=SSH認証なしで、tailscale SSHサーバ自体からログインする。\nportDescription=SSHサーバーが稼働しているポート\nloginAs=ログイン名\nsshGatewayType=ゲートウェイタイプ\nsshGatewayTypeDescription=トンネル経由でターゲットに接続するか、ProxyJumpオプションで接続するか。\ngatewayTunnel=ゲートウェイトンネル\nproxyJump=プロキシジャンプ\ncommandTypeAsyncBackground=バックグラウンドでデタッチドを実行する\ncommandTypeSyncBackground=バックグラウンドで実行し、終了を待つ\ncommandTypeTerminalBackground=ターミナルで開く\nasyncBackgroundCommand=背景コマンド\nsyncBackgroundCommand=バックグラウンドコマンドをブロックする\nterminalBackgroundCommand=ターミナルコマンド\ntestingConnection=接続をテストする\nopenManagementConsole=オープン管理コンソール\nopenLxcTerminal=LXCターミナルを開く\nopenContainerConsole=シリアルコンソールを開く\nkeeper2fa=2FA方式\nkeeper2faDescription=アカウントに設定されているプライマリ2要素認証方法。Keeperアカウントがパスワードにアクセスするために2要素認証が必要な場合、これを有効にする。\nkeeperTotpDuration=カスタム2FAコード期間\nkeeperTotpDurationDescription=2FAコードの有効期間に関するデフォルトの期間をオーバーライドする。組織のポリシーで期間の変更が許可されている場合にのみ適用される。\\n\\n可能な値は以下の通り：$VALUES$\nkeeperOtherAuth=その他（RSA SecurID、Duo Security、Keeper DNAなど）\nextractReusableIdentities=再利用可能なIDを抽出する\nidentitiesAdded=IDが追加された\nsyncMode=同期モード\nsyncModeDescription=変更の同期方法を制御する。\\n\\nインスタントモードはできるだけ早く変更をプッシュし、プルする。スタートアップと終了モードはセッション中に行われたすべての変更を一度に同期する。\ntoggleTerminalDock=トグル・ターミナル・ドック\nscriptDirectory=ディレクトリの場所\nscriptDirectoryDescription=シェルスクリプトファイルを含むローカルディレクトリ\nscriptSourceUrl=リポジトリのURL\nscriptSourceUrlDescription=シェルスクリプトファイルを含むリモートのgitリポジトリへのURL\nscriptCollectionSourceType=ソースタイプ\nscriptCollectionSourceTypeDescription=シェルスクリプトをロードするソースのタイプ\nscriptCollectionSourceEntry=ソース・エントリ\nscriptCollectionSourceEntryDescription=シェルスクリプトをロードするソース\ngitRepository=Gitリポジトリ\nscriptCollectionSource.displayName=スクリプトソース\nscriptCollectionSource.displayDescription=既存のソースからシェルスクリプトを自動的にインポートする\ndirectorySource=ディレクトリソース\ngitRepositorySource=Git リポジトリのソース\nrefreshSource=ソースをリフレッシュする\nscriptTextSourceUrl=スクリプトURL\nscriptTextSourceUrlDescription=スクリプトファイルを取得するURL\nscriptSourceType=スクリプトソース\nscriptSourceTypeDescription=スクリプトのソース\nscriptSourceTypeInPlace=インプレース・スクリプト\nscriptSourceTypeUrl=外部URL\nscriptSourceTypeSource=既存のソース\nimportScripts=インポートスクリプト\nscriptsContained=$NUMBER$ スクリプト\nscriptSourceCollectionImportTitle=ソースからスクリプトをインポートする ($SELECTED$/$COUNT$)\nnoScriptsFound=スクリプトが見つからない\ntunnel=トンネル\nnotInitialized=初期化されていない\nselectCategory=カテゴリを選択する\nscriptSourceName=スクリプト名\nscriptSourceNameDescription=ソース中のスクリプトのファイル名\nworkspaceRestartTitle=ワークスペースの準備\nworkspaceRestartContent=新しいワークスペースへのショートカットが$PATH$ に作成された。このショートカットに移動するか、XPipeを再起動すると、新しいワークスペースが自動的に開く。\nbrowseShortcut=ファイルをブラウズする\nsyncModeInstant=瞬時に同期する\nsyncModeSession=起動時と終了時に同期する\nsyncModeManual=手動で同期する\npushChanges=プッシュ変更\npullChanges=プル変更\nsourcedFrom=ソース$SOURCE$\ninPlaceScript=インプレース・スクリプト\ngeneric=一般的な\nsyncToPlainDirectory=プレーンディレクトリに同期する\nsyncToPlainDirectoryDescription=ローカルディレクトリに同期する場合、このディレクトリを別の git リポジトリとして扱うか、あるいは単なるディレクトリとして扱うかのどちらかを選択できる。プレーンなディレクトリの設定を有効にすると、そのディレクトリは git リポジトリとして初期化されない。\nopenSpiceSession=SPICE セッションを開く\nterminalBehaviour=端末の動作\nnoScanPossible=サポートされている接続が見つからなかった\nnetworkSwitchPorts=ネットワークポート\nnswitchGroup.displayName=ネットワークポート\nnswitchGroup.displayDescription=ネットワークデバイスの利用可能なポートをリストアップする\nnswitchPort.displayName=ネットワークポート\nnswitchPort.displayDescription=ネットワーク・スイッチ・デバイスの個々のポートを制御する\nenablePort=ポートを有効にする\nshutdownPort=ポートをシャットダウンする\nresetPort=ポートのリセット\nuseSystemDefault=システムのデフォルトを使用する\nportStatus=ポートステータス\nclearCounters=クリアカウンター\nshowStatus=ステータスを表示する\nshowAllPorts=すべてのポートを表示する\nactiveLicense=ライセンス\nactiveLicenseDescription=XPipeライセンスキーをアクティベートする\nauthenticatorApp=認証アプリ\nsecurityKey=セキュリティキー\nmcpAdditionalContext=追加のMCPコンテキスト\nmcpAdditionalContextDescription=MCP クライアントに渡す追加の指示。エージェントの動作を制御し、個々の設定に追加のコンテキストを提供するために使用する。\nmcpAdditionalContextSample=- 最初に確認することなく、自動的にサービスやデーモンを再起動しないこと\\n- ネットワークインターフェイスを設定するときは、常に192.168.1.1/24をゲートウェイとして使用すること\nprefsRestartTitle=再起動が必要\nprefsRestartContent=変更したオプションの中には、適用するためにアプリケーションの再起動が必要なものがある。XPipeを再起動するか。\nbashShell=バッシュシェル\n"
  },
  {
    "path": "lang/strings/translations_ko.properties",
    "content": "delete=삭제\nproperties=속성\n#custom\nusedDate=$DATE$ 사용됨\n#custom\nopenDir=폴더 열기\n#custom\nsortLastUsed=마지막으로 사용한 날짜로 정렬\n#custom\nsortAlphabetical=파일 이름을 알파벳순으로 정렬\n#custom\nsortIndexed=인덱스 순으로 정렬\nrestartDescription=재시작으로 빠르게 해결할 수 있는 경우가 많습니다\n#custom\nreportIssue=문제 보고하기\n#custom\nreportIssueDescription=통합 오류 보고기 열기\nusefulActions=유용한 작업\nstored=저장됨\ntroubleshootingOptions=문제 해결 도구\ntroubleshoot=문제 해결\nremote=원격 파일\naddShellStore=셸 추가 ...\naddShellTitle=셸 연결 추가\nsavedConnections=저장된 연결\nsave=저장\n#custom\nclean=청소\nmoveTo=이동 ...\naddDatabase=데이터베이스 ...\nbrowseInternalStorage=내부 저장소 찾아보기\naddTunnel=터널 ...\naddService=서비스 ...\naddScript=스크립트 ...\naddHost=원격 호스트 ...\naddShell=셸 환경 ...\naddCommand=명령 ...\naddAutomatically=자동으로 추가 ...\naddOther=기타 추가 ...\nconnectionAdd=연결 추가\nscriptAdd=스크립트 추가\nscriptGroupAdd=스크립트 그룹 추가\nidentityAdd=ID 추가\nnew=신규\nselectType=유형 선택\nselectTypeDescription=연결 유형 선택\nselectShellType=셸 유형\nselectShellTypeDescription=셸 연결 유형 선택\nname=이름\nstoreIntroHeader=연결 허브\nstoreIntroContent=여기에서 모든 로컬 및 원격 셸 연결을 한곳에서 관리할 수 있습니다. 먼저 사용 가능한 연결을 자동으로 빠르게 감지하고 추가할 연결을 선택할 수 있습니다.\nstoreIntroButton=연결 검색 ...\ndragAndDropFilesHere=또는 파일을 여기로 끌어다 놓기만 하면 됩니다\nconfirmDsCreationAbortTitle=중단 확인\nconfirmDsCreationAbortHeader=데이터 소스 생성을 중단하시겠습니까?\nconfirmDsCreationAbortContent=데이터 소스 생성 진행 상황이 모두 손실됩니다.\nconfirmInvalidStoreTitle=유효성 검사 건너뛰기\nconfirmInvalidStoreContent=연결 유효성 검사를 건너뛰시겠습니까? 연결 유효성을 검사할 수 없는 경우에도 이 연결을 추가하고 나중에 연결 문제를 해결할 수 있습니다.\nexpand=확장\naccessSubConnections=하위 연결 액세스\ncommon=Common\ncolor=색상\nalwaysConfirmElevation=항상 권한 상승을 확인\nalwaysConfirmElevationDescription=시스템에서 명령을 실행하기 위해 상승된 권한이 필요한 경우(예: sudo)를 사용하여 처리하는 방법을 제어합니다.\\n\\n기본적으로 모든 sudo 자격 증명은 세션 중에 캐시되어 있다가 필요할 때 자동으로 제공됩니다. 이 옵션을 사용 설정하면 매번 상승 권한 액세스를 확인하라는 메시지가 표시됩니다.\nallow=허용\n#custom\nask=묻기\ndeny=거부\nshare=Git 리포지토리에 추가\nunshare=Git 리포지토리에서 제거\nremove=제거\ncreateNewCategory=새 하위 카테고리\nprompt=프롬프트\ncustomCommand=사용자 지정 명령\nother=기타\nsetLock=잠금 설정\nselectConnection=연결 선택\nselectEntry=항목 선택\ncreateLock=암호 문구 만들기\nchangeLock=암호 문구 변경\ntest=테스트\nfinish=마침\nerror=오류가 발생했습니다\ndownloadStageDescription=다운로드한 파일을 시스템 다운로드 디렉터리로 이동하여 엽니다.\n#custom\nok=확인\nsearch=검색\n#custom\nrepeatPassword=비밀번호 확인\naskpassAlertTitle=Askpass\nunsupportedOperation=지원되지 않는 작업입니다: $MSG$\nfileConflictAlertTitle=충돌 해결\nfileConflictAlertContent=충돌이 발생했습니다. $FILE$ 파일이 대상 시스템에 이미 존재합니다.\\n\\n어떻게 진행하시겠습니까?\nfileConflictAlertContentMultiple=충돌이 발생했습니다. $FILE$ 파일이 이미 존재합니다.\\n\\n어떻게 진행하시겠습니까? 모두에 적용되는 옵션을 선택하여 자동으로 해결할 수 있는 충돌이 더 있을 수 있습니다.\nmoveAlertTitle=이동 확인\n#custom\nmoveAlertHeader=($COUNT$) 개의 요소를 $TARGET$ 으로 이동하시겠습니까?\ndeleteAlertTitle=삭제 확인\n#custom\ndeleteAlertHeader=($COUNT$) 개의 요소를 삭제하시겠습니까?\nselectedElements=선택된 요소:\n#custom\nmustNotBeEmpty=$VALUE$ 는 값을 입력해야 합니다\n#custom\nvalueMustNotBeEmpty=값을 입력해야 합니다\ntransferDescription=다운로드하려면 파일을 여기로 드래그하세요\ndragLocalFiles=여기에서 다운로드 드래그\nnull=$VALUE$ 는 널이 아니어야 합니다\nroots=루츠\nscripts=스크립트\nsearchFilter=검색 ...\nrecent=최근\nshortcut=바로 가기\nbrowserWelcomeEmptyHeader=파일 브라우저\nbrowserWelcomeEmptyContent=왼쪽에서 파일 브라우저에서 열 시스템을 선택할 수 있습니다. XPipe는 이전에 액세스한 시스템 및 디렉터리를 기억하여 나중에 여기에 있는 빠른 액세스 메뉴에 표시합니다.\nbrowserWelcomeEmptyButton=로컬 파일 브라우저 열기\nbrowserWelcomeSystems=최근에 다음 시스템에 연결되었습니다:\nbrowserWelcomeDocsHeader=문서\nbrowserWelcomeDocsContent=XPipe에 익숙해지기 위해 보다 안내에 따른 접근 방식을 선호한다면 문서 웹사이트를 참조하세요.\nbrowserWelcomeDocsButton=문서 열기\nhostFeatureUnsupported=$FEATURE$ 가 호스트에 설치되어 있지 않습니다\nmissingStore=$NAME$ 가 존재하지 않습니다\nconnectionName=연결 이름\nconnectionNameDescription=이 연결에 사용자 지정 이름을 지정합니다\nopenFileTitle=파일 열기\nunknown=알 수 없음\nscanAlertTitle=연결 추가\nscanAlertChoiceHeader=대상\nscanAlertChoiceHeaderDescription=연결을 검색할 위치를 선택합니다. 사용 가능한 모든 연결을 먼저 찾습니다.\nscanAlertHeader=연결 유형\nscanAlertHeaderDescription=시스템에 자동으로 추가할 연결 유형을 선택합니다.\nnoInformationAvailable=사용 가능한 정보가 없습니다\nyes=예\nno=아니요\nerrorOccured=오류가 발생했습니다\nterminalErrorOccured=터미널 오류가 발생했습니다\nerrorTypeOccured=$TYPE$ 유형의 예외가 발생했습니다\npermissionsAlertTitle=필요한 권한\npermissionsAlertHeader=이 작업을 수행하려면 추가 권한이 필요합니다.\npermissionsAlertContent=팝업에 따라 설정 메뉴에서 XPipe에 필요한 권한을 부여하세요.\nerrorDetails=오류 세부 정보\nupdateReadyAlertTitle=업데이트 준비\nupdateReadyAlertHeader=$VERSION$ 버전에 대한 업데이트를 설치할 준비가 되었습니다\nupdateReadyAlertContent=이렇게 하면 새 버전이 설치되고 설치가 완료되면 XPipe가 다시 시작됩니다.\nerrorNoDetail=오류 세부 정보를 사용할 수 없습니다\nerrorNoExceptionMessage=$TYPE$ 유형의 오류가 발생했습니다\nupdateAvailableTitle=업데이트 사용 가능\nupdateAvailableContent=버전 $VERSION$ 으로의 XPipe 업데이트를 설치할 수 있습니다. XPipe를 시작할 수 없더라도 업데이트를 설치하여 문제를 해결할 수 있습니다.\nclipboardActionDetectedTitle=클립보드 동작 감지\nclipboardActionDetectedContent=XPipe가 클립보드에서 열 수 있는 콘텐츠를 감지했습니다. 지금 열시겠습니까? 클립보드 콘텐츠를 가져오시겠습니까?\ninstall=설치 ...\nignore=무시\npossibleActions=사용 가능한 작업\nreportError=오류 보고\nreportOnGithub=GitHub에서 이슈 리포트 만들기\nreportOnGithubDescription=GitHub 리포지토리에서 새 이슈를 엽니다\nreportErrorDescription=선택적 사용자 피드백 및 진단 정보가 포함된 오류 보고서 보내기\nignoreError=오류 무시\nignoreErrorDescription=이 오류를 무시하고 아무 일도 없었던 것처럼 계속 진행하세요\nprovideEmail=연락 방법(응답을 받고자 하는 경우에만 선택 사항). 신고는 기본적으로 익명으로 처리되므로 여기에서 이메일 주소와 같은 연락처 정보를 제공할 수 있습니다.\nadditionalErrorInfo=추가 정보 제공(선택 사항)\nadditionalErrorAttachments=첨부 파일 선택(선택 사항)\ndataHandlingPolicies=개인정보 처리방침\nsendReport=보고서 보내기\nerrorHandler=오류 처리기\nevents=이벤트\nvalidate=유효성 검사\nstackTrace=스택 추적\npreviousStep=< 이전\nnextStep=다음 >\nfinishStep=Finish\nselect=선택\nbrowseInternal=내부 찾아보기\ncheckOutUpdate=업데이트 확인\nquit=Quit\nnoTerminalSet=터미널 애플리케이션이 자동으로 설정되지 않았습니다. 설정 메뉴에서 수동으로 설정할 수 있습니다.\nconnections=연결\nconnectionHub=연결 허브\nsettings=설정\nexplorePlans=라이선스\nhelp=도움말\nabout=정보\ndeveloper=개발자\nbrowseFileTitle=파일 찾아보기\nbrowser=파일 브라우저\nselectFileFromComputer=이 컴퓨터에서 파일 선택\nlinks=링크\nwebsite=웹사이트\ndiscordDescription=Discord 서버에 가입하기\nredditDescription=XPipe 하위 레딧에 가입하기\nsecurity=보안\nsecurityPolicy=보안 정보\nsecurityPolicyDescription=자세한 보안 정책 읽기\nprivacy=개인정보 처리방침\nprivacyDescription=XPipe 애플리케이션의 개인정보 처리방침을 읽어보세요\nslackDescription=Slack 워크스페이스에 참여\nsupport=지원\ngithubDescription=GitHub 리포지토리를 확인하세요\nopenSourceNotices=오픈 소스 공지\ncheckForUpdates=업데이트 확인\ncheckForUpdatesDescription=업데이트가 있는 경우 다운로드\nlastChecked=마지막 확인\nversion=버전\nbuild=빌드 버전\nruntimeVersion=런타임 버전\nvirtualMachine=가상 머신\nupdateReady=업데이트 설치\nupdateReadyPortable=업데이트 확인\nupdateReadyDescription=업데이트가 다운로드되어 설치할 준비가 되었습니다\nupdateReadyDescriptionPortable=업데이트를 다운로드할 수 있습니다\nupdateRestart=업데이트하려면 다시 시작\nnever=절대로\nupdateAvailableTooltip=업데이트 사용 가능\nptbAvailableTooltip=공개 테스트 빌드 사용 가능\nvisitGithubRepository=GitHub 리포지토리 방문\nupdateAvailable=업데이트 사용 가능: $VERSION$\ndownloadUpdate=업데이트 다운로드\nlegalAccept=최종 사용자 라이선스 계약에 동의합니다\nconfirm=확인\nprint=Print\nwhatsNew=새로운 기능 $VERSION$ ($DATE$)\nantivirusNoticeTitle=바이러스 백신 프로그램에 대한 참고 사항\nupdateChangelogAlertTitle=변경 로그\ngreetingsAlertTitle=XPipe에 오신 것을 환영합니다\neula=최종 사용자 라이선스 계약\nnews=뉴스\nintroduction=소개\nprivacyPolicy=개인정보 처리방침\nagree=동의\ndisagree=동의하지 않음\ndirectories=디렉토리\nlogFile=로그 파일\nlogFiles=로그 파일\nlogFilesAttachment=로그 파일\nissueReporter=이슈 리포터\nopenCurrentLogFile=로그 파일\nopenCurrentLogFileDescription=현재 세션의 로그 파일을 엽니다\nopenLogsDirectory=로그 디렉터리 열기\ninstallationFiles=설치 파일\nopenInstallationDirectory=설치 파일\nopenInstallationDirectoryDescription=XPipe 설치 디렉터리 열기\nlaunchDebugMode=디버그 모드\nlaunchDebugModeDescription=디버그 모드에서 XPipe 다시 시작\nextensionInstallTitle=다운로드\nextensionInstallDescription=이 작업을 수행하려면 XPipe에서 배포하지 않는 추가 타사 라이브러리가 필요합니다. 여기에서 자동으로 설치할 수 있습니다. 그런 다음 공급업체 웹사이트에서 구성 요소를 다운로드합니다:\nextensionInstallLicenseNote=다운로드 및 자동 설치를 수행하면 타사 라이선스 약관에 동의하는 것입니다:\nlicense=라이선스\ninstallRequired=설치 필요\nrestore=복원\nrestoreAllSessions=모든 세션 복원\nlimitedTouchscreenMode=제한된 터치스크린 모드\nlimitedTouchscreenModeDescription=휴대폰 화면과 같은 이색적인 터치스크린 인터페이스에서 이 애플리케이션을 사용하는 경우 일부 메뉴가 제대로 작동하지 않을 수 있습니다. 이 옵션을 활성화하면 메뉴 구현이 보다 제한된 기능을 사용하여 드물게 전송되는 마우스/터치 이벤트에 대해 작동합니다.\nappearance=모양\ndisplay=디스플레이\npersonalization=개인화\ndisplayOptions=표시 옵션\ntheme=테마\nrdpConfiguration=원격 데스크톱 구성\nrdpClient=RDP 클라이언트\nrdpClientDescription=RDP 연결을 시작할 때 호출할 RDP 클라이언트 프로그램입니다.\\n\\n클라이언트마다 기능 및 통합 정도가 다르다는 점에 유의하세요. 일부 클라이언트는 비밀번호 자동 전달 기능을 지원하지 않으므로 시작할 때 비밀번호를 입력해야 합니다.\nlocalShell=로컬 셸\nthemeDescription=기본 설정 표시 테마.\ndontAutomaticallyStartVmSshServer=필요할 때 가상 머신용 SSH 서버를 자동으로 시작하지 않음\ndontAutomaticallyStartVmSshServerDescription=하이퍼바이저에서 실행 중인 VM에 대한 모든 셸 연결은 SSH를 통해 이루어집니다. XPipe는 필요한 경우 설치된 SSH 서버를 자동으로 시작할 수 있습니다. 보안상의 이유로 이를 원하지 않는 경우 이 옵션을 사용하여 이 동작을 비활성화할 수 있습니다.\nconfirmGitShareTitle=Git 동기화\nconfirmGitShareContent=선택한 파일을 git 볼트 리포지토리에 추가하시겠습니까? 그러면 파일의 암호화된 버전이 git 볼트에 복사되고 변경 내용이 커밋됩니다. 그러면 동기화된 모든 데스크톱에서 파일에 액세스할 수 있습니다.\ngitShareFileTooltip=자동으로 동기화되도록 git 볼트 데이터 디렉터리에 파일을 추가합니다.\\n\\n이 작업은 설정에서 git 볼트가 활성화된 경우에만 사용할 수 있습니다.\nperformanceMode=성능 모드\nperformanceModeDescription=애플리케이션 성능 향상을 위해 필요하지 않은 모든 시각 효과를 비활성화합니다.\ndontAcceptNewHostKeys=새 SSH 호스트 키를 자동으로 수락하지 않기\ndontAcceptNewHostKeysDescription=XPipe는 기본적으로 SSH 클라이언트에 이미 저장된 호스트 키가 없는 시스템에서 호스트 키를 자동으로 수락합니다. 그러나 알려진 호스트 키가 변경된 경우 새 호스트 키를 수락하지 않으면 연결이 거부됩니다.\\n\\n이 동작을 비활성화하면 처음에 충돌이 없더라도 모든 호스트 키를 확인할 수 있습니다.\nuiScale=UI 스케일\nuiScaleDescription=시스템 전체 디스플레이 스케일과 독립적으로 설정할 수 있는 사용자 지정 스케일링 값입니다. 값은 퍼센트 단위로 표시되므로 예를 들어 값이 150이면 UI 배율이 150%가 됩니다.\neditorProgram=편집기 프로그램\neditorProgramDescription=모든 종류의 텍스트 데이터를 편집할 때 사용하는 기본 텍스트 편집기입니다.\nwindowOpacity=창 불투명도\nwindowOpacityDescription=백그라운드에서 일어나는 일을 추적할 수 있도록 창 불투명도를 변경합니다.\nuseSystemFont=시스템 글꼴 사용\nopenDataDir=볼트 데이터 디렉토리\nopenDataDirButton=데이터 디렉터리 열기\nopenDataDirDescription=SSH 키와 같은 추가 파일을 여러 시스템에서 git 리포지토리에 동기화하려는 경우 스토리지 데이터 디렉터리에 넣으면 됩니다. 여기서 참조되는 모든 파일은 동기화된 모든 시스템에서 파일 경로가 자동으로 조정됩니다.\nupdates=업데이트\nselectAll=모두 선택\nadvanced=고급\nthirdParty=오픈 소스 공지\neulaDescription=XPipe 애플리케이션에 대한 최종 사용자 라이선스 계약 읽기\nthirdPartyDescription=타사 라이브러리의 오픈 소스 라이선스 보기\nworkspaceLock=마스터 암호 구문\nenableGitStorage=동기화 사용\nsharing=공유\ngitSync=Git 동기화\nenableGitStorageDescription=이 옵션을 활성화하면 XPipe가 로컬 볼트에 대한 git 리포지토리를 초기화하고 변경 사항을 커밋합니다. 이 경우 git이 설치되어 있어야 하며 로드 및 저장 작업 속도가 느려질 수 있습니다.\\n\\n동기화해야 하는 모든 카테고리는 명시적으로 동기화됨으로 표시해야 합니다.\nstorageGitRemote=원격 동기화 URL\nstorageGitRemoteDescription=설정하면 XPipe는 로드할 때 변경 내용을 자동으로 가져오고 저장할 때 변경 내용을 원격 리포지토리에 푸시합니다.\\n\\n이를 통해 여러 XPipe 설치 간에 볼트를 공유할 수 있습니다. HTTP 및 SSH URL과 로컬 디렉터리를 지원합니다.\n#custom\nvault=저장소\nworkspaceLockDescription=XPipe에 저장된 모든 민감한 정보를 암호화하기 위한 사용자 지정 비밀번호를 설정합니다.\\n\\n이렇게 하면 저장된 민감한 정보에 대한 추가 암호화 계층이 제공되므로 보안이 강화됩니다. 그러면 XPipe가 시작될 때 비밀번호를 입력하라는 메시지가 표시됩니다.\nuseSystemFontDescription=기본 시스템 글꼴을 사용할지 아니면 XPipe에 포함된 Inter 글꼴을 사용할지 제어합니다.\ntooltipDelay=툴팁 지연\ntooltipDelayDescription=툴팁이 표시될 때까지 기다릴 시간(밀리초)입니다.\nfontSize=글꼴 크기\nwindowOptions=창 옵션\nsaveWindowLocation=창 위치 저장\nsaveWindowLocationDescription=재시작 시 창 좌표를 저장하고 복원할지 여부를 제어합니다.\nstartupShutdown=시작/종료\nshowChildrenConnectionsInParentCategory=상위 카테고리에 하위 카테고리 표시\nshowChildrenConnectionsInParentCategoryDescription=특정 상위 카테고리가 선택되어 있을 때 하위 카테고리에 있는 모든 연결을 포함할지 여부입니다.\\n\\n이 옵션을 비활성화하면 카테고리는 하위 폴더를 포함하지 않고 직접 콘텐츠만 표시하는 일반 폴더처럼 작동합니다.\ncondenseConnectionDisplay=연결 표시 압축\ncondenseConnectionDisplayDescription=모든 최상위 수준의 연결이 세로 공간을 적게 차지하도록 하여 연결 목록을 보다 간결하게 표시합니다.\nopenConnectionSearchWindowOnConnectionCreation=연결 생성 시 연결 검색 창 열기\nopenConnectionSearchWindowOnConnectionCreationDescription=새 셸 연결을 추가할 때 사용 가능한 하위 연결을 검색할 수 있는 창을 자동으로 열지 여부입니다.\nworkflow=워크플로\nsystem=시스템\napplication=애플리케이션\nstorage=저장소\nrunOnStartup=시작 시 실행\ncloseBehaviour=종료 동작\ncloseBehaviourDescription=메인 창을 닫을 때 XPipe가 어떻게 진행해야 하는지 제어합니다.\nlanguage=언어\nlanguageDescription=사용할 표시 언어입니다. 번역은 커뮤니티의 기여를 통해 개선됩니다. GitHub에서 번역 수정을 제출하여 번역 작업에 도움을 줄 수 있습니다.\nlightTheme=조명 테마\ndarkTheme=어두운 테마\nexit=XPipe 종료\ncontinueInBackground=백그라운드에서 계속\nminimizeToTray=트레이로 최소화\ncloseBehaviourAlertTitle=닫기 동작 설정\ncloseBehaviourAlertTitleHeader=창을 닫을 때 수행할 작업을 선택합니다. 애플리케이션이 종료되면 활성 연결이 모두 닫힙니다.\nstartupBehaviour=시작 동작\nstartupBehaviourDescription=XPipe가 시작될 때 데스크톱 애플리케이션의 기본 동작을 제어합니다.\nclearCachesAlertTitle=캐시 정리\nclearCachesAlertContent=모든 XPipe 캐시를 정리하시겠습니까? 그러면 사용자 환경 개선을 위해 저장된 모든 캐시 데이터가 삭제됩니다.\nstartGui=시작 GUI\nstartInTray=트레이에서 시작\nstartInBackground=백그라운드에서 시작\nclearCaches=캐시 지우기 ...\nclearCachesDescription=모든 캐시 데이터 삭제\ncancel=취소\nnotAnAbsolutePath=절대 경로가 아닙니다\nnotADirectory=디렉터리가 아님\nnotAnEmptyDirectory=빈 디렉터리가 아님\nautomaticallyCheckForUpdates=업데이트 확인\nautomaticallyCheckForUpdatesDescription=이 옵션을 활성화하면 잠시 후 XPipe가 실행되는 동안 새 릴리스 정보를 자동으로 가져옵니다. 여전히 업데이트 설치를 명시적으로 확인해야 합니다.\nsendAnonymousErrorReports=익명의 오류 보고서 보내기\nsendUsageStatistics=익명 사용 통계 전송\nstorageDirectory=저장소 디렉터리\nstorageDirectoryDescription=XPipe가 모든 연결 정보를 저장해야 하는 위치입니다. 이 위치를 변경하면 이전 디렉터리의 데이터는 새 디렉터리에 복사되지 않습니다.\nlogLevel=로그 수준\nappBehaviour=애플리케이션 동작\nlogLevelDescription=로그 파일을 작성할 때 사용해야 하는 로그 수준입니다.\ndeveloperMode=개발자 모드\ndeveloperModeDescription=이 옵션을 활성화하면 개발에 유용한 다양한 추가 옵션에 액세스할 수 있습니다.\neditor=편집기\ncustom=사용자 지정\npasswordManager=비밀번호 관리자\nexternalPasswordManager=외부 비밀번호 관리자\npasswordManagerDescription=통합할 로컬에 설치된 비밀번호 관리자입니다.\\n\\n비밀번호 관리자가 설치되어 있는 경우 XPipe가 비밀번호 관리자로부터 비밀번호를 검색하도록 구성하여 XPipe가 비밀번호를 직접 저장할 필요가 없도록 할 수 있습니다. 이 기능을 활성화하면 연결에 대한 모든 비밀번호 필드가 비밀번호 관리자를 사용하도록 구성할 수 있습니다.\npasswordManagerCommandTest=비밀번호 관리자 테스트\npasswordManagerCommandTestDescription=비밀번호 관리자를 설정한 경우 여기에서 출력이 올바르게 표시되는지 테스트할 수 있습니다.\npreferTerminalTabs=새 탭 열기 선호\npreferTerminalTabsDescription=XPipe가 새 창 대신 선택한 터미널에서 새 탭을 열지 여부를 제어합니다. 모든 터미널이 탭을 지원하는 것은 아닙니다.\ncustomRdpClientCommand=사용자 지정 명령\ncustomRdpClientCommandDescription=사용자 지정 RDP 클라이언트를 시작하기 위해 실행할 명령입니다.\\n\\n플레이스홀더 문자열 $FILE은 호출 시 따옴표로 묶인 절대 .rdp 파일 이름으로 바뀝니다. 실행 경로에 공백이 포함되어 있으면 반드시 따옴표로 묶어야 합니다.\ncustomEditorCommand=사용자 지정 편집기 명령\ncustomEditorCommandDescription=사용자 정의 편집기를 시작하기 위해 실행할 명령입니다.\\n\\n플레이스홀더 문자열 $FILE은 호출 시 따옴표로 묶인 절대 파일명으로 대체됩니다. 편집기 실행 경로에 공백이 포함된 경우 따옴표로 묶어야 합니다.\neditorReloadTimeout=편집기 재로드 시간 초과\neditorReloadTimeoutDescription=파일이 업데이트된 후 파일을 읽기 전에 대기하는 시간(밀리초)입니다. 이렇게 하면 편집기가 파일 잠금을 쓰거나 해제하는 속도가 느릴 때 발생하는 문제를 방지할 수 있습니다.\nencryptAllVaultData=모든 볼트 데이터 암호화\nencryptAllVaultDataDescription=이 기능을 활성화하면 볼트 연결 데이터의 모든 부분이 해당 데이터 내의 비밀만 암호화되는 것이 아니라 사용자 볼트 암호화 키로 암호화됩니다. 이렇게 하면 볼트에 기본적으로 암호화되지 않는 사용자 이름, 호스트 이름 등과 같은 다른 매개변수에 대한 보안이 한층 더 강화됩니다.\\n\\n이 옵션을 사용하면 더 이상 원본 변경 내용은 볼 수 없고 바이너리 변경 내용만 볼 수 있으므로 git 볼트 기록과 Diffs는 쓸모가 없게 됩니다.\n#custom\nvaultSecurity=저장소 보안\ndeveloperDisableUpdateVersionCheck=업데이트 버전 확인 비활성화\ndeveloperDisableUpdateVersionCheckDescription=업데이트 검사기에서 업데이트를 찾을 때 버전 번호를 무시할지 여부를 제어합니다.\ndeveloperDisableGuiRestrictions=GUI 제한 비활성화\ndeveloperDisableGuiRestrictionsDescription=사용자 인터페이스에서 일부 비활성화된 작업을 계속 실행할 수 있는지 여부를 제어합니다.\ndeveloperShowHiddenEntries=숨겨진 항목 표시\ndeveloperShowHiddenEntriesDescription=활성화하면 숨겨진 데이터 원본과 내부 데이터 원본이 표시됩니다.\ndeveloperShowHiddenProviders=숨겨진 공급자 표시\ndeveloperShowHiddenProvidersDescription=생성 대화 상자에 숨겨진 연결 및 내부 연결 및 데이터 소스 공급자를 표시할지 여부를 제어합니다.\ndeveloperDisableConnectorInstallationVersionCheck=커넥터 버전 확인 비활성화\ndeveloperDisableConnectorInstallationVersionCheckDescription=업데이트 검사기가 원격 컴퓨터에 설치된 XPipe 커넥터의 버전을 검사할 때 버전 번호를 무시할지 여부를 제어합니다.\nshellCommandTest=셸 명령 테스트\nshellCommandTestDescription=XPipe에서 내부적으로 사용하는 셸 세션에서 명령을 실행합니다.\nterminal=터미널\nterminalType=터미널 에뮬레이터\nterminalConfiguration=터미널 구성\nterminalCustomization=터미널 사용자 지정\neditorConfiguration=편집기 구성\ndefaultApplication=기본 애플리케이션\ninitialSetup=초기 설정\nterminalTypeDescription=셸 연결을 여는 데 사용할 기본 터미널입니다.\\n\\n기능 지원 수준은 터미널마다 다르며, 각 터미널은 권장 또는 권장되지 않음으로 표시됩니다. 권장 터미널을 사용할 때 사용자 환경이 가장 좋습니다.\nprogram=프로그램\ncustomTerminalCommand=사용자 지정 터미널 명령\ncustomTerminalCommandDescription=지정된 명령으로 사용자 지정 터미널을 열기 위해 실행할 명령입니다.\\n\\nXPipe는 터미널에서 실행할 임시 실행기 셸 스크립트를 만듭니다. 제공한 명령의 자리 표시자 문자열 $CMD는 호출 시 실제 실행기 스크립트로 대체됩니다. 터미널 실행 경로에 공백이 포함되어 있으면 따옴표로 묶어야 합니다.\nclearTerminalOnInit=초기화 시 터미널 지우기\nclearTerminalOnInitDescription=이 옵션을 활성화하면 새 터미널 세션이 시작된 후 XPipe가 지우기 명령을 실행하여 터미널 세션을 시작하는 동안 인쇄된 불필요한 출력을 모두 제거합니다.\ndontCachePasswords=프롬프트 암호를 캐시하지 않기\ndontCachePasswordsDescription=현재 세션에서 비밀번호를 다시 입력할 필요가 없도록 쿼리된 비밀번호를 XPipe에서 내부적으로 캐시할지 여부를 제어합니다.\\n\\n이 동작이 비활성화되어 있으면 시스템에서 자격 증명을 요구할 때마다 다시 입력해야 합니다.\ndenyTempScriptCreation=임시 스크립트 생성 거부\ndenyTempScriptCreationDescription=XPipe는 일부 기능을 구현하기 위해 대상 시스템에 임시 셸 스크립트를 생성하여 간단한 명령을 쉽게 실행할 수 있도록 하는 경우가 있습니다. 이러한 스크립트에는 민감한 정보가 포함되어 있지 않으며 구현 목적으로만 생성됩니다.\\n\\n이 동작을 비활성화하면 XPipe는 원격 시스템에 임시 파일을 만들지 않습니다. 이 옵션은 모든 파일 시스템 변경을 모니터링하는 보안 수준이 높은 환경에서 유용합니다. 이 옵션을 비활성화하면 셸 환경 및 스크립트와 같은 일부 기능이 의도한 대로 작동하지 않습니다.\ndisableCertutilUse=Windows에서 인증서 사용 비활성화\nuseLocalFallbackShell=로컬 폴백 셸 사용\nuseLocalFallbackShellDescription=로컬 작업을 처리하기 위해 다른 로컬 셸을 사용하도록 전환합니다. Windows에서는 PowerShell, 다른 시스템에서는 본 셸이 이에 해당합니다.\\n\\n이 옵션은 일반적인 로컬 기본 셸이 비활성화되었거나 어느 정도 고장난 경우에 사용할 수 있습니다. 이 옵션을 활성화하면 일부 기능이 예상대로 작동하지 않을 수 있습니다.\ndisableCertutilUseDescription=Cmd.exe의 몇 가지 단점과 버그로 인해 인증툴을 사용하여 임시 셸 스크립트를 만들어서 base64 입력을 디코딩하는 데 사용하는데, 이는 cmd.exe가 ASCII가 아닌 입력에서 중단되기 때문입니다. XPipe에서도 PowerShell을 사용할 수 있지만 이 경우 속도가 느려집니다.\\n\\n이렇게 하면 Windows 시스템에서 일부 기능을 구현하기 위해 인증툴을 사용할 수 없게 되고 대신 PowerShell로 돌아갑니다. 일부 바이러스 백신은 certutil 사용을 차단하므로 일부 바이러스 백신은 이를 좋아할 수 있습니다.\ndisableTerminalRemotePasswordPreparation=터미널 원격 비밀번호 준비 비활성화\ndisableTerminalRemotePasswordPreparationDescription=여러 중간 시스템을 거치는 원격 셸 연결이 터미널에서 설정되어야 하는 상황에서는 중간 시스템 중 하나에 필요한 비밀번호를 준비하여 프롬프트가 자동으로 채워질 수 있도록 해야 할 수 있습니다.\\n\\n비밀번호가 중간 시스템으로 전송되는 것을 원하지 않는 경우 이 동작을 비활성화할 수 있습니다. 그러면 터미널을 열 때 필요한 중간 비밀번호가 터미널 자체에서 쿼리됩니다.\nmore=더 보기\ntranslate=번역\nallConnections=모든 연결\nallScripts=모든 스크립트\nallIdentities=모든 ID\nsynced=동기화\npredefined=사전 정의\nsamples=샘플\ngoodMorning=좋은 아침\ngoodAfternoon=좋은 오후\ngoodEvening=좋은 저녁\naddVisual=비주얼 ...\naddDesktop=데스크톱 ...\nssh=SSH\nsshConfiguration=SSH 구성\nsize=크기\nattributes=속성\nmodified=수정됨\nowner=소유자\n#custom\nupdateReadyTitle=$VERSION$ 로 업데이트할 수 있습니다.\ntemplates=템플릿\nretry=재시도\nretryAll=모두 다시 시도\nreplace=바꾸다\nreplaceAll=모두 바꾸기\nhibernateBehaviour=최대 절전 모드 동작\nhibernateBehaviourDescription=시스템이 최대 절전 모드/절전 모드로 전환될 때 애플리케이션이 작동하는 방식을 제어합니다.\noverview=개요\nhistory=역사\nskipAll=모두 건너뛰기\nnotes=참고\naddNotes=메모 추가\norder=Reorder\nkeepFirst=먼저 유지\nkeepLast=마지막에 유지\npinToTop=맨 위에 고정\nunpinFromTop=위에서부터 고정 해제\norderAheadOf=앞서 주문 ...\nclearIndex=색인 재설정\nhttpServer=HTTP 서버\nmcpServer=MCP 서버\napiKey=API 키\napiKeyDescription=XPipe 데몬 API 요청을 인증하기 위한 API 키입니다. 인증 방법에 대한 자세한 내용은 일반 API 설명서를 참조하세요.\ndisableApiAuthentication=API 인증 비활성화\ndisableApiAuthenticationDescription=모든 필수 인증 방법을 비활성화하여 인증되지 않은 요청이 처리되도록 합니다.\\n\\n인증은 개발 목적으로만 비활성화해야 합니다.\napi=API\nstoreIntroImportContent=이미 다른 시스템에서 XPipe를 사용하고 있나요? 원격 git 리포지토리를 통해 여러 시스템에서 기존 연결을 동기화하세요. 아직 설정하지 않은 경우 나중에 언제든지 동기화할 수도 있습니다.\nstoreIntroImportButton=연결 동기화 ...\nstoreIntroImportHeader=연결 가져오기\nshowNonRunningChildren=실행 중이 아닌 자식 표시\nhttpApi=HTTP API\nisOnlySupportedLimit=는 $COUNT$ 이상의 연결이 있는 경우에만 프로페셔널 라이선스에서 지원됩니다\nareOnlySupportedLimit=는 $COUNT$ 이상의 연결이 있는 경우에만 프로페셔널 라이선스에서 지원됩니다\nenabled=Enabled\nenableGitStoragePtbDisabled=공개 테스트 빌드에 대해서는 Git 동기화를 사용하지 않도록 설정하여 일반 릴리스 Git 리포지토리와 함께 사용하지 않도록 하고 PTB 빌드를 일상적인 드라이버로 사용하지 않도록 합니다.\ncopyId=API ID 복사\nrequireDoubleClickForConnections=연결 시 더블 클릭 필요\nrequireDoubleClickForConnectionsDescription=이 옵션을 활성화하면 연결을 두 번 클릭하여 실행해야 합니다. 더블 클릭에 익숙한 경우 유용합니다.\nclearTransferDescription=선택 항목 지우기\nselectTab=탭 선택\ncloseTab=탭 닫기\ncloseOtherTabs=다른 탭 닫기\ncloseAllTabs=모든 탭 닫기\ncloseLeftTabs=왼쪽 탭 닫기\ncloseRightTabs=오른쪽 탭 닫기\naddSerial=직렬 ...\nconnect=연결\nworkspaces=작업 공간\nmanageWorkspaces=작업 공간 관리\naddWorkspace=작업 공간 추가 ...\nworkspaceAdd=새 작업 공간 추가\nworkspaceAddDescription=워크스페이스는 XPipe를 실행하기 위한 별도의 구성입니다. 모든 워크스페이스에는 모든 데이터가 로컬로 저장되는 데이터 디렉터리가 있습니다. 여기에는 연결 데이터, 설정 등이 포함됩니다.\\n\\n동기화 기능을 사용하는 경우 각 워크스페이스를 다른 git 리포지토리와 동기화하도록 선택할 수도 있습니다.\nworkspaceName=작업 공간 이름\nworkspaceNameDescription=작업 공간의 표시 이름\nworkspacePath=작업 공간 경로\nworkspacePathDescription=워크스페이스 데이터 디렉터리의 위치\nworkspaceCreationAlertTitle=작업 공간 생성\ndeveloperForceSshTty=강제 SSH TTY\ndeveloperForceSshTtyDescription=모든 SSH 연결에 pty를 할당하여 누락된 stderr 및 pty에 대한 지원을 테스트합니다.\ndeveloperDisableSshTunnelGateways=SSH 게이트웨이 터널링 사용 안 함\ndeveloperDisableSshTunnelGatewaysDescription=게이트웨이에 터널 세션을 사용하지 말고 시스템에 직접 연결하세요.\nttyWarning=연결이 pty/tty를 강제로 할당했으며 별도의 stderr 스트림을 제공하지 않습니다.\\n\\n이로 인해 몇 가지 문제가 발생할 수 있습니다.\\n\\n가능하다면 연결 명령이 pty를 할당하지 않도록 하세요.\nxshellSetup=Xshell 설정\ntermiusSetup=Termius 설정\ntryPtbDescription=XPipe 개발자 빌드에서 새로운 기능을 먼저 사용해 보세요\n#custom\nconfirmVaultUnencryptTitle=저장소 암호화 해제 확인\n#custom\nconfirmVaultUnencryptContent=정말 고급 저장소 암호화를 비활성화하시겠습니까? 이렇게 하면 저장된 데이터에 대한 추가 암호화가 제거되고 기존 데이터를 덮어쓰게 됩니다.\nenableHttpApi=HTTP API 사용\nenableHttpApiDescription=API를 활성화하여 외부 프로그램에서 XPipe 디먼을 호출하여 관리되는 연결에 대한 작업을 수행할 수 있도록 합니다.\nchooseCustomIcon=사용자 지정 아이콘 선택\n#custom\ngitVault=Git 저장소\nfileBrowser=파일 브라우저\nconfirmAllDeletions=모든 삭제 확인\nconfirmAllDeletionsDescription=모든 삭제 작업에 대해 확인 대화 상자를 표시할지 여부입니다. 기본적으로 디렉터리만 확인이 필요합니다.\nyesterday=어제\ngreen=녹색\nyellow=노란색\nblue=파란색\nred=빨간색\ncyan=Cyan\npurple=Purple\nasktextAlertTitle=프롬프트\nfileWriteSudoTitle=자동 파일 쓰기\nfileWriteSudoContent=쓰려는 파일에 사용자에게 쓰기 권한이 부여되지 않았습니다. Sudo를 사용하여 이 파일을 루트로 작성하시겠습니까? 그러면 기존 자격 증명을 사용하거나 프롬프트를 통해 자동으로 루트로 승격됩니다.\ndontAllowTerminalRestart=터미널 재시작 허용 안 함\ndontAllowTerminalRestartDescription=기본적으로 터미널 세션은 터미널 내에서 종료된 후 다시 시작할 수 있습니다. 이를 허용하기 위해 XPipe는 터미널에서 세션을 다시 시작하라는 다음과 같은 외부 요청을 수락합니다\\n\\nXPipe는 터미널 및 이 호출의 출처에 대한 제어 권한이 없으므로 악성 로컬 애플리케이션도 이 기능을 사용하여 XPipe를 통해 연결을 시작할 수 있습니다. 이 기능을 비활성화하면 이 시나리오를 방지할 수 있습니다.\nopenDocumentation=문서 열기\nopenDocumentationDescription=이 문제에 대한 XPipe 문서 페이지를 방문하세요\nrenameAll=모두 이름 바꾸기\nlogging=로깅\nenableTerminalLogging=터미널 로깅 사용\nenableTerminalLoggingDescription=모든 터미널 세션에 대해 클라이언트 측 로깅을 활성화합니다. 터미널 세션의 모든 입력과 출력은 세션 로그 파일에 기록됩니다. 비밀번호 프롬프트와 같은 민감한 정보는 기록되지 않습니다.\nterminalLoggingDirectory=터미널 세션 로그\nterminalLoggingDirectoryDescription=모든 로그는 로컬 시스템의 XPipe 데이터 디렉터리에 저장됩니다.\nopenSessionLogs=세션 로그 열기\nsessionLogging=터미널 로깅\nsessionActive=이 연결에 대해 백그라운드 세션이 실행 중입니다.\\n\\n이 세션을 수동으로 중지하려면 상태 표시기를 클릭합니다.\nskipValidation=유효성 검사 건너뛰기\nscriptsIntroHeader=스크립트 정보\nscriptsIntroContent=셸 초기화, 파일 브라우저 및 필요에 따라 스크립트를 실행할 수 있습니다. XPipe 내에서 직접 스크립트를 만들거나 로컬 시스템 또는 원격 git 리포지토리에서 기존 스크립트를 가져올 수 있습니다.\nscriptsIntroBottomHeader=스크립트 사용\nscriptsIntroBottomContent=시작할 수 있는 다양한 샘플 스크립트가 있습니다. 개별 스크립트의 편집 버튼을 클릭하면 스크립트가 어떻게 구현되는지 확인할 수 있습니다. 스크립트를 실행하고 메뉴에 표시하려면 먼저 스크립트를 활성화해야 하며, 이를 위한 토글이 모든 스크립트에 있습니다.\nscriptsIntroBottomButton=시작하기\nscriptSourcesIntroHeader=스크립트 소스\nscriptSourcesIntroContent=사용자 지정 스크립트 소스를 추가하여 전체 셸 스크립트 모음에 즉시 액세스할 수 있습니다. 로컬 소스와 원격 git 리포지토리가 모두 소스로 지원됩니다. 소스에서 감지된 모든 스크립트는 자동으로 사용할 수 있게 됩니다.\nscriptSourcesIntroButton=소스 추가 ...\ncheckForSecurityUpdates=보안 업데이트 확인\ncheckForSecurityUpdatesDescription=XPipe는 일반 기능 업데이트와 별도로 잠재적인 보안 업데이트를 확인할 수 있습니다. 이 기능을 활성화하면 일반 업데이트 확인이 비활성화되어 있어도 최소한 중요한 보안 업데이트는 설치가 권장됩니다.\\n\\n이 설정을 비활성화하면 외부 버전 요청이 수행되지 않으며 보안 업데이트에 대한 알림을 받지 못합니다.\nclickToDock=터미널을 도킹하려면 클릭\nterminalStarting=터미널 시작 대기 중 ...\npinTab=핀 탭\nunpinTab=고정 해제 탭\npinned=고정\nenableConnectionHubTerminalDocking=연결 허브 터미널 도킹 사용\nenableConnectionHubTerminalDockingDescription=터미널 창을 연결 허브의 XPipe 애플리케이션 창에 도킹하여 어느 정도 통합된 터미널을 시뮬레이션할 수 있습니다. 그러면 터미널 창이 항상 도크에 맞도록 XPipe에서 관리됩니다.\nenableFileBrowserTerminalDocking=파일 브라우저 터미널 도킹 사용\nenableFileBrowserTerminalDockingDescription=터미널 창을 파일 브라우저의 XPipe 애플리케이션 창에 도킹하여 어느 정도 통합된 터미널을 시뮬레이션할 수 있습니다. 그러면 터미널 창이 항상 도크에 맞도록 XPipe에서 관리됩니다.\ndownloadsDirectory=사용자 지정 다운로드 디렉토리\ndownloadsDirectoryDescription=다운로드 위치로 이동 버튼을 클릭할 때 다운로드한 파일을 넣을 사용자 지정 디렉터리입니다. 기본적으로 XPipe는 사용자 다운로드 디렉터리를 사용합니다.\npinLocalMachineOnStartup=시작 시 로컬 컴퓨터 탭 고정\npinLocalMachineOnStartupDescription=로컬 컴퓨터 탭을 자동으로 열고 고정합니다. 로컬 컴퓨터와 원격 파일 시스템이 열려 있는 상태에서 분할 파일 브라우저를 자주 사용하는 경우에 유용합니다.\nterminalErrorDescription=이 오류는 터미널 오류이며 이 오류를 수정하지 않으면 XPipe를 계속할 수 없습니다.\ngroupName=그룹 이름\nchmodPermissions=새 권한\neditFilesWithDoubleClick=더블 클릭으로 파일 편집\neditFilesWithDoubleClickDescription=활성화하면 파일을 두 번 클릭하면 상황에 맞는 메뉴가 표시되지 않고 텍스트 편집기에서 바로 열립니다.\ncensorMode=검열 모드\ncensorModeDescription=호스트 이름, 사용자 이름, 연결 이름 등과 같은 모든 정보를 흐리게 처리합니다.\\n\\nXPipe를 스크린샷 또는 스크린셰어할 때 정보를 유출하지 않으려는 경우에 유용합니다.\naddIdentity=신원 ...\nidentities=신원\naddMacro=액션 ...\nidentitiesIntroHeader=ID 정보\nidentitiesIntroContent=사용자 아이디, 비밀번호, 키의 일반적인 조합을 재사용하는 경우 재사용 가능한 ID를 만드는 것이 좋습니다. 이렇게 하면 새 연결을 추가할 때 빠르게 참조할 수 있습니다.\nidentitiesIntroBottomHeader=ID 공유\nidentitiesIntroBottomContent=이 기능을 활성화하면 로컬에서 ID를 추가하거나 git 리포지토리에서 동기화할 수도 있습니다. 이를 통해 여러 시스템 및 다른 팀원들과 ID를 선택적으로 공유할 수 있습니다.\nidentitiesIntroBottomButton=설정 동기화\nidentitiesIntroButton=ID 만들기\nuserName=사용자 이름\nuserAuth=사용자 기반 비밀번호 인증\ngroupAuth=그룹 기반 비밀 인증\nteam=팀\nteamSettings=팀 설정\n#custom\nteamVaults=팀 저장소\n#custom\nvaultTypeNameDefault=기본 저장소\n#custom\nvaultTypeNameLegacy=레거시 개인 저장소\n#custom\nvaultTypeNamePersonal=개인 저장소\n#custom\nvaultTypeNameTeam=팀 저장소\nteamVaultsDescription=팀 볼트를 사용하면 여러 사용자와 그룹이 공유 볼트에 안전하게 액세스할 수 있습니다. 연결과 ID를 모든 사용자에게 공유하거나 개별 사용자 및 그룹만 사용할 수 있도록 구성할 수 있으며, 자체 키로 암호화하여 개별 사용자 및 그룹만 사용할 수 있도록 할 수도 있습니다. 다른 볼트 사용자는 키에 액세스할 수 없는 경우 개인 및 그룹 기반 연결과 ID에 액세스할 수 없습니다.\nvaultTypeContentDefault=현재 사용자 및 사용자 지정 암호가 설정되지 않은 기본 볼트를 사용하고 있습니다. 비밀은 로컬 볼트 키로 암호화되어 있습니다. 볼트 사용자 계정을 만들어 개인용 볼트로 업그레이드할 수 있습니다. 이렇게 하면 로그인할 때마다 입력해야 하는 개인용 비밀번호로 볼트 비밀을 암호화하여 볼트를 잠금 해제할 수 있습니다.\n#custom\nvaultTypeContentLegacy=현재 사용자를 위해 레거시 개인 저장소를 사용하고 있습니다. 비밀은 개인 비밀번호로 암호화되어 있습니다. 이 레거시 호환성에는 기능이 제한되어 있으며 팀 볼트로 바로 업그레이드할 수 없습니다.\n#custom\nvaultTypeContentPersonal=현재 사용자를 위해 개인 저장소를 사용하고 있습니다. 비밀은 개인 비밀번호로 암호화되어 있습니다. 볼트 사용자를 추가하여 팀 볼트로 업그레이드할 수 있습니다.\nvaultTypeContentTeam=현재 여러 사용자가 공유 볼트에 안전하게 액세스할 수 있는 팀 볼트를 사용하고 있습니다. 연결과 ID를 모든 사용자에게 공유하거나 개인 또는 그룹 키로 암호화하여 개인 사용자 또는 그룹만 사용할 수 있도록 구성할 수 있습니다. 다른 볼트 사용자는 키에 액세스할 수 없는 경우 개인 및 그룹 기반 연결과 ID에 액세스할 수 없습니다.\ngroupManagement=그룹 관리\ngroupManagementEmpty=그룹 관리\ngroupManagementDescription=기존 볼트 그룹을 관리하거나 새 그룹을 만듭니다. 각 볼트 그룹에는 해당 그룹만 사용할 수 있고 다른 사람은 사용할 수 없는 연결과 신원을 암호화하는 데 사용되는 개별 비밀 키가 있습니다.\ngroupManagementEmptyDescription=기존 볼트 그룹을 관리하거나 새 그룹을 만듭니다. 각 볼트 그룹에는 해당 그룹만 사용할 수 있고 다른 사람은 사용할 수 없는 연결과 신원을 암호화하는 데 사용되는 개별 비밀 키가 있습니다.\\n\\n팀에 대한 그룹 기반 계정은 프로페셔널 요금제에서 지원됩니다.\nuserManagement=사용자 관리\nuserManagementEmpty=사용자 관리\nuserManagementDescription=기존 볼트 사용자를 관리하거나 새 사용자를 만듭니다. 각 볼트 사용자에게는 해당 사용자만 사용할 수 있고 다른 사람은 알 수 없는 연결과 신원을 암호화하는 데 사용되는 고유한 개별 비밀번호가 있습니다.\nuserManagementEmptyDescription=기존 볼트 사용자를 관리하거나 새 사용자를 만듭니다. 각 볼트 사용자에게는 해당 사용자만 사용할 수 있고 다른 사람은 사용할 수 없는 연결과 신원을 암호화하는 데 사용되는 개별 비밀번호가 있습니다. 개인 키로 연결과 신원을 암호화할 수 있는 사용자를 직접 만들 수 있습니다.\\n\\n커뮤니티 에디션에서는 단일 사용자 계정이 지원됩니다. 프로페셔널 요금제에서는 한 팀에 여러 사용자 계정이 지원됩니다.\nuserIntroHeader=사용자 관리\nuserIntroContent=시작하려면 첫 번째 사용자 계정을 직접 만드세요. 이렇게 하면 이 워크스페이스를 비밀번호로 잠글 수 있습니다.\naddReusableIdentity=재사용 가능한 ID 추가\nusers=사용자\nsyncVault=볼트 동기화\nsyncVaultDescription=여러 시스템 또는 여러 팀원들과 볼트를 동기화하려면 이 볼트에 대해 git 동기화를 사용 설정하세요.\nenableGitSync=Git 동기화 사용\nbrowseVault=볼트 데이터\nbrowseVaultDescription=기본 파일 관리자에서 볼트 디렉터리를 직접 살펴볼 수 있습니다. 외부에서 수정하는 것은 권장되지 않으며 여러 가지 문제를 일으킬 수 있습니다.\nbrowseVaultButton=볼트 찾아보기\nvaultUsers=Vault 사용자\ncreateHeapDump=힙 덤프 생성\ncreateHeapDumpDescription=메모리 사용량 문제를 해결하기 위해 메모리 내용을 파일로 덤프합니다\ninitializingApp=연결 로드 중\ncheckingLicense=라이선스 확인\nloadingGit=Git 리포지토리와 동기화\nloadingGpg=Git용 GnuPG 데몬 시작하기\nloadingSettings=로딩 설정\nloadingConnections=연결 로드 중\nunlockingVault=금고 잠금 해제\nloadingUserInterface=사용자 인터페이스 로드\nptbNotice=공개 테스트 빌드에 대한 공지\nuserDeletionTitle=사용자 삭제\nuserDeletionContent=이 볼트 사용자를 삭제하시겠습니까? 그러면 모든 사용자가 사용할 수 있는 볼트 키를 사용하여 모든 개인 신원 및 연결 비밀이 다시 암호화됩니다. 이 작업에는 시간이 걸리며 XPipe가 다시 시작되어 사용자 변경 사항을 적용합니다.\ngroupDeletionTitle=그룹 삭제\ngroupDeletionContent=이 볼트 그룹을 삭제하시겠습니까? 그러면 모든 사용자가 사용할 수 있는 볼트 키를 사용하여 모든 그룹 전용 ID와 연결 비밀이 다시 암호화됩니다. 이 작업에는 시간이 다소 소요되며 그룹 변경 사항을 적용하기 위해 XPipe가 다시 시작됩니다.\nkillTransfer=킬 전송\ndestination=목적지\nconfiguration=구성\nnewFile=새 파일\nnewLink=새 링크\nlinkName=링크 이름\nscanConnections=사용 가능한 연결 찾기 ...\nobserve=관찰 시작\nstopObserve=관찰 중지\ncreateShortcut=바탕화면 바로 가기 만들기\nbrowseFiles=파일 찾아보기\nclone=복제\ntargetPath=대상 경로\nnewDirectory=새 디렉토리\ncopyShareLink=링크 복사\nselectStore=스토어 선택\nsaveSource=나중에 사용할 수 있도록 저장\nexecute=실행\ndeleteChildren=모든 자녀 제거\nscriptGroupDescriptionDescription=이 그룹에 선택적 설명을 입력합니다\nabstractHostDescriptionDescription=이 호스트에 선택적 설명을 입력합니다\nselectSource=소스 선택\ncommandLineRead=업데이트\ncommandLineWrite=쓰기\nadditionalOptions=추가 옵션\ninput=입력\nmachine=Machine\nopen=Open\nedit=편집\nscriptContents=스크립트 내용\nscriptContentsDescription=실행할 스크립트 명령\nsnippets=스크립트 종속성\nsnippetsDescription=먼저 실행할 다른 스크립트\nsnippetsDependenciesDescription=해당되는 경우 실행해야 하는 모든 가능한 스크립트\nisDefault=호환되는 모든 셸의 초기화에서 실행됩니다\nbringToShells=호환되는 모든 셸로 가져오기\nisDefaultGroup=셸 초기화에서 모든 그룹 스크립트 실행\nexecutionType=실행 유형\nexecutionTypeDescription=이 스크립트를 사용할 컨텍스트\nminimumShellDialect=셸 유형\nminimumShellDialectDescription=이 스크립트를 실행할 셸 유형입니다\ndumbOnly=Dumb\nterminalOnly=터미널\nboth=둘 다\nshouldElevate=상승해야 합니다\nshouldElevateDescription=상승된 권한으로 이 스크립트를 실행할지 여부\nscript.displayName=셸 스크립트\nscript.displayDescription=재사용 가능한 셸 스크립트 만들기\nscriptGroup.displayName=스크립트 그룹\nscriptGroup.displayDescription=스크립트를 함께 그룹화하고\nscriptGroup=그룹\nscriptGroupDescription=이 스크립트를 할당할 그룹\nscriptGroupGroupDescription=이 스크립트 그룹을 할당할 부모 그룹(선택 사항)입니다\nopenInNewTab=새 탭에서 열기\nexecuteInBackground=백그라운드에서\nexecuteInTerminal=in $TERM$\nback=뒤로 가기\nbrowseInWindowsExplorer=Windows 탐색기에서 찾아보기\nbrowseInDefaultFileManager=기본 파일 관리자에서 찾아보기\nbrowseInFinder=파인더에서 찾아보기\ncopy=복사\npaste=붙여넣기\ncopyLocation=복사 위치\nabsolutePaths=절대 경로\nabsoluteLinkPaths=절대 링크 경로\nabsolutePathsQuoted=절대 따옴표로 묶인 경로\nfileNames=파일 이름\nlinkFileNames=링크 파일 이름\nfileNamesQuoted=파일 이름(따옴표로 묶음)\ndeleteFile=삭제 $FILE$\neditWithEditor=다음으로 편집 $EDITOR$\nfollowLink=팔로우 링크\ngoForward=앞으로\nshowDetails=세부 정보 표시\nshowDetailsDescription=오류 스택 추적 표시\nopenFileWith=다음으로 열기 ...\nopenWithDefaultApplication=기본 애플리케이션으로 열기\nrename=이름 바꾸기\nrun=실행\nopenInTerminal=터미널에서 열기\nfile=파일\ndirectory=디렉토리\nsymbolicLink=기호 링크\ndesktopEnvironment.displayName=데스크톱 환경\ndesktopEnvironment.displayDescription=재사용 가능한 원격 데스크톱 환경 구성 만들기\ndesktopHost=데스크톱 호스트\ndesktopHostDescription=기본으로 사용할 데스크톱 연결\ndesktopShellDialect=셸 방언\ndesktopShellDialectDescription=스크립트 및 응용 프로그램을 실행하는 데 사용할 셸 방언입니다\ndesktopSnippets=스크립트 스니펫\ndesktopSnippetsDescription=먼저 실행할 재사용 가능한 스크립트 스니펫 목록\ndesktopInitScript=초기화 스크립트\ndesktopInitScriptDescription=이 환경에 특정한 초기화 명령\ndesktopTerminal=터미널 애플리케이션\ndesktopTerminalDescription=데스크톱에서 스크립트를 시작할 때 사용할 터미널입니다\ndesktopApplication.displayName=데스크톱 애플리케이션\ndesktopApplication.displayDescription=원격 데스크톱에서 애플리케이션 실행\ndesktopBase=데스크톱\ndesktopBaseDescription=이 애플리케이션을 실행할 데스크톱\ndesktopEnvironmentBase=데스크톱 환경\ndesktopEnvironmentBaseDescription=이 애플리케이션을 실행할 데스크톱 환경\ndesktopApplicationPath=애플리케이션 경로\ndesktopApplicationPathDescription=실행할 실행 파일의 경로\ndesktopApplicationArguments=인수\ndesktopApplicationArgumentsDescription=애플리케이션에 전달할 선택적 인수\ndesktopCommand.displayName=데스크톱 명령\ndesktopCommand.displayDescription=원격 데스크톱 환경에서 명령 실행\ndesktopCommandScript=명령\ndesktopCommandScriptDescription=환경에서 실행할 명령\nservice.displayName=서비스\nservice.displayDescription=원격 서비스를 로컬 컴퓨터로 전달\nserviceLocalPort=명시적 로컬 포트\nserviceLocalPortDescription=전달할 로컬 포트, 그렇지 않으면 임의의 포트가 사용됩니다\nserviceRemotePort=원격 포트\nserviceRemotePortDescription=서비스가 실행 중인 포트\nserviceHost=서비스 호스트\nserviceHostDescription=서비스가 실행 중인 호스트\nopenWebsite=웹 사이트 열기\ncustomServiceGroup.displayName=서비스 그룹\ncustomServiceGroup.displayDescription=여러 서비스를 하나의 카테고리로 그룹화\ninitScript=초기화 스크립트 - 셸 초기화에서 실행\nshellScript=셸 세션 스크립트 - 셸 세션 중에 스크립트를 실행할 수 있도록 설정합니다\nrunnableScript=실행 가능한 스크립트 - 연결 허브에서 직접 스크립트를 실행할 수 있도록 허용합니다\nfileScript=파일 스크립트 - 파일 브라우저에서 선택한 파일에 대해 스크립트를 호출할 수 있도록 허용합니다\nrunScript=스크립트 실행\ncopyUrl=URL 복사\nfixedServiceGroup.displayName=서비스 그룹\nfixedServiceGroup.displayDescription=시스템에서 사용 가능한 서비스 목록\nmappedService.displayName=서비스\nmappedService.displayDescription=컨테이너에 의해 노출된 서비스와 상호 작용하기\ncustomService.displayName=서비스\ncustomService.displayDescription=로컬 컴퓨터에서 원격 서비스 포트를 자동으로 열거나 터널링합니다\nfixedService.displayName=서비스\nfixedService.displayDescription=미리 정의된 서비스 사용\nnoServices=사용 가능한 서비스 없음\nhasServices=$COUNT$ 사용 가능한 서비스\nhasService=$COUNT$ 사용 가능한 서비스\nnoConnections=사용 가능한 연결이 없습니다\nhasConnections=$COUNT$ 사용 가능한 연결\nhasConnection=$COUNT$ 사용 가능한 연결\nopenHttp=HTTP 서비스 열기\nopenHttps=HTTPS 서비스 열기\nnoScriptsAvailable=사용 가능하고 호환되는 스크립트가 없습니다\nscriptsDisabled=스크립트 비활성화\nchangeIcon=변경 아이콘\ninit=Init\nshell=셸\nhub=Hub\nscript=스크립트\ngenericScript=Generic\ngradleTasks=그레이들 작업\nrunTask=작업 실행\narchiveName=아카이브 이름\ncompress=압축\ncompressContents=콘텐츠 압축\nuntarHere=여기\nuntarDirectory=Untar $DIR$\nunzipDirectory=압축을 푼 위치 $DIR$\nunzipHere=여기에서 압축을 풉니다\nrequiresRestart=적용하려면 다시 시작해야 합니다.\ndownload=다운로드\nservicePath=서비스 경로\nservicePathDescription=브라우저에서 URL을 열 때 선택적 하위 경로입니다\nactive=활성\ninactive=비활성\nstarting=시작\nremotePort=원격 포트\nremotePortNumber=원격 포트 $PORT$\nuserIdentity=개인 신원\nglobalIdentity=글로벌 아이덴티티\nidentityChoice=사용자 ID\nidentityChoiceDescription=미리 정의된 ID를 선택하거나 이 연결에 대해서만 로그인 세부 정보를 지정합니다\ndefineNewIdentityOrSelect=새로 입력하거나 기존\nlocalIdentity.displayName=로컬 ID\nlocalIdentity.displayDescription=이 로컬 데스크톱에 대해 재사용 가능한 ID 만들기\nsyncedIdentity.displayName=동기화된 ID\nsyncedIdentity.displayDescription=여러 시스템에서 동기화되는 재사용 가능한 ID 만들기\nlocalIdentity=로컬 ID\nkeyNotSynced=키 파일이 아직 git 리포지토리에 동기화되지 않았습니다. 키 파일을 추가하려면 git에 추가 버튼을 사용하세요.\nusernameDescription=로그인할 사용자 이름\nidentity.displayName=신원\nidentity.displayDescription=연결에 재사용 가능한 ID 만들기\nlocal=로컬\nshared=글로벌\nuserDescription=로그인할 사용자 이름 또는 미리 정의된 ID입니다\nidentityAccessLevel=액세스 수준\nidentityPerUser=개인 ID 액세스\nidentityPerUserDescription=이 ID 및 관련 연결에 대한 액세스를 볼트 사용자로만 제한합니다\nidentityPerUserDisabled=개인 ID 액세스(비활성화됨)\nidentityPerUserDisabledDescription=이 ID와 연결된 연결에 대한 액세스를 볼트 사용자로만 제한합니다(팀을 구성해야 함)\nidentityPerGroup=그룹 전용 ID 액세스\nidentityPerGroupDescription=이 ID 및 관련 연결에 대한 액세스를 이 볼트 그룹으로만 제한합니다\nlibrary=라이브러리\nlocation=위치\nkeyAuthentication=키 기반 인증\nkeyAuthenticationDescription=키 기반 인증이 필요한 경우 사용할 인증 방법입니다\nlocationDescription=해당 개인 키의 파일 경로\nkeyFile=로컬 키 파일\nkeyPassword=암호 구문\nkey=Key\nyubikeyPiv=유비키 PIV\npageant=대회\ngpgAgent=GPG 에이전트\ncustomPkcs11Library=사용자 정의 PKCS#11 라이브러리\nsshAgent=OpenSSH 에이전트\nnone=없음\nindex=색인 ...\notherExternal=기타 외부 에이전트\nsync=동기화\nvaultSync=볼트 동기화\ncustomUsername=사용자 이름\ncustomUsernameDescription=로그인할 대체 사용자(선택 사항)입니다\ncustomUsernamePassword=비밀번호\ncustomUsernamePasswordDescription=Sudo 인증이 필요할 때 사용할 사용자의 비밀번호입니다\nshowInternalPods=내부 파드 표시\nshowAllNamespaces=모든 네임스페이스 표시\nshowInternalContainers=내부 컨테이너 표시\n#custom\nrefresh=새로고침\nvmwareGui=시작 GUI\nmonitorVm=VM 모니터\naddCluster=클러스터 추가 ...\nshowNonRunningInstances=실행 중이 아닌 인스턴스 표시\nvmwareGuiDescription=가상 머신을 백그라운드에서 시작할지 창에서 시작할지 여부입니다.\nvmwareEncryptionPassword=암호화 비밀번호\nvmwareEncryptionPasswordDescription=가상 머신을 암호화하는 데 사용되는 선택적 암호입니다.\nvmPasswordDescription=게스트 사용자의 필수 비밀번호입니다.\nvmPassword=사용자 비밀번호\nvmUser=게스트 사용자\nrunTempContainer=임시 컨테이너 실행\nvmUserDescription=기본 게스트 사용자의 사용자 이름\ndockerTempRunAlertTitle=임시 컨테이너 실행\ndockerTempRunAlertHeader=임시 컨테이너에서 셸 프로세스를 실행하여 중지되면 자동으로 제거됩니다.\nimageName=이미지 이름\nimageNameDescription=사용할 컨테이너 이미지 식별자\ncontainerName=컨테이너 이름\ncontainerNameDescription=선택적 사용자 지정 컨테이너 이름\nvm=가상 머신\nvmDescription=관련 구성 파일입니다.\nvmwareScan=VMware 데스크톱 하이퍼바이저\nvmwareMachine.displayName=VMware 가상 머신\nvmwareMachine.displayDescription=SSH를 통해 가상 머신에 연결\nvmwareInstallation.displayName=VMware 데스크톱 하이퍼바이저 설치\nvmwareInstallation.displayDescription=CLI를 통해 설치된 VM과 상호 작용합니다\nstart=시작\nstop=Stop\npause=일시 중지\nrdpTunnelHost=대상 호스트\nrdpTunnelHostDescription=RDP 연결을 터널링할 SSH 연결입니다\nrdpTunnelUsername=사용자 이름\nrdpTunnelUsernameDescription=로그인할 사용자 지정 사용자(비워두면 SSH 사용자 사용)입니다\nrdpFileLocation=파일 위치\nrdpFileLocationDescription=.rdp 파일의 파일 경로\nrdpPasswordAuthentication=비밀번호 인증\nrdpFiles=RDP 파일\nrdpPasswordAuthenticationDescription=클라이언트 지원팀에 따라 입력하거나 클립보드에 복사할 비밀번호입니다\nrdpFile.displayName=RDP 파일\nrdpFile.displayDescription=기존 .rdp 파일을 통해 시스템에 연결하기\nrequiredSshServerAlertTitle=SSH 서버 설정\nrequiredSshServerAlertHeader=VM에 설치된 SSH 서버를 찾을 수 없습니다.\nrequiredSshServerAlertContent=VM에 연결하기 위해 XPipe가 실행 중인 SSH 서버를 찾고 있지만 VM에 사용 가능한 SSH 서버가 감지되지 않았습니다.\ncomputerName=컴퓨터 이름\npssComputerNameDescription=연결할 컴퓨터 이름\ncredentialUser=자격 증명 사용자\ncredentialUserDescription=로그인할 사용자입니다.\ncredentialPassword=자격 증명 암호\ncredentialPasswordDescription=사용자의 비밀번호입니다.\nsshConfig=SSH 구성 파일\nautostart=XPipe 시작 시 자동 연결\nacceptHostKey=호스트 키 수락\nmodifyHostKeyPermissions=호스트 키 권한 수정\nattachContainer=첨부\ncontainerLogs=로그 표시\nopenSftpClient=외부 SFTP 클라이언트에서 열기\nopenTermius=Termius에서 열기\nshowInternalInstances=내부 인스턴스 표시\neditPod=파드 편집\nacceptHostKeyDescription=새 호스트 키를 신뢰하고 계속\nmodifyHostKeyPermissionsDescription=OpenSSH가 만족할 수 있도록 원본 파일의 권한을 제거하려고 시도합니다\npsSession.displayName=PowerShell 원격 세션\npsSession.displayDescription=New-PSSession 및 Enter-PSSession을 통한 연결\nsshLocalTunnel.displayName=로컬 SSH 터널\nsshLocalTunnel.displayDescription=원격 호스트에 대한 SSH 터널 설정\nsshRemoteTunnel.displayName=원격 SSH 터널\nsshRemoteTunnel.displayDescription=원격 호스트에서 역방향 SSH 터널 설정하기\nsshDynamicTunnel.displayName=동적 SSH 터널\nsshDynamicTunnel.displayDescription=SSH 연결을 통해 SOCKS 프록시 설정하기\nshellEnvironmentGroup.displayName=셸 환경\nshellEnvironmentGroup.displayDescription=셸 환경\nshellEnvironment.displayName=셸 환경\nshellEnvironment.displayDescription=사용자 지정 셸 시작 환경 만들기\nshellEnvironment.informationFormat=$TYPE$ 환경\nshellEnvironment.elevatedInformationFormat=$ELEVATION$ $TYPE$ 환경\nenvironmentConnectionDescription=다음 환경을 만들기 위한 기본 연결\nenvironmentScriptDescription=셸에서 실행할 사용자 지정 초기화 스크립트(선택 사항)\nenvironmentSnippets=셸 스크립트\ncommandSnippetsDescription=먼저 실행할 미리 정의된 셸 스크립트(선택 사항)\nenvironmentSnippetsDescription=초기화 시 실행할 사전 정의된 셸 스크립트(선택 사항)\nshellTypeDescription=실행할 명시적 셸 유형\noriginPort=원본 포트\noriginAddress=원본 주소\nremoteAddress=원격 주소\nremoteSourceAddress=원격 소스 주소\nremoteSourcePort=원격 소스 포트\noriginDestinationPort=출발지 목적지 포트\noriginDestinationAddress=출발지 목적지 주소\norigin=Origin\nremoteHost=원격 호스트\naddress=주소\nproxmox.displayName=Proxmox\nproxmox.displayDescription=Proxmox 가상 환경의 시스템 연결\nproxmoxVm.displayName=Proxmox VM\nproxmoxVm.displayDescription=SSH를 통해 Proxmox VE의 가상 머신에 연결합니다\nproxmoxContainer.displayName=Proxmox 컨테이너\nproxmoxContainer.displayDescription=Proxmox VE의 컨테이너에 연결하기\nsshDynamicTunnel.hostDescription=SOCKS 프록시로 사용할 시스템\nsshDynamicTunnel.bindingDescription=터널을 바인딩할 주소\nsshRemoteTunnel.hostDescription=오리진에 대한 원격 터널을 시작할 시스템\nsshRemoteTunnel.bindingDescription=터널을 바인딩할 주소\nsshLocalTunnel.hostDescription=터널을 열 시스템\nsshLocalTunnel.bindingDescription=터널을 바인딩할 주소\nsshLocalTunnel.localAddressDescription=바인딩할 로컬 주소\nsshLocalTunnel.remoteAddressDescription=바인딩할 원격 주소\ncmd.displayName=명령\ncmd.displayDescription=시스템에서 임의의 명령을 실행합니다\nk8sPod.displayName=쿠버네티스 파드\nk8sPod.displayDescription=Kubectl을 통해 파드와 그 컨테이너에 연결한다\nk8sContainer.displayName=쿠버네티스 컨테이너\nk8sContainer.displayDescription=컨테이너에 대한 셸 열기\nk8sCluster.displayName=쿠버네티스 클러스터\nk8sCluster.displayDescription=Kubectl을 통해 클러스터와 해당 파드에 연결한다\nsshTunnelGroup.displayName=SSH 터널\nsshTunnelGroup.displayCategory=모든 유형의 SSH 터널\nlocal.displayName=로컬 컴퓨터\nlocal.displayDescription=로컬 컴퓨터의 셸\ncygwin=Cygwin\nmsys2=MSYS2\ngitWindows=Windows용 Git\ngitForWindows.displayName=Windows용 Git\ngitForWindows.displayDescription=로컬 Git For Windows 환경에 액세스\nmsys2.displayName=MSYS2\nmsys2.displayDescription=MSYS2 환경의 셸 액세스\ncygwin.displayName=Cygwin\ncygwin.displayDescription=Cygwin 환경의 셸 액세스\nnamespace=네임스페이스\ngitVaultIdentityStrategy=Git SSH ID\ngitVaultIdentityStrategyDescription=SSH git URL을 원격으로 사용하도록 선택했고 원격 리포지토리에 SSH ID가 필요한 경우 이 옵션을 설정합니다.\\n\\nHTTP URL을 제공한 경우에는 이 옵션을 무시할 수 있습니다.\ndockerContainers=도커 컨테이너\ndockerCmd.displayName=도커 CLI 클라이언트\ndockerCmd.displayDescription=도커 CLI 클라이언트를 통해 도커 컨테이너 액세스\nwslCmd.displayName=WSL 설치\nwslCmd.displayDescription=Wsl CLI 클라이언트를 통해 WSL 인스턴스 액세스\nk8sCmd.displayName=쿠벡틀 클라이언트\nk8sCmd.displayDescription=Kubectl을 통해 쿠버네티스 클러스터에 액세스하기\nk8sClusters=쿠버네티스 클러스터\nshells=사용 가능한 셸\ninspectContainer=검사\ninspectContext=검사\nk8sClusterNameDescription=클러스터가 속한 컨텍스트의 이름입니다.\npod=파드\npodName=파드 이름\nk8sClusterContext=컨텍스트\nk8sClusterContextDescription=클러스터가 속한 컨텍스트의 이름입니다\nk8sClusterNamespace=네임스페이스\nk8sClusterNamespaceDescription=사용자 지정 네임스페이스 또는 비어 있는 경우 기본 네임스페이스입니다\nk8sConfigLocation=구성 파일\nk8sConfigLocationDescription=사용자 정의 kubeconfig 파일 또는 비워둔 경우 기본 파일\ninspectPod=Inspect\nshowAllContainers=실행되지 않는 컨테이너 표시\nshowAllPods=실행 중이 아닌 파드 표시\nk8sPodHostDescription=파드가 위치한 호스트\nk8sContainerDescription=쿠버네티스 컨테이너의 이름\nk8sPodDescription=쿠버네티스 파드의 이름\npodDescription=컨테이너가 위치한 포드\nk8sClusterHostDescription=클러스터에 액세스해야 하는 호스트. 클러스터에 액세스할 수 있도록 kubectl이 설치 및 구성되어 있어야 한다.\nconnection=연결\nshellCommand.displayName=사용자 지정 셸 명령\nshellCommand.displayDescription=사용자 지정 명령을 통해 표준 셸 열기\nssh.displayName=SSH 연결\nssh.displayDescription=SSH 명령줄 클라이언트를 통해 원격 시스템에 연결합니다\nsshConfig.displayName=SSH 구성 파일\nsshConfig.displayDescription=SSH 구성 파일에 정의된 호스트에 연결합니다\nsshConfigHost.displayName=SSH 구성 파일 호스트\nsshConfigHost.displayDescription=SSH 구성 파일에 정의된 호스트에 연결합니다\nsshConfigHost.password=비밀번호\nsshConfigHost.passwordDescription=사용자 로그인을 위한 비밀번호(선택 사항)를 입력합니다.\nsshConfigHost.identityPassphrase=키 암호 문구\nsshConfigHost.identityPassphraseDescription=키에 대한 선택적 암호 문구를 입력합니다.\nshellCommand.hostDescription=명령을 실행할 호스트\nshellCommand.commandDescription=셸을 여는 명령\ncommandType=명령 유형\ncommandTypeDescription=명령을 실행하는 방법\ncommandDescription=호스트에서 실행할 사용자 지정 명령\ncommandHostDescription=명령을 실행할 호스트\ncommandDataFlowDescription=이 명령이 입력 및 출력을 처리하는 방법\ncommandElevationDescription=상승된 권한으로 이 명령을 실행합니다\ncommandShellTypeDescription=이 명령에 사용할 셸\nlimitedSystem=제한된 시스템 또는 임베디드 시스템입니다\nlimitedSystemDescription=제한된 임베디드 시스템 또는 IOT 장치에 필요한 셸 유형을 식별하려고 하지 마십시오\nsshForwardX11=포워드 X11\nsshForwardX11Description=연결에 X11 포워딩을 사용하도록 설정합니다\ncustomAgent=사용자 지정 상담원\nidentityAgent=ID 에이전트\nssh.proxyDescription=SSH 연결을 설정할 때 사용할 프록시 호스트(선택 사항)입니다. SSH 클라이언트가 설치되어 있어야 합니다.\nusage=사용법\nwslHostDescription=WSL 인스턴스가 위치한 호스트입니다. WSL이 설치되어 있어야 합니다.\nwslDistributionDescription=WSL 인스턴스의 이름\nwslUsernameDescription=로그인할 명시적인 사용자 이름입니다. 지정하지 않으면 기본 사용자 아이디가 사용됩니다.\nwslPasswordDescription=Sudo 명령에 사용할 수 있는 사용자의 비밀번호입니다.\ndockerHostDescription=도커 컨테이너가 위치한 호스트입니다. 도커가 설치되어 있어야 합니다.\ndockerContainerDescription=도커 컨테이너의 이름\nlocalMachine=로컬 컴퓨터\nrootScan=수두 셸 환경\nloginEnvironmentScan=사용자 지정 로그인 환경\nk8sScan=쿠버네티스 클러스터\noptions=옵션\ndockerRunningScan=도커 컨테이너 실행\ndockerAllScan=모든 도커 컨테이너\nwslScan=WSL 인스턴스\nsshScan=SSH 구성 연결\nrunAsUser=사용자로 실행\nrunAsUserDescription=이 셸 환경을 다른 사용자로 시작\ndefault=기본값\nadministrator=관리자\nwslHost=WSL 호스트\ntimeout=시간 초과\ninstallLocation=설치 위치\ninstallLocationDescription=$NAME$ 환경이 설치된 위치\nwsl.displayName=Linux용 Windows 하위 시스템\nwsl.displayDescription=Windows에서 실행 중인 WSL 인스턴스에 연결\ndocker.displayName=도커 컨테이너\ndocker.displayDescription=도커 컨테이너에 연결\nport=포트\nuser=사용자\npassword=비밀번호\nmethod=메서드\nuri=URL\nproxy=프록시\ndistribution=배포\nusername=사용자 이름\nshellType=셸 유형\nbrowseFile=파일 찾아보기\nopenShell=터미널의 셸 열기\nopenCommand=터미널에서 명령 실행\neditFile=파일 수정\ndescription=설명\nfurtherCustomization=추가 사용자 지정\nfurtherCustomizationDescription=더 많은 구성 옵션을 보려면 ssh 구성 파일을 사용하세요\nbrowse=찾아보기\nconfigHost=호스트\nconfigHostDescription=구성이 위치한 호스트\nconfigLocation=구성 위치\nconfigLocationDescription=설정 파일의 파일 경로\ngateway=게이트웨이\ngatewayDescription=연결할 때 사용할 게이트웨이(선택 사항)입니다\nconnectionInformation=연결 정보\nconnectionInformationDescription=연결할 시스템\npasswordAuthentication=비밀번호 인증\npasswordAuthenticationDescription=인증에 사용할 비밀번호(선택 사항)\nsshConfigString.displayName=구성 기반 SSH 연결\nsshConfigString.displayDescription=SSH 구성 형식으로 완전히 사용자 지정한 SSH 연결 만들기\nsshConfigStringContent=구성\nsshConfigStringContentDescription=OpenSSH 구성 형식의 연결에 대한 SSH 옵션\nvnc.displayName=SSH를 통한 VNC 연결\nvnc.displayDescription=터널링된 연결을 통해 VNC 세션 열기\nbinding=바인딩\nvncPortDescription=VNC 서버가 수신 대기 중인 포트\nrdpPortDescription=RDP 서버가 수신 대기 중인 포트\nvncUsername=사용자 이름\nvncUsernameDescription=선택적 VNC 사용자 이름\nvncPassword=비밀번호\nvncPasswordDescription=VNC 비밀번호\nx11WslInstance=X11 포워드 WSL 인스턴스\nx11WslInstanceDescription=SSH 연결에서 X11 포워딩을 사용할 때 X11 서버로 사용할 로컬 Linux용 Windows 하위 시스템 배포판입니다. 이 배포는 WSL2 배포여야 합니다.\nopenAsRoot=루트로 열기\nopenInWSL=WSL에서 열기\nlaunch=Launch\nsshTrustKeyContent=호스트 키를 알 수 없으며 수동 호스트 키 확인을 사용 설정했습니다. $CONTENT$\nsshTrustKeyTitle=알 수 없는 호스트 키\nrdpTunnel.displayName=SSH를 통한 RDP 연결\nrdpTunnel.displayDescription=터널 연결을 통해 RDP를 통한 연결\nrdpEnableDesktopIntegration=데스크톱 통합 사용\nrdpEnableDesktopIntegrationDescription=RDP 허용 목록에서 다음을 허용한다고 가정하고 원격 응용 프로그램을 실행합니다\nrdpSetupAdminTitle=RDP 설정 필요\nrdpSetupAllowTitle=RDP 원격 애플리케이션\nrdpSetupAllowContent=현재 이 시스템에서는 원격 애플리케이션을 직접 시작하는 것이 허용되지 않습니다. 이 기능을 사용하시겠습니까? RDP 원격 애플리케이션에 대한 허용 목록을 비활성화하면 XPipe에서 직접 원격 애플리케이션을 실행할 수 있습니다.\nrdpServerEnableTitle=RDP 서버\nrdpServerEnableContent=대상 시스템에서 RDP 서버가 비활성화되어 있습니다. 원격 RDP 연결을 허용하려면 레지스트리에서 사용하도록 설정하시겠습니까?\nrdp=RDP\nrdpScan=SSH를 통한 RDP 터널\nwslX11SetupTitle=WSL X11 설정\nwslX11SetupContent=XPipe는 로컬 WSL 배포를 사용하여 X11 디스플레이 서버로 작동할 수 있습니다. $DIST$ 에서 X11을 설정하시겠습니까? 이렇게 하면 WSL 배포판에 기본 X11 패키지가 설치되며 시간이 걸릴 수 있습니다. 설정 메뉴에서 사용할 배포판을 변경할 수도 있습니다.\ncommand=명령\ncommandGroup=명령 그룹\nvncSystem=VNC 대상 시스템\nvncSystemDescription=상호 작용할 실제 시스템입니다. 일반적으로 터널 호스트와 동일합니다\nvncHost=대상 VNC 호스트\nvncHostDescription=VNC 서버가 실행 중인 시스템\nvncDirectHost=호스트\nvncDirectHostDescription=VNC 서버가 실행 중인 서버의 호스트 항목 또는 수동 주소입니다\nrdpDirectHost=호스트\nrdpDirectHostDescription=RDP 서버가 실행 중인 서버의 호스트 항목 또는 수동 주소입니다\ngitVaultTitle=Git 볼트\ngitVaultForcePushContent=원격 리포지토리로 강제 푸시를 하시겠습니까? 그러면 히스토리를 포함한 모든 원격 리포지토리 콘텐츠가 로컬 리포지토리로 완전히 바뀝니다.\ngitVaultOverwriteLocalContent=로컬 볼트 변경 내용을 덮어쓰시겠습니까? 그러면 모든 원격 변경 내용이 로컬 리포지토리에 적용됩니다.\nrdpSimple.displayName=직접 RDP 연결\nrdpSimple.displayDescription=RDP를 통해 호스트에 연결\nrdpUsername=사용자 이름\nrdpUsernameDescription=로그인할 사용자입니다. 도메인 접두사를 포함할 수 있습니다\naddressDescription=연결할 위치\nrdpAdditionalOptions=추가 RDP 옵션\nrdpAdditionalOptionsDescription=포함할 원시 RDP 옵션(.rdp 파일과 동일한 형식)\nproxmoxVncConfirmTitle=VNC 액세스\nproxmoxVncConfirmContent=VM에 대해 VNC 액세스를 사용하도록 설정하시겠습니까? 이렇게 하면 VM 구성 파일에서 직접 VNC 클라이언트 액세스가 활성화되고 가상 머신이 다시 시작됩니다.\ndockerContext.displayName=도커 컨텍스트\ndockerContext.displayDescription=특정 컨텍스트에 있는 컨테이너와 상호 작용하기\nvmActions=VM 작업\ndockerContextActions=컨텍스트 작업\nk8sPodActions=파드 액션\nopenVnc=VNC 액세스 사용\naddVnc=VNC 연결 추가\ncommandGroup.displayName=명령 그룹\ncommandGroup.displayDescription=시스템에서 사용 가능한 명령 그룹\nserial.displayName=직렬 연결\nserial.displayDescription=터미널에서 직렬 연결 열기\nserialPort=직렬 포트\nserialPortDescription=연결할 직렬 포트/장치\nbaudRate=전송 속도\ndataBits=데이터 비트\nstopBits=정지 비트\nparity=패리티\nflowControlWindow=흐름 제어\nserialImplementation=직렬 구현\nserialImplementationDescription=직렬 포트에 연결하는 데 사용할 도구\nserialHost=호스트\nserialHostDescription=직렬 포트에 액세스할 시스템\nserialPortConfiguration=직렬 포트 구성\nserialPortConfigurationDescription=연결된 직렬 장치의 구성 매개변수\nserialInformation=일련 정보\nopenXShell=XShell에서 열기\ntsh.displayName=텔레포트\ntsh.displayDescription=Tsh를 통해 텔레포트 노드에 연결합니다\ntshNode.displayName=텔레포트 노드\ntshNode.displayDescription=클러스터의 텔레포트 노드에 연결한다\nteleportCluster=클러스터\nteleportClusterDescription=노드가 속한 클러스터\nteleportProxy=프록시\nteleportProxyDescription=노드에 연결하는 데 사용되는 프록시 서버\nteleportHost=호스트\nteleportHostDescription=노드의 호스트 이름\nteleportUser=사용자\nteleportUserDescription=로그인할 사용자\nlogin=로그인\nhyperVInstall.displayName=Hyper-V\nhyperVInstall.displayDescription=Hyper-V에서 관리하는 VM에 연결\nhyperVVm.displayName=Hyper-V VM\nhyperVVm.displayDescription=SSH 또는 PSSession을 통해 Hyper-V VM에 연결합니다\ntrustHost=신뢰 호스트\ntrustHostDescription=신뢰할 수 있는 호스트 목록에 컴퓨터 이름 추가\ncopyIp=IP 복사\nvncDirect.displayName=VNC 직접 연결\nvncDirect.displayDescription=VNC를 통해 시스템에 직접 연결\neditConfiguration=구성 편집\nviewInDashboard=대시보드에서 보기\nsetDefault=기본값 설정\nremoveDefault=기본값 제거\nconnectAsOtherUser=다른 사용자로 연결\nprovideUsername=로그인할 대체 사용자 이름 입력\nvmIdentity=게스트 ID\nvmIdentityDescription=필요한 경우 연결에 사용할 SSH 신원 인증 방법입니다\nvmPort=포트\nvmPortDescription=SSH를 통해 연결할 포트\nforwardAgent=전달 에이전트\nforwardAgentDescription=원격 시스템에서 SSH 에이전트 ID를 사용할 수 있도록 설정하기\nvirshUri=URI\nvirshUriDescription=하이퍼바이저 URI, 별칭도 지원됩니다\nvirshDomain.displayName=libvirt 도메인\nvirshDomain.displayDescription=Libvirt 도메인에 연결\nvirshHypervisor.displayName=libvirt 하이퍼바이저\nvirshHypervisor.displayDescription=Libvirt 지원 하이퍼바이저 드라이버에 연결합니다\nvirshInstall.displayName=libvirt 명령줄 클라이언트\nvirshInstall.displayDescription=Virsh를 통해 사용 가능한 모든 libvirt 하이퍼바이저에 연결합니다\naddHypervisor=하이퍼바이저 추가\ninteractiveTerminal=대화형 터미널\neditDomain=도메인 편집\nlibvirt=libvirt 도메인\ncustomIp=사용자 지정 IP\ncustomIpDescription=고급 네트워킹을 사용하는 경우 기본 로컬 VM IP 감지를 재정의합니다\nautomaticallyDetect=자동 감지\nuserAddDialogTitle=사용자 생성\ngroupAddDialogTitle=그룹 생성\npassphrase=암호 구문\nrepeatPassphrase=반복 암호 구문\ngroupSecret=그룹 비밀\nrepeatGroupSecret=그룹 비밀 반복\nvaultGroup=볼트 그룹\nloginAlertTitle=로그인 필요\nloginAlertHeader=개인 연결에 액세스할 수 있는 볼트 잠금 해제\nvaultUser=Vault 사용자\nme=Me\naddGroup=그룹 추가 ...\naddGroupDescription=이 금고에 대한 새 그룹 만들기\naddUser=사용자 추가 ...\n#custom\naddUserDescription=이 저장소의 새 사용자 만들기\nskip=건너뛰기\nuserChangePasswordAlertTitle=비밀번호 변경\ngroupChangeSecretAlertTitle=비밀 변경\ndocs=문서\nlxd.displayName=LXD 컨테이너\nlxd.displayDescription=Lxc를 통해 LXD 컨테이너에 연결합니다\nlxdCmd.displayName=LXD CLI 클라이언트\nlxdCmd.displayDescription=Lxc CLI 클라이언트를 통해 LXD 컨테이너 액세스\npodman.displayName=포드맨 컨테이너\npodman.displayDescription=Podman 컨테이너에 연결\nincusInstall.displayName=인커스 머신 관리자\nincusInstall.displayDescription=Incus CLI 클라이언트를 통해 incus 컨테이너에 액세스합니다\nincusContainer.displayName=인커스 컨테이너\nincusContainer.displayDescription=인커스 컨테이너에 연결\npodmanCmd.displayName=Podman CLI\npodmanCmd.displayDescription=CLI 클라이언트를 통한 Podman 컨테이너 액세스\nlxdHostDescription=LXD 컨테이너가 위치한 호스트입니다. LXC가 설치되어 있어야 합니다.\nlxdContainerDescription=LXD 컨테이너의 이름\npodmanContainers=Podman 컨테이너\nlxdContainers=LXD 컨테이너\nincusContainers=인커스 컨테이너\ncontainer=컨테이너\nhost=호스트\ncontainerActions=컨테이너 작업\nserialConsole=직렬 콘솔\neditRunConfiguration=실행 구성 편집\ncommunityDescription=개인 사용 사례에 적합한 연결 파워 도구.\nupgradeDescription=전체 서버 인프라에 대한 전문적인 연결 관리.\ndiscoverPlans=업그레이드 옵션 알아보기\nextendProfessional=최신 전문가 기능으로 업그레이드\ncommunityItem1=비상업적 시스템 및 도구에 대한 무제한 연결\ncommunityItem2=설치된 단말기 및 편집기와의 원활한 통합\ncommunityItem3=모든 기능을 갖춘 원격 파일 브라우저\ncommunityItem4=모든 셸을 위한 강력한 스크립팅 시스템\ncommunityItem5=동기화 및 연결 정보 공유를 위한 Git 통합\nupgradeItem1=모든 커뮤니티 에디션 기능 포함\nupgradeItem2=홈랩 요금제는 무제한 하이퍼바이저 및 고급 SSH 기능을 지원합니다\nupgradeItem3=Professional 요금제는 엔터프라이즈 운영 체제 및 도구를 추가로 지원합니다\nupgradeItem4=Enterprise 요금제는 개별 사용 사례를 위한 완전한 유연성을 제공합니다\nupgrade=업그레이드\nupgradeTitle=사용 가능한 요금제\nstatus=상태\ntype=유형\nlicenseAlertTitle=라이선스 필요\nuseCommunity=커뮤니티 계속하기\npreviewDescription=출시 후 몇 주 동안 새로운 기능을 사용해 보세요.\ntryPreview=미리 보기 활성화\npreviewItem1=출시 후 2주간 새로 출시된 전문가 기능에 대한 전체 액세스 권한 부여\npreviewItem2=약정 없이 새로운 기능을 사용해 보세요\nlicensedTo=라이선스 대상\nemail=이메일 주소\napply=적용\nclear=지우기\nactivate=활성화\nvalidUntil=다음까지 유효합니다\nlicenseActivated=활성화된 라이선스\nrestart=다시 시작\n#custom\nlockVault=저장소 잠금\nrestartApp=XPipe 다시 시작\nfree=무료\nupgradeInfo=아래에서 라이선스 업그레이드에 대한 정보를 확인할 수 있습니다.\nupgradeInfoPreview=아래에서 라이선스 업그레이드에 대한 정보를 확인하거나 미리 보기를 사용해 볼 수 있습니다.\nenterLicenseKey=업그레이드할 라이선스 키 입력\nisOnlySupported=는 $TYPE$ 라이선스 이상에서만 지원됩니다\nareOnlySupported=는 $TYPE$ 라이선스 이상에서만 지원됩니다\nlegacyLicense=이 라이선스에는 구매 후 1년 이내에 출시된 새로운 Professional 기능만 포함되어 있습니다.\npreviewExpiredLicense=이 기능은 최근 미리 보기에서 무료로 제공되었으나 현재 이 기간이 만료되었습니다.\nopenApiDocs=API 문서\nopenApiDocsDescription=HTTP API 문서는 OpenAPI .yaml 사양을 포함하여 온라인으로 제공됩니다. 웹 브라우저나 선호하는 HTTP 클라이언트에서 열 수 있습니다.\nopenApiDocsButton=문서 열기\npythonApi=Python API\npersonalConnection=이 연결 및 모든 하위 연결은 개인 ID에 의존하므로 사용자만 사용할 수 있습니다.\ndeveloperPrintInitFiles=초기화 파일 실행 인쇄\ndeveloperPrintInitFilesDescription=터미널이 시작될 때 실행되는 모든 셸 초기화 스크립트를 인쇄합니다.\ndeveloperShowSensitiveCommands=민감한 명령 로그\ndeveloperShowSensitiveCommandsDescription=디버깅을 위해 로그 출력에 민감한 명령을 포함합니다.\ncheckingForUpdates=업데이트 확인\ncheckingForUpdatesDescription=최신 릴리스 정보 가져오기\ndownloadingUpdate=릴리스 검색(버전 $VERSION$)\ndownloadingUpdateDescription=릴리스 패키지 다운로드\nupdateNag=XPipe를 한동안 업데이트하지 않았습니다. 최신 릴리스의 새로운 기능 및 수정 사항을 놓치고 있을 수 있습니다.\nupdateNagTitle=업데이트 알림\nupdateNagButton=릴리스 보기\n#custom\nrefreshServices=서비스 새로고침\nserviceProtocolType=서비스 프로토콜 유형\nserviceProtocolTypeDescription=서비스 열기 방법 제어\nserviceCommand=서비스가 활성화되면 실행할 명령\nserviceCommandDescription=플레이스홀더 $PORT는 실제 터널링된 로컬 포트로 대체됩니다\nvalue=가치\nshowAdvancedOptions=고급 옵션 표시\nsshAdditionalConfigOptions=추가 구성 옵션\nremoteFileManager=원격 파일 관리자\nclearUserData=사용자 데이터 삭제\nclearUserDataDescription=연결을 포함한 모든 사용자 구성 데이터 삭제\nclearUserDataTitle=사용자 데이터 삭제\nclearUserDataContent=이렇게 하면 xpipe에 대한 모든 로컬 사용자 데이터가 삭제되고 다시 시작됩니다. 연결이 중요한 경우 먼저 git 리포지토리와 동기화해야 합니다.\nundefined=정의되지 않음\ncopyAddress=주소 복사\nnetbirdDeviceScan=Netbird 연결\nnetbirdId=피어 공개 키\nnetbirdIdDescription=피어의 내부 넷버드 공개 키 ID입니다\ntailscaleDeviceScan=테일스케일 연결\ntailscaleInstall.displayName=테일스케일 설치\ntailscaleInstall.displayDescription=SSH를 통해 테일넷의 장치에 연결합니다\ntailscaleDevice.displayName=테일스케일 장치\ntailscaleDevice.displayDescription=SSH를 통해 테일넷의 장치에 연결합니다\ntailscaleId=장치 ID\ntailscaleIdDescription=내부 테일스케일 장치 ID\ntailscaleHostName=호스트 이름\ntailscaleHostNameDescription=테일넷에 있는 장치의 호스트 이름입니다\ntailscaleUsername=사용자 이름\ntailscaleUsernameDescription=로그인할 사용자\ntailscalePassword=비밀번호\ntailscalePasswordDescription=Sudo에 사용할 수 있는 선택적 사용자 비밀번호입니다\nscriptName=스크립트 이름\nscriptNameDescription=이 스크립트에 사용자 지정 이름을 지정합니다\nscriptGroupName=스크립트 그룹 이름\nscriptGroupNameDescription=이 스크립트 그룹에 사용자 지정 이름을 지정합니다\nidentityName=ID 이름\nidentityNameDescription=이 ID에 사용자 지정 이름을 지정합니다\ntailscaleTailnet.displayName=Tailnet\ntailscaleTailnet.displayDescription=계정으로 특정 테일넷에 연결\nputtyConnections=PuTTY 연결\nkittyConnections=KiTTY 연결\nicons=아이콘\ncustomIcons=사용자 지정 아이콘\niconSources=아이콘 소스\niconSourcesDescription=여기에서 아이콘 소스를 직접 추가할 수 있습니다. XPipe는 추가된 위치에서 .svg 파일을 선택하여 사용 가능한 아이콘 세트에 추가합니다.\\n\\n로컬 디렉터리와 원격 git 리포지토리 모두 아이콘 위치로 지원됩니다.\n#custom\nrefreshSources=새로고침 아이콘\nrefreshSourcesDescription=사용 가능한 소스에서 모든 아이콘 업데이트\naddDirectoryIconSource=디렉토리 소스 추가 ...\naddDirectoryIconSourceDescription=로컬 디렉터리에서 아이콘 추가\naddGitIconSource=Git 소스 추가 ...\naddGitIconSourceDescription=원격 git 리포지토리에 있는 아이콘을 추가합니다\nrepositoryUrl=Git 리포지토리 URL\niconDirectory=아이콘 디렉토리\naddUnsupportedKexMethod=지원되지 않는 키 교환 방법 추가\naddUnsupportedKexMethodDescription=이 연결에 키 교환 방법 $VAL$ 을 사용하도록 허용합니다\naddUnsupportedHostKeyType=지원되지 않는 호스트 키 유형 추가\naddUnsupportedHostKeyTypeDescription=이 연결에 호스트 키 유형 $VAL$ 을 사용하도록 허용합니다\naddUnsupportedMacType=지원되지 않는 MAC 유형 추가\naddUnsupportedMacTypeDescription=이 연결에 MAC 유형 $VAL$ 을 사용하도록 허용합니다\nrunSilent=백그라운드에서 조용히\nrunInFileBrowser=파일 브라우저의 설명 텍스트\nrunInConnectionHub=연결 허브의\ncommandOutput=명령 출력\niconSourceDeletionTitle=아이콘 소스 삭제\niconSourceDeletionContent=이 아이콘 소스 및 이와 관련된 모든 아이콘을 삭제하시겠습니까?\n#custom\nrefreshIcons=새로고침 아이콘\nrefreshIconsDescription=외부 소스에서 사용 가능한 모든 1000개 이상의 아이콘을 .png 파일로 검색, 렌더링 및 캐싱합니다. 시간이 좀 걸릴 수 있습니다...\n#custom\nvaultUserLegacy=Vault 사용자 (레거시 호환성 모드 제한)\n#custom\nupgradeInstructions=업그레이드 방법\nexternalActionTitle=외부 작업 요청\nexternalActionContent=외부 작업이 요청되었습니다. XPipe 외부에서 작업을 시작하도록 허용하시겠습니까?\n#custom\nnoScriptStateAvailable=스크립트 호환성을 확인하려면 새로고침 ...\ndocumentationDescription=문서 확인\ncustomEditorCommandInTerminal=터미널에서 사용자 지정 명령 실행\ncustomEditorCommandInTerminalDescription=편집기가 터미널 기반인 경우 이 옵션을 활성화하면 터미널을 자동으로 열고 대신 터미널 세션에서 명령을 실행할 수 있습니다.\\n\\nVi, vim, nvim 등의 편집기에 이 옵션을 사용할 수 있습니다.\ndisableHttpsTlsCheck=HTTPS 요청 인증서 확인 사용 안 함\ndisableHttpsTlsCheckDescription=조직에서 SSL 차단을 사용하여 방화벽에서 HTTPS 트래픽을 해독하는 경우, 인증서가 일치하지 않아 업데이트 확인 또는 라이선스 확인이 실패합니다. 이 옵션을 활성화하고 TLS 인증서 유효성 검사를 비활성화하면 이 문제를 해결할 수 있습니다.\nconnectionsSelected=$NUMBER$ 선택된 연결\naddConnections=연결 추가\nbrowseDirectory=디렉토리 찾아보기\nopenTerminal=터미널 열기\ndocumentation=문서\nreport=오류 보고\nkeePassXcNotAssociated=KeePassXC 링크\nkeePassXcNotAssociatedDescription=XPipe가 로컬 KeePassXC 데이터베이스와 연결되어 있지 않습니다. 아래를 클릭하여 XPipe가 비밀번호를 쿼리할 수 있도록 XPipe를 KeePassXC 데이터베이스와 연결하는 한 번의 단계를 수행하세요.\nkeePassXcAssociateMore=더 많은 데이터베이스 연결\nkeePassXcAssociateMoreDescription=여러 개의 KeePassXC 데이터베이스에 동시에 연결할 수 있습니다\nkeePassXcAssociated=KeePassXC 링크\nkeePassXcAssociatedDescription=XPipe는 다음 로컬 KeePassXC 데이터베이스에 연결됩니다:\nkeePassXcNotAssociatedButton=링크 데이터베이스\nidentifier=식별자\npasswordManagerCommand=사용자 지정 명령\npasswordManagerCommandDescription=비밀번호를 가져오기 위해 실행할 사용자 지정 명령입니다. 플레이스홀더 문자열 $KEY는 호출 시 따옴표로 묶인 비밀번호 키로 바뀝니다. 이 명령은 비밀번호 관리자 CLI를 호출하여 비밀번호를 stdout에 출력합니다(예: mypassmgr get $KEY).\nchooseTemplate=템플릿 선택\nkeePassXcPlaceholder=KeePassXC 항목 URL\nterminalEnvironment=터미널 환경\nterminalEnvironmentDescription=터미널 사용자 지정에 로컬 Linux 기반 WSL 환경의 기능을 사용하려는 경우 이를 터미널 환경으로 사용할 수 있습니다.\\n\\n그러면 모든 사용자 지정 터미널 초기화 명령 및 터미널 멀티플렉서 구성이 이 WSL 배포에서 실행됩니다.\nterminalInitScript=터미널 초기화 스크립트\nterminalInitScriptDescription=연결이 시작되기 전에 터미널 환경에서 실행할 명령입니다. 시작 시 터미널 환경을 구성하는 데 사용할 수 있습니다.\nterminalMultiplexer=터미널 멀티플렉서\nterminalMultiplexerDescription=터미널에서 탭 대신 사용할 터미널 멀티플렉서입니다. 탭 처리와 같은 특정 터미널 처리 특성을 멀티플렉서 기능으로 대체합니다.\\n\\n시스템에 해당 멀티플렉서 실행 파일을 설치해야 합니다.\nterminalMultiplexerWindowsDescription=터미널에서 탭 대신 사용할 터미널 멀티플렉서입니다. 탭 처리와 같은 특정 터미널 처리 특성을 멀티플렉서 기능으로 대체합니다.\\n\\nWindows에서 WSL 터미널 환경을 사용해야 하며 멀티플렉서 실행 파일이 WSL 시스템에 설치되어 있어야 합니다.\nterminalAlwaysPauseOnExit=종료 시 항상 일시 중지\nterminalAlwaysPauseOnExitDescription=활성화하면 터미널 세션을 종료할 때 항상 세션을 다시 시작하거나 닫으라는 메시지가 표시됩니다. 비활성화하면 XPipe는 오류와 함께 종료되는 실패한 연결에 대해서만 메시지를 표시합니다.\nquerying=쿼리 ...\n#custom\nretrievedPassword=비밀번호: $PASSWORD$\n#custom\nrefreshOpenpubkey=Openpubkey ID 새로고침\n#custom\nrefreshOpenpubkeyDescription=Opkssh 새로고침을 실행하여 Openpubkey ID를 다시 유효하게 만듭니다\nall=모두\nterminalPrompt=터미널 프롬프트\nterminalPromptDescription=원격 터미널에서 사용할 터미널 프롬프트 도구입니다. 터미널 프롬프트를 활성화하면 터미널 세션을 열 때 대상 시스템에서 프롬프트 도구가 자동으로 설정 및 구성됩니다.\\n\\n이 경우 시스템의 기존 프롬프트 구성이나 프로필 파일은 수정되지 않습니다. 이렇게 하면 원격 시스템에서 프롬프트를 설정하는 동안 처음으로 터미널을 로딩하는 시간이 늘어납니다. 프롬프트를 올바르게 표시하려면 터미널에 추가 글꼴이 필요할 수 있습니다.\nterminalPromptConfiguration=터미널 프롬프트 구성\nterminalPromptConfig=구성 파일\nterminalPromptConfigDescription=프롬프트에 적용할 사용자 지정 구성 파일입니다. 이 구성은 터미널이 초기화될 때 대상 시스템에서 자동으로 설정되며 기본 프롬프트 구성으로 사용됩니다.\\n\\n각 시스템에서 기존 기본 구성 파일을 사용하려면 이 필드를 비워 두면 됩니다.\npasswordManagerKey=비밀번호 관리자 키\npasswordManagerKeyDescription=비밀의 비밀번호 관리자 식별자\npasswordManagerAgent=비밀번호 관리자 에이전트\ndockerComposeProject.displayName=도커 컴포즈 프로젝트\ndockerComposeProject.displayDescription=컴포즈 프로젝트의 컨테이너를 함께 그룹화\nsshVerboseOutput=자세한 SSH 출력 사용\nsshVerboseOutputDescription=SSH를 통해 연결할 때 많은 디버그 정보를 인쇄합니다. SSH 연결 관련 문제를 해결하는 데 유용합니다.\ndontUseGateway=게이트웨이 사용 안 함\ndontUseGatewayDescription=하이퍼바이저 호스트를 게이트웨이로 사용하지 말고 IP에 직접 연결하세요\ncategoryColor=카테고리 색상\ncategoryColorDescription=이 카테고리 내의 연결에 사용할 기본 색상입니다\ncategorySync=Git 리포지토리와 동기화\ncategorySyncDescription=모든 연결을 git 리포지토리와 자동으로 동기화합니다. 연결에 대한 모든 로컬 변경사항이 원격으로 푸시됩니다.\ncategorySyncSpecial=Git 리포지토리와 동기화\\n(특수 카테고리 \"$NAME$\"에는 구성할 수 없음)\ncategoryDontAllowScripts=모든 수정 사항 비활성화\ncategoryDontAllowScriptsDescription=이 범주에 속하는 시스템에서 명령 실행 및 기타 작업을 비활성화하여 수정을 방지합니다. 이렇게 하면 모든 스크립팅 기능, 셸 환경 명령, 프롬프트 등이 비활성화됩니다.\ncategoryConfirmAllModifications=모든 수정 사항 확인\ncategoryConfirmAllModificationsDescription=연결 또는 파일 시스템에 대한 모든 종류의 수정 사항을 먼저 확인하세요. 이렇게 하면 중요한 시스템에서 실수로 작동하는 것을 방지할 수 있습니다.\ncategoryDefaultIdentity=기본 ID\ncategoryDefaultIdentityDescription=이 카테고리에 속한 많은 시스템에서 특정 ID를 자주 사용하는 경우 기본 ID를 설정하면 새 연결을 만들 때 미리 선택할 수 있습니다.\ncategoryConfigTitle=$NAME$ 구성\nconfigure=구성\naddConnection=연결 추가\nnoCompatibleConnection=호환되는 연결을 찾을 수 없습니다\nnoCompatibleIdentity=호환되는 ID를 찾을 수 없습니다\nnewCategory=새 카테고리\ndockerComposeRestricted=이 작성 프로젝트는 $NAME$ 에 의해 제한되며 외부에서 수정할 수 없습니다. 이 작성 프로젝트를 관리하려면 $NAME$ 을 사용하세요.\nrestricted=제한됨\ndisableSshPinCaching=SSH PIN 캐싱 비활성화\ndisableSshPinCachingDescription=XPipe는 어떤 형태의 하드웨어 기반 인증을 사용할 때 키에 입력한 모든 PIN을 자동으로 캐시합니다.\\n\\n이 기능을 비활성화하면 연결을 시도할 때마다 PIN을 다시 입력해야 합니다.\n#custom\ngitSyncPull=원격 Git 변경 사항 동기화 (pull)\n#custom\nenpassVaultFile=저장소 파일\n#custom\nenpassVaultFileDescription=로컬 Enpass 저장소 파일입니다.\n#custom\nflat=이 파일에만 적용\n#custom\nrecursive=하위 폴더에도 적용\nrdpAllowListBlocked=선택한 RemoteApp이 서버의 RDP 허용 목록에 포함되어 있지 않은 것 같습니다.\npsonoServerUrl=서버 URL\npsonoServerUrlDescription=Psono 백엔드 서버의 URL\npsonoApiKey=API 키\npsonoApiKeyDescription=사용할 API 키(uuid 형식)입니다\npsonoApiSecretKey=API 비밀 키\npsonoApiSecretKeyDescription=64바이트 16진수 문자열의 API 비밀 키\npassboltServerUrl=서버 URL\npassboltServerUrlDescription=패스볼트 백엔드 서버의 URL\npassboltPassphrase=암호 구문\npassboltPassphraseDescription=금고 개인 키의 암호 문구입니다\npassboltPrivateKey=개인 키\npassboltPrivateKeyDescription=금고의 비공개 GPG 키 파일\nfocusWindowOnNotifications=알림의 초점 창\nfocusWindowOnNotificationsDescription=연결 또는 터널이 예기치 않게 종료되는 경우와 같이 알림 또는 오류 메시지가 표시될 때 XPipe를 전경으로 가져옵니다.\ngitUsername=사용자 지정 git 사용자 이름\ngitUsernameDescription=Git 원격 리포지토리에 인증할 사용자 지정 사용자. 기본적으로 XPipe는 현재 구성된 git CLI의 자격 증명을 사용합니다.\\n\\n이 설정은 로컬 git CLI 클라이언트에 대해 이미 구성된 모든 기본 자격 증명을 재정의합니다.\ngitPassword=사용자 지정 git 비밀번호/개인 액세스 토큰\ngitPasswordDescription=인증에 사용할 비밀번호 또는 개인 액세스 토큰입니다. 비밀번호 또는 개인 액세스 토큰이 필요한지 여부는 git 원격 공급자에 따라 다릅니다. 이 설정은 로컬 git CLI 클라이언트에 대해 이미 구성된 모든 기본 자격 증명을 재정의합니다.\nsetReadOnly=읽기 전용 설정\nunsetReadOnly=읽기 전용으로 설정 해제\nreadOnlyStoreError=이 항목의 구성이 고정되었습니다. 다른 이름을 선택하여 변경 내용을 새 사본에 저장하세요.\ncategoryFreeze=연결 구성 고정\ncategoryFreezeDescription=연결 구성을 읽기 전용으로 표시합니다. 즉, 이 카테고리의 기존 연결 항목 구성은 수정할 수 없습니다. 하지만 새 연결은 추가할 수 있습니다.\nupdateFail=업데이트 설치에 성공하지 못했습니다\nupdateFailAction=수동으로 업데이트 설치\nupdateFailActionDescription=GitHub에서 최신 릴리스를 확인하세요\nonePasswordPlaceholder=항목 이름 또는 op:// URL\n#custom\ncomputeDirectorySizes=폴더 용량 계산\n#custom\ncomputeSize=용량 계산\ncustomSpiceCommand=사용자 지정 명령\ncustomSpiceCommandDescription=SPICE 세션을 시작하기 위해 실행할 사용자 지정 명령입니다. 플레이스홀더 문자열 $FILE은 호출 시 .vv 파일의 따옴표로 묶인 파일 경로로 바뀝니다.\nvncClient=VNC 클라이언트\nvncClientDescription=XPipe에서 VNC 연결을 열 때 실행할 VNC 클라이언트입니다.\\n\\nXPipe 내에서 통합된 VNC 클라이언트를 사용하거나 더 많은 사용자 지정을 원하는 경우 로컬에 설치된 외부 VNC 클라이언트를 실행하는 옵션이 있습니다.\nintegratedXPipeVncClient=통합 XPipe VNC 클라이언트\ncustomVncCommand=사용자 지정 명령\ncustomVncCommandDescription=VNC 세션을 시작하기 위해 실행할 사용자 지정 명령입니다. 플레이스홀더 문자열 $ADDRESS는 호출 시 따옴표로 묶인 주소로 바뀝니다.\nvncConnections=VNC 연결\npasswordManagerIdentity=비밀번호 관리자 ID\npasswordManagerIdentity.displayName=비밀번호 관리자 ID\npasswordManagerIdentity.displayDescription=비밀번호 관리자에서 아이디의 사용자 이름 및 비밀번호 검색\npasswordCopied=클립보드에 복사된 연결 비밀번호\nerrorOccurred=오류가 발생했습니다\nactionMacro.displayName=액션 매크로\nactionMacro.displayDescription=사용자 지정 트리거를 사용하여 실제 실행\nmacroAdd=매크로 추가\nmacroName=매크로 이름\nmacroNameDescription=이 매크로에 사용자 지정 이름을 지정합니다\nactionId=작업 ID\nactionIdDescription=이 매크로로 실행할 작업\nmacroRefs=연결된 연결\nmacroRefsDescription=작업을 실행할 연결\nconnectionCopy=복사\nactionPickerTitle=동작 선택\nactionPickerDescription=동작을 수행하려면 무언가를 클릭합니다. 작업을 실행하는 대신 작업 바로 가기 선택 모드에서 작업에 대한 바로 가기를 만들고 편집할 수 있습니다.\ncancelActionPicker=작업 선택 취소\nactionShortcut=작업 바로 가기\nactionShortcuts=작업 바로 가기\nactionStore=액션 스토어\nactionStoreDescription=작업을 실행할 스토어 항목\nactionStores=액션 스토어\nactionStoresDescription=작업을 실행할 스토어 항목\nactionDesktopShortcut=바탕 화면 바로 가기\nactionDesktopShortcutDescription=바탕화면에 이 작업에 대한 바로 가기를 만듭니다\nactionUrlShortcut=URL 바로 가기\nactionUrlShortcutDescription=열었을 때 이 작업을 트리거할 수 있는 URL을 복사합니다\nactionUrlShortcutDisabled=URL 바로 가기(사용할 수 없음)\nactionUrlShortcutDisabledDescription=$TYPE$ 설치 유형은 열기 URL을 지원하지 않습니다\nactionApiCall=API 요청\nactionApiCallDescription=HTTP API에서 이 작업을 호출합니다\nactionMacro=액션 매크로\nactionMacroDescription=이 작업에 대한 고급 기능이 있는 매크로 만들기\ncreateMacro=매크로 만들기\nactionConfiguration=매개변수\nactionConfigurationDescription=실행된 액션에 전달할 매개 변수\nconfirmAction=작업 확인\nactionConnections=동작 연결\nactionConnectionsDescription=작업을 실행할 연결\nactionConnection=동작 연결\nactionConnectionDescription=작업을 실행할 연결\nappleContainerInstall.displayName=애플 컨테이너\nappleContainerInstall.displayDescription=컨테이너 CLI를 통해 애플 컨테이너 인스턴스 액세스\nappleContainer.displayName=애플 컨테이너\nappleContainer.displayDescription=컨테이너 CLI를 통해 애플 컨테이너 인스턴스 액세스\nappleContainerHostDescription=사과 컨테이너가 위치한 호스트입니다\nappleContainerDescription=사과 컨테이너의 이름\nappleContainers=애플 컨테이너\nchangeOrderIndexTitle=순서 변경\norderIndex=색인\norderIndexDescription=다른 항목과 비교하여 이 항목의 순서를 지정하는 명시적 인덱스입니다. 가장 낮은 인덱스는 상단에, 가장 높은 인덱스는 하단에 표시됩니다\nmoveToFirst=첫 번째 항목으로 이동\nmoveToLast=마지막으로 이동\ncategory=카테고리\nincludeRoot=루트 포함\nexcludeRoot=루트 제외\nfreezeConfiguration=고정 구성\nunfreezeConfiguration=구성 고정 해제\nwaylandScalingTitle=웨이랜드 스케일링\nactionApiUrl=$URL$ (json 본문 복사)\ncopyBody=복사 요청 본문\ngitRepoTerminalOpen=터미널에서 열린 리포지토리\ngitRepoTerminalOpenDescription=명령줄을 사용하여 리포지토리를 직접 살펴보세요\ngitRepoOverwriteLocal=로컬 리포지토리 덮어쓰기\ngitRepoOverwriteLocalDescription=모든 로컬 변경 사항을 원격의 변경 사항으로 바꾸기\ngitRepoForcePush=원격 리포지토리 덮어쓰기\ngitRepoForcePushDescription=로컬 변경 사항을 원격에 적용하려면 git push --force를 사용한다\ngitRepoDontWarn=더 이상 경고하지 않음\ngitRepoDontWarnDescription=이 오류가 예상되는 경우 향후 XPipe에서 이 오류를 무시하도록 설정하세요\ngitRepoTryAgain=다시 시도\ngitRepoTryAgainDescription=동일한 작업을 다시 시도합니다\ngitRepoEnablePlain=일반 디렉토리 동기화 사용\ngitRepoEnablePlainDescription=변경 내용을 디렉터리에 동기화하기 위해 git 리포지토리를 초기화하지 않습니다\ngitRepoCreateBare=Git 동기화 사용\ngitRepoCreateBareDescription=동기화 디렉터리에서 새 베어 git 리포지토리를 초기화합니다\ngitRepoDisable=지금은 깃 볼트 비활성화\ngitRepoDisableDescription=이 세션 중에는 변경 내용을 커밋하지 마세요\n#custom\ngitRepoPullRefresh=변경 사항 가져오기 및 새로고침\ngitRepoPullRefreshDescription=원격 변경 사항 병합 및 데이터 다시 로드\nbreakOutCategory=브레이크아웃 카테고리\nmergeCategory=카테고리 병합\nopenWinScp=WinSCP에서 열기\nuninstallApplication=제거\nuninstallApplicationDescription=.pkg 설치 스크립트를 실행하여 XPipe를 완전히 제거합니다\nk8sEditPodTitle=변경 사항 적용\nk8sEditPodContent=Kubectl apply 명령을 통해 변경 사항을 적용하시겠습니까? 변경 사항을 적용하려면 재시작이 필요할 수 있다.\nvirshEditDomainTitle=변경 사항 적용\nvirshEditDomainContent=변경 사항을 도메인에 적용하시겠습니까? 변경 사항을 적용하려면 다시 시작해야 할 수 있습니다.\npkcs11Library=PKCS#11 라이브러리\npkcs11LibraryDescription=동적으로 연결된 라이브러리 파일의 경로\nsshAgentSocket=사용자 지정 SSH 에이전트 소켓\nsshAgentSocketDescription=SSH 에이전트와 통신하는 데 사용할 사용자 지정 소켓입니다. 이 사용자 지정 에이전트는 사용자 지정 에이전트 옵션을 선택하여 연결에 사용할 수 있습니다.\npublicKey=공개 키 식별자\npublicKeyDescription=에이전트가 일치하는 개인 키만 제공하도록 하기 위한 선택적 공개 키입니다\nactions=액션\nhcloudServer.displayName=헤츠너 클라우드 서버\nhcloudServer.displayDescription=SSH를 통해 헤츠너 클라우드에서 호스팅되는 서버에 액세스합니다\nhcloudInstall.displayName=헤츠너 클라우드 CLI\nhcloudInstall.displayDescription=Hcloud를 통해 헤츠너 클라우드에서 호스팅되는 서버에 액세스합니다\nhcloudContext.displayName=hcloud 컨텍스트\nhcloudContext.displayDescription=Hcloud 컨텍스트의 서버 액세스\nmetrics=메트릭\nopenInVsCode=VsCode에서 열기\naddCloud=클라우드 ...\nhcloudToken=hcloud 토큰\nhcloudTokenDescription=사용할 헤츠너 클라우드 토큰입니다. 자세한 내용은 다음 문서를 참조하세요\nhcloudLogin=헤츠너 클라우드 로그인\nclearHcloudToken=Hcloud 토큰 지우기\nclearHcloudTokenDescription=다시 로그인할 수 있도록 기존 토큰을 삭제합니다\nselectIdentity=ID 선택\nenableMcpServer=MCP 서버 사용\nenableMcpServerDescription=외부 MCP 클라이언트가 MCP 서버에 요청을 보낼 수 있도록 XPipe MCP 서버를 활성화합니다. 구성에 대한 자세한 내용은 아래를 참조하세요.\\n\\nMCP 기능을 위해 HTTP API를 활성화할 필요는 없습니다.\nenableMcpMutationTools=MCP 변이 도구 사용\nenableMcpMutationToolsDescription=기본적으로 MCP 서버에서는 읽기 전용 도구만 사용하도록 설정되어 있습니다. 이는 시스템을 수정할 수 있는 우발적인 조작이 발생하지 않도록 하기 위한 것입니다.\\n\\nMCP 클라이언트를 통해 시스템을 변경하려는 경우, 이 옵션을 활성화하기 전에 잠재적으로 시스템을 파괴할 수 있는 작업을 확인하도록 MCP 클라이언트가 구성되어 있는지 확인하세요. 적용하려면 MCP 클라이언트를 다시 연결해야 합니다.\nmcpClientConfigurationDetails=MCP 클라이언트 구성\nmcpClientConfigurationDetailsDescription=이 구성 데이터를 사용하여 선택한 MCP 클라이언트에서 XPipe MCP 서버에 연결합니다.\nswitchHostAddress=호스트 주소 변경\naddAnotherHostName=다른 호스트 이름 추가\naddNetwork=네트워크 스캔 ...\nnetworkScan=네트워크 스캔\nnetworkScanStore=대상 호스트\nnetworkScanStoreDescription=로컬 네트워크를 스캔할 호스트\nuseAsGateway=호스트를 게이트웨이로 사용\nuseAsGatewayDescription=대상 호스트를 생성된 연결의 게이트웨이로 사용할지 여부\nnetworkScanPorts=스캔할 포트\nnetworkScanPortsDescription=스캔에 포함할 포트의 쉼표로 구분된 목록입니다\nnetworkScanType=연결 유형\nnetworkScanTypeDescription=찾을 서버 유형\nemptyDirectory=이 디렉터리가 비어 있는 것 같습니다\nhcloudConfigFile=hcloud 구성 파일\nhcloudConfigFileDescription=Hcloud CLI .toml 구성 파일의 위치입니다\npreferMonochromeIcons=단색 아이콘 선호\npreferMonochromeIconsDescription=활성화하면 소스의 아이콘에 대해 별도의 밝은 모드 또는 어두운 모드 아이콘 변형을 사용할 수 있다고 가정할 때 기본 컬러 버전의 아이콘 대신 흑백 아이콘 변수가 선택됩니다.\\n\\n적용하려면 아이콘을 새로 고쳐야 합니다.\nalwaysShowSshMotd=항상 MOTD 표시\nalwaysShowSshMotdDescription=새 터미널 세션에 로그인할 때 원격 시스템에 구성된 오늘의 메시지를 표시할지 여부입니다. 이 값을 변경하면 SSH 연결의 초기화 동작이 변경될 수 있습니다.\nmanageSubscription=구독 관리\nnoListeningServer=수신 서버 없음\nnetworkScanResults=스캔 결과\nnetworkScanResultsDescription=네트워크에서 발견된 시스템 목록\nlocalShellDialect=로컬 셸\nlocalShellDialectDescription=로컬 작업에 사용되는 셸입니다. 일반적인 로컬 기본 셸이 비활성화되었거나 어느 정도 고장난 경우 이 옵션을 사용하여 다른 대체 셸로 돌아갈 수 있습니다.\\n\\n사용자 지정 경로 항목과 같은 일부 구성은 각 셸 프로필 파일에 아직 구성되지 않은 경우 폴백 셸에 적용되지 않을 수 있습니다.\nagentSocketNotFound=활성 에이전트 소켓을 찾을 수 없습니다\nagentSocket=소켓 위치\nagentSocketDescription=상담원 소켓 파일의 경로\nagentSocketNotConfigured=아직 사용자 지정 소켓이 구성되지 않았습니다\ndownloadInProgress=$NAME$ 다운로드 진행 중\nenableTerminalStartupBell=터미널 시작 벨 활성화\nenableTerminalStartupBellDescription=새 터미널 세션에서 삐/벨 명령을 재생합니다. 터미널 에뮬레이터가 벨을 지원하는 경우 새로 시작한 터미널 인스턴스를 쉽게 식별하는 데 사용할 수 있습니다.\ninvalidSshGatewayChain=점프 게이트웨이와 점프 게이트웨이가 아닌 게이트웨이가 혼합된 게이트웨이 체인 구성이 잘못되었습니다.\nsyncFileExists=동기화된 파일 $FILE$ 이 이미 존재합니다\nreplaceFile=파일 바꾸기\nreplaceFileDescription=기존 파일을 이 파일로 바꿨습니다\nrenameFile=파일 이름 바꾸기\nrenameFileDescription=이 파일에 다른 이름을 지정하여 동기화\nnewFileName=새 파일 이름\nparentHostDoesNotSupportTunneling=상위 호스트 $NAME$ 는 터널링을 지원하지 않습니다\nconnectionNotesTemplate=메모 템플릿\nconnectionNotesTemplateDescription=연결에 새 노트 항목을 추가할 때 사용해야 하는 마크다운 템플릿입니다.\nconnectionNotesButton=노트 편집\n#custom\nrdpSmartSizing=자동 크기 조정 사용\nrdpSmartSizingDescription=활성화하면 창이 너무 작아 전체 해상도로 표시할 수 없는 경우 mstsc가 바탕 화면 크기를 축소합니다. 축소 시 바탕 화면의 종횡비는 그대로 유지됩니다.\ndisableStartOnInit=자동 시작 사용 안 함\nenableStartOnInit=자동 시작 사용\nfileReadSudoTitle=Sudo 파일 읽기\nfileReadSudoContent=읽으려는 파일에 현재 사용자 읽기 권한이 부여되어 있지 않습니다. Sudo를 사용하여 루트 사용자로 이 파일을 읽으시겠습니까? 그러면 기존 자격 증명을 사용하거나 프롬프트를 통해 자동으로 루트로 승격됩니다.\nnetbirdInstall.displayName=Netbird 설치\nnetbirdInstall.displayDescription=Netbird 네트워크의 피어에 연결합니다\nnetbirdProfile.displayName=Netbird 프로필\nnetbirdProfile.displayDescription=특정 프로필에 있는 동료 목록\n#custom\nnetbirdPeer.displayName=Netbird 피어\nnetbirdPeer.displayDescription=SSH를 통해 피어에 연결\nnetbirdPublicKey=공개 키\nnetbirdPublicKeyDescription=피어의 내부 공개 키\nnetbirdHostName=호스트 이름\nnetbirdHostNameDescription=네트워크에 있는 피어의 호스트 이름입니다\nvncRefSystem=연결된 시스템\nvncRefSystemDescription=이 VNC 연결을 연결할 연결 항목입니다. 없는 경우 비워 둡니다\nabstractHost.displayName=추상 호스트\nabstractHost.displayDescription=셸 연결을 지원하지 않는 호스트에 대한 항목 만들기\nabstractHostAddress=호스트 주소\nabstractHostAddressDescription=호스트의 주소\nabstractHostGateway=게이트웨이\nabstractHostGatewayDescription=이 호스트에 연결하기 위한 게이트웨이 시스템(선택 사항)\nabstractHostConvert=추상 호스트 항목으로 변환\nhostNoConnections=사용 가능한 연결이 없습니다\n#custom\nhostHasConnections=$COUNT$개의 사용 가능한 연결\n#custom\nhostHasConnection=$COUNT$개의 사용 가능한 연결\nlargeFileWarningTitle=대용량 파일 편집\n#custom\nlargeFileWarningContent=편집하려는 파일이 $SIZE$ 로 상당히 큽니다. 이 파일을 텍스트 편집기에서 열까요?\nrdpAskpassUser=호스트의 RDP 사용자 이름 $HOST$\nrdpAskpassPassword=사용자의 비밀번호 $USER$\ninPlaceKey=Key\ninPlaceKeyText=개인 키 콘텐츠\ninPlaceKeyTextDescription=개인 키 내용\nnetbirdSelfhosted=자체 호스팅 넷버드 인스턴스\nnetbirdSelfhostedDescription=클라우드 호스팅 버전을 사용하는 대신 사용자 지정 URL을 제공하세요\nnetbirdManagementUrl=Netbird 관리 URL\nnetbirdManagementUrlDescription=셀프 호스팅 인스턴스의 관리 URL\nnetbirdSetupKey=설정 키\nnetbirdSetupKeyDescription=설정 키를 사용하는 경우 로그인에 사용할 수 있습니다\nnetbirdLogin=Netbird 로그인\naddProfile=프로필 추가\nnetbirdProfileNameAsktext=새 넷버드 프로필의 이름\nopenSftp=SFTP 세션에서 열기\ncapslockWarning=캡록이 활성화되어 있습니다\ninherit=Inherit\nsshConfigStringSelected=대상 호스트\nsshConfigStringSelectedDescription=호스트가 여러 개인 경우 첫 번째 호스트가 대상으로 사용됩니다. 호스트를 재정렬하여 대상을 변경하세요\ntunnelToLocalhost=로컬호스트로의 터널\ntunnelToLocalhostDescription=원격 포트를 로컬호스트로 자동 터널링합니다\ntags=태그\ntag=태그\naddNewTag=새 태그 만들기\ncreateTag=태그 만들기 ...\ninPlacePublicKey=공개 키\ninPlacePublicKeyDescription=지정된 개인 키에 연결된 공개 키\nsshKeygenTitle=새 SSH 키 생성\nsshKeygenAlgorithm=알고리즘\nsshKeygenAlgorithmDescription=키에 사용할 비대칭 키 생성 알고리즘입니다\nrsaBits=비트\nrsaBitsDescription=생성된 키의 비트 수\nsshKeygenComment=코멘트\nsshKeygenCommentDescription=이 키에 대한 선택적 주석\nsshKeygenPassphrase=암호 구문\nsshKeygenPassphraseDescription=이 키의 선택적 암호 구문\ned25519SkResident=상주 키 만들기\ned25519SkResidentDescription=하드웨어 보안 키에 개인 키 저장\ned25519SkResidentKeyName=상주 키 레이블\ned25519SkResidentKeyNameDescription=키에 레이블을 지정합니다. 보안 키에 여러 개의 키를 저장할 때 필요합니다\ned25519SkPinRequired=PIN 필요\ned25519SkPinRequiredDescription=사용 시 PIN 입력 필요\ned25519SkUserPresenceRequired=사용자 존재 필요\ned25519SkUserPresenceRequiredDescription=사용 시 터치 또는 이와 유사한 방식이 필요합니다. 일부 보안 키는 이 기능을 활성화해야 합니다\ncopyPublicKey=공개 키 복사\ngeneratePublicKey=공개 키 생성\npublicKeyGenerateNotice=개인 키에서 생성 가능\nidentityApplyTargetHost=대상\nidentityApplyTargetHostDescription=ID를 적용할 시스템\nidentityApplyAuthorizedHost=인증된 SSH 키\nidentityApplyAuthorizedHostDescription=SSH 키가 인증된 호스트 파일에 추가됩니다\nidentityApplyAuthorizedHostButton=파일에 키 추가\napplyIdentityToHost=호스트에 ID 적용 ...\nidentityApplyMissingPublicKeyTitle=누락된 공개 키\nidentityApplyMissingPublicKeyContent=ID의 SSH 키에 연결된 공개 키가 없습니다. 자세한 내용은 구성에서 확인하세요.\nvalid=유효\nnotValid=유효하지 않음\nwarning=경고\nidentityApplyTitle=ID 적용\nidentityApplyConfigPasswordEnabled=비밀번호 인증 사용\nidentityApplyConfigPasswordEnabledDescription=암호 인증은 여전히 sshd 구성에서 사용하도록 설정되어 있습니다\nidentityApplyConfigPasswordDisabled=비밀번호 인증 비활성화\nidentityApplyConfigPasswordDisabledDescription=암호 인증은 여전히 sshd 구성에서 비활성화되어 있습니다\nidentityApplyConfigKeyEnabled=키 인증 사용\nidentityApplyConfigKeyEnabledDescription=키 기반 인증은 여전히 sshd 구성에서 사용하도록 설정됩니다\nidentityApplyConfigKeyDisabled=키 인증 비활성화\nidentityApplyConfigKeyDisabledDescription=Sshd 구성에서 키 기반 인증이 여전히 비활성화되어 있습니다\nidentityApplyConfigRootDisabledWarning=루트 로그인 비활성화\nidentityApplyConfigRootDisabledWarningDescription=Sshd 구성에서 루트 사용자 로그인이 활성화되지 않았습니다\nidentityApplyConfigAdminWarning=구성된 관리자 키\nidentityApplyConfigAdminWarningDescription=관리자 사용자의 경우 관리자_authorized_keys에 키를 대신 추가해야 할 수 있습니다\nidentityApplyEditConfig=구성 편집\nidentityApplyEditConfigDescription=편집기에서 sshd 구성을 열어 문제를 해결합니다\nidentityApplyEditAuthorizedKeys=인증된 키 편집\nidentityApplyEditAuthorizedKeysDescription=다른 키를 편집하거나 제거하려면 편집기에서 authorized_keys 파일을 엽니다\nidentityApplyEditConfigButton=Sshd_config 열기\nidentityApplyEditAuthorizedKeysButton=Authorized_keys 열기\nidentityApplySetStoreIdentity=연결 ID 집합\nidentityApplySetStoreIdentityDescription=연결에서 사용하도록 구성된 ID입니다\nidentityApplySetStoreIdentityButton=ID 적용\ngenerateKey=키 생성\ngroupSecretStrategy=그룹 기반 액세스 제어\ngroupSecretStrategyDescription=그룹의 암호화 및 암호 해독에 사용되는 그룹 비밀을 검색하는 방법입니다. 선택한 검색 방법은 사용자가 시작 시 볼트에 로그인할 때 실행됩니다.\\n\\n이 설정은 그룹별로 구성됩니다. 현재 활성화된 그룹이 아닌 다른 그룹에 대해 이 설정을 변경하려면 해당 그룹의 구성원으로 볼트에 로그인해야 합니다.\nfileSecret=파일 기반 비밀\ncommandSecret=명령\nhttpRequestSecret=HTTP 응답\nfileSecretChoice=파일 위치\nfileSecretChoiceDescription=그룹 암호화 비밀이 포함된 파일의 경로입니다. 이 파일은 모든 플랫폼에서 쿼리할 수 있으므로 경로에 ~를 사용하여 홈 디렉터리를 참조할 수 있습니다. 이 파일은 볼트를 잠금 해제하는 모든 시스템에서 사용할 수 있어야 하며, 그렇지 않으면 로그인에 실패합니다.\ncommandSecretField=검색 스크립트\ncommandSecretFieldDescription=현재 그룹의 비밀 암호화 키를 반환하는 명령입니다. 이 명령은 로컬 시스템 기본 셸에서 실행되며 키는 stdout에 출력되어야 합니다.\nhttpRequestSecretField=요청 URI\nhttpRequestSecretFieldDescription=HTTP 요청을 보낼 URI입니다. 그룹 비밀은 HTTP 응답 본문에서 가져옵니다.\nvaultAuthentication=금고 인증\nvaultAuthenticationDescription=볼트 데이터를 인증/잠금 해제하는 방법. 볼트 데이터를 공유하려는 대상에 따라 볼트 데이터를 암호화하고 잠금 해제하는 방법은 여러 가지가 있습니다.\ngroupAuthFailed=비밀 인증에 실패했습니다\nuserAuthFailed=비밀번호 인증에 실패했습니다\nsavingChanges=변경 사항 저장\nawsDeviceScan=AWS\naws=AWS\nawsCliInstallTitle=AWS CLI 필요\nawsCliInstallContent=AWS 통합을 사용하려면 로컬 시스템에 AWS CLI가 설치되어 있어야 합니다\nawsProfileCreateTitle=새 AWS 프로필\nawsProfileAccessKey=액세스 키\nawsProfileName=프로필 이름\nawsProfileNameDescription=새 프로필의 표시 이름\nawsProfileRegion=지역\nawsProfileRegionDescription=프로필과 연결된 AWS 리전\nawsProfileAccessKeyId=액세스 키 ID\nawsProfileAccessKeyIdDescription=IAM 사용자 액세스 키 ID\nawsProfileSecretAccessKey=비밀 액세스 키\nawsProfileSecretAccessKeyDescription=관련 비밀 액세스 키\nawsInstall.displayName=AWS CLI 설치\nawsInstall.displayDescription=AWS CLI를 통해 AWS 시스템에 연결하기\nawsProfile.displayName=AWS CLI 프로필\nawsProfile.displayDescription=특정 프로필을 통해 AWS에 액세스\nawsInstanceId=인스턴스 ID\nawsInstanceIdDescription=이 인스턴스의 내부 ID\nawsInstanceUseSsm=SSM을 통해 연결\nawsInstanceUseSsmDescription=SSM 도구를 사용하여 SSH를 통해 인스턴스에 연결합니다\nawsEc2Instance.displayName=AWS EC2 인스턴스\nawsEc2Instance.displayDescription=SSH를 통해 EC2 인스턴스에 연결\nawsS3Group.displayName=S3 버킷\nawsS3Group.displayDescription=AWS 프로필의 S3 버킷에 액세스\nawsS3Bucket.displayName=S3 버킷\nawsS3Bucket.displayDescription=AWS 프로필의 S3 버킷 액세스\nawsEc2Group.displayName=EC2 인스턴스\nawsEc2Group.displayDescription=AWS 프로필의 EC2 인스턴스 액세스\nawsEc2InstanceSsmTerminal=열린 SSM 터미널\ngenericS3Bucket.displayName=일반 S3 버킷\ngenericS3Bucket.displayDescription=AWS CLI를 통해 일반 S3 버킷에 액세스하기\naddFileSystem=파일 시스템 ...\ngenericS3BucketHost=호스트\ngenericS3BucketHostDescription=S3 서버의 호스트 항목 또는 수동 주소\ngenericS3BucketPortDescription=S3 서버가 수신 대기 중인 포트\ngenericS3BucketAccessKeyId=액세스 키 ID\ngenericS3BucketAccessKeyIdDescription=IAM 사용자 액세스 키 ID\ngenericS3BucketSecretAccessKey=비밀 액세스 키\ngenericS3BucketSecretAccessKeyDescription=관련 비밀 액세스 키\ngenericS3BucketHttps=HTTPS 사용\ngenericS3BucketHttpsDescription=서버에 연결하려면 HTTPS를 사용합니다. 일부 공급자는 HTTPS를 요구할 수 있습니다\ntunnelled=터널\nawsInstallSync=구성 동기화\nawsInstallSyncDescription=AWS CLI 구성 파일을 git 볼트에 동기화하기\nawsInstallLocation=사용자 데이터 위치\nawsInstallLocationDescription=AWS CLI 구성 파일의 소스 경로입니다\ninstanceActions=인스턴스 작업\nopenSplit=분할 터미널에서 열기\nterminalSplitStrategy=보기 방향 분할\nterminalSplitStrategyDescription=일괄 모드에서 분할 보기 기능을 사용하여 여러 터미널 세션을 나란히 열 때 터미널 탭이 분할되는 방식을 제어합니다.\nterminalSplitStrategyDisabledDescription=일괄 모드에서 분할 보기 기능을 사용하여 여러 터미널 세션을 나란히 열 때 터미널 탭이 분할되는 방식을 제어합니다.\\n\\n현재 터미널 구성이 분할 보기를 지원하지 않습니다.\nhorizontal=가로\nvertical=세로\nbalanced=균형\nclose=닫기\nhelpButton=$TOPIC$ 문서 링크\nquickAccess=빠른 액세스\ntoggleEnabled=상태 토글\ncurrentPath=현재 경로\ndirectoryContents=디렉토리 내용\ndirectoryOptions=디렉토리 옵션\nchooseConnectionType=연결 유형 선택\nbatchMode=배치 모드\ntoggleButton=토글 버튼\ntailscaleUseSsh=테일스케일 SSH 인증 사용\ntailscaleUseSshDescription=SSH 인증 없이 Tailscale SSH 서버 자체를 통해 로그인합니다\nportDescription=SSH 서버가 실행 중인 포트\nloginAs=다음 계정으로 로그인\nsshGatewayType=게이트웨이 유형\nsshGatewayTypeDescription=터널을 통해 대상에 연결할지 또는 ProxyJump 옵션을 사용하여 연결할지 여부입니다\ngatewayTunnel=게이트웨이 터널\nproxyJump=프록시 점프\ncommandTypeAsyncBackground=백그라운드에서 분리된 상태로 실행\ncommandTypeSyncBackground=백그라운드에서 실행되고 완료될 때까지 대기\ncommandTypeTerminalBackground=터미널에서 열기\nasyncBackgroundCommand=백그라운드 명령\nsyncBackgroundCommand=백그라운드 명령 차단\nterminalBackgroundCommand=터미널 명령\ntestingConnection=연결 테스트 중 ...\nopenManagementConsole=관리 콘솔 열기\nopenLxcTerminal=LXC 터미널 열기\nopenContainerConsole=직렬 콘솔 열기\nkeeper2fa=2FA 방법\nkeeper2faDescription=계정에 대해 구성된 기본 2단계 인증 방법입니다. Keeper 계정에서 비밀번호에 액세스하기 위해 2단계 인증이 필요한 경우 이 옵션을 사용 설정합니다.\nkeeperTotpDuration=사용자 지정 2FA 코드 기간\nkeeperTotpDurationDescription=2FA 코드의 유효 기간에 대한 기본 기간을 재정의합니다. 조직 정책에서 기간 변경을 허용하는 경우에만 적용됩니다.\\n\\n가능한 값은 다음과 같습니다: $VALUES$\nkeeperOtherAuth=기타(RSA SecurID, Duo Security, Keeper DNA 등)\nextractReusableIdentities=재사용 가능한 ID 추출\nidentitiesAdded=추가된 ID\nsyncMode=동기화 모드\nsyncModeDescription=변경 사항을 동기화하는 방법을 제어합니다.\\n\\n즉시 모드는 가능한 한 빨리 변경 내용을 푸시 및 당기고, 시작 및 종료 모드는 세션 중에 이루어진 모든 변경 내용을 한 번에 동기화하며, 수동 모드는 사용자가 세션을 시작할 때만 동기화합니다.\ntoggleTerminalDock=터미널 독 토글\nscriptDirectory=디렉토리 위치\nscriptDirectoryDescription=셸 스크립트 파일이 포함된 로컬 디렉터리\nscriptSourceUrl=리포지토리 URL\nscriptSourceUrlDescription=셸 스크립트 파일이 포함된 원격 git 리포지토리에 대한 URL입니다\nscriptCollectionSourceType=소스 유형\nscriptCollectionSourceTypeDescription=셸 스크립트를 로드해야 하는 소스 유형입니다\nscriptCollectionSourceEntry=소스 항목\nscriptCollectionSourceEntryDescription=셸 스크립트를 로드해야 하는 소스\ngitRepository=Git 리포지토리\nscriptCollectionSource.displayName=스크립트 소스\nscriptCollectionSource.displayDescription=기존 소스에서 셸 스크립트 자동 가져오기\ndirectorySource=디렉토리 소스\ngitRepositorySource=Git 리포지토리 소스\nrefreshSource=소스 새로 고침\nscriptTextSourceUrl=스크립트 URL\nscriptTextSourceUrlDescription=스크립트 파일을 검색할 URL입니다\nscriptSourceType=스크립트 소스\nscriptSourceTypeDescription=스크립트의 소스 출처\nscriptSourceTypeInPlace=인플레이스 스크립트\nscriptSourceTypeUrl=외부 URL\nscriptSourceTypeSource=기존 소스\nimportScripts=스크립트 가져오기\nscriptsContained=$NUMBER$ 스크립트\nscriptSourceCollectionImportTitle=소스에서 스크립트 가져오기 ($SELECTED$/$COUNT$)\nnoScriptsFound=스크립트를 찾을 수 없습니다\ntunnel=터널\nnotInitialized=초기화되지 않음\nselectCategory=카테고리 선택 ...\nscriptSourceName=스크립트 이름\nscriptSourceNameDescription=소스에 있는 스크립트의 파일 이름\nworkspaceRestartTitle=작업 공간 준비\nworkspaceRestartContent=새 작업 공간에 대한 바로 가기가 $PATH$ 에 만들어졌습니다. 바로 가기로 이동하거나 지금 XPipe를 다시 시작하면 새 작업 영역이 자동으로 열립니다.\nbrowseShortcut=파일 찾아보기\nsyncModeInstant=즉시 동기화\nsyncModeSession=시작 및 종료 시 동기화\nsyncModeManual=수동으로 동기화\npushChanges=푸시 변경 사항\npullChanges=변경 내용 가져오기\nsourcedFrom=출처 $SOURCE$\ninPlaceScript=인플레이스 스크립트\ngeneric=Generic\nsyncToPlainDirectory=일반 디렉터리로 동기화\nsyncToPlainDirectoryDescription=로컬 디렉터리로 동기화할 때 이 디렉터리를 다른 git 리포지토리로 취급하거나 그냥 일반 디렉터리로 취급할 수 있습니다. 일반 디렉터리 설정을 활성화하면 디렉터리가 git 리포지토리로 초기화되지 않습니다.\nopenSpiceSession=SPICE 세션 열기\nterminalBehaviour=터미널 동작\nnoScanPossible=지원되는 연결을 찾을 수 없습니다\nnetworkSwitchPorts=네트워크 포트\nnswitchGroup.displayName=네트워크 포트\nnswitchGroup.displayDescription=네트워크 장치에서 사용 가능한 포트 목록\nnswitchPort.displayName=네트워크 포트\nnswitchPort.displayDescription=네트워크 스위치 장치의 개별 포트 제어\nenablePort=포트 사용\nshutdownPort=포트 종료\nresetPort=포트 재설정\nuseSystemDefault=시스템 기본값 사용\nportStatus=포트 상태\nclearCounters=카운터 지우기\nshowStatus=상태 표시\nshowAllPorts=모든 포트 표시\nactiveLicense=라이선스\nactiveLicenseDescription=XPipe 라이선스 키 활성화\nauthenticatorApp=인증 앱\nsecurityKey=보안 키\nmcpAdditionalContext=추가 MCP 컨텍스트\nmcpAdditionalContextDescription=MCP 클라이언트에 전달할 추가 지침입니다. 이를 사용하여 상담원 동작을 제어하고 개별 설정에 대한 추가 컨텍스트를 제공할 수 있습니다.\nmcpAdditionalContextSample=- 먼저 확인하지 않고 서비스 및 데몬을 자동으로 다시 시작하지 마세요\\n- 네트워크 인터페이스를 구성할 때는 항상 192.168.1.1/24를 게이트웨이로 사용하세요\nprefsRestartTitle=재시작 필요\nprefsRestartContent=변경한 일부 옵션을 적용하려면 애플리케이션을 다시 시작해야 합니다. 지금 XPipe를 다시 시작하시겠습니까?\nbashShell=배쉬 셸\n"
  },
  {
    "path": "lang/strings/translations_nl.properties",
    "content": "delete=Verwijderen\nproperties=Eigenschappen\nusedDate=Gebruikt $DATE$\nopenDir=Open Directory\nsortLastUsed=Sorteren op laatst gebruikte datum\nsortAlphabetical=Sorteer alfabetisch op naam\nsortIndexed=Sorteren op volgorde-index\nrestartDescription=Een herstart kan vaak een snelle oplossing zijn\nreportIssue=Een probleem melden\nreportIssueDescription=Open de geïntegreerde probleemrapporter\nusefulActions=Nuttige acties\nstored=Opgeslagen\ntroubleshootingOptions=Hulpmiddelen voor probleemoplossing\ntroubleshoot=Problemen oplossen\nremote=Bestand op afstand\naddShellStore=Shell toevoegen ...\naddShellTitle=Shell-verbinding toevoegen\nsavedConnections=Opgeslagen verbindingen\nsave=Opslaan\nclean=Schoon\nmoveTo=Naar ...\naddDatabase=Databank ...\nbrowseInternalStorage=Bladeren door interne opslag\naddTunnel=Tunnel ...\naddService=Service ...\naddScript=Script ...\naddHost=Externe host ...\naddShell=Shell-omgeving ...\naddCommand=Opdracht ...\naddAutomatically=Automatisch toevoegen ...\naddOther=Andere toevoegen ...\nconnectionAdd=Verbinding toevoegen\nscriptAdd=Script toevoegen\nscriptGroupAdd=Scriptgroep toevoegen\nidentityAdd=Identiteit toevoegen\nnew=Nieuw\nselectType=Selecteer type\nselectTypeDescription=Verbindingstype selecteren\nselectShellType=Shell Type\nselectShellTypeDescription=Selecteer het type van de Shell-verbinding\nname=Naam\nstoreIntroHeader=Verbindingshub\nstoreIntroContent=Hier kun je al je lokale en externe shellverbindingen op één plek beheren. Om te beginnen kun je snel beschikbare verbindingen automatisch detecteren en kiezen welke je wilt toevoegen.\nstoreIntroButton=Zoeken naar verbindingen ...\ndragAndDropFilesHere=Of sleep gewoon een bestand hierheen\nconfirmDsCreationAbortTitle=Afbreken bevestigen\nconfirmDsCreationAbortHeader=Wil je het maken van de gegevensbron afbreken?\nconfirmDsCreationAbortContent=Het aanmaken van gegevensbronnen gaat verloren.\nconfirmInvalidStoreTitle=Validatie overslaan\nconfirmInvalidStoreContent=Wil je de validatie van verbindingen overslaan? Je kunt deze verbinding toevoegen, zelfs als deze niet kon worden gevalideerd, en de verbindingsproblemen later oplossen.\nexpand=Uitbreiden\naccessSubConnections=Toegang subverbindingen\ncommon=Gebruikelijk\ncolor=Kleur\nalwaysConfirmElevation=Toestemmingsverhoging altijd bevestigen\nalwaysConfirmElevationDescription=Regelt hoe om te gaan met gevallen waarin verhoogde rechten nodig zijn om een commando op een systeem uit te voeren, bijvoorbeeld met sudo.\\n\\nStandaard worden alle sudo referenties in de cache geplaatst tijdens een sessie en automatisch verstrekt als ze nodig zijn. Als deze optie is ingeschakeld, wordt je elke keer gevraagd om de verhoogde toegang te bevestigen.\nallow=Toestaan\nask=Vraag\ndeny=Weigeren\nshare=Toevoegen aan git repository\nunshare=Verwijderen uit git repository\nremove=Verwijderen\ncreateNewCategory=Nieuwe subcategorie\nprompt=Prompt\ncustomCommand=Aangepaste opdracht\nother=Andere\nsetLock=Slot instellen\nselectConnection=Verbinding selecteren\nselectEntry=Invoer selecteren\ncreateLock=Passphrase aanmaken\nchangeLock=Wachtwoordzin wijzigen\ntest=Test\nfinish=Afsluiten\nerror=Er is een fout opgetreden\ndownloadStageDescription=Verplaatst gedownloade bestanden naar de downloadmap van je systeem en opent deze.\nok=Ok\nsearch=Zoeken\nrepeatPassword=Herhaal wachtwoord\naskpassAlertTitle=Askpass\nunsupportedOperation=Niet-ondersteunde bewerking: $MSG$\nfileConflictAlertTitle=Conflict oplossen\nfileConflictAlertContent=Er is een conflict opgetreden. Het bestand $FILE$ bestaat al op het doelsysteem.\\n\\nHoe wilt u verdergaan?\nfileConflictAlertContentMultiple=Er is een conflict opgetreden. Het bestand $FILE$ bestaat al.\\n\\nHoe zou je verder willen gaan? Er kunnen meer conflicten zijn die je automatisch kunt oplossen door een optie te kiezen die voor iedereen geldt.\nmoveAlertTitle=Zet bevestigen\nmoveAlertHeader=Wil je de ($COUNT$) geselecteerde elementen verplaatsen naar $TARGET$?\ndeleteAlertTitle=Verwijdering bevestigen\ndeleteAlertHeader=Wil je de ($COUNT$) geselecteerde elementen verwijderen?\nselectedElements=Geselecteerde elementen:\nmustNotBeEmpty=$VALUE$ mag niet leeg zijn\nvalueMustNotBeEmpty=Waarde mag niet leeg zijn\ntransferDescription=Sleep bestanden hierheen om te downloaden\ndragLocalFiles=Downloads van hier slepen\nnull=$VALUE$ moet not null zijn\nroots=Wortels\nscripts=Scripts\nsearchFilter=Zoeken ...\nrecent=Recent\nshortcut=Snelkoppeling\nbrowserWelcomeEmptyHeader=Bestandsbrowser\nbrowserWelcomeEmptyContent=Je kunt links kiezen welke systemen je wilt openen in de bestandsbrowser. XPipe onthoudt welke systemen en mappen je eerder hebt geopend en toont deze in de toekomst in een snelmenu.\nbrowserWelcomeEmptyButton=Lokale bestandsbrowser openen\nbrowserWelcomeSystems=Je was onlangs verbonden met de volgende systemen:\nbrowserWelcomeDocsHeader=Documentatie\nbrowserWelcomeDocsContent=Als je de voorkeur geeft aan een meer begeleide aanpak om jezelf bekend te maken met XPipe, bekijk dan de documentatie website.\nbrowserWelcomeDocsButton=Open documentatie\nhostFeatureUnsupported=$FEATURE$ is niet geïnstalleerd op de host\nmissingStore=$NAME$ bestaat niet\nconnectionName=Naam verbinding\nconnectionNameDescription=Geef deze verbinding een aangepaste naam\nopenFileTitle=Bestand openen\nunknown=Onbekend\nscanAlertTitle=Verbindingen toevoegen\nscanAlertChoiceHeader=Doel\nscanAlertChoiceHeaderDescription=Kies waar je wilt zoeken naar verbindingen. Hiermee worden eerst alle beschikbare verbindingen gezocht.\nscanAlertHeader=Typen verbindingen\nscanAlertHeaderDescription=Selecteer types verbindingen die je automatisch wilt toevoegen voor het systeem.\nnoInformationAvailable=Geen informatie beschikbaar\nyes=Ja\nno=Geen\nerrorOccured=Er is een fout opgetreden\nterminalErrorOccured=Er is een terminalfout opgetreden\nerrorTypeOccured=Er is een uitzondering van het type $TYPE$ gegooid\npermissionsAlertTitle=Vereiste machtigingen\npermissionsAlertHeader=Er zijn extra rechten nodig om deze bewerking uit te voeren.\npermissionsAlertContent=Volg de pop-up om XPipe de vereiste rechten te geven in het instellingenmenu.\nerrorDetails=Foutgegevens\nupdateReadyAlertTitle=Gereed voor bijwerken\nupdateReadyAlertHeader=Een update naar versie $VERSION$ is klaar om geïnstalleerd te worden\nupdateReadyAlertContent=Dit zal de nieuwe versie installeren en XPipe herstarten zodra de installatie is voltooid.\nerrorNoDetail=Er zijn geen foutgegevens beschikbaar\nerrorNoExceptionMessage=Er is een fout van het type $TYPE$ gegooid\nupdateAvailableTitle=Update beschikbaar\nupdateAvailableContent=Er is een XPipe-update naar versie $VERSION$ beschikbaar om te installeren. Hoewel XPipe niet kon worden gestart, kun je proberen de update te installeren om het probleem mogelijk op te lossen.\nclipboardActionDetectedTitle=Klembord actie gedetecteerd\nclipboardActionDetectedContent=XPipe heeft inhoud op je klembord gedetecteerd die geopend kan worden. Wil je het nu openen? Wil je de inhoud van je klembord importeren?\ninstall=Installeren ...\nignore=Negeren\npossibleActions=Beschikbare acties\nreportError=Fout rapporteren\nreportOnGithub=Een probleemrapport maken op GitHub\nreportOnGithubDescription=Open een nieuw probleem in de GitHub repository\nreportErrorDescription=Een foutenrapport verzenden met optionele feedback van de gebruiker en diagnostische info\nignoreError=Fout negeren\nignoreErrorDescription=Negeer deze foutmelding en ga verder alsof er niets is gebeurd\nprovideEmail=Hoe kunnen we contact met je opnemen (optioneel, alleen als je een reactie wilt)? Je melding is standaard anoniem, dus je kunt hier contactgegevens opgeven zoals een e-mailadres.\nadditionalErrorInfo=Aanvullende informatie geven (optioneel)\nadditionalErrorAttachments=Selecteer bijlagen (optioneel)\ndataHandlingPolicies=Privacybeleid\nsendReport=Rapport verzenden\nerrorHandler=Foutbehandelaar\nevents=Gebeurtenissen\nvalidate=Valideren\nstackTrace=Stacktrack\npreviousStep=< Vorig\nnextStep=Volgende >\nfinishStep=Voltooien\nselect=Selecteer\nbrowseInternal=Intern bladeren\ncheckOutUpdate=Update uitchecken\nquit=Stop\nnoTerminalSet=Er is geen terminaltoepassing automatisch ingesteld. Je kunt dit handmatig doen in het instellingenmenu.\nconnections=Verbindingen\nconnectionHub=Verbindingsknooppunt\nsettings=Instellingen\nexplorePlans=Licentie\nhelp=Help\nabout=Over\ndeveloper=Ontwikkelaar\nbrowseFileTitle=Bestand doorbladeren\nbrowser=Bestandsbrowser\nselectFileFromComputer=Selecteer een bestand van deze computer\nlinks=Links\nwebsite=Website\ndiscordDescription=Word lid van de Discord server\nredditDescription=Word lid van de XPipe subreddit\nsecurity=Beveiliging\nsecurityPolicy=Beveiligingsinformatie\nsecurityPolicyDescription=Lees het gedetailleerde beveiligingsbeleid\nprivacy=Privacybeleid\nprivacyDescription=Lees het privacybeleid voor de XPipe-toepassing\nslackDescription=Word lid van de Slack werkruimte\nsupport=Ondersteuning\ngithubDescription=Bekijk de GitHub repository\nopenSourceNotices=Open Source Mededelingen\ncheckForUpdates=Controleren op updates\ncheckForUpdatesDescription=Download een update als die er is\nlastChecked=Laatst gecontroleerd\nversion=Versie\nbuild=Versie bouwen\nruntimeVersion=Runtime versie\nvirtualMachine=Virtuele machine\nupdateReady=Update installeren\nupdateReadyPortable=Update uitchecken\nupdateReadyDescription=Een update is gedownload en kan worden geïnstalleerd\nupdateReadyDescriptionPortable=Er is een update beschikbaar om te downloaden\nupdateRestart=Opnieuw opstarten om bij te werken\nnever=Nooit\nupdateAvailableTooltip=Update beschikbaar\nptbAvailableTooltip=Publieke testversie beschikbaar\nvisitGithubRepository=Bezoek GitHub archief\nupdateAvailable=Update beschikbaar: $VERSION$\ndownloadUpdate=Update downloaden\nlegalAccept=Ik accepteer de licentieovereenkomst voor eindgebruikers\nconfirm=Bevestigen\nprint=Afdrukken\nwhatsNew=Wat is er nieuw in versie $VERSION$ ($DATE$)\nantivirusNoticeTitle=Een opmerking over antivirusprogramma's\nupdateChangelogAlertTitle=Changelog\ngreetingsAlertTitle=Welkom bij XPipe\neula=Licentieovereenkomst voor eindgebruikers\nnews=Nieuws\nintroduction=Inleiding\nprivacyPolicy=Privacybeleid\nagree=Akkoord\ndisagree=Oneens\ndirectories=Directories\nlogFile=Logbestand\nlogFiles=Logbestanden\nlogFilesAttachment=Logbestanden\nissueReporter=Probleemrapporteur\nopenCurrentLogFile=Logbestanden\nopenCurrentLogFileDescription=Open het logbestand van de huidige sessie\nopenLogsDirectory=Open logs map\ninstallationFiles=Installatiebestanden\nopenInstallationDirectory=Installatiebestanden\nopenInstallationDirectoryDescription=Open XPipe installatiemap\nlaunchDebugMode=Debug-modus\nlaunchDebugModeDescription=XPipe herstarten in debugmodus\nextensionInstallTitle=Downloaden\nextensionInstallDescription=Deze actie vereist aanvullende bibliotheken van derden die niet door XPipe worden gedistribueerd. Je kunt ze hier automatisch installeren. De componenten worden dan gedownload van de website van de leverancier:\nextensionInstallLicenseNote=Door de download en automatische installatie uit te voeren ga je akkoord met de voorwaarden van de licenties van derden:\nlicense=Licentie\ninstallRequired=Vereiste installatie\nrestore=Herstellen\nrestoreAllSessions=Alle sessies herstellen\nlimitedTouchscreenMode=Beperkte aanraakschermmodus\nlimitedTouchscreenModeDescription=Als je deze applicatie gebruikt op een meer exotische touchscreen interface zoals een telefoonscherm, werken sommige menu's mogelijk niet goed. Als deze optie is ingeschakeld, gebruikt de menu-implementatie beperktere functionaliteit om te werken met spaarzaam verzonden muis/aanrakingsevents.\nappearance=Uiterlijk\ndisplay=Weergave\npersonalization=Personalisatie\ndisplayOptions=Weergaveopties\ntheme=Thema\nrdpConfiguration=Configuratie van bureaublad op afstand\nrdpClient=RDP-client\nrdpClientDescription=Het RDP-clientprogramma dat wordt aangeroepen bij het starten van RDP-verbindingen.\\n\\nMerk op dat verschillende clients verschillende niveaus van mogelijkheden en integraties hebben. Sommige clients ondersteunen het automatisch doorgeven van wachtwoorden niet, dus je moet ze nog steeds invullen bij het opstarten.\nlocalShell=Lokale shell\nthemeDescription=Het weergavethema van je voorkeur.\ndontAutomaticallyStartVmSshServer=SSH-server voor VM's niet automatisch starten wanneer nodig\ndontAutomaticallyStartVmSshServerDescription=Elke shellverbinding met een VM die draait in een hypervisor wordt gemaakt via SSH. XPipe kan automatisch de geïnstalleerde SSH-server starten wanneer dat nodig is. Als je dit om veiligheidsredenen niet wilt, dan kun je dit gedrag gewoon uitschakelen met deze optie.\nconfirmGitShareTitle=Git sync\nconfirmGitShareContent=Wil je het geselecteerde bestand toevoegen aan je git vault repository? Dan kopieer je een versleutelde versie van het bestand naar je git vault en commit je je wijzigingen. Je hebt dan toegang tot het bestand op alle gesynchroniseerde desktops.\ngitShareFileTooltip=Voeg een bestand toe aan de git vault datamap zodat het automatisch gesynchroniseerd wordt.\\n\\nDeze actie kan alleen worden gebruikt als de git vault is ingeschakeld in de instellingen.\nperformanceMode=Prestatiemodus\nperformanceModeDescription=Schakelt alle visuele effecten uit die niet nodig zijn om de prestaties van de toepassing te verbeteren.\ndontAcceptNewHostKeys=Nieuwe SSH-hostsleutels niet automatisch accepteren\ndontAcceptNewHostKeysDescription=XPipe accepteert standaard automatisch hostsleutels van systemen waar je SSH-client nog geen bekende hostsleutel heeft opgeslagen. Als een bekende hostsleutel echter is gewijzigd, zal het weigeren om verbinding te maken tenzij je de nieuwe accepteert.\\n\\nDoor dit gedrag uit te schakelen kun je alle hostsleutels controleren, zelfs als er in eerste instantie geen conflict is.\nuiScale=UI Schaal\nuiScaleDescription=Een aangepaste schaalwaarde die onafhankelijk van je systeembrede schermschaal kan worden ingesteld. Waarden zijn in procenten, dus bijvoorbeeld een waarde van 150 resulteert in een UI-schaal van 150%.\neditorProgram=Bewerkingsprogramma\neditorProgramDescription=De standaard teksteditor om te gebruiken bij het bewerken van tekstgegevens.\nwindowOpacity=Venster ondoorzichtigheid\nwindowOpacityDescription=Verandert de ondoorzichtigheid van het venster om bij te houden wat er op de achtergrond gebeurt.\nuseSystemFont=Gebruik het lettertype van het systeem\nopenDataDir=Directory voor kluisgegevens\nopenDataDirButton=Open gegevensmap\nopenDataDirDescription=Als je aanvullende bestanden, zoals SSH sleutels, wilt synchroniseren tussen systemen met je git repository, dan kun je ze in de storage data directory zetten. Van alle bestanden waarnaar daar verwezen wordt, worden de bestandspaden automatisch aangepast op elk gesynchroniseerd systeem.\nupdates=Updates\nselectAll=Alles selecteren\nadvanced=Geavanceerd\nthirdParty=Open bron meldingen\neulaDescription=Lees de licentieovereenkomst voor eindgebruikers voor de XPipe-toepassing\nthirdPartyDescription=De open source licenties van bibliotheken van derden bekijken\nworkspaceLock=Master wachtwoordzin\nenableGitStorage=Synchronisatie inschakelen\nsharing=Delen\ngitSync=Git sync\nenableGitStorageDescription=Als dit is ingeschakeld, initialiseert XPipe een git repository voor de lokale kluis en commit alle wijzigingen daarop. Merk op dat hiervoor git geïnstalleerd moet zijn en het laden en opslaan kan vertragen.\\n\\nAlle categorieën die gesynchroniseerd moeten worden, moeten expliciet gemarkeerd worden als gesynchroniseerd.\nstorageGitRemote=URL voor synchronisatie op afstand\nstorageGitRemoteDescription=Als dit is ingesteld, zal XPipe automatisch wijzigingen ophalen bij het laden en terugzetten naar de remote repository bij het opslaan.\\n\\nHierdoor kun je je kluis delen tussen meerdere XPipe installaties. Het ondersteunt HTTP en SSH URL's, plus lokale mappen.\nvault=Kluis\nworkspaceLockDescription=Stelt een aangepast wachtwoord in om gevoelige informatie die is opgeslagen in XPipe te versleutelen.\\n\\nDit resulteert in een verhoogde beveiliging omdat het een extra coderingslaag biedt voor je opgeslagen gevoelige informatie. Je wordt dan gevraagd om het wachtwoord in te voeren wanneer XPipe start.\nuseSystemFontDescription=Bepaalt of je het standaard systeemlettertype gebruikt of het Inter lettertype, dat wordt meegeleverd met XPipe.\ntooltipDelay=Tooltip vertraging\ntooltipDelayDescription=Het aantal milliseconden dat moet worden gewacht tot een tooltip wordt weergegeven.\nfontSize=Lettergrootte\nwindowOptions=Venster Opties\nsaveWindowLocation=Vensterlocatie opslaan\nsaveWindowLocationDescription=Regelt of de coördinaten van het venster moeten worden opgeslagen en hersteld bij opnieuw opstarten.\nstartupShutdown=Opstarten / Afsluiten\nshowChildrenConnectionsInParentCategory=Toon kindcategorieën in bovenliggende categorie\nshowChildrenConnectionsInParentCategoryDescription=Het al dan niet opnemen van alle verbindingen in subcategorieën wanneer een bepaalde bovenliggende categorie is geselecteerd.\\n\\nAls dit is uitgeschakeld, gedragen de categorieën zich meer als klassieke mappen die alleen hun directe inhoud tonen zonder submappen op te nemen.\ncondenseConnectionDisplay=Verbindingsweergave samenvatten\ncondenseConnectionDisplayDescription=Laat elke verbinding op het hoogste niveau minder verticale ruimte innemen voor een meer beknopte verbindingslijst.\nopenConnectionSearchWindowOnConnectionCreation=Verbindingszoekvenster openen bij het maken van een verbinding\nopenConnectionSearchWindowOnConnectionCreationDescription=Het al dan niet automatisch openen van het venster om te zoeken naar beschikbare subconnecties bij het toevoegen van een nieuwe shellverbinding.\nworkflow=Werkstroom\nsystem=Systeem\napplication=Toepassing\nstorage=Opslag\nrunOnStartup=Uitvoeren bij opstarten\ncloseBehaviour=Exit gedrag\ncloseBehaviourDescription=Regelt hoe XPipe verder moet gaan na het sluiten van het hoofdvenster.\nlanguage=Taal\nlanguageDescription=De te gebruiken weergavetaal. De vertalingen worden verbeterd door bijdragen van de gemeenschap. Je kunt de vertalingen helpen door vertaaloplossingen in te sturen op GitHub.\nlightTheme=Licht thema\ndarkTheme=Donker thema\nexit=XPipe afsluiten\ncontinueInBackground=Doorgaan op de achtergrond\nminimizeToTray=Minimaliseren naar lade\ncloseBehaviourAlertTitle=Sluitgedrag instellen\ncloseBehaviourAlertTitleHeader=Selecteer wat er moet gebeuren bij het sluiten van het venster. Alle actieve verbindingen worden gesloten wanneer de toepassing wordt afgesloten.\nstartupBehaviour=Opstartgedrag\nstartupBehaviourDescription=Regelt het standaardgedrag van de bureaubladtoepassing wanneer XPipe wordt gestart.\nclearCachesAlertTitle=Cache opschonen\nclearCachesAlertContent=Wil je alle XPipe-caches opschonen? Hiermee verwijder je alle cachegegevens die zijn opgeslagen om de gebruikerservaring te verbeteren.\nstartGui=GUI starten\nstartInTray=Starten in de lade\nstartInBackground=Op achtergrond starten\nclearCaches=Caches wissen ...\nclearCachesDescription=Alle cachegegevens verwijderen\ncancel=Annuleren\nnotAnAbsolutePath=Geen absoluut pad\nnotADirectory=Geen directory\nnotAnEmptyDirectory=Geen lege map\nautomaticallyCheckForUpdates=Controleren op updates\nautomaticallyCheckForUpdatesDescription=Als deze optie is ingeschakeld, wordt na een tijdje automatisch nieuwe release-informatie opgehaald terwijl XPipe wordt uitgevoerd. Je moet nog steeds elke installatie van een update expliciet bevestigen.\nsendAnonymousErrorReports=Anoniem foutrapporten verzenden\nsendUsageStatistics=Anonieme gebruiksstatistieken verzenden\nstorageDirectory=Opslagmap\nstorageDirectoryDescription=De locatie waar XPipe alle verbindingsinformatie moet opslaan. Als je dit verandert, worden de gegevens in de oude map niet gekopieerd naar de nieuwe.\nlogLevel=Logniveau\nappBehaviour=Gedrag van de toepassing\nlogLevelDescription=Het logniveau dat gebruikt moet worden bij het schrijven van logbestanden.\ndeveloperMode=Ontwikkelaar modus\ndeveloperModeDescription=Als deze optie is ingeschakeld, heb je toegang tot een aantal extra opties die handig zijn voor ontwikkeling.\neditor=Bewerker\ncustom=Aangepaste\npasswordManager=Wachtwoord manager\nexternalPasswordManager=Externe wachtwoordmanager\npasswordManagerDescription=De lokaal geïnstalleerde wachtwoordmanager om mee te integreren.\\n\\nAls je een wachtwoordmanager hebt geïnstalleerd, kun je XPipe configureren om wachtwoorden op te halen zodat XPipe de wachtwoorden niet zelf hoeft op te slaan. Als dit is ingeschakeld, kan elk wachtwoordveld voor een verbinding worden geconfigureerd om de wachtwoordmanager te gebruiken.\npasswordManagerCommandTest=Test wachtwoordmanager\npasswordManagerCommandTestDescription=Je kunt hier testen of de uitvoer er correct uitziet als je een wachtwoordmanager hebt ingesteld.\npreferTerminalTabs=Open liever nieuwe tabbladen\npreferTerminalTabsDescription=Bepaalt of XPipe zal proberen nieuwe tabbladen te openen in je gekozen terminal in plaats van nieuwe vensters. Niet elke terminal ondersteunt tabbladen.\ncustomRdpClientCommand=Aangepaste opdracht\ncustomRdpClientCommandDescription=Het commando dat moet worden uitgevoerd om de aangepaste RDP-client te starten.\\n\\nDe tekenreeks $FILE wordt vervangen door de absolute .rdp bestandsnaam wanneer deze wordt aangeroepen. Vergeet niet om het uitvoerbare pad aan te halen als het spaties bevat.\ncustomEditorCommand=Aangepaste editor opdracht\ncustomEditorCommandDescription=Het commando dat moet worden uitgevoerd om de aangepaste editor te starten.\\n\\nDe tekenreeks $FILE wordt vervangen door de absolute bestandsnaam als deze wordt aangeroepen. Vergeet niet om het uitvoerbare pad van je editor te citeren als het spaties bevat.\neditorReloadTimeout=Editor herlaad timeout\neditorReloadTimeoutDescription=Het aantal milliseconden dat moet worden gewacht voordat een bestand wordt gelezen nadat het is bijgewerkt. Dit voorkomt problemen in gevallen waarin je editor traag is met het schrijven of vrijgeven van bestandsloten.\nencryptAllVaultData=Versleutel alle kluisgegevens\nencryptAllVaultDataDescription=Als deze optie is ingeschakeld, wordt elk deel van de verbindingsgegevens van de vault versleuteld met de versleutelingssleutel van je gebruikerskluis, in tegenstelling tot alleen de geheimen binnen die gegevens. Dit voegt een extra beveiligingslaag toe voor andere parameters zoals gebruikersnamen, hostnamen, etc., die standaard niet versleuteld zijn in de kluis.\\n\\nDeze optie maakt je git vault geschiedenis en diffs nutteloos, omdat je de originele wijzigingen niet meer kunt zien, alleen de binaire wijzigingen.\nvaultSecurity=Kluisbeveiliging\ndeveloperDisableUpdateVersionCheck=Updateversiecontrole uitschakelen\ndeveloperDisableUpdateVersionCheckDescription=Regelt of de updatechecker het versienummer negeert bij het zoeken naar een update.\ndeveloperDisableGuiRestrictions=GUI-beperkingen uitschakelen\ndeveloperDisableGuiRestrictionsDescription=Regelt of sommige uitgeschakelde acties nog steeds kunnen worden uitgevoerd vanuit de gebruikersinterface.\ndeveloperShowHiddenEntries=Verborgen items weergeven\ndeveloperShowHiddenEntriesDescription=Als deze optie is ingeschakeld, worden verborgen en interne gegevensbronnen getoond.\ndeveloperShowHiddenProviders=Verborgen aanbieders tonen\ndeveloperShowHiddenProvidersDescription=Regelt of verborgen en interne verbindings- en gegevensbronproviders worden getoond in het aanmaakvenster.\ndeveloperDisableConnectorInstallationVersionCheck=Connector versiecontrole uitschakelen\ndeveloperDisableConnectorInstallationVersionCheckDescription=Regelt of de updatechecker het versienummer negeert bij het inspecteren van de versie van een XPipe-connector die op een externe machine is geïnstalleerd.\nshellCommandTest=Shell commando test\nshellCommandTestDescription=Een commando uitvoeren in de shellsessie die intern door XPipe wordt gebruikt.\nterminal=Terminal\nterminalType=Terminal emulator\nterminalConfiguration=Terminal configuratie\nterminalCustomization=Terminal aanpassing\neditorConfiguration=Configuratie editor\ndefaultApplication=Standaard toepassing\ninitialSetup=Eerste installatie\nterminalTypeDescription=De standaard terminal om te gebruiken voor het openen van shell verbindingen.\\n\\nHet ondersteuningsniveau van de functies verschilt per terminal en elke terminal is gemarkeerd als aanbevolen of niet aanbevolen. Je gebruikerservaring is het beste als je een aanbevolen terminal gebruikt.\nprogram=Programma\ncustomTerminalCommand=Aangepast terminalcommando\ncustomTerminalCommandDescription=Het commando dat moet worden uitgevoerd om de aangepaste terminal te openen met een gegeven commando.\\n\\nXPipe maakt een tijdelijk launcher-shell script aan om je terminal uit te voeren. De placeholder string $CMD in het commando dat je opgeeft zal worden vervangen door het eigenlijke launcher script wanneer het wordt aangeroepen. Vergeet niet het uitvoerbare pad van je terminal te citeren als het spaties bevat.\nclearTerminalOnInit=Terminal wissen bij init\nclearTerminalOnInitDescription=Indien ingeschakeld zal XPipe een clear commando uitvoeren nadat een nieuwe terminal sessie is gestart om alle onnodige uitvoer te verwijderen die is afgedrukt tijdens het starten van de terminal sessie.\ndontCachePasswords=Gevraagde wachtwoorden niet in de cache opslaan\ndontCachePasswordsDescription=Bepaalt of opgevraagde wachtwoorden intern door XPipe in de cache moeten worden opgeslagen, zodat je ze niet opnieuw hoeft in te voeren in de huidige sessie.\\n\\nAls dit gedrag is uitgeschakeld, moet je alle opgevraagde wachtwoorden opnieuw invoeren elke keer dat het systeem ze nodig heeft.\ndenyTempScriptCreation=Het maken van een tijdelijk script weigeren\ndenyTempScriptCreationDescription=Om sommige functionaliteiten te realiseren, maakt XPipe soms tijdelijke shell scripts aan op een doelsysteem om eenvoudige commando's eenvoudig uit te kunnen voeren. Deze bevatten geen gevoelige informatie en worden alleen gemaakt voor implementatiedoeleinden.\\n\\nAls dit gedrag is uitgeschakeld, maakt XPipe geen tijdelijke bestanden aan op een systeem op afstand. Deze optie is handig in omgevingen met een hoge beveiligingsgraad waar elke wijziging aan het bestandssysteem wordt gecontroleerd. Als dit is uitgeschakeld, zullen sommige functionaliteiten, zoals shell omgevingen en scripts, niet werken zoals bedoeld.\ndisableCertutilUse=Het gebruik van certutil onder Windows uitschakelen\nuseLocalFallbackShell=Lokale fallback-shell gebruiken\nuseLocalFallbackShellDescription=Schakel over op het gebruik van een andere lokale shell om lokale operaties af te handelen. Dit is PowerShell op Windows en bourne shell op andere systemen.\\n\\nDeze optie kan worden gebruikt als de normale lokale standaard shell is uitgeschakeld of tot op zekere hoogte kapot is. Sommige functies kunnen echter niet werken zoals verwacht wanneer deze optie is ingeschakeld.\ndisableCertutilUseDescription=Vanwege verschillende tekortkomingen en bugs in cmd.exe worden tijdelijke shellscripts gemaakt met certutil door het te gebruiken om base64 invoer te decoderen, omdat cmd.exe breekt op niet-ASCII invoer. XPipe kan hiervoor ook PowerShell gebruiken, maar dit is langzamer.\\n\\nDit schakelt elk gebruik van certutil op Windows systemen uit om bepaalde functionaliteit te realiseren en in plaats daarvan terug te vallen op PowerShell. Dit zou sommige AV's kunnen plezieren, omdat sommige AV's het gebruik van certutil blokkeren.\ndisableTerminalRemotePasswordPreparation=Voorbereiding voor wachtwoord op afstand van terminal uitschakelen\ndisableTerminalRemotePasswordPreparationDescription=In situaties waar een remote shell verbinding die via meerdere intermediaire systemen loopt in de terminal tot stand moet worden gebracht, kan het nodig zijn om alle benodigde wachtwoorden op een van de intermediaire systemen voor te bereiden, zodat eventuele prompts automatisch kunnen worden ingevuld.\\n\\nAls je niet wilt dat de wachtwoorden ooit worden verzonden naar een tussenliggend systeem, dan kun je dit gedrag uitschakelen. Elk vereist tussenliggend wachtwoord zal dan worden opgevraagd in de terminal zelf wanneer deze wordt geopend.\nmore=Meer\ntranslate=Vertalingen\nallConnections=Alle verbindingen\nallScripts=Alle scripts\nallIdentities=Alle identiteiten\nsynced=Gesynchroniseerd\npredefined=Voorgedefinieerd\nsamples=Voorbeelden\ngoodMorning=Goedemorgen\ngoodAfternoon=Goedemiddag\ngoodEvening=Goedenavond\naddVisual=Visuele ...\naddDesktop=Desktop ...\nssh=SSH\nsshConfiguration=SSH-configuratie\nsize=Grootte\nattributes=Attributen\nmodified=Gewijzigd\nowner=Eigenaar\nupdateReadyTitle=Bijwerken naar $VERSION$ klaar\ntemplates=Sjablonen\nretry=Opnieuw proberen\nretryAll=Alles opnieuw proberen\nreplace=Vervangen\nreplaceAll=Alles vervangen\nhibernateBehaviour=Slaapstand gedrag\nhibernateBehaviourDescription=Regelt hoe de toepassing zich gedraagt wanneer je systeem in slaapstand wordt gezet.\noverview=Overzicht\nhistory=Geschiedenis\nskipAll=Alles overslaan\nnotes=Opmerkingen\naddNotes=Opmerkingen toevoegen\norder=Opnieuw sorteren\nkeepFirst=Bewaar eerst\nkeepLast=Bewaar als laatste\npinToTop=Speld naar boven\nunpinFromTop=Van bovenaf spelden\norderAheadOf=Vooruitbestellen ...\nclearIndex=Reset index\nhttpServer=HTTP-server\nmcpServer=MCP-server\napiKey=API-sleutel\napiKeyDescription=De API sleutel om XPipe daemon API verzoeken te authenticeren. Voor meer informatie over hoe te authenticeren, zie de algemene API documentatie.\ndisableApiAuthentication=API-authenticatie uitschakelen\ndisableApiAuthenticationDescription=Schakelt alle vereiste authenticatiemethoden uit zodat elk niet-geauthenticeerd verzoek wordt afgehandeld.\\n\\nAuthenticatie zou alleen uitgeschakeld moeten worden voor ontwikkelingsdoeleinden.\napi=API\nstoreIntroImportContent=Gebruik je XPipe al op een ander systeem? Synchroniseer je bestaande verbindingen over meerdere systemen via een remote git repository. Je kunt ook later synchroniseren op elk gewenst moment als het nog niet is ingesteld.\nstoreIntroImportButton=Synchroniseer verbindingen ...\nstoreIntroImportHeader=Verbindingen importeren\nshowNonRunningChildren=Niet-lopende kinderen tonen\nhttpApi=HTTP API\nisOnlySupportedLimit=wordt alleen ondersteund met een professionele licentie bij meer dan $COUNT$ verbindingen\nareOnlySupportedLimit=worden alleen ondersteund met een professionele licentie bij meer dan $COUNT$ verbindingen\nenabled=Ingeschakeld\nenableGitStoragePtbDisabled=Git synchronisatie is uitgeschakeld voor publieke test builds om gebruik met reguliere release git repositories te voorkomen en om het gebruik van een PTB build als je dagelijkse driver te ontmoedigen.\ncopyId=API-ID kopiëren\nrequireDoubleClickForConnections=Dubbelklikken vereist voor verbindingen\nrequireDoubleClickForConnectionsDescription=Als dit is ingeschakeld, moet je dubbelklikken op verbindingen om ze te starten. Dit is handig als je gewend bent om op dingen te dubbelklikken.\nclearTransferDescription=Selectie wissen\nselectTab=Tabblad selecteren\ncloseTab=Tabblad sluiten\ncloseOtherTabs=Andere tabbladen sluiten\ncloseAllTabs=Alle tabbladen sluiten\ncloseLeftTabs=Tabbladen naar links sluiten\ncloseRightTabs=Tabbladen naar rechts sluiten\naddSerial=Seriële ...\nconnect=Maak verbinding met\nworkspaces=Werkruimten\nmanageWorkspaces=Werkruimten beheren\naddWorkspace=Werkruimte toevoegen ...\nworkspaceAdd=Een nieuwe werkruimte toevoegen\nworkspaceAddDescription=Workspaces zijn verschillende configuraties voor het uitvoeren van XPipe. Elke workspace heeft een datamap waar alle gegevens lokaal worden opgeslagen. Dit omvat verbindingsgegevens, instellingen en meer.\\n\\nAls je de synchronisatiefunctie gebruikt, kun je er ook voor kiezen om elke workspace met een andere git repository te synchroniseren.\nworkspaceName=Naam werkruimte\nworkspaceNameDescription=De weergavenaam van de werkruimte\nworkspacePath=Werkruimte pad\nworkspacePathDescription=De locatie van de gegevensmap van de werkruimte\nworkspaceCreationAlertTitle=Werkruimte maken\ndeveloperForceSshTty=SSH TTY afdwingen\ndeveloperForceSshTtyDescription=Laat alle SSH-verbindingen een pty toewijzen om de ondersteuning voor een ontbrekende stderr en een pty te testen.\ndeveloperDisableSshTunnelGateways=SSH-gateway-tunneling uitschakelen\ndeveloperDisableSshTunnelGatewaysDescription=Gebruik geen tunnelsessies voor gateways en maak in plaats daarvan direct verbinding met het systeem.\nttyWarning=De verbinding heeft geforceerd een pty/tty toegewezen en biedt geen aparte stderr stream.\\n\\nDit kan tot een paar problemen leiden.\\n\\nAls je kunt, kijk dan of je het connection commando geen pty kunt laten toewijzen.\nxshellSetup=Xshell installatie\ntermiusSetup=Termius installatie\ntryPtbDescription=Nieuwe functies in een vroeg stadium uitproberen in XPipe developer builds\nconfirmVaultUnencryptTitle=Bevestig kluis ontsleuteling\nconfirmVaultUnencryptContent=Wil je echt de geavanceerde kluisversleuteling uitschakelen? Dan wordt de extra versleuteling voor opgeslagen gegevens verwijderd en worden bestaande gegevens overschreven.\nenableHttpApi=HTTP API inschakelen\nenableHttpApiDescription=Schakelt de API in, waardoor externe programma's de XPipe daemon kunnen aanroepen om acties uit te voeren met je beheerde verbindingen.\nchooseCustomIcon=Aangepast pictogram kiezen\ngitVault=Git kluis\nfileBrowser=Bestandsbrowser\nconfirmAllDeletions=Alle verwijderingen bevestigen\nconfirmAllDeletionsDescription=Of een bevestigingsvenster moet worden weergegeven voor alle verwijderbewerkingen. Standaard vereisen alleen mappen een bevestiging.\nyesterday=Gisteren\ngreen=Groen\nyellow=Geel\nblue=Blauw\nred=Rood\ncyan=Cyaan\npurple=Paars\nasktextAlertTitle=Prompt\nfileWriteSudoTitle=Sudo bestand schrijven\nfileWriteSudoContent=Het bestand dat je probeert te schrijven geeft geen schrijfrechten aan jouw gebruiker. Wil je dit bestand schrijven als root met sudo? Dit zal je automatisch tot root verheffen met de bestaande inloggegevens of via een prompt.\ndontAllowTerminalRestart=Terminal opnieuw opstarten niet toestaan\ndontAllowTerminalRestartDescription=Standaard kunnen terminalsessies opnieuw worden gestart nadat ze vanuit de terminal zijn beëindigd. Om dit mogelijk te maken, accepteert XPipe deze externe verzoeken van de terminal om de sessie opnieuw te starten\\n\\nXPipe heeft geen controle over de terminal en waar deze oproep vandaan komt, dus kwaadwillende lokale applicaties kunnen deze functionaliteit ook gebruiken om verbindingen via XPipe te starten. Het uitschakelen van deze functionaliteit voorkomt dit scenario.\nopenDocumentation=Open documentatie\nopenDocumentationDescription=Bezoek de XPipe docs pagina voor dit probleem\nrenameAll=Hernoem alle\nlogging=Loggen\nenableTerminalLogging=Terminal logging inschakelen\nenableTerminalLoggingDescription=Schakelt client-side logging in voor alle terminalsessies. Alle inputs en outputs van de terminalsessie worden in een sessie logbestand geschreven. Merk op dat gevoelige informatie zoals wachtwoordaanvragen niet worden geregistreerd.\nterminalLoggingDirectory=Terminal sessie logs\nterminalLoggingDirectoryDescription=Alle logs worden opgeslagen in de XPipe datamap op je lokale systeem.\nopenSessionLogs=Open sessie logs\nsessionLogging=Terminal registratie\nsessionActive=Er wordt een achtergrondsessie uitgevoerd voor deze verbinding.\\n\\nKlik op de statusindicator om deze sessie handmatig te stoppen.\nskipValidation=Validatie overslaan\nscriptsIntroHeader=Over scripts\nscriptsIntroContent=Je kunt scripts uitvoeren op shell init, in de bestandsbrowser en op aanvraag. Je kunt zelf scripts maken binnen XPipe of bestaande scripts importeren van je lokale systeem of van een remote git repository.\nscriptsIntroBottomHeader=Scripts gebruiken\nscriptsIntroBottomContent=Er zijn verschillende voorbeeldscripts om mee te beginnen. Je kunt op de bewerkknop van de individuele scripts klikken om te zien hoe ze zijn geïmplementeerd. Scripts moeten eerst worden ingeschakeld om te worden uitgevoerd en te worden weergegeven in menu's. Elk script heeft daarvoor een schakelaartje.\nscriptsIntroBottomButton=Aan de slag\nscriptSourcesIntroHeader=Script bronnen\nscriptSourcesIntroContent=Je kunt aangepaste scriptbronnen toevoegen om direct toegang te hebben tot een hele verzameling shellscripts. Zowel lokale bronnen als externe git repositories worden ondersteund als bron. Alle gedetecteerde scripts van de bron zullen automatisch beschikbaar worden.\nscriptSourcesIntroButton=Bron toevoegen ...\ncheckForSecurityUpdates=Controleren op beveiligingsupdates\ncheckForSecurityUpdatesDescription=XPipe kan apart van normale functie-updates controleren op mogelijke beveiligingsupdates. Als dit is ingeschakeld, worden ten minste belangrijke beveiligingsupdates aanbevolen voor installatie, zelfs als de normale updatecontrole is uitgeschakeld.\\n\\nAls je deze instelling uitschakelt, wordt er geen externe versie opgevraagd en krijg je geen melding over beveiligingsupdates.\nclickToDock=Klik om terminal te docken\nterminalStarting=Wachten op opstarten terminal ...\npinTab=Pin tab\nunpinTab=Tabblad verwijderen\npinned=Vastgepind\nenableConnectionHubTerminalDocking=Aansluit hub terminal docking inschakelen\nenableConnectionHubTerminalDockingDescription=Je kunt terminalvensters koppelen aan het venster van de XPipe-toepassing in de verbindingshub om een enigszins geïntegreerde terminal te simuleren. De terminalvensters worden dan beheerd door XPipe zodat ze altijd in het dock passen.\nenableFileBrowserTerminalDocking=Bestandsbrowser terminal docking inschakelen\nenableFileBrowserTerminalDockingDescription=Je kunt terminalvensters koppelen aan het venster van de XPipe-toepassing in de bestandsbrowser om een enigszins geïntegreerde terminal te simuleren. De terminalvensters worden dan door XPipe beheerd zodat ze altijd in het dock passen.\ndownloadsDirectory=Aangepaste downloadmap\ndownloadsDirectoryDescription=De aangepaste map om gedownloade bestanden in te plaatsen als je op de knop Verplaats naar downloads klikt. Standaard gebruikt XPipe je gebruikersdownloadmap.\npinLocalMachineOnStartup=Tabblad lokale machine vastpinnen bij het opstarten\npinLocalMachineOnStartupDescription=Automatisch een lokale machine tab openen en vastpinnen. Dit is handig als je vaak een gesplitste bestandsbrowser gebruikt met de lokale machine en het externe bestandssysteem open.\nterminalErrorDescription=Deze fout is terminaal en XPipe kan niet verder zonder deze fout op te lossen.\ngroupName=Groepsnaam\nchmodPermissions=Nieuwe machtigingen\neditFilesWithDoubleClick=Bestanden bewerken met dubbelklikken\neditFilesWithDoubleClickDescription=Als deze optie is ingeschakeld, zal dubbelklikken op bestanden deze direct openen in je tekstverwerker in plaats van het contextmenu te tonen.\ncensorMode=Censor modus\ncensorModeDescription=Vervaagt alle informatie zoals hostnamen, gebruikersnamen, verbindingsnamen en meer.\\n\\nDit is handig als je XPipe wilt screenshotten of screensharen en geen informatie wilt lekken.\naddIdentity=Identiteit ...\nidentities=Identiteiten\naddMacro=Actie ...\nidentitiesIntroHeader=Over identiteiten\nidentitiesIntroContent=Als je veelgebruikte combinaties van gebruikersnamen, wachtwoorden en sleutels hergebruikt, kan het zinvol zijn om herbruikbare identiteiten aan te maken. Zo kun je ze snel gebruiken bij het toevoegen van nieuwe verbindingen.\nidentitiesIntroBottomHeader=Identiteiten delen\nidentitiesIntroBottomContent=Je kunt identiteiten lokaal toevoegen of ze ook synchroniseren in de git repository als dit is ingeschakeld. Hierdoor kun je selectief identiteiten delen op meerdere systemen en met andere teamleden.\nidentitiesIntroBottomButton=Synchronisatie instellen\nidentitiesIntroButton=Identiteit aanmaken\nuserName=Gebruikersnaam\nuserAuth=Wachtwoordverificatie op basis van de gebruiker\ngroupAuth=Groepsgebaseerde geheime verificatie\nteam=Team\nteamSettings=Team instellingen\nteamVaults=Teamkluizen\nvaultTypeNameDefault=Standaard kluis\nvaultTypeNameLegacy=Persoonlijke kluis uit het verleden\nvaultTypeNamePersonal=Persoonlijke kluis\nvaultTypeNameTeam=Teamkluis\nteamVaultsDescription=Met teamkluizen hebben meerdere gebruikers en groepen veilige toegang tot een gedeelde kluis. Je kunt verbindingen en identiteiten zo configureren dat ze gedeeld worden voor alle gebruikers of dat ze alleen beschikbaar zijn voor individuele gebruikers en groepen door ze te versleutelen met hun eigen sleutel. Andere kluisgebruikers hebben geen toegang tot persoonlijke en groepsgebaseerde verbindingen en identiteiten als ze geen toegang hebben tot de sleutel.\nvaultTypeContentDefault=Je gebruikt momenteel een standaard kluis zonder gebruiker en aangepaste wachtwoordzin. Geheimen worden versleuteld met de lokale kluissleutel. Je kunt upgraden naar een persoonlijke kluis door een gebruikersaccount voor de kluis aan te maken. Hiermee kun je kluisgeheimen versleutelen met je eigen persoonlijke wachtwoordzin die je bij elke aanmelding moet invoeren om de kluis te ontgrendelen.\nvaultTypeContentLegacy=Je gebruikt momenteel een oude persoonlijke kluis voor je gebruiker. Geheimen worden versleuteld met je persoonlijke wachtwoordzin. Deze oude compatibiliteit heeft beperkte mogelijkheden en kan niet ter plekke worden geüpgraded naar een teamkluis.\nvaultTypeContentPersonal=Je gebruikt momenteel een persoonlijke kluis voor je gebruiker. Geheimen worden versleuteld met je persoonlijke wachtwoordzin. Je kunt upgraden naar een teamkluis door extra kluisgebruikers toe te voegen of een groepsgebaseerde toegangsconfiguratie toe te voegen.\nvaultTypeContentTeam=Je gebruikt momenteel een teamkluis, waarmee meerdere gebruikers veilige toegang hebben tot een gedeelde kluis. Je kunt verbindingen en identiteiten zo configureren dat ze gedeeld worden voor alle gebruikers of dat ze alleen beschikbaar zijn voor je persoonlijke gebruiker of groep door ze te versleutelen met je persoonlijke of groepssleutel. Andere kluisgebruikers hebben geen toegang tot je persoonlijke en groepsgebaseerde verbindingen en identiteiten als ze geen toegang hebben tot de sleutel.\ngroupManagement=Beheer van groepen\ngroupManagementEmpty=Beheer van groepen\ngroupManagementDescription=Beheer bestaande kluisgroepen of maak nieuwe aan. Elke kluisgroep heeft zijn eigen individuele geheime sleutel die wordt gebruikt om verbindingen en identiteiten te versleutelen die alleen beschikbaar mogen zijn voor de groep en niet voor anderen.\ngroupManagementEmptyDescription=Beheer bestaande kluisgroepen of maak nieuwe aan. Elke kluisgroep heeft zijn eigen individuele geheime sleutel die wordt gebruikt om verbindingen en identiteiten te versleutelen die alleen beschikbaar mogen zijn voor de groep en niet voor anderen.\\n\\nGroepsgebaseerde accounts voor een team worden ondersteund in het professionele plan.\nuserManagement=Gebruikersbeheer\nuserManagementEmpty=Gebruikersbeheer\nuserManagementDescription=Bestaande kluisgebruikers beheren of nieuwe aanmaken. Elke kluisgebruiker heeft zijn eigen individuele wachtwoord dat wordt gebruikt om verbindingen en identiteiten te versleutelen die alleen beschikbaar mogen zijn voor de gebruiker en niet voor anderen.\nuserManagementEmptyDescription=Bestaande kluisgebruikers beheren of nieuwe aanmaken. Elke kluisgebruiker heeft zijn eigen individuele wachtwoord dat wordt gebruikt om verbindingen en identiteiten te versleutelen die alleen beschikbaar mogen zijn voor de gebruiker en niet voor anderen. Maak een gebruiker voor jezelf aan om verbindingen en identiteiten te kunnen versleutelen met je persoonlijke sleutel.\\n\\nEén gebruikersaccount wordt ondersteund in de community-editie. Meerdere gebruikersaccounts voor een team worden ondersteund in het professional plan.\nuserIntroHeader=Gebruikersbeheer\nuserIntroContent=Maak de eerste gebruikersaccount voor jezelf om te beginnen. Hiermee kun je deze werkruimte vergrendelen met een wachtwoord.\naddReusableIdentity=Herbruikbare identiteit toevoegen\nusers=Gebruikers\nsyncVault=Kluis synchronisatie\nsyncVaultDescription=Om je kluis te synchroniseren met meerdere systemen of met meerdere teamleden, schakel je git synchronisatie in voor deze kluis.\nenableGitSync=Git sync inschakelen\nbrowseVault=Kluisgegevens\nbrowseVaultDescription=Je kunt de kluismap zelf bekijken in je eigen bestandsbeheerder. Merk op dat externe bewerkingen niet worden aanbevolen en allerlei problemen kunnen veroorzaken.\nbrowseVaultButton=Bladeren door kluis\nvaultUsers=Gebruikers van kluizen\ncreateHeapDump=Een heap dump maken\ncreateHeapDumpDescription=Dump de geheugeninhoud naar een bestand om het geheugengebruik te controleren\ninitializingApp=Verbindingen laden\ncheckingLicense=Licentie controleren\nloadingGit=Synchroniseren met git repo\nloadingGpg=GnuPG daemon voor git starten\nloadingSettings=Instellingen laden\nloadingConnections=Verbindingen laden\nunlockingVault=Kluis openen\nloadingUserInterface=Gebruikersinterface laden\nptbNotice=Aankondiging voor de openbare test build\nuserDeletionTitle=Verwijdering van gebruikers\nuserDeletionContent=Wil je deze kluisgebruiker verwijderen? Dit zal al je persoonlijke identiteiten en verbindingsgeheimen opnieuw versleutelen met behulp van de kluissleutel die beschikbaar is voor alle gebruikers. Dit duurt even en XPipe zal opnieuw opstarten om de gebruikerswijzigingen toe te passen.\ngroupDeletionTitle=Groep verwijderen\ngroupDeletionContent=Wil je deze kluisgroep verwijderen? Dit zal alle groep-alleen identiteiten en verbindingsgeheimen opnieuw versleutelen met behulp van de kluissleutel die beschikbaar is voor alle gebruikers. Dit duurt even en XPipe zal opnieuw opstarten om de groepswijzigingen toe te passen.\nkillTransfer=Kill overdracht\ndestination=Bestemming\nconfiguration=Configuratie\nnewFile=Nieuw bestand\nnewLink=Nieuwe link\nlinkName=Link naam\nscanConnections=Beschikbare verbindingen zoeken ...\nobserve=Beginnen met observeren\nstopObserve=Stop met observeren\ncreateShortcut=Snelkoppeling op het bureaublad maken\nbrowseFiles=Door bestanden bladeren\nclone=Kloon\ntargetPath=Doelpad\nnewDirectory=Nieuwe map\ncopyShareLink=Link kopiëren\nselectStore=Winkel selecteren\nsaveSource=Opslaan voor later\nexecute=Uitvoeren\ndeleteChildren=Alle kinderen verwijderen\nscriptGroupDescriptionDescription=Geef deze groep een optionele beschrijving\nabstractHostDescriptionDescription=Geef deze host een optionele beschrijving\nselectSource=Bron selecteren\ncommandLineRead=Bijwerken\ncommandLineWrite=Schrijf\nadditionalOptions=Extra opties\ninput=Invoer\nmachine=Machine\nopen=Open\nedit=Bewerk\nscriptContents=Inhoud van het script\nscriptContentsDescription=De uit te voeren scriptcommando's\nsnippets=Script afhankelijkheden\nsnippetsDescription=Andere scripts om eerst uit te voeren\nsnippetsDependenciesDescription=Alle mogelijke scripts die moeten worden uitgevoerd, indien van toepassing\nisDefault=Wordt uitgevoerd op init in alle compatibele shells\nbringToShells=Brengen naar alle compatibele shells\nisDefaultGroup=Alle groepsscripts uitvoeren op shell init\nexecutionType=Type uitvoering\nexecutionTypeDescription=In welke contexten kun je dit script gebruiken\nminimumShellDialect=Shell type\nminimumShellDialectDescription=Het shelltype om dit script in uit te voeren\ndumbOnly=Stom\nterminalOnly=Terminal\nboth=Beide\nshouldElevate=Moet verheffen\nshouldElevateDescription=Of dit script met verhoogde rechten moet worden uitgevoerd\nscript.displayName=Shell-script\nscript.displayDescription=Een herbruikbaar shellscript maken\nscriptGroup.displayName=Script-groep\nscriptGroup.displayDescription=Scripts groeperen en organiseren binnen\nscriptGroup=Groep\nscriptGroupDescription=De groep om dit script aan toe te wijzen\nscriptGroupGroupDescription=De optionele bovenliggende groep om deze scriptgroep aan toe te wijzen\nopenInNewTab=In nieuw tabblad openen\nexecuteInBackground=op de achtergrond\nexecuteInTerminal=in $TERM$\nback=Teruggaan\nbrowseInWindowsExplorer=Bladeren in Windows verkenner\nbrowseInDefaultFileManager=Bladeren in standaard bestandsbeheer\nbrowseInFinder=Bladeren in finder\ncopy=Kopiëren\npaste=Plakken\ncopyLocation=Locatie kopiëren\nabsolutePaths=Absolute paden\nabsoluteLinkPaths=Absolute linkpaden\nabsolutePathsQuoted=Absoluut aangehaalde paden\nfileNames=Bestandsnamen\nlinkFileNames=Bestandsnamen koppelen\nfileNamesQuoted=Bestandsnamen (Geciteerd)\ndeleteFile=Verwijderen $FILE$\neditWithEditor=Bewerken met $EDITOR$\nfollowLink=Link volgen\ngoForward=Doorgaan\nshowDetails=Details tonen\nshowDetailsDescription=Stapeltrace van fout weergeven\nopenFileWith=Openen met ...\nopenWithDefaultApplication=Openen met standaardtoepassing\nrename=Hernoemen\nrun=Uitvoeren\nopenInTerminal=Openen in terminal\nfile=Bestand\ndirectory=Directory\nsymbolicLink=Symbolische link\ndesktopEnvironment.displayName=Bureaubladomgeving\ndesktopEnvironment.displayDescription=Een herbruikbare configuratie voor de externe desktopomgeving maken\ndesktopHost=Desktop host\ndesktopHostDescription=De desktopverbinding om als basis te gebruiken\ndesktopShellDialect=Shell-dialect\ndesktopShellDialectDescription=Het te gebruiken shell dialect om scripts en applicaties uit te voeren\ndesktopSnippets=Scriptfragmenten\ndesktopSnippetsDescription=Lijst met herbruikbare scriptfragmenten om eerst uit te voeren\ndesktopInitScript=Init script\ndesktopInitScriptDescription=Init commando's specifiek voor deze omgeving\ndesktopTerminal=Terminaltoepassing\ndesktopTerminalDescription=De terminal op het bureaublad om scripts in te starten\ndesktopApplication.displayName=Desktop toepassing\ndesktopApplication.displayDescription=Een toepassing uitvoeren op een extern bureaublad\ndesktopBase=Desktop\ndesktopBaseDescription=Het bureaublad waarop deze toepassing wordt uitgevoerd\ndesktopEnvironmentBase=Bureaubladomgeving\ndesktopEnvironmentBaseDescription=De desktopomgeving om deze toepassing op uit te voeren\ndesktopApplicationPath=Pad van toepassing\ndesktopApplicationPathDescription=Het pad van het uitvoerbare bestand dat moet worden uitgevoerd\ndesktopApplicationArguments=Argumenten\ndesktopApplicationArgumentsDescription=De optionele argumenten om door te geven aan de toepassing\ndesktopCommand.displayName=Desktop opdracht\ndesktopCommand.displayDescription=Een opdracht uitvoeren in een externe desktopomgeving\ndesktopCommandScript=Opdrachten\ndesktopCommandScriptDescription=De commando's die in de omgeving moeten worden uitgevoerd\nservice.displayName=Service\nservice.displayDescription=Een service op afstand doorsturen naar je lokale machine\nserviceLocalPort=Expliciete lokale poort\nserviceLocalPortDescription=De lokale poort om naar door te sturen, anders wordt een willekeurige poort gebruikt\nserviceRemotePort=Externe poort\nserviceRemotePortDescription=De poort waarop de service draait\nserviceHost=Service host\nserviceHostDescription=De host waarop de service draait\nopenWebsite=Open website\ncustomServiceGroup.displayName=Servicegroep\ncustomServiceGroup.displayDescription=Groepeer meerdere diensten in één categorie\ninitScript=Init script - Uitvoeren op shell init\nshellScript=Shell-sessiescript - Script beschikbaar maken om uit te voeren tijdens een shellsessie\nrunnableScript=Runnable script - Maakt het mogelijk om een script direct vanuit de verbindingshub uit te voeren\nfileScript=Bestandsscript - Laat script aanroepen voor geselecteerde bestanden in de bestandsbrowser\nrunScript=Script uitvoeren\ncopyUrl=URL kopiëren\nfixedServiceGroup.displayName=Servicegroep\nfixedServiceGroup.displayDescription=Een lijst van beschikbare services op een systeem\nmappedService.displayName=Service\nmappedService.displayDescription=Interactie met een service die wordt aangeboden door een container\ncustomService.displayName=Service\ncustomService.displayDescription=Automatisch een servicepoort op afstand openen of tunnelen op je lokale machine\nfixedService.displayName=Service\nfixedService.displayDescription=Een vooraf gedefinieerde service gebruiken\nnoServices=Geen beschikbare diensten\nhasServices=$COUNT$ beschikbare diensten\nhasService=$COUNT$ beschikbare dienst\nnoConnections=Geen beschikbare verbindingen\nhasConnections=$COUNT$ beschikbare verbindingen\nhasConnection=$COUNT$ beschikbare verbinding\nopenHttp=Open HTTP service\nopenHttps=Open HTTPS service\nnoScriptsAvailable=Geen ingeschakelde en compatibele scripts beschikbaar\nscriptsDisabled=Scripts uitgeschakeld\nchangeIcon=Pictogram wijzigen\ninit=Init\nshell=Shell\nhub=Hub\nscript=script\ngenericScript=Algemeen\ngradleTasks=Gradle taken\nrunTask=Taak uitvoeren\narchiveName=Naam archief\ncompress=Comprimeren\ncompressContents=Inhoud comprimeren\nuntarHere=Untar hier\nuntarDirectory=Naar $DIR$\nunzipDirectory=Uitpakken naar $DIR$\nunzipHere=Hier uitpakken\nrequiresRestart=Vereist een herstart om toe te passen.\ndownload=Downloaden\nservicePath=Servicepad\nservicePathDescription=Het optionele subpad bij het openen van de URL in een browser\nactive=Actief\ninactive=Inactief\nstarting=Beginnen met\nremotePort=Externe poort\nremotePortNumber=Externe poort $PORT$\nuserIdentity=Persoonlijke identiteit\nglobalIdentity=Globale identiteit\nidentityChoice=Identiteit gebruiker\nidentityChoiceDescription=Kies een vooraf gedefinieerde identiteit of specificeer inloggegevens alleen voor deze verbinding\ndefineNewIdentityOrSelect=Nieuw invoeren of bestaand kiezen\nlocalIdentity.displayName=Lokale identiteit\nlocalIdentity.displayDescription=Maak een herbruikbare identiteit voor deze lokale desktop\nsyncedIdentity.displayName=Gesynchroniseerde identiteit\nsyncedIdentity.displayDescription=Een herbruikbare identiteit creëren die in verschillende systemen wordt gesynchroniseerd\nlocalIdentity=Lokale identiteit\nkeyNotSynced=Sleutelbestand is nog niet gesynchroniseerd met git repository. Gebruik de add to git knop voor het sleutelbestand om het toe te voegen.\nusernameDescription=De gebruikersnaam om als in te loggen\nidentity.displayName=Identiteit\nidentity.displayDescription=Een herbruikbare identiteit voor verbindingen maken\nlocal=Lokaal\nshared=Wereldwijde\nuserDescription=De gebruikersnaam of vooraf gedefinieerde identiteit om als in te loggen\nidentityAccessLevel=Toegangsniveau\nidentityPerUser=Persoonlijke identiteitstoegang\nidentityPerUserDescription=Beperk de toegang tot deze identiteit en de bijbehorende verbindingen alleen tot je kluisgebruiker\nidentityPerUserDisabled=Persoonlijke identiteitstoegang (uitgeschakeld)\nidentityPerUserDisabledDescription=Beperk de toegang tot deze identiteit en de bijbehorende verbindingen tot alleen je kluisgebruiker (Vereist dat team geconfigureerd is)\nidentityPerGroup=Alleen groepstoegang tot identiteit\nidentityPerGroupDescription=Beperk de toegang tot deze identiteit en de bijbehorende verbindingen alleen tot deze kluisgroep\nlibrary=Bibliotheek\nlocation=Locatie\nkeyAuthentication=Verificatie op basis van sleutels\nkeyAuthenticationDescription=De te gebruiken verificatiemethode als verificatie op basis van sleutels vereist is\nlocationDescription=Het bestandspad van je corresponderende privésleutel\nkeyFile=Lokaal sleutelbestand\nkeyPassword=Passphrase\nkey=Sleutel\nyubikeyPiv=Yubikey PIV\npageant=Verkiezing\ngpgAgent=GPG-agent\ncustomPkcs11Library=Aangepaste PKCS#11-bibliotheek\nsshAgent=OpenSSH agent\nnone=Geen\nindex=Index ...\notherExternal=Andere externe agent\nsync=Synchroniseren\nvaultSync=Kluis sync\ncustomUsername=Gebruikersnaam\ncustomUsernameDescription=De optionele alternatieve gebruiker om als in te loggen\ncustomUsernamePassword=Wachtwoord\ncustomUsernamePasswordDescription=Het wachtwoord van de gebruiker dat moet worden gebruikt als sudo-authenticatie vereist is\nshowInternalPods=Toon interne pods\nshowAllNamespaces=Toon alle naamruimtes\nshowInternalContainers=Toon interne containers\nrefresh=Vernieuwen\nvmwareGui=GUI starten\nmonitorVm=Monitor VM\naddCluster=Cluster toevoegen ...\nshowNonRunningInstances=Niet-lopende instanties tonen\nvmwareGuiDescription=Of een virtuele machine op de achtergrond of in een venster moet worden gestart.\nvmwareEncryptionPassword=Encryptie wachtwoord\nvmwareEncryptionPasswordDescription=Het optionele wachtwoord dat wordt gebruikt om de VM te versleutelen.\nvmPasswordDescription=Het vereiste wachtwoord voor de gastgebruiker.\nvmPassword=Wachtwoord gebruiker\nvmUser=Gast gebruiker\nrunTempContainer=Tijdelijke container uitvoeren\nvmUserDescription=De gebruikersnaam van je primaire gastgebruiker\ndockerTempRunAlertTitle=Tijdelijke container uitvoeren\ndockerTempRunAlertHeader=Hiermee wordt een shell-proces uitgevoerd in een tijdelijke container die automatisch wordt verwijderd zodra het wordt gestopt.\nimageName=Naam afbeelding\nimageNameDescription=De container image identifier om te gebruiken\ncontainerName=Containernaam\ncontainerNameDescription=De optionele aangepaste containernaam\nvm=Virtuele machine\nvmDescription=Het bijbehorende configuratiebestand.\nvmwareScan=VMware desktop hypervisors\nvmwareMachine.displayName=VMware virtuele machine\nvmwareMachine.displayDescription=Verbinding maken met een virtuele machine via SSH\nvmwareInstallation.displayName=VMware desktop hypervisor installatie\nvmwareInstallation.displayDescription=Interactie met de geïnstalleerde VM's via de CLI\nstart=Start\nstop=Stop\npause=Pauze\nrdpTunnelHost=Doelhost\nrdpTunnelHostDescription=De SSH-verbinding om de RDP-verbinding naar toe te tunnelen\nrdpTunnelUsername=Gebruikersnaam\nrdpTunnelUsernameDescription=De aangepaste gebruiker om als in te loggen, gebruikt de SSH-gebruiker als deze leeg wordt gelaten\nrdpFileLocation=Bestandslocatie\nrdpFileLocationDescription=Het bestandspad van het .rdp bestand\nrdpPasswordAuthentication=Wachtwoord verificatie\nrdpFiles=RDP bestanden\nrdpPasswordAuthenticationDescription=Het wachtwoord om in te vullen of te kopiëren naar het klembord, afhankelijk van de clientondersteuning\nrdpFile.displayName=RDP-bestand\nrdpFile.displayDescription=Verbinding maken met een systeem via een bestaand .rdp bestand\nrequiredSshServerAlertTitle=SSH-server instellen\nrequiredSshServerAlertHeader=Kan geen geïnstalleerde SSH-server in de VM vinden.\nrequiredSshServerAlertContent=Om verbinding te maken met de VM zoekt XPipe naar een draaiende SSH-server, maar er is geen beschikbare SSH-server gedetecteerd voor de VM.\ncomputerName=Computer Naam\npssComputerNameDescription=De computernaam om verbinding mee te maken\ncredentialUser=Credential Gebruiker\ncredentialUserDescription=De gebruiker om als in te loggen.\ncredentialPassword=Wachtwoord\ncredentialPasswordDescription=Het wachtwoord van de gebruiker.\nsshConfig=SSH configuratiebestanden\nautostart=Automatisch verbinding maken bij het opstarten van XPipe\nacceptHostKey=Hostsleutel accepteren\nmodifyHostKeyPermissions=Machtigingen voor hostsleutels wijzigen\nattachContainer=Bijvoegen\ncontainerLogs=Logboeken tonen\nopenSftpClient=Openen in een externe SFTP-client\nopenTermius=Openen in Termius\nshowInternalInstances=Interne instanties tonen\neditPod=Pod bewerken\nacceptHostKeyDescription=Vertrouw de nieuwe hostsleutel en ga verder\nmodifyHostKeyPermissionsDescription=Probeer de rechten van het originele bestand te verwijderen zodat OpenSSH tevreden is\npsSession.displayName=PowerShell sessie op afstand\npsSession.displayDescription=Verbinding maken via New-PSSession en Enter-PSSession\nsshLocalTunnel.displayName=Lokale SSH-tunnel\nsshLocalTunnel.displayDescription=Een SSH-tunnel opzetten naar een host op afstand\nsshRemoteTunnel.displayName=SSH-tunnel op afstand\nsshRemoteTunnel.displayDescription=Een omgekeerde SSH-tunnel opzetten vanaf een host op afstand\nsshDynamicTunnel.displayName=Dynamische SSH-tunnel\nsshDynamicTunnel.displayDescription=Een SOCKS proxy opzetten via een SSH verbinding\nshellEnvironmentGroup.displayName=Shell-omgevingen\nshellEnvironmentGroup.displayDescription=Shell-omgevingen\nshellEnvironment.displayName=Shell-omgeving\nshellEnvironment.displayDescription=Een aangepaste shell opstartomgeving maken\nshellEnvironment.informationFormat=$TYPE$ omgeving\nshellEnvironment.elevatedInformationFormat=$ELEVATION$ $TYPE$ omgeving\nenvironmentConnectionDescription=De basisverbinding om een omgeving te creëren voor\nenvironmentScriptDescription=Het optionele aangepaste init-script om in de shell te draaien\nenvironmentSnippets=Shell-scripts\ncommandSnippetsDescription=De optionele voorgedefinieerde shellscripts om eerst uit te voeren\nenvironmentSnippetsDescription=De optionele voorgedefinieerde shellscripts om uit te voeren bij initialisatie\nshellTypeDescription=Het expliciete shell-type om te starten\noriginPort=Oorsprong poort\noriginAddress=Adres van herkomst\nremoteAddress=Adres op afstand\nremoteSourceAddress=Bronadres op afstand\nremoteSourcePort=Bronpoort op afstand\noriginDestinationPort=Oorsprong bestemmingspoort\noriginDestinationAddress=Herkomst bestemmingsadres\norigin=Oorsprong\nremoteHost=Externe host\naddress=Adres\nproxmox.displayName=Proxmox\nproxmox.displayDescription=Verbinding maken met systemen in een Proxmox virtuele omgeving\nproxmoxVm.displayName=Proxmox VM\nproxmoxVm.displayDescription=Verbinding maken met een virtuele machine in een Proxmox VE via SSH\nproxmoxContainer.displayName=Proxmox Container\nproxmoxContainer.displayDescription=Verbinding maken met een container in een Proxmox VE\nsshDynamicTunnel.hostDescription=Het systeem om te gebruiken als SOCKS proxy\nsshDynamicTunnel.bindingDescription=Aan welke adressen de tunnel moet worden gebonden\nsshRemoteTunnel.hostDescription=Het systeem van waaruit de tunnel op afstand naar de oorsprong wordt gestart\nsshRemoteTunnel.bindingDescription=Aan welke adressen de tunnel moet worden gebonden\nsshLocalTunnel.hostDescription=Het systeem om de tunnel naar te openen\nsshLocalTunnel.bindingDescription=Aan welke adressen de tunnel moet worden gebonden\nsshLocalTunnel.localAddressDescription=Het lokale adres om te binden\nsshLocalTunnel.remoteAddressDescription=Het adres op afstand om te binden\ncmd.displayName=Opdracht\ncmd.displayDescription=Een willekeurig commando op een systeem uitvoeren\nk8sPod.displayName=Kubernetes Pod\nk8sPod.displayDescription=Verbinding maken met een pod en zijn containers via kubectl\nk8sContainer.displayName=Kubernetes Container\nk8sContainer.displayDescription=Open een shell naar een container\nk8sCluster.displayName=Kubernetes Cluster\nk8sCluster.displayDescription=Verbinding maken met een cluster en zijn pods via kubectl\nsshTunnelGroup.displayName=SSH-tunnels\nsshTunnelGroup.displayCategory=Alle soorten SSH-tunnels\nlocal.displayName=Lokale machine\nlocal.displayDescription=De shell van de lokale machine\ncygwin=Cygwin\nmsys2=MSYS2\ngitWindows=Git voor Windows\ngitForWindows.displayName=Git voor Windows\ngitForWindows.displayDescription=Toegang tot je lokale Git For Windows omgeving\nmsys2.displayName=MSYS2\nmsys2.displayDescription=Toegangsshells van je MSYS2-omgeving\ncygwin.displayName=Cygwin\ncygwin.displayDescription=Toegang tot shells van je Cygwin-omgeving\nnamespace=Naamruimte\ngitVaultIdentityStrategy=Git SSH identiteit\ngitVaultIdentityStrategyDescription=Als je ervoor hebt gekozen om een SSH git URL te gebruiken als de remote en je remote repository vereist een SSH identiteit, stel dan deze optie in.\\n\\nAls je een HTTP url hebt opgegeven, dan kun je deze optie negeren.\ndockerContainers=Docker containers\ndockerCmd.displayName=docker CLI-client\ndockerCmd.displayDescription=Toegang tot Docker-containers via de docker CLI-client\nwslCmd.displayName=WSL installeren\nwslCmd.displayDescription=Toegang tot WSL instanties via de wsl CLI client\nk8sCmd.displayName=kubectl cliënt\nk8sCmd.displayDescription=Toegang tot Kubernetes-clusters via kubectl\nk8sClusters=Kubernetes clusters\nshells=Beschikbare schelpen\ninspectContainer=Inspecteer\ninspectContext=Inspecteer\nk8sClusterNameDescription=De naam van de context waarin het cluster zich bevindt.\npod=Pod\npodName=Pod naam\nk8sClusterContext=Context\nk8sClusterContextDescription=De naam van de context waarin het cluster zich bevindt\nk8sClusterNamespace=Naamruimte\nk8sClusterNamespaceDescription=De aangepaste naamruimte of de standaard naamruimte als deze leeg is\nk8sConfigLocation=Configuratiebestand\nk8sConfigLocationDescription=Het aangepaste kubeconfig bestand of het standaard kubeconfig bestand indien leeg gelaten\ninspectPod=Inspecteer\nshowAllContainers=Niet-lopende containers tonen\nshowAllPods=Niet-lopende pods tonen\nk8sPodHostDescription=De host waarop de pod zich bevindt\nk8sContainerDescription=De naam van de Kubernetes container\nk8sPodDescription=De naam van de Kubernetes pod\npodDescription=De pod waarop de container zich bevindt\nk8sClusterHostDescription=De host via welke het cluster benaderd moet worden. Kubectl moet geïnstalleerd en geconfigureerd zijn om toegang te krijgen tot het cluster.\nconnection=Verbinding\nshellCommand.displayName=Aangepast shell commando\nshellCommand.displayDescription=Een standaard shell openen via een aangepast commando\nssh.displayName=SSH-verbinding\nssh.displayDescription=Verbinding maken met een systeem op afstand via de opdrachtregelclient SSH\nsshConfig.displayName=SSH config bestand\nsshConfig.displayDescription=Verbinding maken met hosts gedefinieerd in een SSH config bestand\nsshConfigHost.displayName=SSH config bestand host\nsshConfigHost.displayDescription=Verbinding maken met een host gedefinieerd in een SSH config bestand\nsshConfigHost.password=Wachtwoord\nsshConfigHost.passwordDescription=Geef het optionele wachtwoord voor het inloggen van de gebruiker.\nsshConfigHost.identityPassphrase=Sleutel wachtwoordzin\nsshConfigHost.identityPassphraseDescription=Geef de optionele wachtwoordzin voor je sleutel op.\nshellCommand.hostDescription=De host waarop het commando moet worden uitgevoerd\nshellCommand.commandDescription=Het commando dat een shell opent\ncommandType=Type opdracht\ncommandTypeDescription=Hoe het commando uit te voeren\ncommandDescription=De aangepaste commando's om uit te voeren op de host\ncommandHostDescription=De host waarop het commando moet worden uitgevoerd\ncommandDataFlowDescription=Hoe dit commando omgaat met invoer en uitvoer\ncommandElevationDescription=Voer dit commando uit met verhoogde rechten\ncommandShellTypeDescription=De shell die gebruikt moet worden voor dit commando\nlimitedSystem=Dit is een beperkt of ingebed systeem\nlimitedSystemDescription=Probeer niet het type shell te identificeren, noodzakelijk voor beperkte ingebedde systemen of IOT-apparaten\nsshForwardX11=X11 doorsturen\nsshForwardX11Description=Schakelt X11-forwarding in voor de verbinding\ncustomAgent=Aangepaste agent\nidentityAgent=Identiteitsagent\nssh.proxyDescription=De optionele proxy host om te gebruiken bij het maken van de SSH verbinding. Er moet een ssh-client geïnstalleerd zijn.\nusage=Gebruik\nwslHostDescription=De host waarop de WSL instantie zich bevindt. Moet wsl geïnstalleerd hebben.\nwslDistributionDescription=De naam van de WSL instantie\nwslUsernameDescription=De expliciete gebruikersnaam om mee in te loggen. Als deze niet wordt opgegeven, wordt de standaard gebruikersnaam gebruikt.\nwslPasswordDescription=Het wachtwoord van de gebruiker dat gebruikt kan worden voor sudo commando's.\ndockerHostDescription=De host waarop de docker container zich bevindt. Moet docker geïnstalleerd hebben.\ndockerContainerDescription=De naam van de docker container\nlocalMachine=Lokale machine\nrootScan=Sudo shell-omgeving\nloginEnvironmentScan=Aangepaste inlogomgeving\nk8sScan=Kubernetes cluster\noptions=Opties\ndockerRunningScan=Docker-containers draaien\ndockerAllScan=Alle docker-containers\nwslScan=WSL instanties\nsshScan=SSH config verbindingen\nrunAsUser=Uitvoeren als gebruiker\nrunAsUserDescription=Start deze shell-omgeving als een andere gebruiker\ndefault=Standaard\nadministrator=Beheerder\nwslHost=WSL host\ntimeout=Time-out\ninstallLocation=Locatie installeren\ninstallLocationDescription=De locatie waar je $NAME$ omgeving is geïnstalleerd\nwsl.displayName=Windows Subsysteem voor Linux\nwsl.displayDescription=Verbinding maken met een WSL-instantie die op Windows draait\ndocker.displayName=Docker Container\ndocker.displayDescription=Verbinding maken met een docker-container\nport=Poort\nuser=Gebruiker\npassword=Wachtwoord\nmethod=Methode\nuri=URL\nproxy=Proxy\ndistribution=Distributie\nusername=Gebruikersnaam\nshellType=Shell type\nbrowseFile=Bestand doorbladeren\nopenShell=Open shell in terminal\nopenCommand=Opdracht uitvoeren in terminal\neditFile=Bestand bewerken\ndescription=Beschrijving\nfurtherCustomization=Verdere aanpassing\nfurtherCustomizationDescription=Gebruik voor meer configuratieopties de ssh config bestanden\nbrowse=Bladeren op\nconfigHost=Host\nconfigHostDescription=De host waarop de config zich bevindt\nconfigLocation=Configuratie locatie\nconfigLocationDescription=Het bestandspad van het configuratiebestand\ngateway=Gateway\ngatewayDescription=De optionele gateway om te gebruiken bij het verbinden\nconnectionInformation=Verbindingsinformatie\nconnectionInformationDescription=Met welk systeem verbinding maken\npasswordAuthentication=Wachtwoord verificatie\npasswordAuthenticationDescription=Het optionele wachtwoord voor verificatie\nsshConfigString.displayName=Op configuratie gebaseerde SSH-verbinding\nsshConfigString.displayDescription=Een volledig aangepaste SSH-verbinding maken in het SSH config-formaat\nsshConfigStringContent=Configuratie\nsshConfigStringContentDescription=SSH opties voor de verbinding in het OpenSSH config formaat\nvnc.displayName=VNC-verbinding over SSH\nvnc.displayDescription=Open een VNC-sessie over een getunnelde verbinding\nbinding=Binden\nvncPortDescription=De poort waarop de VNC-server luistert\nrdpPortDescription=De poort waarop de RDP-server luistert\nvncUsername=Gebruikersnaam\nvncUsernameDescription=De optionele VNC-gebruikersnaam\nvncPassword=Wachtwoord\nvncPasswordDescription=Het VNC-wachtwoord\nx11WslInstance=X11 Voorwaartse WSL-instantie\nx11WslInstanceDescription=De lokale Windows Subsystem for Linux distributie om te gebruiken als X11 server bij het gebruik van X11 forwarding in een SSH verbinding. Deze distributie moet een WSL2 distributie zijn.\nopenAsRoot=Openen als root\nopenInWSL=Openen in WSL\nlaunch=Start\nsshTrustKeyContent=De hostsleutel is niet bekend en je hebt handmatige hostsleutelverificatie ingeschakeld. $CONTENT$\nsshTrustKeyTitle=Onbekende hostsleutel\nrdpTunnel.displayName=RDP-verbinding over SSH\nrdpTunnel.displayDescription=Verbinding maken via RDP over een getunnelde verbinding\nrdpEnableDesktopIntegration=Desktopintegratie inschakelen\nrdpEnableDesktopIntegrationDescription=Toepassingen op afstand uitvoeren in de veronderstelling dat de RDP toestaanlijst dat toestaat\nrdpSetupAdminTitle=RDP installatie vereist\nrdpSetupAllowTitle=RDP-toepassing op afstand\nrdpSetupAllowContent=Externe toepassingen rechtstreeks starten is momenteel niet toegestaan op dit systeem. Wil je dit inschakelen? Hiermee kun je je externe toepassingen rechtstreeks vanuit XPipe starten door de toestaanlijst voor RDP-toepassingen op afstand uit te schakelen.\nrdpServerEnableTitle=RDP server\nrdpServerEnableContent=De RDP-server is uitgeschakeld op het doelsysteem. Wil je deze inschakelen in het register om RDP-verbindingen op afstand mogelijk te maken?\nrdp=RDP\nrdpScan=RDP tunnel over SSH\nwslX11SetupTitle=WSL X11 instelling\nwslX11SetupContent=XPipe kan je lokale WSL distributie gebruiken om als X11 weergaveserver te fungeren. Wil je X11 instellen op $DIST$? Dit installeert de basis X11 pakketten op de WSL distributie en kan even duren. Je kunt ook in het instellingenmenu veranderen welke distributie wordt gebruikt.\ncommand=Opdracht\ncommandGroup=Opdrachtgroep\nvncSystem=VNC doelsysteem\nvncSystemDescription=Het eigenlijke systeem om mee te communiceren. Dit is meestal hetzelfde als de tunnelhost\nvncHost=Doel VNC host\nvncHostDescription=Het systeem waarop de VNC-server draait\nvncDirectHost=Host\nvncDirectHostDescription=De hostvermelding of het handmatige adres van de server waarop de VNC-server draait\nrdpDirectHost=Host\nrdpDirectHostDescription=De hostvermelding of het handmatige adres van de server waarop de RDP-server draait\ngitVaultTitle=Git kluis\ngitVaultForcePushContent=Wil je een push naar de remote repository forceren? Dit zal alle inhoud van het remote repository volledig vervangen door je lokale, inclusief de geschiedenis.\ngitVaultOverwriteLocalContent=Wil je je lokale kluiswijzigingen overschrijven? Dit zal alle wijzigingen op afstand toepassen op je lokale repository.\nrdpSimple.displayName=Directe RDP-verbinding\nrdpSimple.displayDescription=Verbinding maken met een host via RDP\nrdpUsername=Gebruikersnaam\nrdpUsernameDescription=De gebruiker om als in te loggen. Kan een domeinvoorvoegsel bevatten\naddressDescription=Waar je verbinding mee moet maken\nrdpAdditionalOptions=Extra RDP opties\nrdpAdditionalOptionsDescription=Rauwe RDP-opties om op te nemen, in dezelfde opmaak als in .rdp-bestanden\nproxmoxVncConfirmTitle=VNC-toegang\nproxmoxVncConfirmContent=Wil je VNC-toegang inschakelen voor de VM? Hiermee wordt directe VNC-clienttoegang ingeschakeld in het VM-configuratiebestand en wordt de virtuele machine opnieuw opgestart.\ndockerContext.displayName=Docker context\ndockerContext.displayDescription=Interactie met containers in een specifieke context\nvmActions=VM-acties\ndockerContextActions=Context acties\nk8sPodActions=Pod acties\nopenVnc=VNC-toegang inschakelen\naddVnc=VNC-verbinding toevoegen\ncommandGroup.displayName=Opdrachtgroep\ncommandGroup.displayDescription=Groep beschikbare commando's voor een systeem\nserial.displayName=Seriële verbinding\nserial.displayDescription=Een seriële verbinding in een terminal openen\nserialPort=Seriële poort\nserialPortDescription=De seriële poort / het apparaat waarmee verbinding moet worden gemaakt\nbaudRate=Baudrate\ndataBits=Gegevensbits\nstopBits=Stopbits\nparity=Pariteit\nflowControlWindow=Debietregeling\nserialImplementation=Seriële implementatie\nserialImplementationDescription=Het gereedschap om verbinding te maken met de seriële poort\nserialHost=Host\nserialHostDescription=Het systeem om toegang te krijgen tot de seriële poort op\nserialPortConfiguration=Seriële poort configuratie\nserialPortConfigurationDescription=Configuratieparameters van het aangesloten seriële apparaat\nserialInformation=Seriële informatie\nopenXShell=Openen in XShell\ntsh.displayName=Teleport\ntsh.displayDescription=Verbinding maken met je teleportknooppunten via tsh\ntshNode.displayName=Teleport knooppunt\ntshNode.displayDescription=Verbinding maken met een teleport knooppunt in een cluster\nteleportCluster=Cluster\nteleportClusterDescription=Het cluster waar het knooppunt zich in bevindt\nteleportProxy=Proxy\nteleportProxyDescription=De proxyserver die wordt gebruikt om verbinding te maken met het knooppunt\nteleportHost=Host\nteleportHostDescription=De hostnaam van het knooppunt\nteleportUser=Gebruiker\nteleportUserDescription=De gebruiker om als in te loggen\nlogin=Inloggen\nhyperVInstall.displayName=Hyper-V\nhyperVInstall.displayDescription=Verbinding maken met VM's die worden beheerd door Hyper-V\nhyperVVm.displayName=Hyper-V VM\nhyperVVm.displayDescription=Verbinding maken met een Hyper-V VM via SSH of PSSession\ntrustHost=Vertrouwende host\ntrustHostDescription=ComputerNaam toevoegen aan vertrouwde hosts lijst\ncopyIp=IP kopiëren\nvncDirect.displayName=Directe VNC-verbinding\nvncDirect.displayDescription=Rechtstreeks verbinding maken met een systeem via VNC\neditConfiguration=Configuratie bewerken\nviewInDashboard=Weergave in dashboard\nsetDefault=Standaard instellen\nremoveDefault=Standaard verwijderen\nconnectAsOtherUser=Verbinden als andere gebruiker\nprovideUsername=Geef een alternatieve gebruikersnaam om mee in te loggen\nvmIdentity=Identiteit van de gast\nvmIdentityDescription=De SSH-identiteitsverificatiemethode om eventueel te gebruiken om verbinding te maken\nvmPort=Poort\nvmPortDescription=De poort om verbinding mee te maken via SSH\nforwardAgent=Doorstuuragent\nforwardAgentDescription=SSH-agent identiteiten beschikbaar maken op het externe systeem\nvirshUri=URI\nvirshUriDescription=De hypervisor URI, aliassen worden ook ondersteund\nvirshDomain.displayName=libvirt domein\nvirshDomain.displayDescription=Verbinding maken met een libvirt-domein\nvirshHypervisor.displayName=libvirt hypervisor\nvirshHypervisor.displayDescription=Verbinding maken met een door libvirt ondersteund hypervisor-stuurprogramma\nvirshInstall.displayName=libvirt opdrachtregelclient\nvirshInstall.displayDescription=Verbinding maken met alle beschikbare libvirt hypervisors via virsh\naddHypervisor=Hypervisor toevoegen\ninteractiveTerminal=Interactieve terminal\neditDomain=Domein bewerken\nlibvirt=libvirt domeinen\ncustomIp=Aangepast IP\ncustomIpDescription=De standaard lokale VM IP-detectie opheffen als je geavanceerde netwerken gebruikt\nautomaticallyDetect=Automatisch detecteren\nuserAddDialogTitle=Gebruiker aanmaken\ngroupAddDialogTitle=Groep maken\npassphrase=Passphrase\nrepeatPassphrase=Herhaal wachtwoordzin\ngroupSecret=Groepsgeheim\nrepeatGroupSecret=Groepsgeheim herhalen\nvaultGroup=Kluisgroep\nloginAlertTitle=Inloggen vereist\nloginAlertHeader=Ontgrendel kluis om toegang te krijgen tot je persoonlijke verbindingen\nvaultUser=Kluis gebruiker\nme=Ik\naddGroup=Groep toevoegen ...\naddGroupDescription=Maak een nieuwe groep voor deze kluis\naddUser=Gebruiker toevoegen ...\naddUserDescription=Maak een nieuwe gebruiker voor deze kluis\nskip=Overslaan\nuserChangePasswordAlertTitle=Wachtwoord wijzigen\ngroupChangeSecretAlertTitle=Geheime verandering\ndocs=Documentatie\nlxd.displayName=LXD-container\nlxd.displayDescription=Verbinding maken met een LXD-container via lxc\nlxdCmd.displayName=LXD CLI-client\nlxdCmd.displayDescription=Toegang tot LXD-containers via de lxc CLI-client\npodman.displayName=Podman Container\npodman.displayDescription=Verbinding maken met een Podman-container\nincusInstall.displayName=Incus machinebeheerder\nincusInstall.displayDescription=Toegang tot incus containers via de incus CLI client\nincusContainer.displayName=Incus container\nincusContainer.displayDescription=Verbinding maken met een incus container\npodmanCmd.displayName=Podman CLI\npodmanCmd.displayDescription=Toegang tot Podman containers via de CLI-client\nlxdHostDescription=De host waarop de LXD container zich bevindt. Moet lxc geïnstalleerd hebben.\nlxdContainerDescription=De naam van de LXD-container\npodmanContainers=Podman containers\nlxdContainers=LXD containers\nincusContainers=Incus containers\ncontainer=Container\nhost=Host\ncontainerActions=Container acties\nserialConsole=Seriële console\neditRunConfiguration=Run-configuratie bewerken\ncommunityDescription=Een power-tool voor verbindingen, perfect voor persoonlijk gebruik.\nupgradeDescription=Professioneel verbindingsbeheer voor je hele serverinfrastructuur.\ndiscoverPlans=Upgrade-opties ontdekken\nextendProfessional=Upgrade naar de nieuwste professionele functies\ncommunityItem1=Onbeperkte verbindingen met niet-commerciële systemen en hulpmiddelen\ncommunityItem2=Naadloze integratie met je geïnstalleerde terminals en editors\ncommunityItem3=Volledig uitgeruste bestandsbrowser op afstand\ncommunityItem4=Krachtig scriptsysteem voor alle shells\ncommunityItem5=Git-integratie voor synchronisatie en het delen van verbindingsinformatie\nupgradeItem1=Omvat alle community-editie functies\nupgradeItem2=Het Homelab-plan ondersteunt onbeperkte hypervisors en geavanceerde SSH-functies\nupgradeItem3=Het Professional plan ondersteunt bovendien bedrijfsbesturingssystemen en -tools\nupgradeItem4=Het Enterprise-plan biedt volledige flexibiliteit voor jouw individuele gebruikssituatie\nupgrade=Upgrade\nupgradeTitle=Beschikbare plannen\nstatus=Status\ntype=Type\nlicenseAlertTitle=Licentie vereist\nuseCommunity=Verder met gemeenschap\npreviewDescription=Probeer nieuwe functies een paar weken na de release uit.\ntryPreview=Voorbeeld activeren\npreviewItem1=Volledige toegang tot nieuwe professionele functies gedurende 2 weken na de release\npreviewItem2=Nieuwe functies uitproberen zonder enige verplichting\nlicensedTo=Gelicentieerd aan\nemail=E-mailadres\napply=Toepassen\nclear=Wis\nactivate=Activeer\nvalidUntil=Geldig tot\nlicenseActivated=Licentie geactiveerd\nrestart=Herstart\nlockVault=Kluis\nrestartApp=XPipe opnieuw opstarten\nfree=Gratis\nupgradeInfo=Informatie over het upgraden naar een licentie vind je hieronder.\nupgradeInfoPreview=Je kunt hieronder informatie vinden over het upgraden naar een licentie of de preview uitproberen.\nenterLicenseKey=Licentiesleutel invoeren om te upgraden\nisOnlySupported=wordt alleen ondersteund met minimaal een $TYPE$ licentie\nareOnlySupported=worden alleen ondersteund met minimaal een $TYPE$ licentie\nlegacyLicense=Deze licentie omvat alleen nieuwe professionele functies die binnen een jaar na aankoop worden uitgebracht.\npreviewExpiredLicense=Deze functie was onlangs gratis beschikbaar in preview, maar deze periode is nu verstreken.\nopenApiDocs=API-documentatie\nopenApiDocsDescription=De HTTP API documentatie is online beschikbaar, inclusief een OpenAPI .yaml specificatie. Je kunt deze openen in je webbrowser of in de HTTP-client van je voorkeur.\nopenApiDocsButton=Open documenten\npythonApi=Python API\npersonalConnection=Deze verbinding en al haar kinderen zijn alleen beschikbaar voor jouw gebruiker omdat ze afhankelijk zijn van een persoonlijke identiteit.\ndeveloperPrintInitFiles=Uitvoering init-bestand afdrukken\ndeveloperPrintInitFilesDescription=Alle shell init scripts afdrukken die worden uitgevoerd wanneer een terminal wordt gestart.\ndeveloperShowSensitiveCommands=Log gevoelige commando's\ndeveloperShowSensitiveCommandsDescription=Gevoelige commando's opnemen in loguitvoer voor debuggen.\ncheckingForUpdates=Controleren op updates\ncheckingForUpdatesDescription=Informatie over de laatste release ophalen\ndownloadingUpdate=Vrijgave ophalen (Versie $VERSION$)\ndownloadingUpdateDescription=Een release downloaden\nupdateNag=Je hebt XPipe al een tijdje niet bijgewerkt. Mogelijk mis je dan nieuwe functies en fixes van nieuwere releases.\nupdateNagTitle=Herinnering bijwerken\nupdateNagButton=Bekijk uitgaven\nrefreshServices=Diensten verversen\nserviceProtocolType=Type serviceprotocol\nserviceProtocolTypeDescription=Bepalen hoe de service moet worden geopend\nserviceCommand=Het commando dat wordt uitgevoerd zodra de service actief is\nserviceCommandDescription=De placeholder $PORT wordt vervangen door de werkelijke getunnelde lokale poort\nvalue=Waarde\nshowAdvancedOptions=Geavanceerde opties weergeven\nsshAdditionalConfigOptions=Extra configuratie-opties\nremoteFileManager=Bestandsbeheer op afstand\nclearUserData=Gebruikersgegevens verwijderen\nclearUserDataDescription=Alle configuratiegegevens van gebruikers verwijderen, inclusief verbindingen\nclearUserDataTitle=Verwijderen van gebruikersgegevens\nclearUserDataContent=Hiermee worden alle lokale gebruikersgegevens voor xpipe verwijderd en opnieuw opgestart. Als je om je connecties geeft, zorg er dan voor dat je ze eerst synchroniseert met een git repository.\nundefined=Ongedefinieerd\ncopyAddress=Adres kopiëren\nnetbirdDeviceScan=Netbird verbindingen\nnetbirdId=Openbare sleutel van een peer\nnetbirdIdDescription=De interne netbird publieke sleutel id van de peer\ntailscaleDeviceScan=Tailscale verbindingen\ntailscaleInstall.displayName=Tailscale installatie\ntailscaleInstall.displayDescription=Maak verbinding met apparaten in je tailnet via SSH\ntailscaleDevice.displayName=Tailscale apparaat\ntailscaleDevice.displayDescription=Maak verbinding met een apparaat in je tailnet via SSH\ntailscaleId=Apparaat-ID\ntailscaleIdDescription=De interne ID van het Tailscale apparaat\ntailscaleHostName=Hostnaam\ntailscaleHostNameDescription=De hostnaam van het apparaat in het tailnet\ntailscaleUsername=Gebruikersnaam\ntailscaleUsernameDescription=De gebruiker om als in te loggen\ntailscalePassword=Wachtwoord\ntailscalePasswordDescription=Het optionele gebruikerswachtwoord dat kan worden gebruikt voor sudo\nscriptName=Scriptnaam\nscriptNameDescription=Geef dit script een aangepaste naam\nscriptGroupName=Script groepsnaam\nscriptGroupNameDescription=Geef deze scriptgroep een aangepaste naam\nidentityName=Identiteit naam\nidentityNameDescription=Geef deze identiteit een aangepaste naam\ntailscaleTailnet.displayName=Tailnet\ntailscaleTailnet.displayDescription=Maak verbinding met een specifiek tailnet met je account\nputtyConnections=PuTTY verbindingen\nkittyConnections=KiTTY verbindingen\nicons=Pictogrammen\ncustomIcons=Aangepaste pictogrammen\niconSources=Bronnen van pictogrammen\niconSourcesDescription=Je kunt hier je eigen bronnen voor pictogrammen toevoegen. XPipe zal alle .svg-bestanden op de toegevoegde locatie oppikken en toevoegen aan de beschikbare verzameling pictogrammen.\\n\\nZowel lokale mappen als externe git repositories worden ondersteund als pictogramlocaties.\nrefreshSources=Pictogrammen verversen\nrefreshSourcesDescription=Update alle pictogrammen uit de beschikbare bronnen\naddDirectoryIconSource=Directory bron toevoegen ...\naddDirectoryIconSourceDescription=Pictogrammen toevoegen vanuit een lokale map\naddGitIconSource=Git broncode toevoegen ...\naddGitIconSourceDescription=Pictogrammen toevoegen die zich in een remote git repository bevinden\nrepositoryUrl=Git opslag URL\niconDirectory=Pictogrammenmap\naddUnsupportedKexMethod=Niet-ondersteunde sleuteluitwisselingsmethode toevoegen\naddUnsupportedKexMethodDescription=Sta toe dat de sleuteluitwisselingsmethode $VAL$ wordt gebruikt voor deze verbinding\naddUnsupportedHostKeyType=Niet-ondersteunde hostsleutel toevoegen\naddUnsupportedHostKeyTypeDescription=Sta het gebruik van het hostsleuteltype $VAL$ toe voor deze verbinding\naddUnsupportedMacType=Niet-ondersteund MAC-type toevoegen\naddUnsupportedMacTypeDescription=Sta het gebruik van het MAC-type $VAL$ toe voor deze verbinding\nrunSilent=geruisloos op de achtergrond\nrunInFileBrowser=in bestandsbrowser\nrunInConnectionHub=in verbindingshub\ncommandOutput=Opdrachtuitvoer\niconSourceDeletionTitle=Bron pictogram verwijderen\niconSourceDeletionContent=Wil je deze pictogrambron en alle bijbehorende pictogrammen verwijderen?\nrefreshIcons=Pictogrammen verversen\nrefreshIconsDescription=Het ophalen, renderen en cachen van alle beschikbare 1000+ pictogrammen van externe bronnen naar .png-bestanden. Dit kan even duren ...\nvaultUserLegacy=Kluisgebruiker (Beperkte oude compatibiliteitsmodus)\nupgradeInstructions=Upgrade-instructies\nexternalActionTitle=Extern actieverzoek\nexternalActionContent=Er is een externe actie aangevraagd. Wil je het starten van acties van buiten XPipe toestaan?\nnoScriptStateAvailable=Vernieuwen om scriptcompatibiliteit te bepalen ...\ndocumentationDescription=Bekijk de documentatie\ncustomEditorCommandInTerminal=Een aangepast commando uitvoeren in een terminal\ncustomEditorCommandInTerminalDescription=Als je editor terminalgebaseerd is, kun je deze optie inschakelen om automatisch een terminal te openen en in plaats daarvan het commando in de terminalsessie uit te voeren.\\n\\nJe kunt deze optie gebruiken voor editors zoals vi, vim, nvim en andere.\ndisableHttpsTlsCheck=HTTPS verzoek certificaat verificatie uitschakelen\ndisableHttpsTlsCheckDescription=Als je organisatie je HTTPS-verkeer in firewalls ontsleutelt met behulp van SSL-interceptie, zullen eventuele updatecontroles of licentiecontroles mislukken omdat de certificaten niet overeenkomen. Je kunt dit oplossen door deze optie in te schakelen en TLS-certificaatvalidatie uit te schakelen.\nconnectionsSelected=$NUMBER$ geselecteerde verbindingen\naddConnections=Verbindingen toevoegen\nbrowseDirectory=Bladeren door directory\nopenTerminal=Terminal openen\ndocumentation=Documentatie\nreport=Fout rapporteren\nkeePassXcNotAssociated=KeePassXC link\nkeePassXcNotAssociatedDescription=XPipe is niet gekoppeld aan je lokale KeePassXC database. Klik hieronder om de eenmalige stap uit te voeren om XPipe te associëren met de KeePassXC database zodat XPipe wachtwoorden kan opvragen.\nkeePassXcAssociateMore=Meer databases verbinden\nkeePassXcAssociateMoreDescription=Je kunt tegelijkertijd verbonden zijn met meerdere KeePassXC databases\nkeePassXcAssociated=KeePassXC koppelingen\nkeePassXcAssociatedDescription=XPipe is verbonden met de volgende lokale KeePassXC databases:\nkeePassXcNotAssociatedButton=Link database\nidentifier=Identificatie\npasswordManagerCommand=Aangepaste opdracht\npasswordManagerCommandDescription=Het aangepaste commando dat moet worden uitgevoerd om wachtwoorden op te halen. De tekenreeks $KEY wordt bij het aanroepen vervangen door de geciteerde wachtwoordsleutel. Dit moet je wachtwoordmanager CLI aanroepen om het wachtwoord naar stdout af te drukken, bijvoorbeeld mypassmgr get $KEY.\nchooseTemplate=Sjabloon kiezen\nkeePassXcPlaceholder=URL voor invoer KeePassXC\nterminalEnvironment=Terminalomgeving\nterminalEnvironmentDescription=Als je functies van een lokale Linux-gebaseerde WSL-omgeving wilt gebruiken voor je terminalaanpassingen, dan kun je die gebruiken als terminalomgeving.\\n\\nAlle aangepaste terminal init commando's en terminal multiplexer configuratie worden dan uitgevoerd in deze WSL distributie.\nterminalInitScript=Terminal init script\nterminalInitScriptDescription=Commando's om uit te voeren in de terminalomgeving voordat de verbinding wordt gestart. Je kunt dit gebruiken om de terminalomgeving te configureren bij het opstarten.\nterminalMultiplexer=Terminal multiplexer\nterminalMultiplexerDescription=De terminal multiplexer om te gebruiken als alternatief voor tabbladen in een terminal. Hierdoor worden bepaalde eigenschappen van de terminal, zoals tabbladen, vervangen door de functionaliteit van de multiplexer.\\n\\nVereist dat de betreffende multiplexer executable op het systeem is geïnstalleerd.\nterminalMultiplexerWindowsDescription=De terminal multiplexer om te gebruiken als alternatief voor tabbladen in een terminal. Hierdoor worden bepaalde eigenschappen van de terminal, zoals tabbladen, vervangen door de functionaliteit van de multiplexer.\\n\\nVereist het gebruik van een WSL terminalomgeving op Windows en de multiplexer executable moet geïnstalleerd zijn op het WSL systeem.\nterminalAlwaysPauseOnExit=Altijd pauzeren bij afsluiten\nterminalAlwaysPauseOnExitDescription=Indien ingeschakeld, zal het afsluiten van een terminalsessie je altijd vragen om de sessie opnieuw op te starten of te sluiten. Indien uitgeschakeld, zal XPipe dit alleen doen bij mislukte verbindingen die met een foutmelding worden afgesloten.\nquerying=Zoeken ...\nretrievedPassword=Verkregen: $PASSWORD$\nrefreshOpenpubkey=Vernieuwen openpubkey identiteit\nrefreshOpenpubkeyDescription=Voer opkssh refresh uit om de openpubkey identiteit weer geldig te maken\nall=Alle\nterminalPrompt=Terminal prompt\nterminalPromptDescription=De terminal prompt tool om te gebruiken in je terminals op afstand. Als je een terminal prompt inschakelt, wordt het prompt gereedschap automatisch ingesteld en geconfigureerd op het doelsysteem bij het openen van een terminal sessie.\\n\\nBestaande promptconfiguraties of profielbestanden op een systeem worden hierdoor niet gewijzigd. Dit verhoogt de laadtijd van de terminal voor de eerste keer terwijl de prompt wordt ingesteld op het externe systeem. Je terminal heeft misschien extra lettertypen nodig om de prompt correct weer te geven.\nterminalPromptConfiguration=Terminal prompt configuratie\nterminalPromptConfig=Configuratiebestand\nterminalPromptConfigDescription=Het aangepaste configuratiebestand om toe te passen op de prompt. Deze configuratie wordt automatisch ingesteld op het doelsysteem wanneer de terminal wordt geïnitialiseerd en wordt gebruikt als de standaard configuratie van de prompt.\\n\\nAls je het bestaande standaard configuratiebestand op elk systeem wilt gebruiken, kun je dit veld leeg laten.\npasswordManagerKey=Wachtwoordmanager sleutel\npasswordManagerKeyDescription=De wachtwoordmanager-ID van het geheim\npasswordManagerAgent=Agent voor wachtwoordbeheer\ndockerComposeProject.displayName=Docker compose project\ndockerComposeProject.displayDescription=Groepeer containers van een compose project\nsshVerboseOutput=Uitgebreide SSH-uitvoer inschakelen\nsshVerboseOutputDescription=Hiermee wordt veel debug-informatie afgedrukt bij het verbinden via SSH. Nuttig voor het oplossen van problemen met SSH-verbindingen.\ndontUseGateway=Gebruik geen gateway\ndontUseGatewayDescription=Gebruik de hypervisor host niet als gateway en maak direct verbinding met het IP\ncategoryColor=Categorie kleur\ncategoryColorDescription=De standaard te gebruiken kleur voor verbindingen binnen deze categorie\ncategorySync=Synchroniseren met git repository\ncategorySyncDescription=Synchroniseer alle verbindingen automatisch met een git repository. Alle lokale wijzigingen aan verbindingen worden naar de remote gepushed.\ncategorySyncSpecial=Synchroniseren met git repository\\n(Niet configureerbaar voor speciale categorie \"$NAME$\")\ncategoryDontAllowScripts=Alle wijzigingen uitschakelen\ncategoryDontAllowScriptsDescription=Schakel het uitvoeren van commando's en andere bewerkingen op systemen binnen deze categorie uit om wijzigingen te voorkomen. Dit schakelt alle scriptfunctionaliteit, shell-omgevingscommando's, prompts en meer uit.\ncategoryConfirmAllModifications=Bevestig alle wijzigingen\ncategoryConfirmAllModificationsDescription=Bevestig elke wijziging aan een verbinding of bestandssysteem eerst. Dit kan onbedoelde bewerkingen op belangrijke systemen voorkomen.\ncategoryDefaultIdentity=Standaard identiteit\ncategoryDefaultIdentityDescription=Als je vaak een bepaalde identiteit gebruikt op veel van de systemen in deze categorie, dan kun je door een standaard identiteit in te stellen deze vooraf selecteren bij het maken van nieuwe verbindingen.\ncategoryConfigTitle=$NAME$ configuratie\nconfigure=Configureren\naddConnection=Verbinding toevoegen\nnoCompatibleConnection=Geen compatibele verbinding gevonden\nnoCompatibleIdentity=Geen compatibele identiteit gevonden\nnewCategory=Nieuwe categorie\ndockerComposeRestricted=Het compose project is beperkt door $NAME$ en kan niet extern gewijzigd worden. Gebruik $NAME$ om dit compose project te beheren.\nrestricted=Beperkt\ndisableSshPinCaching=SSH PIN caching uitschakelen\ndisableSshPinCachingDescription=XPipe slaat automatisch alle PIN-codes op die zijn ingevoerd voor een sleutel bij gebruik van een vorm van hardwaregebaseerde verificatie.\\n\\nAls je dit uitschakelt, moet je bij elke verbindingspoging de PIN opnieuw invoeren.\ngitSyncPull=Pull om remote git wijzigingen te synchroniseren\nenpassVaultFile=Kluisbestand\nenpassVaultFileDescription=Het lokale Enpass kluisbestand.\nflat=Plat\nrecursive=Recursief\nrdpAllowListBlocked=De geselecteerde RemoteApp lijkt niet te zijn opgenomen in de RDP allow lijst voor de server.\npsonoServerUrl=Server URL\npsonoServerUrlDescription=URL van de psono backend server\npsonoApiKey=API-sleutel\npsonoApiKeyDescription=De te gebruiken API-sleutel, geformatteerd als een uuid\npsonoApiSecretKey=API geheime sleutel\npsonoApiSecretKeyDescription=De geheime API-sleutel als hexadecimale tekenreeks van 64 bytes\npassboltServerUrl=Server URL\npassboltServerUrlDescription=URL van de passbolt backend server\npassboltPassphrase=Passphrase\npassboltPassphraseDescription=De wachtwoordzin voor de privésleutel van de kluis\npassboltPrivateKey=Privé sleutel\npassboltPrivateKeyDescription=Het privé gpg sleutelbestand voor de kluis\nfocusWindowOnNotifications=Focusvenster op meldingen\nfocusWindowOnNotificationsDescription=Breng XPipe naar de voorgrond wanneer een melding of foutmelding wordt getoond, bijvoorbeeld wanneer een verbinding of tunnel onverwacht wordt verbroken.\ngitUsername=Aangepaste git gebruikersnaam\ngitUsernameDescription=De aangepaste gebruiker om te authenticeren naar de git remote repository. Standaard zal XPipe de huidige geconfigureerde referenties van de git CLI gebruiken.\\n\\nDeze instelling overschrijft alle standaard referenties die al zijn geconfigureerd voor je lokale git CLI client.\ngitPassword=Aangepast git wachtwoord / persoonlijk toegangs token\ngitPasswordDescription=Het wachtwoord of persoonlijke toegangscode om te authenticeren. Of je een wachtwoord of persoonlijk toegangstoken nodig hebt hangt af van de git remote provider. Deze instelling zal alle standaard referenties overschrijven die al geconfigureerd zijn voor je lokale git CLI client.\nsetReadOnly=Alleen-lezen instellen\nunsetReadOnly=Unset alleen-lezen\nreadOnlyStoreError=De configuratie van dit item is bevroren. Kies een andere naam om je wijzigingen op te slaan in een nieuwe kopie.\ncategoryFreeze=Verbindingsconfiguraties bevriezen\ncategoryFreezeDescription=Markeert verbindingsconfiguraties als alleen-lezen. Dit betekent dat geen enkele bestaande verbindingsconfiguratie in deze categorie kan worden gewijzigd. Nieuwe verbindingen kunnen wel worden toegevoegd.\nupdateFail=Installatie update is niet gelukt\nupdateFailAction=Update handmatig installeren\nupdateFailActionDescription=Bekijk de nieuwste versies op GitHub\nonePasswordPlaceholder=Naam van het item of op:// URL\ncomputeDirectorySizes=Directory-groottes berekenen\ncomputeSize=Grootte berekenen\ncustomSpiceCommand=Aangepaste opdracht\ncustomSpiceCommandDescription=Het aangepaste commando dat moet worden uitgevoerd om SPICE-sessies te starten. De placeholderstring $FILE wordt bij het aanroepen vervangen door het aangehaalde bestandspad naar het .vv-bestand.\nvncClient=VNC-client\nvncClientDescription=De VNC-client die wordt gestart bij het openen van VNC-verbindingen in XPipe.\\n\\nJe hebt de mogelijkheid om ofwel de geïntegreerde VNC-client in XPipe te gebruiken of als alternatief een externe lokaal geïnstalleerde VNC-client te starten als je op zoek bent naar meer maatwerk.\nintegratedXPipeVncClient=Geïntegreerde XPipe VNC-client\ncustomVncCommand=Aangepaste opdracht\ncustomVncCommandDescription=Het aangepaste commando dat moet worden uitgevoerd om VNC-sessies te starten. De placeholderstring $ADDRESS wordt bij het aanroepen vervangen door het aangehaalde adres.\nvncConnections=VNC-verbindingen\npasswordManagerIdentity=Identiteit wachtwoordbeheerder\npasswordManagerIdentity.displayName=Identiteit wachtwoordbeheerder\npasswordManagerIdentity.displayDescription=Gebruikersnaam en wachtwoord van een identiteit ophalen uit je wachtwoordmanager\npasswordCopied=Wachtwoord van verbinding gekopieerd naar klembord\nerrorOccurred=Fout opgetreden\nactionMacro.displayName=Actiemacro\nactionMacro.displayDescription=Actief uitvoeren met aangepaste triggers\nmacroAdd=Macro toevoegen\nmacroName=Macro naam\nmacroNameDescription=Geef deze macro een aangepaste naam\nactionId=Actie-ID\nactionIdDescription=De actie die met deze macro moet worden uitgevoerd\nmacroRefs=Gekoppelde verbindingen\nmacroRefsDescription=De verbindingen waarmee de actie wordt uitgevoerd\nconnectionCopy=Kopiëren\nactionPickerTitle=Kies actie\nactionPickerDescription=Klik ergens op om een actie uit te voeren. In plaats van de actie uit te voeren, kun je snelkoppelingen naar de actie maken en bewerken in de modus Snelkoppeling kiezen.\ncancelActionPicker=Actie annuleren\nactionShortcut=Actie snelkoppeling\nactionShortcuts=Actie snelkoppelingen\nactionStore=Actie opslaan\nactionStoreDescription=Het winkelitem om de actie op uit te voeren\nactionStores=Actie slaat op\nactionStoresDescription=De winkelvermeldingen om de actie op uit te voeren\nactionDesktopShortcut=Snelkoppeling op het bureaublad\nactionDesktopShortcutDescription=Maak een snelkoppeling voor deze actie op je bureaublad\nactionUrlShortcut=URL snelkoppeling\nactionUrlShortcutDescription=Kopieer een URL die bij het openen deze acties kan activeren\nactionUrlShortcutDisabled=URL snelkoppeling (Niet beschikbaar)\nactionUrlShortcutDisabledDescription=Het installatietype $TYPE$ ondersteunt het openen van URL's niet\nactionApiCall=API-verzoek\nactionApiCallDescription=Roep deze actie aan vanuit de HTTP API\nactionMacro=Actiemacro\nactionMacroDescription=Maak een macro met geavanceerde functionaliteit voor deze actie\ncreateMacro=Macro maken\nactionConfiguration=Parameters\nactionConfigurationDescription=De parameters om door te geven aan de uitgevoerde actie\nconfirmAction=Actie bevestigen\nactionConnections=Actie verbindingen\nactionConnectionsDescription=De verbindingen om de actie op uit te voeren\nactionConnection=Actie verbinding\nactionConnectionDescription=De verbinding om de actie op uit te voeren\nappleContainerInstall.displayName=Apple containers\nappleContainerInstall.displayDescription=Toegang tot apple containerinstanties via de container CLI\nappleContainer.displayName=Apple container\nappleContainer.displayDescription=Toegang tot apple containerinstanties via de container CLI\nappleContainerHostDescription=De host waarop de apple container staat\nappleContainerDescription=De naam van de apple container\nappleContainers=Apple containers\nchangeOrderIndexTitle=Volgorde veranderen\norderIndex=Index\norderIndexDescription=Expliciete index om dit item te rangschikken ten opzichte van andere. De laagste indexen staan bovenaan, de hoogste onderaan\nmoveToFirst=Verplaats naar eerste\nmoveToLast=Naar laatste\ncategory=Categorie\nincludeRoot=Inclusief root\nexcludeRoot=Wortel uitsluiten\nfreezeConfiguration=Configuratie bevriezen\nunfreezeConfiguration=Configuratie ontdooien\nwaylandScalingTitle=Wayland schaling\nactionApiUrl=$URL$ (Kopieer json lichaam)\ncopyBody=Inhoud verzoek kopiëren\ngitRepoTerminalOpen=Open een repository in een terminal\ngitRepoTerminalOpenDescription=Bekijk de repository zelf met de commandoregel\ngitRepoOverwriteLocal=Lokale repository overschrijven\ngitRepoOverwriteLocalDescription=Alle lokale wijzigingen vervangen door wijzigingen op afstand\ngitRepoForcePush=Remote repository overschrijven\ngitRepoForcePushDescription=Gebruik git push --force om je lokale wijzigingen op de remote toe te passen\ngitRepoDontWarn=Niet meer waarschuwen\ngitRepoDontWarnDescription=Als dit wordt verwacht, zorg er dan voor dat XPipe deze fout in de toekomst negeert\ngitRepoTryAgain=Opnieuw proberen\ngitRepoTryAgainDescription=Dezelfde handeling opnieuw proberen\ngitRepoEnablePlain=Gewone directory sync gebruiken\ngitRepoEnablePlainDescription=Een git repository niet initialiseren om veranderingen naar de map te synchroniseren\ngitRepoCreateBare=Git sync gebruiken\ngitRepoCreateBareDescription=Initialiseer een nieuwe kale git repository in de sync directory\ngitRepoDisable=Schakel git vault voorlopig uit\ngitRepoDisableDescription=Leg geen wijzigingen vast tijdens deze sessie\ngitRepoPullRefresh=Wijzigingen trekken en vernieuwen\ngitRepoPullRefreshDescription=Wijzigingen op afstand samenvoegen en gegevens opnieuw laden\nbreakOutCategory=Categorie uitbreken\nmergeCategory=Categorie samenvoegen\nopenWinScp=Openen in WinSCP\nuninstallApplication=Verwijderen\nuninstallApplicationDescription=Voert het .pkg installatiescript uit om XPipe volledig te verwijderen\nk8sEditPodTitle=Wijzigingen toepassen\nk8sEditPodContent=Wil je de wijzigingen toepassen die zijn gemaakt via het commando kubectl apply? Er is waarschijnlijk een herstart nodig om de wijzigingen toe te passen.\nvirshEditDomainTitle=Wijzigingen toepassen\nvirshEditDomainContent=Wil je de wijzigingen toepassen op het domein? Waarschijnlijk is een herstart nodig om de wijzigingen toe te passen.\npkcs11Library=PKCS#11 bibliotheek\npkcs11LibraryDescription=Het pad van het dynamisch gekoppelde bibliotheekbestand\nsshAgentSocket=Aangepaste SSH-agent socket\nsshAgentSocketDescription=De aangepaste socket om te gebruiken om te communiceren met de SSH agent. Deze aangepaste agent kan worden gebruikt voor een verbinding door de aangepaste agent optie ervoor te selecteren.\npublicKey=Identificatiecode van de openbare sleutel\npublicKeyDescription=De optionele publieke sleutel om de agent te dwingen alleen de overeenkomende privésleutel aan te bieden\nactions=Acties\nhcloudServer.displayName=Hetzner cloud server\nhcloudServer.displayDescription=Toegang krijgen tot een server gehost op Hetzner cloud via SSH\nhcloudInstall.displayName=Hetzner cloud CLI\nhcloudInstall.displayDescription=Toegang tot servers gehost op Hetzner cloud via hcloud\nhcloudContext.displayName=hcloud context\nhcloudContext.displayDescription=Toegangsservers van een hcloud-context\nmetrics=Metriek\nopenInVsCode=Openen in VsCode\naddCloud=Cloud ...\nhcloudToken=hcloud token\nhcloudTokenDescription=Het Hetzner cloud token om te gebruiken. Zie voor meer informatie de documentatie\nhcloudLogin=Hetzner cloud login\nclearHcloudToken=Wis hcloud token\nclearHcloudTokenDescription=Bestaand token verwijderen zodat je opnieuw kunt inloggen\nselectIdentity=Selecteer identiteit\nenableMcpServer=MCP-server inschakelen\nenableMcpServerDescription=Schakelt de XPipe MCP-server in, waardoor externe MCP-clients verzoeken naar de MCP-server kunnen sturen. Zie hieronder voor de configuratiedetails.\\n\\nMerk op dat de HTTP API niet ingeschakeld hoeft te zijn voor de MCP functionaliteit.\nenableMcpMutationTools=MCP mutatiegereedschappen inschakelen\nenableMcpMutationToolsDescription=Standaard zijn alleen alleen-lezen tools ingeschakeld in de MCP-server. Dit is om ervoor te zorgen dat er niet per ongeluk bewerkingen kunnen worden uitgevoerd die een systeem mogelijk kunnen wijzigen.\\n\\nAls je van plan bent om wijzigingen in systemen aan te brengen via MCP-clients, controleer dan of je MCP-client geconfigureerd is om mogelijk destructieve acties te bevestigen voordat je deze optie inschakelt. Vereist een herstart van MCP-clients om toe te passen.\nmcpClientConfigurationDetails=MCP-client configuratie\nmcpClientConfigurationDetailsDescription=Gebruik deze configuratiegegevens om verbinding te maken met de XPipe MCP-server vanaf een MCP-client naar keuze.\nswitchHostAddress=Hostadres wijzigen\naddAnotherHostName=Een andere hostnaam toevoegen\naddNetwork=Netwerkscan ...\nnetworkScan=Netwerk scan\nnetworkScanStore=Doelhost\nnetworkScanStoreDescription=De host waarvoor het lokale netwerk moet worden gescand\nuseAsGateway=Host als gateway gebruiken\nuseAsGatewayDescription=Of de doelhost als gateway moet worden gebruikt voor de aangemaakte verbindingen\nnetworkScanPorts=Poorten om te scannen\nnetworkScanPortsDescription=De door komma's gescheiden lijst van poorten die in de scan moeten worden opgenomen\nnetworkScanType=Type verbinding\nnetworkScanTypeDescription=Het type servers om naar te zoeken\nemptyDirectory=Deze map lijkt leeg te zijn\nhcloudConfigFile=hcloud configuratiebestand\nhcloudConfigFileDescription=De locatie van het hcloud CLI .toml config bestand\npreferMonochromeIcons=Voorkeur voor monochrome pictogrammen\npreferMonochromeIconsDescription=Als deze optie is ingeschakeld, worden monochrome pictogramvariabelen verkozen boven de standaard gekleurde versies van een pictogram, ervan uitgaande dat er een aparte lichte of donkere pictogramvariant beschikbaar is voor een pictogram van een bron.\\n\\nVereist een verversing van de pictogrammen om toe te passen.\nalwaysShowSshMotd=Toon altijd MOTD\nalwaysShowSshMotdDescription=Of het bericht van de dag dat is ingesteld op een systeem op afstand wel of niet moet worden weergegeven bij het aanmelden in een nieuwe terminalsessie. Merk op dat het veranderen hiervan het initialisatiegedrag van SSH verbindingen kan veranderen.\nmanageSubscription=Abonnement beheren\nnoListeningServer=Geen luisterserver\nnetworkScanResults=Scanresultaten\nnetworkScanResultsDescription=De lijst van gevonden systemen in het netwerk\nlocalShellDialect=Lokale shell\nlocalShellDialectDescription=De shell die wordt gebruikt voor lokale operaties. In het geval dat de normale lokale standaard shell is uitgeschakeld of tot op zekere hoogte kapot is, kan deze optie worden gebruikt om terug te vallen op een ander alternatief.\\n\\nSommige configuraties zoals aangepaste PATH entries zijn mogelijk niet van toepassing op de fallback shell als ze nog niet geconfigureerd zijn in de respectievelijke shell profielbestanden.\nagentSocketNotFound=Er is geen actieve agent socket gevonden\nagentSocket=Locatie van een socket\nagentSocketDescription=Het pad van het agent socket bestand\nagentSocketNotConfigured=Er is nog geen aangepaste socket geconfigureerd\ndownloadInProgress=$NAME$ download bezig\nenableTerminalStartupBell=Terminal opstartbel inschakelen\nenableTerminalStartupBellDescription=Een piep/bel commando afspelen in een nieuwe terminal sessie. Als je terminal emulator bellen ondersteunt, kan dit worden gebruikt om het identificeren van nieuw opgestarte terminal instanties makkelijker te maken.\ninvalidSshGatewayChain=Ongeldige gemengde gatewayketenconfiguratie met jump gateways en non-jump gateways.\nsyncFileExists=Gesynchroniseerd bestand $FILE$ bestaat al\nreplaceFile=Bestand vervangen\nreplaceFileDescription=Het bestaande bestand vervangen door dit bestand\nrenameFile=Bestand hernoemen\nrenameFileDescription=Geef dit bestand een andere naam om te synchroniseren\nnewFileName=Nieuwe bestandsnaam\nparentHostDoesNotSupportTunneling=Moederhost $NAME$ ondersteunt geen tunneling\nconnectionNotesTemplate=Notities sjabloon\nconnectionNotesTemplateDescription=Het markdownsjabloon dat moet worden gebruikt bij het toevoegen van een nieuw notitie-item aan een verbinding.\nconnectionNotesButton=Opmerkingen bewerken\nrdpSmartSizing=Slimme grootte inschakelen\nrdpSmartSizingDescription=Indien ingeschakeld zal mstsc het bureaublad verkleinen als het venster te klein is om het in de volledige resolutie weer te geven. De beeldverhouding van het bureaublad blijft behouden bij het verkleinen.\ndisableStartOnInit=Automatisch opstarten uitschakelen\nenableStartOnInit=Automatisch opstarten inschakelen\nfileReadSudoTitle=Sudo bestand lezen\nfileReadSudoContent=Het bestand dat je probeert te lezen geeft je huidige gebruiker geen leesrechten. Wil je dit bestand lezen als de root gebruiker met sudo? Dit zal je automatisch verheffen tot root met de bestaande inloggegevens of via een prompt.\nnetbirdInstall.displayName=Netbird installatie\nnetbirdInstall.displayDescription=Verbinding maken met peers in je Netbird netwerk\nnetbirdProfile.displayName=Netbird profiel\nnetbirdProfile.displayDescription=Lijst met peers in een specifiek profiel\nnetbirdPeer.displayName=Netbird peer\nnetbirdPeer.displayDescription=Verbinding maken met een peer via SSH\nnetbirdPublicKey=Openbare sleutel\nnetbirdPublicKeyDescription=De interne openbare sleutel van de peer\nnetbirdHostName=Hostnaam\nnetbirdHostNameDescription=De hostnaam van de peer in het netwerk\nvncRefSystem=Gekoppeld systeem\nvncRefSystemDescription=Het verbindingsitem waarmee deze VNC-verbinding wordt geassocieerd. Leeg laten als er geen is\nabstractHost.displayName=Abstracte gastheer\nabstractHost.displayDescription=Maak een entry voor een host die geen shellverbindingen ondersteunt\nabstractHostAddress=Hostadres\nabstractHostAddressDescription=Het adres van de host\nabstractHostGateway=Gateway\nabstractHostGatewayDescription=Het optionele gatewaysysteem om deze host te bereiken\nabstractHostConvert=Converteren naar abstracte hostvermelding\nhostNoConnections=Geen beschikbare verbindingen\nhostHasConnections=$COUNT$ beschikbare verbindingen\nhostHasConnection=$COUNT$ beschikbare verbinding\nlargeFileWarningTitle=Groot bestand bewerken\nlargeFileWarningContent=Het bestand dat je wilt bewerken is vrij groot met $SIZE$. Wil je dit bestand echt openen in je teksteditor?\nrdpAskpassUser=RDP gebruikersnaam voor host $HOST$\nrdpAskpassPassword=Wachtwoord voor gebruiker $USER$\ninPlaceKey=Sleutel\ninPlaceKeyText=Inhoud van de privésleutel\ninPlaceKeyTextDescription=De inhoud van de privésleutel\nnetbirdSelfhosted=Zelf gehoste Netbird instantie\nnetbirdSelfhostedDescription=Een aangepaste URL opgeven in plaats van de cloud-hosted versie te gebruiken\nnetbirdManagementUrl=Netbird beheer URL\nnetbirdManagementUrlDescription=De beheer-URL van je zelf gehoste instantie\nnetbirdSetupKey=Instellingstoets\nnetbirdSetupKeyDescription=Als je instelsleutels gebruikt, kun je er een gebruiken om in te loggen\nnetbirdLogin=Netbird inloggen\naddProfile=Profiel toevoegen\nnetbirdProfileNameAsktext=Naam van nieuw netbird profiel\nopenSftp=Openen in SFTP-sessie\ncapslockWarning=Je hebt capslock ingeschakeld\ninherit=Erven\nsshConfigStringSelected=Doelhost\nsshConfigStringSelectedDescription=Bij meerdere hosts wordt de eerste host als doel gebruikt. Wijzig de volgorde van je hosts om het doel te veranderen\ntunnelToLocalhost=Tunnel naar localhost\ntunnelToLocalhostDescription=Automatisch de poort op afstand tunnelen naar localhost\ntags=Tags\ntag=Tag\naddNewTag=Nieuwe tag maken\ncreateTag=Tag maken ...\ninPlacePublicKey=Openbare sleutel\ninPlacePublicKeyDescription=De bijbehorende openbare sleutel voor de opgegeven privésleutel\nsshKeygenTitle=Nieuwe SSH-sleutel genereren\nsshKeygenAlgorithm=Algoritme\nsshKeygenAlgorithmDescription=Het asymmetrische sleutelgen algoritme om te gebruiken voor de sleutel\nrsaBits=Bits\nrsaBitsDescription=Aantal bits in de gegenereerde sleutel\nsshKeygenComment=Opmerking\nsshKeygenCommentDescription=Het optionele commentaar voor deze sleutel\nsshKeygenPassphrase=Passphrase\nsshKeygenPassphraseDescription=De optionele wachtwoordzin voor deze sleutel\ned25519SkResident=Inwonende sleutel maken\ned25519SkResidentDescription=Privé sleutel opslaan op de hardware beveiligingssleutel\ned25519SkResidentKeyName=Label voor inwonende sleutel\ned25519SkResidentKeyNameDescription=Geef de sleutel een label. Nodig bij het opslaan van meerdere sleutels op de beveiligingssleutel\ned25519SkPinRequired=PIN vereisen\ned25519SkPinRequiredDescription=Invoer van pincode bij gebruik vereisen\ned25519SkUserPresenceRequired=Aanwezigheid van de gebruiker vereisen\ned25519SkUserPresenceRequiredDescription=Aanraking of iets dergelijks vereisen bij gebruik. Sommige beveiligingstoetsen vereisen dat dit is ingeschakeld\ncopyPublicKey=Openbare sleutel kopiëren\ngeneratePublicKey=Openbare sleutel genereren\npublicKeyGenerateNotice=Kan worden gegenereerd uit een privésleutel\nidentityApplyTargetHost=Doel\nidentityApplyTargetHostDescription=Het systeem om de identiteit op toe te passen\nidentityApplyAuthorizedHost=SSH sleutel geautoriseerd\nidentityApplyAuthorizedHostDescription=De SSH-sleutel wordt toegevoegd aan het bestand met geautoriseerde hosts\nidentityApplyAuthorizedHostButton=Sleutel toevoegen aan bestand\napplyIdentityToHost=Identiteit toepassen op host ...\nidentityApplyMissingPublicKeyTitle=Ontbrekende openbare sleutel\nidentityApplyMissingPublicKeyContent=Aan de SSH-sleutel van de identiteit is geen openbare sleutel gekoppeld. Bekijk de configuratie voor meer informatie.\nvalid=Geldig\nnotValid=Niet geldig\nwarning=Waarschuwing\nidentityApplyTitle=Identiteit toepassen\nidentityApplyConfigPasswordEnabled=Wachtwoord auth ingeschakeld\nidentityApplyConfigPasswordEnabledDescription=Wachtwoordauthenticatie is nog steeds ingeschakeld in de sshd configuratie\nidentityApplyConfigPasswordDisabled=Wachtwoord auth uitgeschakeld\nidentityApplyConfigPasswordDisabledDescription=Wachtwoordauthenticatie is nog steeds uitgeschakeld in de sshd configuratie\nidentityApplyConfigKeyEnabled=Auth sleutel ingeschakeld\nidentityApplyConfigKeyEnabledDescription=Sleutelgebaseerde authenticatie is nog steeds ingeschakeld in de sshd configuratie\nidentityApplyConfigKeyDisabled=Sleutel auth uitgeschakeld\nidentityApplyConfigKeyDisabledDescription=Sleutelgebaseerde authenticatie is nog steeds uitgeschakeld in de sshd configuratie\nidentityApplyConfigRootDisabledWarning=Root login uitgeschakeld\nidentityApplyConfigRootDisabledWarningDescription=Root user login is niet ingeschakeld in de sshd configuratie\nidentityApplyConfigAdminWarning=Beheerderstoetsen geconfigureerd\nidentityApplyConfigAdminWarningDescription=De sleutel moet misschien in plaats daarvan worden toegevoegd aan administrators_authorized_keys voor admin-gebruikers\nidentityApplyEditConfig=Configuratie bewerken\nidentityApplyEditConfigDescription=Open de sshd config in de editor om eventuele problemen op te lossen\nidentityApplyEditAuthorizedKeys=Geautoriseerde sleutels bewerken\nidentityApplyEditAuthorizedKeysDescription=Open het bestand authorized_keys in de editor om andere sleutels te bewerken of te verwijderen\nidentityApplyEditConfigButton=Open sshd_config\nidentityApplyEditAuthorizedKeysButton=Geautoriseerde_sleutels openen\nidentityApplySetStoreIdentity=Identiteit verbindingsset\nidentityApplySetStoreIdentityDescription=De identiteit is geconfigureerd om te worden gebruikt door de verbinding\nidentityApplySetStoreIdentityButton=Identiteit toepassen\ngenerateKey=Sleutel genereren\ngroupSecretStrategy=Groepsgebaseerde toegangscontrole\ngroupSecretStrategyDescription=Hoe haal je het groepsgeheim op dat gebruikt wordt voor encryptie en decryptie voor de groep. De ophaalmethode die je kiest wordt uitgevoerd wanneer een gebruiker zich aanmeldt bij het opstarten van de kluis.\\n\\nDeze instelling wordt per groep geconfigureerd. Om deze instelling te wijzigen voor een andere groep dan de momenteel actieve groep, moet je inloggen op de kluis als lid van die groep.\nfileSecret=Bestandsgebaseerd geheim\ncommandSecret=Opdracht\nhttpRequestSecret=HTTP reactie\nfileSecretChoice=Bestandslocatie\nfileSecretChoiceDescription=Het pad naar het bestand dat het groepscoderingsgeheim bevat. Omdat dit bestand op alle platformen kan worden opgevraagd, kun je ~ in het pad gebruiken om naar de thuismap te verwijzen. Het bestand moet beschikbaar zijn op alle systemen waarvandaan je de kluis ontgrendelt, anders mislukt het aanmelden.\ncommandSecretField=Ophaalscript\ncommandSecretFieldDescription=Het commando dat de geheime encryptiesleutel voor de huidige groep teruggeeft. Het commando wordt uitgevoerd in de standaard shell van het lokale systeem en de sleutel moet worden afgedrukt naar stdout.\nhttpRequestSecretField=Aanvraag URI\nhttpRequestSecretFieldDescription=De URI om een HTTP verzoek naar toe te sturen. Het groepsgeheim wordt uit de HTTP response body gehaald.\nvaultAuthentication=Kluis authenticatie\nvaultAuthenticationDescription=Hoe verifieer / ontgrendel je de kluisgegevens? Er zijn verschillende manieren om kluisgegevens te versleutelen en te ontgrendelen, afhankelijk van met wie je de kluisgegevens wilt delen.\ngroupAuthFailed=Geheime verificatie mislukt\nuserAuthFailed=Wachtwoordverificatie mislukt\nsavingChanges=Wijzigingen opslaan\nawsDeviceScan=AWS\naws=AWS\nawsCliInstallTitle=AWS CLI vereist\nawsCliInstallContent=Voor de AWS-integratie moet de AWS CLI op je lokale systeem zijn geïnstalleerd\nawsProfileCreateTitle=Nieuw AWS profiel\nawsProfileAccessKey=Toegangssleutel\nawsProfileName=Naam profiel\nawsProfileNameDescription=De weergavenaam van het nieuwe profiel\nawsProfileRegion=Regio\nawsProfileRegionDescription=De AWS-regio die bij het profiel hoort\nawsProfileAccessKeyId=Toegangssleutel ID\nawsProfileAccessKeyIdDescription=De toegangscode-ID van de IAM-gebruiker\nawsProfileSecretAccessKey=Geheime toegangssleutel\nawsProfileSecretAccessKeyDescription=De bijbehorende geheime toegangssleutel\nawsInstall.displayName=AWS CLI installatie\nawsInstall.displayDescription=Verbinding maken met je AWS systemen via de AWS CLI\nawsProfile.displayName=AWS CLI profiel\nawsProfile.displayDescription=Toegang tot AWS via een specifiek profiel\nawsInstanceId=Instance-ID\nawsInstanceIdDescription=De interne ID van deze instantie\nawsInstanceUseSsm=Verbinden via SSM\nawsInstanceUseSsmDescription=Gebruik het SSM-hulpprogramma om via SSH verbinding te maken met de instantie\nawsEc2Instance.displayName=AWS EC2-instantie\nawsEc2Instance.displayDescription=Verbinding maken met een EC2-instantie via SSH\nawsS3Group.displayName=S3 emmers\nawsS3Group.displayDescription=Toegang tot S3 buckets van een AWS profiel\nawsS3Bucket.displayName=S3 emmer\nawsS3Bucket.displayDescription=Toegang tot een S3 emmer van een AWS profiel\nawsEc2Group.displayName=EC2-instanties\nawsEc2Group.displayDescription=Toegang tot EC2-instanties van een AWS-profiel\nawsEc2InstanceSsmTerminal=SSM-terminal openen\ngenericS3Bucket.displayName=Generieke S3 emmer\ngenericS3Bucket.displayDescription=Toegang tot een generieke S3 emmer via de AWS CLI\naddFileSystem=Bestandssysteem ...\ngenericS3BucketHost=Host\ngenericS3BucketHostDescription=De hostvermelding of het handmatige adres van de S3-server\ngenericS3BucketPortDescription=De poort waarop de S3 server luistert\ngenericS3BucketAccessKeyId=Toegangssleutel ID\ngenericS3BucketAccessKeyIdDescription=De toegangscode-ID van de IAM-gebruiker\ngenericS3BucketSecretAccessKey=Geheime toegangssleutel\ngenericS3BucketSecretAccessKeyDescription=De bijbehorende geheime toegangssleutel\ngenericS3BucketHttps=HTTPS inschakelen\ngenericS3BucketHttpsDescription=Gebruik HTTPS om verbinding te maken met de server. Sommige providers vereisen HTTPS\ntunnelled=Getunneld\nawsInstallSync=Configuratie sync\nawsInstallSyncDescription=Synchroniseer de AWS CLI configuratiebestanden naar de git kluis\nawsInstallLocation=Locatie van gebruikersgegevens\nawsInstallLocationDescription=Het pad vanwaar de AWS CLI configuratiebestanden afkomstig zijn\ninstanceActions=Instantie acties\nopenSplit=Openen in een gesplitste terminal\nterminalSplitStrategy=Gesplitste kijkrichting\nterminalSplitStrategyDescription=Regelt hoe de terminal tabs worden gesplitst bij gebruik van de gesplitste weergave functionaliteit in batchmodus om meerdere terminalsessies naast elkaar te openen.\nterminalSplitStrategyDisabledDescription=Regelt hoe de terminal tabs worden gesplitst bij gebruik van de gesplitste weergave functionaliteit in batchmodus om meerdere terminalsessies naast elkaar te openen.\\n\\nJe huidige terminalconfiguratie ondersteunt geen gesplitste weergaven.\nhorizontal=Horizontaal\nvertical=Verticaal\nbalanced=Uitgebalanceerd\nclose=Sluit\nhelpButton=$TOPIC$ documentatielink\nquickAccess=Snelle toegang\ntoggleEnabled=Schakeltoestand\ncurrentPath=Huidig pad\ndirectoryContents=Inhoud van een directory\ndirectoryOptions=Directory-opties\nchooseConnectionType=Verbindingstype kiezen\nbatchMode=Batchmodus\ntoggleButton=Schakelknop\ntailscaleUseSsh=Gebruik tailscale SSH auth\ntailscaleUseSshDescription=Inloggen via de tailscale SSH server zelf zonder SSH auth\nportDescription=De poort waarop de SSH-server draait\nloginAs=Inloggen als\nsshGatewayType=Type gateway\nsshGatewayTypeDescription=Of er verbinding moet worden gemaakt met het doel via een tunnel of met de optie ProxyJump\ngatewayTunnel=Gateway tunnel\nproxyJump=Proxy sprong\ncommandTypeAsyncBackground=Vrijstaand op de achtergrond uitvoeren\ncommandTypeSyncBackground=Op de achtergrond draaien en wachten tot het klaar is\ncommandTypeTerminalBackground=Openen in terminal\nasyncBackgroundCommand=Opdracht op de achtergrond\nsyncBackgroundCommand=Achtergrondcommando blokkeren\nterminalBackgroundCommand=Terminal commando\ntestingConnection=Verbinding testen ...\nopenManagementConsole=Open beheerconsole\nopenLxcTerminal=LXC-terminal openen\nopenContainerConsole=Seriële console openen\nkeeper2fa=2FA methode\nkeeper2faDescription=De primaire twee-factor authenticatiemethode die is geconfigureerd voor je account. Schakel dit in als je Keeper-account tweefactorauthenticatie vereist om toegang te krijgen tot wachtwoorden.\nkeeperTotpDuration=Aangepaste 2FA code duur\nkeeperTotpDurationDescription=Overschrijf de standaardduur van de geldigheid van een 2FA code. Alleen van toepassing als het beleid van je organisatie het wijzigen van de duur toestaat.\\n\\nMogelijke waarden zijn: $VALUES$\nkeeperOtherAuth=Andere (RSA SecurID, Duo Security, Keeper DNA, etc.)\nextractReusableIdentities=Herbruikbare identiteiten extraheren\nidentitiesAdded=Identiteiten toegevoegd\nsyncMode=Synchronisatiemodus\nsyncModeDescription=Regelt hoe wijzigingen moeten worden gesynchroniseerd.\\n\\nInstant modus zal wijzigingen zo snel mogelijk pushen en pullen, de opstart- en afsluitmodus zal alle wijzigingen die tijdens een sessie zijn gemaakt in één keer synchroniseren, en de handmatige modus zal alleen synchroniseren als jij het start.\ntoggleTerminalDock=Terminal dock schakelen\nscriptDirectory=Directory-locatie\nscriptDirectoryDescription=De lokale map met shellscriptbestanden\nscriptSourceUrl=URL archief\nscriptSourceUrlDescription=De URL naar een remote git repository die shell script bestanden bevat\nscriptCollectionSourceType=Bron\nscriptCollectionSourceTypeDescription=Het type bron van waaruit shellscripts geladen moeten worden\nscriptCollectionSourceEntry=Bronvermelding\nscriptCollectionSourceEntryDescription=De bron van waaruit shellscripts geladen moeten worden\ngitRepository=Git archief\nscriptCollectionSource.displayName=Script bron\nscriptCollectionSource.displayDescription=Automatisch shellscripts importeren vanuit een bestaande bron\ndirectorySource=Directory bron\ngitRepositorySource=Git repository bron\nrefreshSource=Bron verversen\nscriptTextSourceUrl=Script URL\nscriptTextSourceUrlDescription=De URL om het scriptbestand van op te halen\nscriptSourceType=Script bron\nscriptSourceTypeDescription=Waar haal je het script vandaan?\nscriptSourceTypeInPlace=In-plaats script\nscriptSourceTypeUrl=Externe URL\nscriptSourceTypeSource=Bestaande bron\nimportScripts=Scripts importeren\nscriptsContained=$NUMBER$ scripts\nscriptSourceCollectionImportTitle=Scripts importeren vanuit bron ($SELECTED$/$COUNT$)\nnoScriptsFound=Geen scripts gevonden\ntunnel=Tunnel\nnotInitialized=Niet geïnitialiseerd\nselectCategory=Selecteer categorie ...\nscriptSourceName=Scriptnaam\nscriptSourceNameDescription=De bestandsnaam van het script in de bron\nworkspaceRestartTitle=Werkruimte gereed\nworkspaceRestartContent=Er is een snelkoppeling naar de nieuwe werkruimte gemaakt op $PATH$. Je kunt naar de snelkoppeling navigeren of XPipe nu opnieuw starten om de nieuwe werkruimte automatisch te openen.\nbrowseShortcut=Bestand doorbladeren\nsyncModeInstant=Direct synchroniseren\nsyncModeSession=Synchroniseren bij opstarten en afsluiten\nsyncModeManual=Handmatig synchroniseren\npushChanges=Veranderingen duwen\npullChanges=Trek wijzigingen\nsourcedFrom=Afkomstig van $SOURCE$\ninPlaceScript=In-plaats script\ngeneric=Algemeen\nsyncToPlainDirectory=Synchroniseren met gewone map\nsyncToPlainDirectoryDescription=Bij het synchroniseren naar een lokale map, kun je deze map behandelen als een andere git repository of gewoon als een gewone map. Als de gewone map instelling is ingeschakeld, wordt de map niet geïnitialiseerd als een git repository.\nopenSpiceSession=Open SPICE-sessie\nterminalBehaviour=Gedrag van terminals\nnoScanPossible=Er zijn geen ondersteunde verbindingen gevonden\nnetworkSwitchPorts=Netwerkpoorten\nnswitchGroup.displayName=Netwerkpoorten\nnswitchGroup.displayDescription=Lijst van beschikbare poorten op een netwerkapparaat\nnswitchPort.displayName=Netwerkpoort\nnswitchPort.displayDescription=Een individuele poort op een netwerkschakelaar besturen\nenablePort=Poort inschakelen\nshutdownPort=Poort afsluiten\nresetPort=Poort resetten\nuseSystemDefault=Standaard systeem gebruiken\nportStatus=Poortstatus\nclearCounters=Tellers wissen\nshowStatus=Status weergeven\nshowAllPorts=Toon alle poorten\nactiveLicense=Licentie\nactiveLicenseDescription=Een XPipe-licentiesleutel activeren\nauthenticatorApp=Authenticator app\nsecurityKey=Beveiligingssleutel\nmcpAdditionalContext=Extra MCP-context\nmcpAdditionalContextDescription=Extra instructies om door te geven aan de MCP-client. Gebruik dit om het gedrag van de agent te regelen en aanvullende context te leveren voor je individuele opstelling.\nmcpAdditionalContextSample=- Start geen services en daemons automatisch opnieuw op zonder dit eerst te bevestigen\\n- Gebruik bij het configureren van een netwerkinterface altijd 192.168.1.1/24 als gateway\nprefsRestartTitle=Opnieuw opstarten vereist\nprefsRestartContent=Sommige opties die je hebt gewijzigd vereisen een herstart van de toepassing om te kunnen worden toegepast. Wil je XPipe nu opnieuw opstarten?\nbashShell=Bash-shell\n"
  },
  {
    "path": "lang/strings/translations_pl.properties",
    "content": "delete=Usuń\nproperties=Właściwości\nusedDate=Używany $DATE$\nopenDir=Otwarty katalog\nsortLastUsed=Sortuj według daty ostatniego użycia\nsortAlphabetical=Sortuj alfabetycznie według nazwy\nsortIndexed=Sortuj według indeksu kolejności\nrestartDescription=Restart często może być szybkim rozwiązaniem\nreportIssue=Zgłoś problem\nreportIssueDescription=Otwórz zintegrowaną aplikację do zgłaszania problemów\nusefulActions=Przydatne działania\nstored=Zapisany\ntroubleshootingOptions=Narzędzia do rozwiązywania problemów\ntroubleshoot=Rozwiązywanie problemów\nremote=Zdalny plik\naddShellStore=Dodaj powłokę ...\naddShellTitle=Dodaj połączenie powłoki\nsavedConnections=Zapisane połączenia\nsave=Zapisz\nclean=Czysty\nmoveTo=Przejdź do ...\naddDatabase=Baza danych ...\nbrowseInternalStorage=Przeglądaj pamięć wewnętrzną\naddTunnel=Tunel ...\naddService=Serwis ...\naddScript=Skrypt ...\naddHost=Zdalny host ...\naddShell=Środowisko powłoki ...\naddCommand=Polecenie ...\naddAutomatically=Dodaj automatycznie ...\naddOther=Dodaj inne ...\nconnectionAdd=Dodaj połączenie\nscriptAdd=Dodaj skrypt\nscriptGroupAdd=Dodaj grupę skryptów\nidentityAdd=Dodaj tożsamość\nnew=Nowość\nselectType=Wybierz typ\nselectTypeDescription=Wybierz typ połączenia\nselectShellType=Typ powłoki\nselectShellTypeDescription=Wybierz typ połączenia powłoki\nname=Nazwa\nstoreIntroHeader=Koncentrator połączeń\nstoreIntroContent=Tutaj możesz zarządzać wszystkimi lokalnymi i zdalnymi połączeniami powłoki w jednym miejscu. Na początek możesz szybko automatycznie wykryć dostępne połączenia i wybrać te, które chcesz dodać.\nstoreIntroButton=Wyszukaj połączenia ...\ndragAndDropFilesHere=Lub po prostu przeciągnij i upuść plik tutaj\nconfirmDsCreationAbortTitle=Potwierdź przerwanie\nconfirmDsCreationAbortHeader=Czy chcesz przerwać tworzenie źródła danych?\nconfirmDsCreationAbortContent=Wszelkie postępy tworzenia źródła danych zostaną utracone.\nconfirmInvalidStoreTitle=Pomiń walidację\nconfirmInvalidStoreContent=Czy chcesz pominąć weryfikację połączenia? Możesz dodać to połączenie, nawet jeśli nie udało się go zweryfikować i naprawić problemy z połączeniem później.\nexpand=Rozwiń\naccessSubConnections=Uzyskaj dostęp do połączeń podrzędnych\ncommon=Wspólny\ncolor=Kolor\nalwaysConfirmElevation=Zawsze potwierdzaj podniesienie uprawnień\nalwaysConfirmElevationDescription=Kontroluje sposób obsługi przypadków, w których do uruchomienia polecenia w systemie wymagane są podwyższone uprawnienia, np. za pomocą sudo.\\n\\nDomyślnie wszelkie poświadczenia sudo są buforowane podczas sesji i automatycznie podawane w razie potrzeby. Jeśli ta opcja jest włączona, za każdym razem będziesz proszony o potwierdzenie dostępu z podwyższonym poziomem uprawnień.\nallow=Pozwól\nask=Zapytaj\ndeny=Odmów\nshare=Dodaj do repozytorium git\nunshare=Usuń z repozytorium git\nremove=Usuń\ncreateNewCategory=Nowa podkategoria\nprompt=Podpowiedź\ncustomCommand=Polecenie niestandardowe\nother=Inne\nsetLock=Ustaw blokadę\nselectConnection=Wybierz połączenie\nselectEntry=Wybierz wpis\ncreateLock=Utwórz hasło\nchangeLock=Zmień hasło\ntest=Test\nfinish=Zakończ\nerror=Wystąpił błąd\ndownloadStageDescription=Przenosi pobrane pliki do systemowego katalogu pobierania i otwiera go.\nok=Ok\nsearch=Wyszukaj\nrepeatPassword=Powtórz hasło\naskpassAlertTitle=Askpass\nunsupportedOperation=Nieobsługiwana operacja: $MSG$\nfileConflictAlertTitle=Rozwiąż konflikt\nfileConflictAlertContent=Napotkano konflikt. Plik $FILE$ już istnieje w systemie docelowym.\\n\\nJak chcesz kontynuować?\nfileConflictAlertContentMultiple=Napotkano konflikt. Plik $FILE$ już istnieje.\\n\\nJak chcesz kontynuować? Może istnieć więcej konfliktów, które możesz automatycznie rozwiązać, wybierając opcję, która ma zastosowanie do wszystkich.\nmoveAlertTitle=Potwierdź ruch\nmoveAlertHeader=Czy chcesz przenieść ($COUNT$) wybrane elementy do $TARGET$?\ndeleteAlertTitle=Potwierdź usunięcie\ndeleteAlertHeader=Czy chcesz usunąć ($COUNT$) wybrane elementy?\nselectedElements=Wybrane elementy:\nmustNotBeEmpty=$VALUE$ nie może być pusty\nvalueMustNotBeEmpty=Wartość nie może być pusta\ntransferDescription=Przeciągnij pliki tutaj, aby je pobrać\ndragLocalFiles=Przeciągnij pliki do pobrania stąd\nnull=$VALUE$ nie może mieć wartości null\nroots=Korzenie\nscripts=Skrypty\nsearchFilter=Wyszukaj ...\nrecent=Ostatni\nshortcut=Skrót\nbrowserWelcomeEmptyHeader=Przeglądarka plików\nbrowserWelcomeEmptyContent=Możesz wybrać po lewej stronie, które systemy mają być otwarte w przeglądarce plików. XPipe zapamięta systemy i katalogi, do których wcześniej uzyskałeś dostęp i pokaże je w menu szybkiego dostępu w przyszłości.\nbrowserWelcomeEmptyButton=Otwórz lokalną przeglądarkę plików\nbrowserWelcomeSystems=Ostatnio byłeś połączony z następującymi systemami:\nbrowserWelcomeDocsHeader=Dokumentacja\nbrowserWelcomeDocsContent=Jeśli wolisz bardziej ukierunkowane podejście do zapoznania się z XPipe, sprawdź stronę internetową z dokumentacją.\nbrowserWelcomeDocsButton=Otwarta dokumentacja\nhostFeatureUnsupported=$FEATURE$ nie jest zainstalowany na hoście\nmissingStore=$NAME$ nie istnieje\nconnectionName=Nazwa połączenia\nconnectionNameDescription=Nadaj temu połączeniu niestandardową nazwę\nopenFileTitle=Otwórz plik\nunknown=Nieznany\nscanAlertTitle=Dodaj połączenia\nscanAlertChoiceHeader=Cel\nscanAlertChoiceHeaderDescription=Wybierz miejsce wyszukiwania połączeń. W pierwszej kolejności wyszukiwane będą wszystkie dostępne połączenia.\nscanAlertHeader=Typy połączeń\nscanAlertHeaderDescription=Wybierz typy połączeń, które mają być automatycznie dodawane do systemu.\nnoInformationAvailable=Brak dostępnych informacji\nyes=Tak\nno=Nie\nerrorOccured=Wystąpił błąd\nterminalErrorOccured=Wystąpił błąd terminala\nerrorTypeOccured=Zgłoszono wyjątek typu $TYPE$\npermissionsAlertTitle=Wymagane uprawnienia\npermissionsAlertHeader=Do wykonania tej operacji wymagane są dodatkowe uprawnienia.\npermissionsAlertContent=Postępuj zgodnie z wyskakującym okienkiem, aby nadać XPipe wymagane uprawnienia w menu ustawień.\nerrorDetails=Szczegóły błędu\nupdateReadyAlertTitle=Gotowość do aktualizacji\nupdateReadyAlertHeader=Aktualizacja do wersji $VERSION$ jest gotowa do zainstalowania\nupdateReadyAlertContent=Spowoduje to zainstalowanie nowej wersji i ponowne uruchomienie XPipe po zakończeniu instalacji.\nerrorNoDetail=Szczegóły błędu nie są dostępne\nerrorNoExceptionMessage=Wystąpił błąd typu $TYPE$\nupdateAvailableTitle=Dostępna aktualizacja\nupdateAvailableContent=Aktualizacja XPipe do wersji $VERSION$ jest dostępna do zainstalowania. Mimo że nie można uruchomić XPipe, możesz spróbować zainstalować aktualizację, aby potencjalnie naprawić problem.\nclipboardActionDetectedTitle=Wykryto działanie schowka\nclipboardActionDetectedContent=XPipe wykrył w twoim schowku zawartość, którą można otworzyć. Czy chcesz ją teraz otworzyć? Czy chcesz zaimportować zawartość schowka?\ninstall=Zainstaluj ...\nignore=Ignoruj\npossibleActions=Dostępne akcje\nreportError=Zgłoś błąd\nreportOnGithub=Utwórz raport zgłoszenia w serwisie GitHub\nreportOnGithubDescription=Otwórz nowe zgłoszenie w repozytorium GitHub\nreportErrorDescription=Wyślij raport o błędzie z opcjonalną informacją zwrotną od użytkownika i informacjami diagnostycznymi\nignoreError=Ignoruj błąd\nignoreErrorDescription=Zignoruj ten błąd i kontynuuj, jakby nic się nie stało\nprovideEmail=Jak możemy się z Tobą skontaktować (opcjonalnie, tylko jeśli chcesz otrzymać odpowiedź). Twój raport jest domyślnie anonimowy, więc możesz podać tutaj informacje kontaktowe, takie jak adres e-mail.\nadditionalErrorInfo=Podaj dodatkowe informacje (opcjonalnie)\nadditionalErrorAttachments=Wybierz załączniki (opcjonalnie)\ndataHandlingPolicies=Polityka prywatności\nsendReport=Wyślij raport\nerrorHandler=Obsługa błędów\nevents=Wydarzenia\nvalidate=Zatwierdź\nstackTrace=Śledzenie stosu\npreviousStep=< Poprzedni\nnextStep=Następny >\nfinishStep=Zakończ\nselect=Wybierz\nbrowseInternal=Przeglądaj wewnętrznie\ncheckOutUpdate=Sprawdź aktualizację\nquit=Zakończ\nnoTerminalSet=Żadna aplikacja terminala nie została ustawiona automatycznie. Możesz to zrobić ręcznie w menu ustawień.\nconnections=Połączenia\nconnectionHub=Koncentrator połączeń\nsettings=Ustawienia\nexplorePlans=Licencja\nhelp=Pomoc\nabout=O\ndeveloper=Deweloper\nbrowseFileTitle=Przeglądaj plik\nbrowser=Przeglądarka plików\nselectFileFromComputer=Wybierz plik z tego komputera\nlinks=Łącza\nwebsite=Strona internetowa\ndiscordDescription=Dołącz do serwera Discord\nredditDescription=Dołącz do subreddita XPipe\nsecurity=Bezpieczeństwo\nsecurityPolicy=Informacje o zabezpieczeniach\nsecurityPolicyDescription=Przeczytaj szczegółowe zasady bezpieczeństwa\nprivacy=Polityka prywatności\nprivacyDescription=Przeczytaj politykę prywatności aplikacji XPipe\nslackDescription=Dołącz do obszaru roboczego Slack\nsupport=Wsparcie\ngithubDescription=Sprawdź repozytorium GitHub\nopenSourceNotices=Powiadomienia Open Source\ncheckForUpdates=Sprawdź aktualizacje\ncheckForUpdatesDescription=Pobierz aktualizację, jeśli jest dostępna\nlastChecked=Ostatnio sprawdzane\nversion=Wersja\nbuild=Wersja kompilacji\nruntimeVersion=Wersja uruchomieniowa\nvirtualMachine=Maszyna wirtualna\nupdateReady=Zainstaluj aktualizację\nupdateReadyPortable=Sprawdź aktualizację\nupdateReadyDescription=Aktualizacja została pobrana i jest gotowa do zainstalowania\nupdateReadyDescriptionPortable=Aktualizacja jest dostępna do pobrania\nupdateRestart=Uruchom ponownie, aby zaktualizować\nnever=Nigdy\nupdateAvailableTooltip=Dostępna aktualizacja\nptbAvailableTooltip=Dostępna publiczna wersja testowa\nvisitGithubRepository=Odwiedź repozytorium GitHub\nupdateAvailable=Aktualizacja dostępna: $VERSION$\ndownloadUpdate=Pobierz aktualizację\nlegalAccept=Akceptuję umowę licencyjną użytkownika końcowego\nconfirm=Potwierdź\nprint=Drukuj\nwhatsNew=Co nowego w wersji $VERSION$ ($DATE$)\nantivirusNoticeTitle=Uwaga dotycząca programów antywirusowych\nupdateChangelogAlertTitle=Changelog\ngreetingsAlertTitle=Witamy w XPipe\neula=Umowa licencyjna użytkownika końcowego\nnews=Wiadomości\nintroduction=Wprowadzenie\nprivacyPolicy=Polityka prywatności\nagree=Zgadzam się\ndisagree=Niezgoda\ndirectories=Katalogi\nlogFile=Plik dziennika\nlogFiles=Pliki dziennika\nlogFilesAttachment=Pliki dziennika\nissueReporter=Reporter błędów\nopenCurrentLogFile=Pliki dziennika\nopenCurrentLogFileDescription=Otwórz plik dziennika bieżącej sesji\nopenLogsDirectory=Otwórz katalog dzienników\ninstallationFiles=Pliki instalacyjne\nopenInstallationDirectory=Pliki instalacyjne\nopenInstallationDirectoryDescription=Otwórz katalog instalacyjny XPipe\nlaunchDebugMode=Tryb debugowania\nlaunchDebugModeDescription=Uruchom ponownie XPipe w trybie debugowania\nextensionInstallTitle=Pobierz\nextensionInstallDescription=Ta akcja wymaga dodatkowych bibliotek stron trzecich, które nie są dystrybuowane przez XPipe. Możesz je automatycznie zainstalować tutaj. Komponenty są następnie pobierane ze strony internetowej dostawcy:\nextensionInstallLicenseNote=Wykonując pobieranie i automatyczną instalację, zgadzasz się na warunki licencji stron trzecich:\nlicense=Licencja\ninstallRequired=Wymagana instalacja\nrestore=Przywróć\nrestoreAllSessions=Przywróć wszystkie sesje\nlimitedTouchscreenMode=Ograniczony tryb ekranu dotykowego\nlimitedTouchscreenModeDescription=Podczas korzystania z tej aplikacji na bardziej egzotycznym interfejsie dotykowym, takim jak ekran telefonu, niektóre menu mogą nie działać poprawnie. Gdy ta opcja jest włączona, implementacja menu wykorzystuje bardziej ograniczoną funkcjonalność do pracy z rzadko wysyłanymi zdarzeniami myszy/dotyku.\nappearance=Wygląd\ndisplay=Wyświetlacz\npersonalization=Personalizacja\ndisplayOptions=Opcje wyświetlania\ntheme=Temat\nrdpConfiguration=Konfiguracja pulpitu zdalnego\nrdpClient=Klient RDP\nrdpClientDescription=Program klienta RDP do wywołania podczas uruchamiania połączeń RDP.\\n\\nZauważ, że różne klienty mają różne stopnie możliwości i integracji. Niektóre klienty nie obsługują automatycznego przekazywania haseł, więc nadal musisz je wypełnić podczas uruchamiania.\nlocalShell=Powłoka lokalna\nthemeDescription=Twój preferowany motyw wyświetlania.\ndontAutomaticallyStartVmSshServer=Nie uruchamiaj automatycznie serwera SSH dla maszyn wirtualnych w razie potrzeby\ndontAutomaticallyStartVmSshServerDescription=Każde połączenie powłoki z maszyną wirtualną działającą w hiperwizorze jest wykonywane przez SSH. XPipe może automatycznie uruchomić zainstalowany serwer SSH w razie potrzeby. Jeśli nie chcesz tego ze względów bezpieczeństwa, możesz po prostu wyłączyć to zachowanie za pomocą tej opcji.\nconfirmGitShareTitle=Synchronizacja Git\nconfirmGitShareContent=Czy chcesz dodać wybrany plik do repozytorium git vault? Spowoduje to skopiowanie zaszyfrowanej wersji pliku do Twojego repozytorium git vault i zatwierdzenie zmian. Będziesz mieć dostęp do pliku na wszystkich zsynchronizowanych komputerach.\ngitShareFileTooltip=Dodaj plik do katalogu danych skarbca git, aby był automatycznie synchronizowany.\\n\\nTa akcja może być użyta tylko wtedy, gdy git vault jest włączony w ustawieniach.\nperformanceMode=Tryb wydajności\nperformanceModeDescription=Wyłącza wszystkie efekty wizualne, które nie są wymagane w celu poprawy wydajności aplikacji.\ndontAcceptNewHostKeys=Nie akceptuj automatycznie nowych kluczy hosta SSH\ndontAcceptNewHostKeysDescription=XPipe domyślnie automatycznie akceptuje klucze hosta z systemów, w których twój klient SSH nie ma już zapisanego znanego klucza hosta. Jeśli jednak jakikolwiek znany klucz hosta uległ zmianie, odmówi połączenia, chyba że zaakceptujesz nowy.\\n\\nWyłączenie tego zachowania pozwala sprawdzić wszystkie klucze hosta, nawet jeśli początkowo nie ma konfliktu.\nuiScale=Skala interfejsu użytkownika\nuiScaleDescription=Niestandardowa wartość skalowania, którą można ustawić niezależnie od skali wyświetlania w całym systemie. Wartości są wyrażone w procentach, więc np. wartość 150 spowoduje skalowanie interfejsu użytkownika o 150%.\neditorProgram=Edytor programu\neditorProgramDescription=Domyślny edytor tekstu używany podczas edycji dowolnego rodzaju danych tekstowych.\nwindowOpacity=Krycie okna\nwindowOpacityDescription=Zmienia krycie okna, aby śledzić, co dzieje się w tle.\nuseSystemFont=Użyj czcionki systemowej\nopenDataDir=Katalog danych skarbca\nopenDataDirButton=Otwarty katalog danych\nopenDataDirDescription=Jeśli chcesz zsynchronizować dodatkowe pliki, takie jak klucze SSH, między systemami z repozytorium git, możesz umieścić je w katalogu danych przechowywania. Wszelkie pliki, do których się tam odwołujesz, będą miały automatycznie dostosowane ścieżki plików w każdym synchronizowanym systemie.\nupdates=Aktualizacje\nselectAll=Wybierz wszystko\nadvanced=Zaawansowany\nthirdParty=Powiadomienia o otwartym kodzie źródłowym\neulaDescription=Przeczytaj umowę licencyjną użytkownika końcowego aplikacji XPipe\nthirdPartyDescription=Wyświetl licencje open source bibliotek innych firm\nworkspaceLock=Główne hasło\nenableGitStorage=Włącz synchronizację\nsharing=Udostępnianie\ngitSync=Synchronizacja Git\nenableGitStorageDescription=Po włączeniu XPipe zainicjuje repozytorium git dla lokalnego sejfu i zatwierdzi w nim wszelkie zmiany. Zwróć uwagę, że wymaga to zainstalowania git i może spowolnić operacje ładowania i zapisywania.\\n\\nWszelkie kategorie, które powinny zostać zsynchronizowane, muszą być wyraźnie oznaczone jako zsynchronizowane.\nstorageGitRemote=Adres URL zdalnej synchronizacji\nstorageGitRemoteDescription=Po ustawieniu XPipe automatycznie pobierze wszelkie zmiany podczas ładowania i wypchnie wszelkie zmiany do zdalnego repozytorium podczas zapisywania.\\n\\nPozwala to na współdzielenie skarbca między wieloma instalacjami XPipe. Obsługuje adresy URL HTTP i SSH oraz katalogi lokalne.\nvault=Skarbiec\nworkspaceLockDescription=Ustawia niestandardowe hasło do szyfrowania wszelkich poufnych informacji przechowywanych w XPipe.\\n\\nPowoduje to zwiększenie bezpieczeństwa, ponieważ zapewnia dodatkową warstwę szyfrowania przechowywanych poufnych informacji. Po uruchomieniu XPipe zostaniesz poproszony o wprowadzenie hasła.\nuseSystemFontDescription=Kontroluje, czy używać domyślnej czcionki systemowej, czy czcionki Inter, która jest dołączona do XPipe.\ntooltipDelay=Opóźnienie podpowiedzi\ntooltipDelayDescription=Liczba milisekund oczekiwania na wyświetlenie podpowiedzi.\nfontSize=Rozmiar czcionki\nwindowOptions=Opcje okna\nsaveWindowLocation=Zapisz lokalizację okna\nsaveWindowLocationDescription=Kontroluje, czy współrzędne okna powinny być zapisywane i przywracane przy ponownym uruchomieniu.\nstartupShutdown=Uruchamianie / wyłączanie\nshowChildrenConnectionsInParentCategory=Pokaż kategorie podrzędne w kategorii nadrzędnej\nshowChildrenConnectionsInParentCategoryDescription=Czy uwzględniać wszystkie połączenia znajdujące się w podkategoriach, gdy wybrana jest określona kategoria nadrzędna.\\n\\nJeśli ta opcja jest wyłączona, kategorie zachowują się bardziej jak klasyczne foldery, które pokazują tylko swoją bezpośrednią zawartość bez uwzględniania folderów podrzędnych.\ncondenseConnectionDisplay=Skondensuj wyświetlanie połączenia\ncondenseConnectionDisplayDescription=Spraw, aby każde połączenie najwyższego poziomu zajmowało mniej miejsca w pionie, aby umożliwić bardziej skondensowaną listę połączeń.\nopenConnectionSearchWindowOnConnectionCreation=Otwórz okno wyszukiwania połączenia podczas tworzenia połączenia\nopenConnectionSearchWindowOnConnectionCreationDescription=Określenie, czy po dodaniu nowego połączenia powłoki ma być automatycznie otwierane okno wyszukiwania dostępnych połączeń podrzędnych.\nworkflow=Przepływ pracy\nsystem=System\napplication=Aplikacja\nstorage=Przechowywanie\nrunOnStartup=Uruchom przy starcie\ncloseBehaviour=Zachowanie przy wyjściu\ncloseBehaviourDescription=Kontroluje sposób, w jaki XPipe powinien działać po zamknięciu głównego okna.\nlanguage=Język\nlanguageDescription=Język wyświetlania do użycia. Tłumaczenia są ulepszane dzięki wkładowi społeczności. Możesz pomóc w tłumaczeniu, przesyłając poprawki tłumaczenia na GitHub.\nlightTheme=Motyw światła\ndarkTheme=Ciemny motyw\nexit=Zamknij XPipe\ncontinueInBackground=Kontynuuj w tle\nminimizeToTray=Zminimalizuj do zasobnika\ncloseBehaviourAlertTitle=Ustaw zachowanie podczas zamykania\ncloseBehaviourAlertTitleHeader=Wybierz, co ma się dziać podczas zamykania okna. Wszelkie aktywne połączenia zostaną zamknięte po zamknięciu aplikacji.\nstartupBehaviour=Zachowanie podczas uruchamiania\nstartupBehaviourDescription=Kontroluje domyślne zachowanie aplikacji desktopowej po uruchomieniu XPipe.\nclearCachesAlertTitle=Wyczyść pamięć podręczną\nclearCachesAlertContent=Czy chcesz wyczyścić wszystkie pamięci podręczne XPipe? Spowoduje to usunięcie wszystkich danych pamięci podręcznej, które są przechowywane w celu poprawy komfortu użytkowania.\nstartGui=Uruchom GUI\nstartInTray=Uruchom w zasobniku\nstartInBackground=Uruchom w tle\nclearCaches=Wyczyść pamięć podręczną ...\nclearCachesDescription=Usuń wszystkie dane z pamięci podręcznej\ncancel=Anuluj\nnotAnAbsolutePath=Nie jest ścieżką bezwzględną\nnotADirectory=Nie katalog\nnotAnEmptyDirectory=Nie pusty katalog\nautomaticallyCheckForUpdates=Sprawdź aktualizacje\nautomaticallyCheckForUpdatesDescription=Po włączeniu, informacje o nowych wersjach są automatycznie pobierane, gdy XPipe jest uruchomiony po pewnym czasie. Nadal musisz wyraźnie potwierdzić instalację aktualizacji.\nsendAnonymousErrorReports=Wysyłaj anonimowe raporty o błędach\nsendUsageStatistics=Wysyłaj anonimowe statystyki użytkowania\nstorageDirectory=Katalog pamięci masowej\nstorageDirectoryDescription=Lokalizacja, w której XPipe powinien przechowywać wszystkie informacje o połączeniu. Podczas zmiany tego ustawienia, dane w starym katalogu nie są kopiowane do nowego.\nlogLevel=Poziom dziennika\nappBehaviour=Zachowanie aplikacji\nlogLevelDescription=Poziom dziennika, który powinien być używany podczas zapisywania plików dziennika.\ndeveloperMode=Tryb programisty\ndeveloperModeDescription=Po włączeniu będziesz mieć dostęp do wielu dodatkowych opcji, które są przydatne podczas programowania.\neditor=Edytor\ncustom=Niestandardowy\npasswordManager=Menedżer haseł\nexternalPasswordManager=Zewnętrzny menedżer haseł\npasswordManagerDescription=Lokalnie zainstalowany menedżer haseł do integracji.\\n\\nJeśli masz zainstalowany menedżer haseł, możesz skonfigurować XPipe do pobierania z niego haseł, aby XPipe nie musiał przechowywać haseł samodzielnie. Po włączeniu, każde pole hasła dla połączenia może być następnie skonfigurowane do korzystania z menedżera haseł.\npasswordManagerCommandTest=Przetestuj menedżera haseł\npasswordManagerCommandTestDescription=Możesz tutaj sprawdzić, czy dane wyjściowe wyglądają poprawnie, jeśli skonfigurowałeś menedżera haseł.\npreferTerminalTabs=Preferuj otwieranie nowych kart\npreferTerminalTabsDescription=Kontroluje, czy XPipe będzie próbował otwierać nowe karty w wybranym terminalu zamiast nowych okien. Nie każdy terminal obsługuje zakładki.\ncustomRdpClientCommand=Polecenie niestandardowe\ncustomRdpClientCommandDescription=Polecenie do wykonania w celu uruchomienia niestandardowego klienta RDP.\\n\\nŁańcuch zastępczy $FILE zostanie zastąpiony cytowaną bezwzględną nazwą pliku .rdp po wywołaniu. Pamiętaj, aby zacytować ścieżkę wykonywalną, jeśli zawiera spacje.\ncustomEditorCommand=Niestandardowe polecenie edytora\ncustomEditorCommandDescription=Polecenie do wykonania w celu uruchomienia edytora niestandardowego.\\n\\nŁańcuch zastępczy $FILE zostanie zastąpiony cytowaną bezwzględną nazwą pliku po wywołaniu. Pamiętaj, aby zacytować ścieżkę wykonywalną edytora, jeśli zawiera spacje.\neditorReloadTimeout=Limit czasu przeładowania edytora\neditorReloadTimeoutDescription=Ilość milisekund, które należy odczekać przed odczytaniem pliku po jego aktualizacji. Pozwala to uniknąć problemów w przypadkach, gdy twój edytor wolno zapisuje lub zwalnia blokady plików.\nencryptAllVaultData=Zaszyfruj wszystkie dane w sejfie\nencryptAllVaultDataDescription=Po włączeniu tej funkcji każda część danych połączenia skarbca będzie szyfrowana kluczem szyfrowania skarbca użytkownika, a nie tylko sekrety zawarte w tych danych. Dodaje to kolejną warstwę zabezpieczeń dla innych parametrów, takich jak nazwy użytkowników, nazwy hostów itp., które nie są domyślnie szyfrowane w skarbcu.\\n\\nOpcja ta sprawi, że twoja historia skarbca git i różnice będą bezużyteczne, ponieważ nie możesz już zobaczyć oryginalnych zmian, a jedynie zmiany binarne.\nvaultSecurity=Zabezpieczenie skarbca\ndeveloperDisableUpdateVersionCheck=Wyłącz sprawdzanie wersji aktualizacji\ndeveloperDisableUpdateVersionCheckDescription=Kontroluje, czy narzędzie do sprawdzania aktualizacji zignoruje numer wersji podczas wyszukiwania aktualizacji.\ndeveloperDisableGuiRestrictions=Wyłącz ograniczenia GUI\ndeveloperDisableGuiRestrictionsDescription=Kontroluje, czy niektóre wyłączone akcje mogą być nadal wykonywane z poziomu interfejsu użytkownika.\ndeveloperShowHiddenEntries=Pokaż ukryte wpisy\ndeveloperShowHiddenEntriesDescription=Po włączeniu wyświetlane będą ukryte i wewnętrzne źródła danych.\ndeveloperShowHiddenProviders=Pokaż ukrytych dostawców\ndeveloperShowHiddenProvidersDescription=Określa, czy w oknie dialogowym tworzenia będą wyświetlani ukryci i wewnętrzni dostawcy połączeń i źródeł danych.\ndeveloperDisableConnectorInstallationVersionCheck=Wyłącz sprawdzanie wersji złącza\ndeveloperDisableConnectorInstallationVersionCheckDescription=Kontroluje, czy narzędzie do sprawdzania aktualizacji zignoruje numer wersji podczas sprawdzania wersji łącznika XPipe zainstalowanego na zdalnym komputerze.\nshellCommandTest=Test poleceń powłoki\nshellCommandTestDescription=Uruchom polecenie w sesji powłoki używanej wewnętrznie przez XPipe.\nterminal=Terminal\nterminalType=Emulator terminala\nterminalConfiguration=Konfiguracja terminala\nterminalCustomization=Dostosowywanie terminala\neditorConfiguration=Konfiguracja edytora\ndefaultApplication=Aplikacja domyślna\ninitialSetup=Konfiguracja początkowa\nterminalTypeDescription=Domyślny terminal używany do otwierania połączeń powłoki.\\n\\nPoziom obsługi funkcji różni się w zależności od terminala, a każdy z nich jest oznaczony jako zalecany lub niezalecany. Twój komfort użytkowania będzie najlepszy, jeśli korzystasz z zalecanego terminala.\nprogram=Program\ncustomTerminalCommand=Niestandardowe polecenie terminala\ncustomTerminalCommandDescription=Polecenie do wykonania w celu otwarcia terminala niestandardowego z danym poleceniem.\\n\\nXPipe utworzy tymczasowy skrypt powłoki uruchamiającej dla twojego terminala do wykonania. Łańcuch zastępczy $CMD w poleceniu, które podasz, zostanie zastąpiony przez rzeczywisty skrypt uruchamiający po wywołaniu. Pamiętaj, aby zacytować ścieżkę wykonywalną terminala, jeśli zawiera spacje.\nclearTerminalOnInit=Wyczyść terminal podczas inicjowania\nclearTerminalOnInitDescription=Po włączeniu XPipe uruchomi polecenie czyszczenia po uruchomieniu nowej sesji terminala, aby usunąć wszelkie niepotrzebne dane wyjściowe, które zostały wydrukowane podczas uruchamiania sesji terminala.\ndontCachePasswords=Nie buforuj wyświetlanych haseł\ndontCachePasswordsDescription=Kontroluje, czy wyszukiwane hasła powinny być buforowane wewnętrznie przez XPipe, abyś nie musiał ich ponownie wprowadzać w bieżącej sesji.\\n\\nJeśli to zachowanie jest wyłączone, musisz ponownie wprowadzać monitowane dane uwierzytelniające za każdym razem, gdy są one wymagane przez system.\ndenyTempScriptCreation=Odmowa utworzenia tymczasowego skryptu\ndenyTempScriptCreationDescription=Aby zrealizować niektóre ze swoich funkcji, XPipe czasami tworzy tymczasowe skrypty powłoki w systemie docelowym, aby umożliwić łatwe wykonywanie prostych poleceń. Nie zawierają one żadnych poufnych informacji i są tworzone wyłącznie w celach wdrożeniowych.\\n\\nJeśli to zachowanie jest wyłączone, XPipe nie będzie tworzyć żadnych plików tymczasowych w zdalnym systemie. Opcja ta jest przydatna w kontekstach o wysokim poziomie bezpieczeństwa, gdzie każda zmiana systemu plików jest monitorowana. Jeśli ta opcja jest wyłączona, niektóre funkcje, np. środowiska powłoki i skrypty, nie będą działać zgodnie z przeznaczeniem.\ndisableCertutilUse=Wyłącz korzystanie z certutil w systemie Windows\nuseLocalFallbackShell=Użyj lokalnej powłoki awaryjnej\nuseLocalFallbackShellDescription=Przełącz się na używanie innej powłoki lokalnej do obsługi operacji lokalnych. Może to być PowerShell w systemie Windows lub powłoka Bourne w innych systemach.\\n\\nOpcja ta może być używana w przypadku, gdy normalna domyślna powłoka lokalna jest wyłączona lub w pewnym stopniu uszkodzona. Niektóre funkcje mogą jednak nie działać zgodnie z oczekiwaniami, gdy ta opcja jest włączona.\ndisableCertutilUseDescription=Ze względu na kilka niedociągnięć i błędów w cmd.exe, tymczasowe skrypty powłoki są tworzone za pomocą certutil, używając go do dekodowania danych wejściowych base64, ponieważ cmd.exe łamie się na danych wejściowych innych niż ASCII. XPipe może również używać do tego PowerShell, ale będzie to wolniejsze.\\n\\nWyłącza to jakiekolwiek użycie certutil w systemach Windows do realizacji niektórych funkcji i zamiast tego powraca do PowerShell. Może to zadowolić niektóre programy antywirusowe, ponieważ niektóre z nich blokują użycie certutil.\ndisableTerminalRemotePasswordPreparation=Wyłącz przygotowanie zdalnego hasła terminala\ndisableTerminalRemotePasswordPreparationDescription=W sytuacjach, w których w terminalu należy ustanowić zdalne połączenie powłoki, które przechodzi przez wiele systemów pośrednich, może istnieć wymóg przygotowania wszelkich wymaganych haseł w jednym z systemów pośrednich, aby umożliwić automatyczne wypełnianie wszelkich monitów.\\n\\nJeśli nie chcesz, aby hasła były kiedykolwiek przesyłane do jakiegokolwiek systemu pośredniego, możesz wyłączyć to zachowanie. Wszelkie wymagane hasła pośrednie będą następnie sprawdzane w samym terminalu po jego otwarciu.\nmore=Więcej\ntranslate=Tłumaczenia\nallConnections=Wszystkie połączenia\nallScripts=Wszystkie skrypty\nallIdentities=Wszystkie tożsamości\nsynced=Zsynchronizowany\npredefined=Predefiniowany\nsamples=Próbki\ngoodMorning=Dzień dobry\ngoodAfternoon=Dzień dobry\ngoodEvening=Dobry wieczór\naddVisual=Visual ...\naddDesktop=Pulpit ...\nssh=SSH\nsshConfiguration=Konfiguracja SSH\nsize=Rozmiar\nattributes=Atrybuty\nmodified=Zmodyfikowano\nowner=Właściciel\nupdateReadyTitle=Zaktualizuj do $VERSION$ ready\ntemplates=Szablony\nretry=Retry\nretryAll=Ponów wszystkie\nreplace=Wymień\nreplaceAll=Zastąp wszystko\nhibernateBehaviour=Zachowanie w stanie hibernacji\nhibernateBehaviourDescription=Kontroluje zachowanie aplikacji, gdy system jest w stanie hibernacji/uśpienia.\noverview=Przegląd\nhistory=Historia\nskipAll=Pomiń wszystko\nnotes=Uwagi\naddNotes=Dodaj notatki\norder=Zmień kolejność\nkeepFirst=Zachowaj pierwszy\nkeepLast=Zachowaj ostatnie\npinToTop=Przypnij do góry\nunpinFromTop=Odepnij od góry\norderAheadOf=Zamów przed ...\nclearIndex=Zresetuj indeks\nhttpServer=Serwer HTTP\nmcpServer=Serwer MCP\napiKey=Klucz API\napiKeyDescription=Klucz API do uwierzytelniania żądań API demona XPipe. Aby uzyskać więcej informacji na temat uwierzytelniania, zapoznaj się z ogólną dokumentacją API.\ndisableApiAuthentication=Wyłącz uwierzytelnianie API\ndisableApiAuthenticationDescription=Wyłącza wszystkie wymagane metody uwierzytelniania, dzięki czemu każde nieuwierzytelnione żądanie zostanie obsłużone.\\n\\nUwierzytelnianie powinno być wyłączone tylko do celów programistycznych.\napi=API\nstoreIntroImportContent=Używasz już XPipe w innym systemie? Zsynchronizuj istniejące połączenia w wielu systemach za pomocą zdalnego repozytorium git. Możesz również zsynchronizować później w dowolnym momencie, jeśli nie jest jeszcze skonfigurowany.\nstoreIntroImportButton=Synchronizuj połączenia ...\nstoreIntroImportHeader=Importuj połączenia\nshowNonRunningChildren=Pokaż niedziałające dzieci\nhttpApi=HTTP API\nisOnlySupportedLimit=jest obsługiwany tylko z licencją profesjonalną, gdy masz więcej niż $COUNT$ połączeń\nareOnlySupportedLimit=są obsługiwane tylko z licencją profesjonalną, gdy masz więcej niż $COUNT$ połączeń\nenabled=Włączony\nenableGitStoragePtbDisabled=Synchronizacja Git jest wyłączona dla publicznych kompilacji testowych, aby zapobiec używaniu ich z regularnymi repozytoriami Git i zniechęcić do używania kompilacji PTB jako codziennego sterownika.\ncopyId=Kopiuj identyfikator API\nrequireDoubleClickForConnections=Wymagaj podwójnego kliknięcia dla połączeń\nrequireDoubleClickForConnectionsDescription=Jeśli ta opcja jest włączona, musisz dwukrotnie kliknąć połączenia, aby je uruchomić. Jest to przydatne, jeśli jesteś przyzwyczajony do dwukrotnego klikania.\nclearTransferDescription=Wyczyść zaznaczenie\nselectTab=Wybierz kartę\ncloseTab=Zamknij kartę\ncloseOtherTabs=Zamknij inne karty\ncloseAllTabs=Zamknij wszystkie karty\ncloseLeftTabs=Zamknij karty po lewej stronie\ncloseRightTabs=Zamknij karty po prawej stronie\naddSerial=Szeregowe ...\nconnect=Połącz\nworkspaces=Przestrzenie robocze\nmanageWorkspaces=Zarządzaj obszarami roboczymi\naddWorkspace=Dodaj obszar roboczy ...\nworkspaceAdd=Dodaj nowy obszar roboczy\nworkspaceAddDescription=Przestrzenie robocze są odrębnymi konfiguracjami do uruchamiania XPipe. Każdy obszar roboczy ma katalog danych, w którym wszystkie dane są przechowywane lokalnie. Obejmuje to dane połączenia, ustawienia i inne.\\n\\nJeśli korzystasz z funkcji synchronizacji, możesz również zsynchronizować każdy obszar roboczy z innym repozytorium git.\nworkspaceName=Nazwa obszaru roboczego\nworkspaceNameDescription=Wyświetlana nazwa obszaru roboczego\nworkspacePath=Ścieżka obszaru roboczego\nworkspacePathDescription=Lokalizacja katalogu danych obszaru roboczego\nworkspaceCreationAlertTitle=Tworzenie obszaru roboczego\ndeveloperForceSshTty=Wymuś SSH TTY\ndeveloperForceSshTtyDescription=Spraw, aby wszystkie połączenia SSH przydzielały pty, aby przetestować obsługę brakującego stderr i pty.\ndeveloperDisableSshTunnelGateways=Wyłącz tunelowanie bramy SSH\ndeveloperDisableSshTunnelGatewaysDescription=Nie używaj sesji tunelowych dla bram i zamiast tego połącz się bezpośrednio z systemem.\nttyWarning=Połączenie na siłę przydzieliło pty/tty i nie zapewnia oddzielnego strumienia stderr.\\n\\nMoże to prowadzić do kilku problemów.\\n\\nJeśli możesz, spróbuj sprawić, by polecenie połączenia nie alokowało pty.\nxshellSetup=Konfiguracja Xshell\ntermiusSetup=Konfiguracja Termius\ntryPtbDescription=Wypróbuj nowe funkcje we wczesnych wersjach deweloperskich XPipe\nconfirmVaultUnencryptTitle=Potwierdź odszyfrowanie sejfu\nconfirmVaultUnencryptContent=Czy naprawdę chcesz wyłączyć zaawansowane szyfrowanie sejfu? Spowoduje to usunięcie dodatkowego szyfrowania przechowywanych danych i nadpisanie istniejących danych.\nenableHttpApi=Włącz interfejs API HTTP\nenableHttpApiDescription=Włącza interfejs API, umożliwiając zewnętrznym programom wywoływanie demona XPipe w celu wykonywania działań z zarządzanymi połączeniami.\nchooseCustomIcon=Wybierz ikonę niestandardową\ngitVault=Skarbiec Git\nfileBrowser=Przeglądarka plików\nconfirmAllDeletions=Potwierdź wszystkie usunięcia\nconfirmAllDeletionsDescription=Czy wyświetlać okno dialogowe potwierdzenia dla wszystkich operacji usuwania. Domyślnie tylko katalogi wymagają potwierdzenia.\nyesterday=Wczoraj\ngreen=Zielony\nyellow=Żółty\nblue=Niebieski\nred=Czerwony\ncyan=Cyan\npurple=Fioletowy\nasktextAlertTitle=Podpowiedź\nfileWriteSudoTitle=Zapis pliku Sudo\nfileWriteSudoContent=Plik, który próbujesz zapisać, nie przyznaje uprawnień do zapisu Twojemu użytkownikowi. Czy chcesz zapisać ten plik jako root z sudo? Spowoduje to automatyczne podniesienie uprawnień do roota za pomocą istniejących poświadczeń lub za pomocą monitu.\ndontAllowTerminalRestart=Nie zezwalaj na ponowne uruchomienie terminala\ndontAllowTerminalRestartDescription=Domyślnie sesje terminalowe mogą być ponownie uruchamiane po ich zakończeniu z poziomu terminala. Aby to umożliwić, XPipe zaakceptuje te zewnętrzne żądania z terminala, aby ponownie uruchomić sesję\\n\\nXPipe nie ma żadnej kontroli nad terminalem i skąd pochodzi to połączenie, więc złośliwe aplikacje lokalne mogą również korzystać z tej funkcji do uruchamiania połączeń za pośrednictwem XPipe. Wyłączenie tej funkcji zapobiega takiemu scenariuszowi.\nopenDocumentation=Otwórz dokumentację\nopenDocumentationDescription=Odwiedź stronę dokumentacji XPipe dla tego zagadnienia\nrenameAll=Zmień nazwę wszystkich\nlogging=Rejestrowanie\nenableTerminalLogging=Włącz rejestrowanie terminala\nenableTerminalLoggingDescription=Włącza logowanie po stronie klienta dla wszystkich sesji terminalowych. Wszystkie dane wejściowe i wyjściowe sesji terminala są zapisywane w pliku dziennika sesji. Zwróć uwagę, że wszelkie poufne informacje, takie jak monity o hasło, nie są rejestrowane.\nterminalLoggingDirectory=Dzienniki sesji terminala\nterminalLoggingDirectoryDescription=Wszystkie dzienniki są przechowywane w katalogu danych XPipe w systemie lokalnym.\nopenSessionLogs=Otwórz dzienniki sesji\nsessionLogging=Rejestrowanie terminala\nsessionActive=Dla tego połączenia uruchomiona jest sesja w tle.\\n\\nAby zatrzymać tę sesję ręcznie, kliknij wskaźnik stanu.\nskipValidation=Pomiń walidację\nscriptsIntroHeader=O skryptach\nscriptsIntroContent=Możesz uruchamiać skrypty podczas inicjowania powłoki, w przeglądarce plików i na żądanie. Możesz samodzielnie tworzyć skrypty w XPipe lub importować istniejące z lokalnego systemu lub ze zdalnego repozytorium git.\nscriptsIntroBottomHeader=Używanie skryptów\nscriptsIntroBottomContent=Na początek możesz skorzystać z wielu przykładowych skryptów. Możesz kliknąć przycisk edycji poszczególnych skryptów, aby zobaczyć, jak zostały zaimplementowane. Skrypty muszą być najpierw włączone, aby można je było uruchamiać i wyświetlać w menu.\nscriptsIntroBottomButton=Rozpocznij\nscriptSourcesIntroHeader=Źródła skryptów\nscriptSourcesIntroContent=Możesz dodać niestandardowe źródła skryptów, aby mieć natychmiastowy dostęp do całej kolekcji skryptów powłoki. Jako źródła obsługiwane są zarówno źródła lokalne, jak i zdalne repozytoria git. Wszystkie wykryte skrypty ze źródła staną się dostępne automatycznie.\nscriptSourcesIntroButton=Dodaj źródło ...\ncheckForSecurityUpdates=Sprawdź aktualizacje zabezpieczeń\ncheckForSecurityUpdatesDescription=XPipe może sprawdzać potencjalne aktualizacje zabezpieczeń niezależnie od normalnych aktualizacji funkcji. Gdy ta funkcja jest włączona, przynajmniej ważne aktualizacje zabezpieczeń będą zalecane do zainstalowania, nawet jeśli normalne sprawdzanie aktualizacji jest wyłączone.\\n\\nWyłączenie tego ustawienia spowoduje, że nie będzie wykonywane zewnętrzne żądanie wersji i nie będziesz powiadamiany o żadnych aktualizacjach zabezpieczeń.\nclickToDock=Kliknij, aby zadokować terminal\nterminalStarting=Oczekiwanie na uruchomienie terminala ...\npinTab=Zakładka pin\nunpinTab=Odepnij kartę\npinned=Przypięty\nenableConnectionHubTerminalDocking=Włącz dokowanie terminala koncentratora połączeń\nenableConnectionHubTerminalDockingDescription=Możesz zadokować okna terminala do okna aplikacji XPipe w koncentratorze połączeń, aby symulować nieco zintegrowany terminal. Okna terminala są następnie zarządzane przez XPipe, aby zawsze pasowały do stacji dokującej.\nenableFileBrowserTerminalDocking=Włącz dokowanie terminala przeglądarki plików\nenableFileBrowserTerminalDockingDescription=Możesz zadokować okna terminala do okna aplikacji XPipe w przeglądarce plików, aby symulować nieco zintegrowany terminal. Okna terminala są następnie zarządzane przez XPipe, aby zawsze mieściły się w doku.\ndownloadsDirectory=Niestandardowy katalog pobierania\ndownloadsDirectoryDescription=Niestandardowy katalog, w którym mają być umieszczane pobrane pliki po kliknięciu przycisku przeniesienia do pobranych plików. Domyślnie XPipe użyje twojego katalogu pobierania użytkownika.\npinLocalMachineOnStartup=Przypnij kartę komputera lokalnego podczas uruchamiania\npinLocalMachineOnStartupDescription=Automatycznie otwórz kartę komputera lokalnego i przypnij ją. Jest to przydatne, jeśli często korzystasz z podzielonej przeglądarki plików z otwartym komputerem lokalnym i zdalnym systemem plików.\nterminalErrorDescription=Ten błąd jest terminalem i XPipe nie może kontynuować bez jego naprawienia.\ngroupName=Nazwa grupy\nchmodPermissions=Nowe uprawnienia\neditFilesWithDoubleClick=Edytuj pliki za pomocą podwójnego kliknięcia\neditFilesWithDoubleClickDescription=Po włączeniu, dwukrotne kliknięcie plików otworzy je bezpośrednio w edytorze tekstu, zamiast wyświetlać menu kontekstowe.\ncensorMode=Tryb cenzora\ncensorModeDescription=Zamazuje wszelkie informacje, takie jak nazwy hostów, nazwy użytkowników, nazwy połączeń i inne.\\n\\nJest to przydatne, jeśli zamierzasz wykonać zrzut ekranu lub udostępnić ekran XPipe i nie chcesz, aby wyciekły jakiekolwiek informacje.\naddIdentity=Tożsamość ...\nidentities=Tożsamości\naddMacro=Działanie ...\nidentitiesIntroHeader=O tożsamości\nidentitiesIntroContent=Jeśli ponownie używasz typowych kombinacji nazw użytkownika, haseł i kluczy, sensowne może być utworzenie tożsamości wielokrotnego użytku. Dzięki temu możesz szybko odwoływać się do nich podczas dodawania nowych połączeń.\nidentitiesIntroBottomHeader=Udostępnianie tożsamości\nidentitiesIntroBottomContent=Możesz dodawać tożsamości lokalnie lub synchronizować je w repozytorium git, gdy jest ono włączone. Pozwala to na selektywne udostępnianie tożsamości w wielu systemach i innym członkom zespołu.\nidentitiesIntroBottomButton=Synchronizacja ustawień\nidentitiesIntroButton=Utwórz tożsamość\nuserName=Nazwa użytkownika\nuserAuth=Uwierzytelnianie za pomocą hasła użytkownika\ngroupAuth=Uwierzytelnianie tajne oparte na grupach\nteam=Zespół\nteamSettings=Ustawienia zespołu\nteamVaults=Sejfy zespołów\nvaultTypeNameDefault=Domyślny skarbiec\nvaultTypeNameLegacy=Osobisty skarbiec\nvaultTypeNamePersonal=Skarbiec osobisty\nvaultTypeNameTeam=Skarbiec zespołu\nteamVaultsDescription=Sejfy zespołowe umożliwiają wielu użytkownikom i grupom bezpieczny dostęp do współdzielonego sejfu. Możesz skonfigurować połączenia i tożsamości tak, aby były udostępniane wszystkim użytkownikom lub były dostępne tylko dla poszczególnych użytkowników i grup, szyfrując je za pomocą własnego klucza. Inni użytkownicy skarbca nie mogą uzyskać dostępu do osobistych i grupowych połączeń i tożsamości, jeśli nie mają dostępu do klucza.\nvaultTypeContentDefault=Obecnie używasz domyślnego sejfu bez użytkownika i niestandardowego hasła. Sekrety są szyfrowane za pomocą lokalnego klucza skarbca. Możesz uaktualnić do skarbca osobistego, tworząc konto użytkownika skarbca. Dzięki temu możesz szyfrować sekrety skarbca za pomocą osobistego hasła, które musisz wprowadzić przy każdym logowaniu, aby odblokować skarbiec.\nvaultTypeContentLegacy=Obecnie używasz starszego sejfu osobistego dla swojego użytkownika. Sekrety są szyfrowane Twoim osobistym hasłem. Ta starsza kompatybilność ma ograniczone funkcje i nie można jej uaktualnić do skarbca zespołu na miejscu.\nvaultTypeContentPersonal=Obecnie korzystasz z osobistego sejfu dla swojego użytkownika. Sekrety są szyfrowane za pomocą Twojego osobistego hasła. Możesz uaktualnić do skarbca zespołu, dodając dodatkowych użytkowników skarbca lub dodając konfigurację dostępu opartą na grupach.\nvaultTypeContentTeam=Obecnie korzystasz z sejfu zespołowego, który umożliwia wielu użytkownikom bezpieczny dostęp do współdzielonego sejfu. Możesz skonfigurować połączenia i tożsamości tak, aby były udostępniane wszystkim użytkownikom lub były dostępne tylko dla Twojego osobistego użytkownika lub grupy, szyfrując je kluczem osobistym lub grupowym. Inni użytkownicy skarbca nie mogą uzyskać dostępu do twoich osobistych i grupowych połączeń i tożsamości, jeśli nie mają dostępu do klucza.\ngroupManagement=Zarządzanie grupami\ngroupManagementEmpty=Zarządzanie grupami\ngroupManagementDescription=Zarządzaj istniejącymi grupami sejfów lub twórz nowe. Każda grupa sejfów ma swój własny klucz tajny, który jest używany do szyfrowania połączeń i tożsamości, które powinny być dostępne tylko dla grupy, a nie dla innych.\ngroupManagementEmptyDescription=Zarządzaj istniejącymi grupami sejfów lub twórz nowe. Każda grupa sejfów ma swój własny klucz tajny, który jest używany do szyfrowania połączeń i tożsamości, które powinny być dostępne tylko dla grupy, a nie dla innych.\\n\\nKonta grupowe dla zespołu są obsługiwane w planie profesjonalnym.\nuserManagement=Zarządzanie użytkownikami\nuserManagementEmpty=Zarządzanie użytkownikami\nuserManagementDescription=Zarządzaj istniejącymi użytkownikami sejfu lub twórz nowych. Każdy użytkownik sejfu ma swoje indywidualne hasło, które służy do szyfrowania połączeń i tożsamości, które powinny być dostępne tylko dla niego, a nie dla innych.\nuserManagementEmptyDescription=Zarządzaj istniejącymi użytkownikami sejfu lub twórz nowych. Każdy użytkownik skarbca ma swoje indywidualne hasło, które jest używane do szyfrowania połączeń i tożsamości, które powinny być dostępne tylko dla użytkownika, a nie dla innych. Utwórz użytkownika dla siebie, aby móc szyfrować połączenia i tożsamości za pomocą osobistego klucza.\\n\\nPojedyncze konto użytkownika jest obsługiwane w edycji społecznościowej. Wiele kont użytkowników dla zespołu jest obsługiwanych w planie profesjonalnym.\nuserIntroHeader=Zarządzanie użytkownikami\nuserIntroContent=Na początek utwórz dla siebie pierwsze konto użytkownika. Dzięki temu możesz zablokować ten obszar roboczy hasłem.\naddReusableIdentity=Dodaj tożsamość wielokrotnego użytku\nusers=Użytkownicy\nsyncVault=Synchronizacja sejfu\nsyncVaultDescription=Aby zsynchronizować swój sejf z wieloma systemami lub z wieloma członkami zespołu, włącz synchronizację git dla tego sejfu.\nenableGitSync=Włącz synchronizację git\nbrowseVault=Dane skarbca\nbrowseVaultDescription=Możesz samodzielnie zajrzeć do katalogu skarbca w swoim natywnym menedżerze plików. Pamiętaj, że zewnętrzne zmiany nie są zalecane i mogą powodować różne problemy.\nbrowseVaultButton=Przeglądaj skarbiec\nvaultUsers=Użytkownicy sejfu\ncreateHeapDump=Utwórz zrzut sterty\ncreateHeapDumpDescription=Zrzuć zawartość pamięci do pliku, aby rozwiązać problem z wykorzystaniem pamięci\ninitializingApp=Ładowanie połączeń\ncheckingLicense=Sprawdzanie licencji\nloadingGit=Synchronizacja z repozytorium git\nloadingGpg=Uruchamianie demona GnuPG dla git\nloadingSettings=Ustawienia ładowania\nloadingConnections=Ładowanie połączeń\nunlockingVault=Odblokowywanie sejfu\nloadingUserInterface=Ładowanie interfejsu użytkownika\nptbNotice=Powiadomienie o publicznej wersji testowej\nuserDeletionTitle=Usuwanie użytkownika\nuserDeletionContent=Czy chcesz usunąć tego użytkownika skarbca? Spowoduje to ponowne zaszyfrowanie wszystkich twoich osobistych tożsamości i sekretów połączeń przy użyciu klucza skarbca, który jest dostępny dla wszystkich użytkowników. Zajmie to trochę czasu i XPipe uruchomi się ponownie, aby zastosować zmiany użytkownika.\ngroupDeletionTitle=Usuwanie grupy\ngroupDeletionContent=Czy chcesz usunąć tę grupę sejfów? Spowoduje to ponowne zaszyfrowanie wszystkich tożsamości i sekretów połączeń tylko dla grupy przy użyciu klucza skarbca, który jest dostępny dla wszystkich użytkowników. Zajmie to trochę czasu i XPipe uruchomi się ponownie, aby zastosować zmiany w grupie.\nkillTransfer=Kill transfer\ndestination=Przeznaczenie\nconfiguration=Konfiguracja\nnewFile=Nowy plik\nnewLink=Nowy link\nlinkName=Nazwa łącza\nscanConnections=Znajdź dostępne połączenia ...\nobserve=Rozpocznij obserwację\nstopObserve=Przestań obserwować\ncreateShortcut=Utwórz skrót na pulpicie\nbrowseFiles=Przeglądaj pliki\nclone=Klon\ntargetPath=Ścieżka docelowa\nnewDirectory=Nowy katalog\ncopyShareLink=Kopiuj łącze\nselectStore=Wybierz sklep\nsaveSource=Zapisz na później\nexecute=Wykonaj\ndeleteChildren=Usuń wszystkie dzieci\nscriptGroupDescriptionDescription=Nadaj tej grupie opcjonalny opis\nabstractHostDescriptionDescription=Nadaj temu hostowi opcjonalny opis\nselectSource=Wybierz źródło\ncommandLineRead=Aktualizacja\ncommandLineWrite=Napisz\nadditionalOptions=Opcje dodatkowe\ninput=Wprowadzanie\nmachine=Maszyna\nopen=Otwarty\nedit=Edytuj\nscriptContents=Zawartość skryptu\nscriptContentsDescription=Polecenia skryptu do wykonania\nsnippets=Zależności skryptu\nsnippetsDescription=Inne skrypty do uruchomienia w pierwszej kolejności\nsnippetsDependenciesDescription=Wszystkie możliwe skrypty, które powinny zostać uruchomione, jeśli dotyczy\nisDefault=Uruchamiany w init we wszystkich kompatybilnych powłokach\nbringToShells=Doprowadź do wszystkich kompatybilnych powłok\nisDefaultGroup=Uruchom wszystkie skrypty grupy w powłoce init\nexecutionType=Typ wykonania\nexecutionTypeDescription=W jakich kontekstach używać tego skryptu\nminimumShellDialect=Typ powłoki\nminimumShellDialectDescription=Typ powłoki, w której chcesz uruchomić ten skrypt\ndumbOnly=Głupi\nterminalOnly=Terminal\nboth=Oba\nshouldElevate=Powinien podnosić\nshouldElevateDescription=Czy uruchomić ten skrypt z podwyższonymi uprawnieniami?\nscript.displayName=Skrypt powłoki\nscript.displayDescription=Utwórz skrypt powłoki wielokrotnego użytku\nscriptGroup.displayName=Grupa skryptów\nscriptGroup.displayDescription=Grupuj skrypty i organizuj je w ramach\nscriptGroup=Grupa\nscriptGroupDescription=Grupa, do której chcesz przypisać ten skrypt\nscriptGroupGroupDescription=Opcjonalna grupa nadrzędna, do której ma zostać przypisana ta grupa skryptów\nopenInNewTab=Otwórz w nowej karcie\nexecuteInBackground=w tle\nexecuteInTerminal=w $TERM$\nback=Wróć\nbrowseInWindowsExplorer=Przeglądaj w Eksploratorze Windows\nbrowseInDefaultFileManager=Przeglądaj w domyślnym menedżerze plików\nbrowseInFinder=Przeglądaj w wyszukiwarce\ncopy=Kopia\npaste=Wklej\ncopyLocation=Kopiuj lokalizację\nabsolutePaths=Ścieżki bezwzględne\nabsoluteLinkPaths=Bezwzględne ścieżki łącza\nabsolutePathsQuoted=Bezwzględne cytowane ścieżki\nfileNames=Nazwy plików\nlinkFileNames=Połącz nazwy plików\nfileNamesQuoted=Nazwy plików (cytowane)\ndeleteFile=Usuń $FILE$\neditWithEditor=Edytuj za pomocą $EDITOR$\nfollowLink=Podążaj za linkiem\ngoForward=Idź do przodu\nshowDetails=Pokaż szczegóły\nshowDetailsDescription=Pokaż ślad stosu błędu\nopenFileWith=Otwórz za pomocą ...\nopenWithDefaultApplication=Otwórz za pomocą domyślnej aplikacji\nrename=Zmień nazwę\nrun=Uruchom\nopenInTerminal=Otwórz w terminalu\nfile=Plik\ndirectory=Katalog\nsymbolicLink=Łącze symboliczne\ndesktopEnvironment.displayName=Środowisko pulpitu\ndesktopEnvironment.displayDescription=Utwórz konfigurację środowiska pulpitu zdalnego wielokrotnego użytku\ndesktopHost=Host pulpitu\ndesktopHostDescription=Połączenie pulpitu do użycia jako podstawa\ndesktopShellDialect=Dialekt powłoki\ndesktopShellDialectDescription=Dialekt powłoki używany do uruchamiania skryptów i aplikacji\ndesktopSnippets=Fragmenty skryptów\ndesktopSnippetsDescription=Lista fragmentów skryptów wielokrotnego użytku do uruchomienia w pierwszej kolejności\ndesktopInitScript=Skrypt początkowy\ndesktopInitScriptDescription=Polecenia Init specyficzne dla tego środowiska\ndesktopTerminal=Aplikacja terminalowa\ndesktopTerminalDescription=Terminal używany na pulpicie do uruchamiania skryptów\ndesktopApplication.displayName=Aplikacja komputerowa\ndesktopApplication.displayDescription=Uruchom aplikację na pulpicie zdalnym\ndesktopBase=Pulpit\ndesktopBaseDescription=Pulpit, na którym chcesz uruchomić tę aplikację\ndesktopEnvironmentBase=Środowisko pulpitu\ndesktopEnvironmentBaseDescription=Środowisko pulpitu do uruchomienia tej aplikacji\ndesktopApplicationPath=Ścieżka aplikacji\ndesktopApplicationPathDescription=Ścieżka pliku wykonywalnego do uruchomienia\ndesktopApplicationArguments=Argumenty\ndesktopApplicationArgumentsDescription=Opcjonalne argumenty przekazywane do aplikacji\ndesktopCommand.displayName=Polecenie pulpitu\ndesktopCommand.displayDescription=Uruchom polecenie w środowisku pulpitu zdalnego\ndesktopCommandScript=Polecenia\ndesktopCommandScriptDescription=Polecenia uruchamiane w środowisku\nservice.displayName=Usługa\nservice.displayDescription=Prześlij usługę zdalną do komputera lokalnego\nserviceLocalPort=Jawny port lokalny\nserviceLocalPortDescription=Lokalny port do przekierowania, w przeciwnym razie używany jest losowy port\nserviceRemotePort=Port zdalny\nserviceRemotePortDescription=Port, na którym uruchomiona jest usługa\nserviceHost=Host usługi\nserviceHostDescription=Host, na którym działa usługa\nopenWebsite=Otwórz stronę internetową\ncustomServiceGroup.displayName=Grupa usług\ncustomServiceGroup.displayDescription=Grupuj wiele usług w jedną kategorię\ninitScript=Skrypt inicjujący - uruchamiany podczas inicjowania powłoki\nshellScript=Skrypt sesji powłoki - udostępnij skrypt do uruchomienia podczas sesji powłoki\nrunnableScript=Runnable script - Zezwalaj na uruchamianie skryptów bezpośrednio z koncentratora połączeń\nfileScript=Skrypt pliku - Zezwalaj na wywoływanie skryptów dla wybranych plików w przeglądarce plików\nrunScript=Uruchom skrypt\ncopyUrl=Kopiuj adres URL\nfixedServiceGroup.displayName=Grupa usług\nfixedServiceGroup.displayDescription=Wyświetl listę usług dostępnych w systemie\nmappedService.displayName=Usługa\nmappedService.displayDescription=Wejdź w interakcję z usługą udostępnianą przez kontener\ncustomService.displayName=Usługa\ncustomService.displayDescription=Automatycznie otwieraj lub tuneluj port usługi zdalnej na komputerze lokalnym\nfixedService.displayName=Usługa\nfixedService.displayDescription=Użyj predefiniowanej usługi\nnoServices=Brak dostępnych usług\nhasServices=$COUNT$ dostępne usługi\nhasService=$COUNT$ dostępna usługa\nnoConnections=Brak dostępnych połączeń\nhasConnections=$COUNT$ dostępne połączenia\nhasConnection=$COUNT$ dostępne połączenie\nopenHttp=Otwarta usługa HTTP\nopenHttps=Otwórz usługę HTTPS\nnoScriptsAvailable=Brak włączonych i kompatybilnych skryptów\nscriptsDisabled=Skrypty wyłączone\nchangeIcon=Zmień ikonę\ninit=Inicjał\nshell=Powłoka\nhub=Hub\nscript=skrypt\ngenericScript=Ogólny\ngradleTasks=Zadania Gradle\nrunTask=Uruchom zadanie\narchiveName=Nazwa archiwum\ncompress=Kompresja\ncompressContents=Kompresuj zawartość\nuntarHere=Untar here\nuntarDirectory=Untar to $DIR$\nunzipDirectory=Rozpakuj do $DIR$\nunzipHere=Rozpakuj tutaj\nrequiresRestart=Wymaga ponownego uruchomienia aplikacji.\ndownload=Pobierz\nservicePath=Ścieżka usługi\nservicePathDescription=Opcjonalna podścieżka podczas otwierania adresu URL w przeglądarce\nactive=Aktywny\ninactive=Nieaktywny\nstarting=Uruchamianie\nremotePort=Port zdalny\nremotePortNumber=Port zdalny $PORT$\nuserIdentity=Tożsamość osobista\nglobalIdentity=Tożsamość globalna\nidentityChoice=Tożsamość użytkownika\nidentityChoiceDescription=Wybierz predefiniowaną tożsamość lub określ dane logowania tylko dla tego połączenia\ndefineNewIdentityOrSelect=Wprowadź nowy lub wybierz istniejący\nlocalIdentity.displayName=Tożsamość lokalna\nlocalIdentity.displayDescription=Utwórz tożsamość wielokrotnego użytku dla tego pulpitu lokalnego\nsyncedIdentity.displayName=Zsynchronizowana tożsamość\nsyncedIdentity.displayDescription=Utwórz tożsamość wielokrotnego użytku, która jest synchronizowana między systemami\nlocalIdentity=Tożsamość lokalna\nkeyNotSynced=Plik klucza nie jest jeszcze zsynchronizowany z repozytorium git. Użyj przycisku dodaj do git dla pliku klucza, aby go dodać.\nusernameDescription=Nazwa użytkownika do zalogowania się\nidentity.displayName=Tożsamość\nidentity.displayDescription=Utwórz tożsamość wielokrotnego użytku dla połączeń\nlocal=Lokalny\nshared=Globalny\nuserDescription=Nazwa użytkownika lub predefiniowana tożsamość do zalogowania się jako\nidentityAccessLevel=Poziom dostępu\nidentityPerUser=Dostęp do tożsamości osobistej\nidentityPerUserDescription=Ogranicz dostęp do tej tożsamości i powiązanych z nią połączeń tylko do użytkownika skarbca\nidentityPerUserDisabled=Dostęp do tożsamości osobistej (wyłączony)\nidentityPerUserDisabledDescription=Ogranicz dostęp do tej tożsamości i powiązanych z nią połączeń tylko do użytkownika skarbca (wymaga skonfigurowania zespołu)\nidentityPerGroup=Dostęp do tożsamości tylko dla grup\nidentityPerGroupDescription=Ogranicz dostęp do tej tożsamości i powiązanych z nią połączeń tylko do tej grupy sejfów\nlibrary=Biblioteka\nlocation=Lokalizacja\nkeyAuthentication=Uwierzytelnianie oparte na kluczach\nkeyAuthenticationDescription=Metoda uwierzytelniania używana, jeśli wymagane jest uwierzytelnianie oparte na kluczach\nlocationDescription=Ścieżka pliku odpowiadającego Ci klucza prywatnego\nkeyFile=Plik klucza lokalnego\nkeyPassword=Hasło\nkey=Klucz\nyubikeyPiv=Yubikey PIV\npageant=Pageant\ngpgAgent=Agent GPG\ncustomPkcs11Library=Niestandardowa biblioteka PKCS#11\nsshAgent=Agent OpenSSH\nnone=Brak\nindex=Indeks ...\notherExternal=Inny agent zewnętrzny\nsync=Synchronizacja\nvaultSync=Synchronizacja skarbca\ncustomUsername=Nazwa użytkownika\ncustomUsernameDescription=Opcjonalny alternatywny użytkownik do zalogowania się jako\ncustomUsernamePassword=Hasło\ncustomUsernamePasswordDescription=Hasło użytkownika używane, gdy wymagane jest uwierzytelnianie sudo\nshowInternalPods=Pokaż wewnętrzne kapsuły\nshowAllNamespaces=Pokaż wszystkie przestrzenie nazw\nshowInternalContainers=Pokaż wewnętrzne kontenery\nrefresh=Odśwież\nvmwareGui=Uruchom GUI\nmonitorVm=Monitoruj maszynę wirtualną\naddCluster=Dodaj klaster ...\nshowNonRunningInstances=Pokaż niedziałające instancje\nvmwareGuiDescription=Czy uruchomić maszynę wirtualną w tle czy w oknie.\nvmwareEncryptionPassword=Hasło szyfrowania\nvmwareEncryptionPasswordDescription=Opcjonalne hasło używane do szyfrowania maszyny wirtualnej.\nvmPasswordDescription=Wymagane hasło dla użytkownika-gościa.\nvmPassword=Hasło użytkownika\nvmUser=Użytkownik-gość\nrunTempContainer=Uruchom tymczasowy kontener\nvmUserDescription=Nazwa użytkownika twojego głównego użytkownika-gościa\ndockerTempRunAlertTitle=Uruchom tymczasowy kontener\ndockerTempRunAlertHeader=Spowoduje to uruchomienie procesu powłoki w tymczasowym kontenerze, który zostanie automatycznie usunięty po jego zatrzymaniu.\nimageName=Nazwa obrazu\nimageNameDescription=Identyfikator obrazu kontenera do użycia\ncontainerName=Nazwa kontenera\ncontainerNameDescription=Opcjonalna niestandardowa nazwa kontenera\nvm=Maszyna wirtualna\nvmDescription=Powiązany plik konfiguracyjny.\nvmwareScan=Hiperwizory desktopowe VMware\nvmwareMachine.displayName=Maszyna wirtualna VMware\nvmwareMachine.displayDescription=Połącz się z maszyną wirtualną przez SSH\nvmwareInstallation.displayName=Instalacja hiperwizora pulpitu VMware\nvmwareInstallation.displayDescription=Interakcja z zainstalowanymi maszynami wirtualnymi za pośrednictwem interfejsu CLI\nstart=Start\nstop=Zatrzymaj się\npause=Pauza\nrdpTunnelHost=Host docelowy\nrdpTunnelHostDescription=Połączenie SSH, do którego tunelowane jest połączenie RDP\nrdpTunnelUsername=Nazwa użytkownika\nrdpTunnelUsernameDescription=Niestandardowy użytkownik do logowania, używa użytkownika SSH, jeśli jest pusty\nrdpFileLocation=Lokalizacja pliku\nrdpFileLocationDescription=Ścieżka dostępu do pliku .rdp\nrdpPasswordAuthentication=Uwierzytelnianie hasłem\nrdpFiles=Pliki RDP\nrdpPasswordAuthenticationDescription=Hasło do wypełnienia lub skopiowania do schowka, w zależności od obsługi klienta\nrdpFile.displayName=Plik RDP\nrdpFile.displayDescription=Połącz się z systemem za pomocą istniejącego pliku .rdp\nrequiredSshServerAlertTitle=Skonfiguruj serwer SSH\nrequiredSshServerAlertHeader=Nie można znaleźć zainstalowanego serwera SSH w maszynie wirtualnej.\nrequiredSshServerAlertContent=Aby połączyć się z maszyną wirtualną, XPipe szuka działającego serwera SSH, ale nie wykryto żadnego dostępnego serwera SSH dla maszyny wirtualnej.\ncomputerName=Nazwa komputera\npssComputerNameDescription=Nazwa komputera, z którym chcesz się połączyć\ncredentialUser=Poświadczenie użytkownika\ncredentialUserDescription=Użytkownik, jako który chcesz się zalogować.\ncredentialPassword=Hasło uwierzytelniające\ncredentialPasswordDescription=Hasło użytkownika.\nsshConfig=Pliki konfiguracyjne SSH\nautostart=Automatycznie łącz się podczas uruchamiania XPipe\nacceptHostKey=Zaakceptuj klucz hosta\nmodifyHostKeyPermissions=Zmodyfikuj uprawnienia klucza hosta\nattachContainer=Dołącz\ncontainerLogs=Pokaż dzienniki\nopenSftpClient=Otwórz w zewnętrznym kliencie SFTP\nopenTermius=Otwórz w Termius\nshowInternalInstances=Pokaż instancje wewnętrzne\neditPod=Edytuj pod\nacceptHostKeyDescription=Zaufaj nowemu kluczowi hosta i kontynuuj\nmodifyHostKeyPermissionsDescription=Spróbuj usunąć uprawnienia oryginalnego pliku, aby OpenSSH był zadowolony\npsSession.displayName=Sesja zdalna PowerShell\npsSession.displayDescription=Połącz się przez New-PSSession i Enter-PSSession\nsshLocalTunnel.displayName=Lokalny tunel SSH\nsshLocalTunnel.displayDescription=Utwórz tunel SSH do zdalnego hosta\nsshRemoteTunnel.displayName=Zdalny tunel SSH\nsshRemoteTunnel.displayDescription=Utwórz odwrotny tunel SSH ze zdalnego hosta\nsshDynamicTunnel.displayName=Dynamiczny tunel SSH\nsshDynamicTunnel.displayDescription=Utwórz serwer proxy SOCKS przez połączenie SSH\nshellEnvironmentGroup.displayName=Środowiska powłoki\nshellEnvironmentGroup.displayDescription=Środowiska powłoki\nshellEnvironment.displayName=Środowisko powłoki\nshellEnvironment.displayDescription=Utwórz niestandardowe środowisko uruchomieniowe powłoki\nshellEnvironment.informationFormat=$TYPE$ środowisko\nshellEnvironment.elevatedInformationFormat=$ELEVATION$ $TYPE$ środowisko\nenvironmentConnectionDescription=Połączenie bazowe do tworzenia środowiska dla\nenvironmentScriptDescription=Opcjonalny niestandardowy skrypt inicjujący uruchamiany w powłoce\nenvironmentSnippets=Skrypty powłoki\ncommandSnippetsDescription=Opcjonalne predefiniowane skrypty powłoki do uruchomienia w pierwszej kolejności\nenvironmentSnippetsDescription=Opcjonalne predefiniowane skrypty powłoki uruchamiane podczas inicjalizacji\nshellTypeDescription=Jawny typ powłoki do uruchomienia\noriginPort=Port początkowy\noriginAddress=Adres pochodzenia\nremoteAddress=Adres zdalny\nremoteSourceAddress=Zdalny adres źródłowy\nremoteSourcePort=Zdalny port źródłowy\noriginDestinationPort=Port docelowy pochodzenia\noriginDestinationAddress=Adres docelowy pochodzenia\norigin=Pochodzenie\nremoteHost=Zdalny host\naddress=Adres\nproxmox.displayName=Proxmox\nproxmox.displayDescription=Połącz się z systemami w wirtualnym środowisku Proxmox\nproxmoxVm.displayName=Proxmox VM\nproxmoxVm.displayDescription=Połącz się z maszyną wirtualną w Proxmox VE przez SSH\nproxmoxContainer.displayName=Kontener Proxmox\nproxmoxContainer.displayDescription=Połącz się z kontenerem w Proxmox VE\nsshDynamicTunnel.hostDescription=System używany jako serwer proxy SOCKS\nsshDynamicTunnel.bindingDescription=Z jakimi adresami powiązać tunel\nsshRemoteTunnel.hostDescription=System, z którego ma zostać uruchomiony zdalny tunel do źródła\nsshRemoteTunnel.bindingDescription=Z jakimi adresami powiązać tunel\nsshLocalTunnel.hostDescription=System otwierający tunel do\nsshLocalTunnel.bindingDescription=Z jakimi adresami powiązać tunel\nsshLocalTunnel.localAddressDescription=Lokalny adres do powiązania\nsshLocalTunnel.remoteAddressDescription=Zdalny adres do powiązania\ncmd.displayName=Polecenie\ncmd.displayDescription=Wykonaj dowolne polecenie w systemie\nk8sPod.displayName=Kubernetes Pod\nk8sPod.displayDescription=Połącz się z kapsułą i jej kontenerami za pomocą kubectl\nk8sContainer.displayName=Kontener Kubernetes\nk8sContainer.displayDescription=Otwórz powłokę do kontenera\nk8sCluster.displayName=Klaster Kubernetes\nk8sCluster.displayDescription=Połącz się z klastrem i jego zasobnikami za pomocą kubectl\nsshTunnelGroup.displayName=Tunele SSH\nsshTunnelGroup.displayCategory=Wszystkie typy tuneli SSH\nlocal.displayName=Maszyna lokalna\nlocal.displayDescription=Powłoka komputera lokalnego\ncygwin=Cygwin\nmsys2=MSYS2\ngitWindows=Git dla Windows\ngitForWindows.displayName=Git dla Windows\ngitForWindows.displayDescription=Uzyskaj dostęp do lokalnego środowiska Git For Windows\nmsys2.displayName=MSYS2\nmsys2.displayDescription=Uzyskaj dostęp do powłok środowiska MSYS2\ncygwin.displayName=Cygwin\ncygwin.displayDescription=Uzyskaj dostęp do powłoki środowiska Cygwin\nnamespace=Przestrzeń nazw\ngitVaultIdentityStrategy=Tożsamość Git SSH\ngitVaultIdentityStrategyDescription=Jeśli zdecydowałeś się użyć adresu URL SSH git jako zdalnego, a zdalne repozytorium wymaga tożsamości SSH, ustaw tę opcję.\\n\\nJeśli podałeś adres URL HTTP, możesz zignorować tę opcję.\ndockerContainers=Kontenery Docker\ndockerCmd.displayName=klient CLI docker\ndockerCmd.displayDescription=Uzyskaj dostęp do kontenerów Docker za pośrednictwem klienta docker CLI\nwslCmd.displayName=Instalacja WSL\nwslCmd.displayDescription=Uzyskaj dostęp do instancji WSL za pośrednictwem klienta wsl CLI\nk8sCmd.displayName=klient kubectl\nk8sCmd.displayDescription=Uzyskaj dostęp do klastrów Kubernetes za pomocą kubectl\nk8sClusters=Klastry Kubernetes\nshells=Dostępne powłoki\ninspectContainer=Sprawdź\ninspectContext=Sprawdź\nk8sClusterNameDescription=Nazwa kontekstu, w którym znajduje się klaster.\npod=Pod\npodName=Pod name\nk8sClusterContext=Kontekst\nk8sClusterContextDescription=Nazwa kontekstu, w którym znajduje się klaster\nk8sClusterNamespace=Przestrzeń nazw\nk8sClusterNamespaceDescription=Niestandardowa przestrzeń nazw lub domyślna, jeśli jest pusta\nk8sConfigLocation=Plik konfiguracyjny\nk8sConfigLocationDescription=Niestandardowy plik kubeconfig lub domyślny, jeśli jest pusty\ninspectPod=Sprawdź\nshowAllContainers=Pokaż niedziałające kontenery\nshowAllPods=Pokaż niedziałające pody\nk8sPodHostDescription=Host, na którym znajduje się kapsuła\nk8sContainerDescription=Nazwa kontenera Kubernetes\nk8sPodDescription=Nazwa kapsuły Kubernetes\npodDescription=Pod, na którym znajduje się kontener\nk8sClusterHostDescription=Host, za pośrednictwem którego należy uzyskać dostęp do klastra. Musi mieć zainstalowany i skonfigurowany kubectl, aby móc uzyskać dostęp do klastra.\nconnection=Połączenie\nshellCommand.displayName=Niestandardowe polecenie powłoki\nshellCommand.displayDescription=Otwórz standardową powłokę za pomocą niestandardowego polecenia\nssh.displayName=Połączenie SSH\nssh.displayDescription=Połącz się ze zdalnym systemem za pomocą klienta wiersza poleceń SSH\nsshConfig.displayName=Plik konfiguracyjny SSH\nsshConfig.displayDescription=Połącz się z hostami zdefiniowanymi w pliku konfiguracyjnym SSH\nsshConfigHost.displayName=Host pliku konfiguracyjnego SSH\nsshConfigHost.displayDescription=Połącz się z hostem zdefiniowanym w pliku konfiguracyjnym SSH\nsshConfigHost.password=Hasło\nsshConfigHost.passwordDescription=Podaj opcjonalne hasło do logowania użytkownika.\nsshConfigHost.identityPassphrase=Kluczowe hasło\nsshConfigHost.identityPassphraseDescription=Podaj opcjonalne hasło dla swojego klucza.\nshellCommand.hostDescription=Host, na którym ma zostać wykonane polecenie\nshellCommand.commandDescription=Polecenie, które otworzy powłokę\ncommandType=Typ polecenia\ncommandTypeDescription=Jak wykonać polecenie\ncommandDescription=Niestandardowe polecenia do wykonania na hoście\ncommandHostDescription=Host, na którym chcesz uruchomić polecenie\ncommandDataFlowDescription=Jak to polecenie obsługuje dane wejściowe i wyjściowe\ncommandElevationDescription=Uruchom to polecenie z podwyższonymi uprawnieniami\ncommandShellTypeDescription=Powłoka używana dla tego polecenia\nlimitedSystem=Jest to system ograniczony lub wbudowany\nlimitedSystemDescription=Nie próbuj identyfikować typu powłoki, niezbędnej dla ograniczonych systemów wbudowanych lub urządzeń IOT\nsshForwardX11=Forward X11\nsshForwardX11Description=Włącza przekierowanie X11 dla połączenia\ncustomAgent=Agent niestandardowy\nidentityAgent=Agent tożsamości\nssh.proxyDescription=Opcjonalny host proxy do użycia podczas nawiązywania połączenia SSH. Musi mieć zainstalowanego klienta ssh.\nusage=Użycie\nwslHostDescription=Host, na którym znajduje się instancja WSL. Musi mieć zainstalowaną usługę WSL.\nwslDistributionDescription=Nazwa instancji WSL\nwslUsernameDescription=Wyraźna nazwa użytkownika do zalogowania. Jeśli nie zostanie określona, zostanie użyta domyślna nazwa użytkownika.\nwslPasswordDescription=Hasło użytkownika, które może być używane dla poleceń sudo.\ndockerHostDescription=Host, na którym znajduje się kontener docker. Musi mieć zainstalowaną aplikację docker.\ndockerContainerDescription=Nazwa kontenera docker\nlocalMachine=Maszyna lokalna\nrootScan=Środowisko powłoki Sudo\nloginEnvironmentScan=Niestandardowe środowisko logowania\nk8sScan=Klaster Kubernetes\noptions=Opcje\ndockerRunningScan=Uruchamianie kontenerów docker\ndockerAllScan=Wszystkie kontenery docker\nwslScan=Instancje WSL\nsshScan=Połączenia konfiguracyjne SSH\nrunAsUser=Uruchom jako użytkownik\nrunAsUserDescription=Uruchom to środowisko powłoki jako inny użytkownik\ndefault=Domyślny\nadministrator=Administrator\nwslHost=Host WSL\ntimeout=Limit czasu\ninstallLocation=Zainstaluj lokalizację\ninstallLocationDescription=Lokalizacja, w której zainstalowane jest twoje środowisko $NAME$\nwsl.displayName=Podsystem Windows dla systemu Linux\nwsl.displayDescription=Połącz się z instancją WSL działającą w systemie Windows\ndocker.displayName=Kontener Docker\ndocker.displayDescription=Połącz się z kontenerem docker\nport=Port\nuser=Użytkownik\npassword=Hasło\nmethod=Metoda\nuri=URL\nproxy=Proxy\ndistribution=Dystrybucja\nusername=Nazwa użytkownika\nshellType=Typ powłoki\nbrowseFile=Przeglądaj plik\nopenShell=Otwórz powłokę w terminalu\nopenCommand=Wykonaj polecenie w terminalu\neditFile=Edytuj plik\ndescription=Opis\nfurtherCustomization=Dalsze dostosowywanie\nfurtherCustomizationDescription=Aby uzyskać więcej opcji konfiguracyjnych, użyj plików konfiguracyjnych ssh\nbrowse=Przeglądaj\nconfigHost=Host\nconfigHostDescription=Host, na którym znajduje się konfiguracja\nconfigLocation=Lokalizacja konfiguracji\nconfigLocationDescription=Ścieżka do pliku konfiguracyjnego\ngateway=Bramka\ngatewayDescription=Opcjonalna brama do użycia podczas łączenia\nconnectionInformation=Informacje o połączeniu\nconnectionInformationDescription=Z którym systemem chcesz się połączyć\npasswordAuthentication=Uwierzytelnianie hasłem\npasswordAuthenticationDescription=Opcjonalne hasło używane do uwierzytelniania\nsshConfigString.displayName=Połączenie SSH oparte na konfiguracji\nsshConfigString.displayDescription=Utwórz w pełni spersonalizowane połączenie SSH w formacie SSH config\nsshConfigStringContent=Konfiguracja\nsshConfigStringContentDescription=Opcje SSH dla połączenia w formacie konfiguracyjnym OpenSSH\nvnc.displayName=Połączenie VNC przez SSH\nvnc.displayDescription=Otwórz sesję VNC przez połączenie tunelowane\nbinding=Wiązanie\nvncPortDescription=Port, na którym nasłuchuje serwer VNC\nrdpPortDescription=Port, na którym nasłuchuje serwer RDP\nvncUsername=Nazwa użytkownika\nvncUsernameDescription=Opcjonalna nazwa użytkownika VNC\nvncPassword=Hasło\nvncPasswordDescription=Hasło VNC\nx11WslInstance=Instancja X11 Forward WSL\nx11WslInstanceDescription=Lokalna dystrybucja podsystemu Windows dla systemu Linux, która ma być używana jako serwer X11 podczas korzystania z przekierowania X11 w połączeniu SSH. Ta dystrybucja musi być dystrybucją WSL2.\nopenAsRoot=Otwórz jako root\nopenInWSL=Otwórz w WSL\nlaunch=Uruchomienie\nsshTrustKeyContent=Klucz hosta nie jest znany i włączono ręczną weryfikację klucza hosta. $CONTENT$\nsshTrustKeyTitle=Nieznany klucz hosta\nrdpTunnel.displayName=Połączenie RDP przez SSH\nrdpTunnel.displayDescription=Połącz się przez RDP przez połączenie tunelowane\nrdpEnableDesktopIntegration=Włącz integrację pulpitu\nrdpEnableDesktopIntegrationDescription=Uruchamiaj zdalne aplikacje, zakładając, że lista zezwoleń RDP zezwala na to\nrdpSetupAdminTitle=Wymagana konfiguracja RDP\nrdpSetupAllowTitle=Aplikacja zdalna RDP\nrdpSetupAllowContent=Bezpośrednie uruchamianie zdalnych aplikacji nie jest obecnie dozwolone w tym systemie. Czy chcesz to włączyć? Umożliwi to uruchamianie zdalnych aplikacji bezpośrednio z XPipe poprzez wyłączenie listy zezwoleń dla zdalnych aplikacji RDP.\nrdpServerEnableTitle=Serwer RDP\nrdpServerEnableContent=Serwer RDP jest wyłączony w systemie docelowym. Czy chcesz go włączyć w rejestrze, aby umożliwić zdalne połączenia RDP?\nrdp=RDP\nrdpScan=Tunel RDP przez SSH\nwslX11SetupTitle=Konfiguracja WSL X11\nwslX11SetupContent=XPipe może używać twojej lokalnej dystrybucji WSL do działania jako serwer wyświetlania X11. Czy chcesz skonfigurować X11 na $DIST$? Spowoduje to zainstalowanie podstawowych pakietów X11 w dystrybucji WSL i może chwilę potrwać. Możesz także zmienić używaną dystrybucję w menu ustawień.\ncommand=Polecenie\ncommandGroup=Grupa poleceń\nvncSystem=System docelowy VNC\nvncSystemDescription=Rzeczywisty system do interakcji. Zazwyczaj jest to to samo, co host tunelu\nvncHost=Docelowy host VNC\nvncHostDescription=System, na którym działa serwer VNC\nvncDirectHost=Host\nvncDirectHostDescription=Wpis hosta lub ręczny adres serwera, na którym działa serwer VNC\nrdpDirectHost=Host\nrdpDirectHostDescription=Wpis hosta lub adres ręczny serwera, na którym działa serwer RDP\ngitVaultTitle=Skarbiec Git\ngitVaultForcePushContent=Czy chcesz wymusić wypchnięcie do zdalnego repozytorium? Spowoduje to całkowite zastąpienie całej zawartości zdalnego repozytorium twoim lokalnym, łącznie z historią.\ngitVaultOverwriteLocalContent=Czy chcesz zastąpić zmiany w lokalnym sejfie? Spowoduje to zastosowanie wszystkich zdalnych zmian do twojego lokalnego repozytorium.\nrdpSimple.displayName=Bezpośrednie połączenie RDP\nrdpSimple.displayDescription=Połącz się z hostem przez RDP\nrdpUsername=Nazwa użytkownika\nrdpUsernameDescription=Użytkownik, jako który chcesz się zalogować. Może zawierać prefiks domeny\naddressDescription=Gdzie się połączyć\nrdpAdditionalOptions=Dodatkowe opcje RDP\nrdpAdditionalOptionsDescription=Surowe opcje RDP do uwzględnienia, sformatowane tak samo jak w plikach .rdp\nproxmoxVncConfirmTitle=Dostęp VNC\nproxmoxVncConfirmContent=Czy chcesz włączyć dostęp VNC dla maszyny wirtualnej? Spowoduje to włączenie bezpośredniego dostępu klienta VNC w pliku konfiguracyjnym maszyny wirtualnej i ponowne uruchomienie maszyny wirtualnej.\ndockerContext.displayName=Kontekst Docker\ndockerContext.displayDescription=Interakcja z kontenerami znajdującymi się w określonym kontekście\nvmActions=Działania maszyny wirtualnej\ndockerContextActions=Działania kontekstowe\nk8sPodActions=Poddziałania\nopenVnc=Włącz dostęp VNC\naddVnc=Dodaj połączenie VNC\ncommandGroup.displayName=Grupa poleceń\ncommandGroup.displayDescription=Grupa dostępnych poleceń dla systemu\nserial.displayName=Połączenie szeregowe\nserial.displayDescription=Otwórz połączenie szeregowe w terminalu\nserialPort=Port szeregowy\nserialPortDescription=Port szeregowy / urządzenie, z którym chcesz się połączyć\nbaudRate=Szybkość transmisji\ndataBits=Bity danych\nstopBits=Bity stopu\nparity=Parzystość\nflowControlWindow=Kontrola przepływu\nserialImplementation=Implementacja szeregowa\nserialImplementationDescription=Narzędzie do połączenia z portem szeregowym\nserialHost=Host\nserialHostDescription=System dostępu do portu szeregowego na\nserialPortConfiguration=Konfiguracja portu szeregowego\nserialPortConfigurationDescription=Parametry konfiguracyjne podłączonego urządzenia szeregowego\nserialInformation=Informacje seryjne\nopenXShell=Otwórz w XShell\ntsh.displayName=Teleport\ntsh.displayDescription=Połącz się z węzłami teleportacyjnymi przez tsh\ntshNode.displayName=Węzeł teleportu\ntshNode.displayDescription=Połącz się z węzłem teleportu w klastrze\nteleportCluster=Klaster\nteleportClusterDescription=Klaster, w którym znajduje się węzeł\nteleportProxy=Proxy\nteleportProxyDescription=Serwer proxy używany do łączenia się z węzłem\nteleportHost=Host\nteleportHostDescription=Nazwa hosta węzła\nteleportUser=Użytkownik\nteleportUserDescription=Użytkownik, który ma się zalogować jako\nlogin=Zaloguj się\nhyperVInstall.displayName=Hyper-V\nhyperVInstall.displayDescription=Połącz się z maszynami wirtualnymi zarządzanymi przez Hyper-V\nhyperVVm.displayName=Maszyna wirtualna Hyper-V\nhyperVVm.displayDescription=Połącz się z maszyną wirtualną Hyper-V przez SSH lub PSSession\ntrustHost=Zaufany host\ntrustHostDescription=Dodaj ComputerName do listy zaufanych hostów\ncopyIp=Kopiuj adres IP\nvncDirect.displayName=Bezpośrednie połączenie VNC\nvncDirect.displayDescription=Połącz się bezpośrednio z systemem przez VNC\neditConfiguration=Edytuj konfigurację\nviewInDashboard=Widok na pulpicie nawigacyjnym\nsetDefault=Ustaw domyślne\nremoveDefault=Usuń domyślne\nconnectAsOtherUser=Połącz jako inny użytkownik\nprovideUsername=Podaj alternatywną nazwę użytkownika do zalogowania się\nvmIdentity=Tożsamość gościa\nvmIdentityDescription=Metoda uwierzytelniania tożsamości SSH używana do łączenia się w razie potrzeby\nvmPort=Port\nvmPortDescription=Port do połączenia przez SSH\nforwardAgent=Agent przekazywania\nforwardAgentDescription=Udostępnij tożsamość agenta SSH w systemie zdalnym\nvirshUri=URI\nvirshUriDescription=URI hiperwizora, obsługiwane są również aliasy\nvirshDomain.displayName=domena libvirt\nvirshDomain.displayDescription=Połącz się z domeną libvirt\nvirshHypervisor.displayName=hiperwizor libvirt\nvirshHypervisor.displayDescription=Połącz się ze sterownikiem hiperwizora obsługiwanym przez libvirt\nvirshInstall.displayName=klient wiersza poleceń libvirt\nvirshInstall.displayDescription=Połącz się ze wszystkimi dostępnymi hiperwizorami libvirt przez virsh\naddHypervisor=Dodaj hypervisor\ninteractiveTerminal=Interaktywny terminal\neditDomain=Edytuj domenę\nlibvirt=domeny libvirt\ncustomIp=Niestandardowy adres IP\ncustomIpDescription=Zastąp domyślne lokalne wykrywanie IP maszyny wirtualnej, jeśli korzystasz z zaawansowanej sieci\nautomaticallyDetect=Automatycznie wykrywaj\nuserAddDialogTitle=Tworzenie użytkownika\ngroupAddDialogTitle=Tworzenie grupy\npassphrase=Hasło\nrepeatPassphrase=Powtórz hasło\ngroupSecret=Sekret grupy\nrepeatGroupSecret=Powtórz sekret grupy\nvaultGroup=Grupa sejfów\nloginAlertTitle=Wymagane logowanie\nloginAlertHeader=Odblokuj sejf, aby uzyskać dostęp do osobistych połączeń\nvaultUser=Użytkownik sejfu\n#custom\nme=Mi\naddGroup=Dodaj grupę ...\naddGroupDescription=Utwórz nową grupę dla tego sejfu\naddUser=Dodaj użytkownika ...\naddUserDescription=Utwórz nowego użytkownika dla tego sejfu\nskip=Pomiń\nuserChangePasswordAlertTitle=Zmiana hasła\ngroupChangeSecretAlertTitle=Tajna zmiana\ndocs=Dokumentacja\nlxd.displayName=Kontener LXD\nlxd.displayDescription=Połącz się z kontenerem LXD przez lxc\nlxdCmd.displayName=Klient LXD CLI\nlxdCmd.displayDescription=Uzyskaj dostęp do kontenerów LXD za pośrednictwem klienta lxc CLI\npodman.displayName=Kontener Podman\npodman.displayDescription=Połącz się z kontenerem Podman\nincusInstall.displayName=Menedżer maszyn Incus\nincusInstall.displayDescription=Uzyskaj dostęp do kontenerów incus za pośrednictwem klienta incus CLI\nincusContainer.displayName=Kontener Incus\nincusContainer.displayDescription=Połącz się z kontenerem incus\npodmanCmd.displayName=Podman CLI\npodmanCmd.displayDescription=Uzyskaj dostęp do kontenerów Podman za pośrednictwem klienta CLI\nlxdHostDescription=Host, na którym znajduje się kontener LXD. Musi mieć zainstalowaną aplikację lxc.\nlxdContainerDescription=Nazwa kontenera LXD\npodmanContainers=Kontenery Podman\nlxdContainers=Kontenery LXD\nincusContainers=Kontenery Incus\ncontainer=Kontener\nhost=Host\ncontainerActions=Działania kontenera\nserialConsole=Konsola szeregowa\neditRunConfiguration=Edytuj konfigurację uruchamiania\ncommunityDescription=Narzędzie wspomagające połączenia, idealne do Twoich osobistych zastosowań.\nupgradeDescription=Profesjonalne zarządzanie połączeniami dla całej Twojej infrastruktury serwerowej.\ndiscoverPlans=Odkryj opcje aktualizacji\nextendProfessional=Uaktualnij do najnowszych profesjonalnych funkcji\ncommunityItem1=Nieograniczone połączenia z niekomercyjnymi systemami i narzędziami\ncommunityItem2=Bezproblemowa integracja z zainstalowanymi terminalami i edytorami\ncommunityItem3=W pełni funkcjonalna zdalna przeglądarka plików\ncommunityItem4=Potężny system skryptów dla wszystkich powłok\ncommunityItem5=Integracja Git w celu synchronizacji i udostępniania informacji o połączeniach\nupgradeItem1=Zawiera wszystkie funkcje edycji społecznościowej\nupgradeItem2=Plan Homelab obsługuje nieograniczoną liczbę hiperwizorów i zaawansowane funkcje SSH\nupgradeItem3=Plan Professional dodatkowo obsługuje systemy operacyjne i narzędzia klasy korporacyjnej\nupgradeItem4=Plan Enterprise zapewnia pełną elastyczność dla Twojego indywidualnego przypadku użycia\nupgrade=Aktualizacja\nupgradeTitle=Dostępne plany\nstatus=Status\ntype=Typ\nlicenseAlertTitle=Wymagana licencja\nuseCommunity=Kontynuuj ze społecznością\npreviewDescription=Wypróbuj nowe funkcje przez kilka tygodni po ich wydaniu.\ntryPreview=Aktywuj podgląd\npreviewItem1=Pełny dostęp do nowo wydanych profesjonalnych funkcji przez 2 tygodnie po premierze\npreviewItem2=Wypróbuj nowe funkcje bez żadnych zobowiązań\nlicensedTo=Licencja\nemail=Adres e-mail\napply=Zastosuj\nclear=Wyczyść\nactivate=Aktywuj\nvalidUntil=Ważny do\nlicenseActivated=Aktywowana licencja\nrestart=Restart\nlockVault=Zamknięty skarbiec\nrestartApp=Uruchom ponownie XPipe\nfree=Darmowy\nupgradeInfo=Poniżej znajdziesz informacje na temat aktualizacji do licencji.\nupgradeInfoPreview=Poniżej znajdziesz informacje na temat uaktualnienia do licencji lub wypróbowania wersji zapoznawczej.\nenterLicenseKey=Wprowadź klucz licencyjny do aktualizacji\nisOnlySupported=jest obsługiwany tylko z licencją $TYPE$\nareOnlySupported=są obsługiwane tylko z co najmniej licencją $TYPE$\nlegacyLicense=Ta licencja obejmuje tylko nowe funkcje Professional wydane w ciągu jednego roku od zakupu.\npreviewExpiredLicense=Ta funkcja była niedawno dostępna za darmo w wersji zapoznawczej, ale okres ten już wygasł.\nopenApiDocs=Dokumentacja API\nopenApiDocsDescription=Dokumentacja API HTTP jest dostępna online, w tym specyfikacja OpenAPI .yaml. Możesz ją otworzyć w przeglądarce internetowej lub preferowanym kliencie HTTP.\nopenApiDocsButton=Otwórz dokumenty\npythonApi=API Python\npersonalConnection=To połączenie i wszystkie jego elementy podrzędne są dostępne tylko dla Twojego użytkownika, ponieważ zależą od tożsamości osobistej.\ndeveloperPrintInitFiles=Wydrukuj wykonanie pliku inicjującego\ndeveloperPrintInitFilesDescription=Wydrukuj wszystkie skrypty init powłoki, które są uruchamiane po uruchomieniu terminala.\ndeveloperShowSensitiveCommands=Rejestruj wrażliwe polecenia\ndeveloperShowSensitiveCommandsDescription=Dołącz wrażliwe polecenia do danych wyjściowych dziennika w celu debugowania.\ncheckingForUpdates=Sprawdzanie dostępności aktualizacji\ncheckingForUpdatesDescription=Pobieranie informacji o najnowszej wersji\ndownloadingUpdate=Pobieranie wersji (wersja $VERSION$)\ndownloadingUpdateDescription=Pobieranie pakietu wersji\nupdateNag=Nie aktualizowałeś XPipe od jakiegoś czasu. Możesz przegapić nowe funkcje i poprawki w nowszych wersjach.\nupdateNagTitle=Przypomnienie o aktualizacji\nupdateNagButton=Zobacz wydania\nrefreshServices=Usługi odświeżania\nserviceProtocolType=Typ protokołu usługi\nserviceProtocolTypeDescription=Kontroluj sposób otwierania usługi\nserviceCommand=Polecenie do uruchomienia, gdy usługa jest aktywna\nserviceCommandDescription=Symbol zastępczy $PORT zostanie zastąpiony rzeczywistym tunelowanym portem lokalnym\nvalue=Wartość\nshowAdvancedOptions=Pokaż opcje zaawansowane\nsshAdditionalConfigOptions=Dodatkowe opcje konfiguracji\nremoteFileManager=Zdalny menedżer plików\nclearUserData=Usuń dane użytkownika\nclearUserDataDescription=Usuń wszystkie dane konfiguracyjne użytkownika, w tym połączenia\nclearUserDataTitle=Usuwanie danych użytkownika\nclearUserDataContent=Spowoduje to usunięcie wszystkich lokalnych danych użytkownika xpipe i ponowne uruchomienie. Jeśli zależy ci na połączeniach, zsynchronizuj je najpierw z repozytorium git.\nundefined=Niezdefiniowany\ncopyAddress=Kopiuj adres\nnetbirdDeviceScan=Połączenia Netbird\nnetbirdId=Klucz publiczny peer\nnetbirdIdDescription=Wewnętrzny identyfikator klucza publicznego netbird peera\ntailscaleDeviceScan=Połączenia Tailscale\ntailscaleInstall.displayName=Instalacja Tailscale\ntailscaleInstall.displayDescription=Połącz się z urządzeniami w sieci tailnet przez SSH\ntailscaleDevice.displayName=Urządzenie Tailscale\ntailscaleDevice.displayDescription=Połącz się z urządzeniem w sieci tailnet przez SSH\ntailscaleId=Identyfikator urządzenia\ntailscaleIdDescription=Wewnętrzny identyfikator urządzenia tailscale\ntailscaleHostName=Nazwa hosta\ntailscaleHostNameDescription=Nazwa hosta urządzenia w sieci ogonowej\ntailscaleUsername=Nazwa użytkownika\ntailscaleUsernameDescription=Użytkownik, który ma się zalogować jako\ntailscalePassword=Hasło\ntailscalePasswordDescription=Opcjonalne hasło użytkownika, które może być użyte dla sudo\nscriptName=Nazwa skryptu\nscriptNameDescription=Nadaj temu skryptowi niestandardową nazwę\nscriptGroupName=Nazwa grupy skryptów\nscriptGroupNameDescription=Nadaj tej grupie skryptów nazwę niestandardową\nidentityName=Nazwa tożsamości\nidentityNameDescription=Nadaj tej tożsamości niestandardową nazwę\ntailscaleTailnet.displayName=Tailnet\ntailscaleTailnet.displayDescription=Połącz się z określoną siecią tailnet za pomocą swojego konta\nputtyConnections=Połączenia PuTTY\nkittyConnections=Połączenia KiTTY\nicons=Ikony\ncustomIcons=Ikony niestandardowe\niconSources=Źródła ikon\niconSourcesDescription=Tutaj możesz dodać własne źródła ikon. XPipe pobierze wszystkie pliki .svg w dodanej lokalizacji i doda je do dostępnego zestawu ikon.\\n\\nZarówno lokalne katalogi, jak i zdalne repozytoria git są obsługiwane jako lokalizacje ikon.\nrefreshSources=Ikony odświeżania\nrefreshSourcesDescription=Zaktualizuj wszystkie ikony z dostępnych źródeł\naddDirectoryIconSource=Dodaj źródło katalogu ...\naddDirectoryIconSourceDescription=Dodaj ikony z lokalnego katalogu\naddGitIconSource=Dodaj źródło git ...\naddGitIconSourceDescription=Dodaj ikony znajdujące się w zdalnym repozytorium git\nrepositoryUrl=Adres URL repozytorium Git\niconDirectory=Katalog ikon\naddUnsupportedKexMethod=Dodaj nieobsługiwaną metodę wymiany kluczy\naddUnsupportedKexMethodDescription=Zezwól na użycie metody wymiany kluczy $VAL$ dla tego połączenia\naddUnsupportedHostKeyType=Dodaj nieobsługiwany typ klucza hosta\naddUnsupportedHostKeyTypeDescription=Zezwól na użycie klucza hosta typu $VAL$ dla tego połączenia\naddUnsupportedMacType=Dodaj nieobsługiwany typ MAC\naddUnsupportedMacTypeDescription=Zezwól na użycie typu MAC $VAL$ dla tego połączenia\nrunSilent=cicho w tle\nrunInFileBrowser=w przeglądarce plików\nrunInConnectionHub=w koncentratorze połączeń\ncommandOutput=Wyjście polecenia\niconSourceDeletionTitle=Usuń źródło ikony\niconSourceDeletionContent=Czy chcesz usunąć to źródło ikon i wszystkie powiązane z nim ikony?\nrefreshIcons=Ikony odświeżania\nrefreshIconsDescription=Pobieranie, renderowanie i buforowanie wszystkich dostępnych ponad 1000 ikon z zewnętrznych źródeł do plików .png. Może to trochę potrwać ...\nvaultUserLegacy=Użytkownik Vault (ograniczony tryb zgodności ze starszymi wersjami)\nupgradeInstructions=Instrukcje aktualizacji\nexternalActionTitle=Żądanie akcji zewnętrznej\nexternalActionContent=Zażądano zewnętrznej akcji. Czy chcesz zezwolić na uruchamianie akcji spoza XPipe?\nnoScriptStateAvailable=Odśwież, aby określić zgodność skryptu ...\ndocumentationDescription=Sprawdź dokumentację\ncustomEditorCommandInTerminal=Uruchom niestandardowe polecenie w terminalu\ncustomEditorCommandInTerminalDescription=Jeśli twój edytor jest oparty na terminalu, możesz włączyć tę opcję, aby automatycznie otworzyć terminal i zamiast tego uruchomić polecenie w sesji terminala.\\n\\nMożesz użyć tej opcji dla edytorów takich jak vi, vim, nvim i innych.\ndisableHttpsTlsCheck=Wyłącz weryfikację certyfikatu żądania HTTPS\ndisableHttpsTlsCheckDescription=Jeśli Twoja organizacja odszyfrowuje ruch HTTPS w zaporach sieciowych przy użyciu przechwytywania SSL, wszelkie kontrole aktualizacji lub licencji zakończą się niepowodzeniem z powodu niezgodności certyfikatów. Możesz to naprawić, włączając tę opcję i wyłączając sprawdzanie poprawności certyfikatu TLS.\nconnectionsSelected=$NUMBER$ wybrane połączenia\naddConnections=Dodaj połączenia\nbrowseDirectory=Przeglądaj katalog\nopenTerminal=Otwórz terminal\ndocumentation=Dokumentacja\nreport=Zgłoś błąd\nkeePassXcNotAssociated=Łącze KeePassXC\nkeePassXcNotAssociatedDescription=XPipe nie jest powiązany z Twoją lokalną bazą danych KeePassXC. Kliknij poniżej, aby wykonać jednorazowy krok skojarzenia XPipe z bazą danych KeePassXC, aby XPipe mógł wyszukiwać hasła.\nkeePassXcAssociateMore=Połącz więcej baz danych\nkeePassXcAssociateMoreDescription=Możesz być połączony z wieloma bazami danych KeePassXC w tym samym czasie\nkeePassXcAssociated=Linki do KeePassXC\nkeePassXcAssociatedDescription=XPipe jest połączony z następującymi lokalnymi bazami danych KeePassXC:\nkeePassXcNotAssociatedButton=Połącz bazę danych\nidentifier=Identyfikator\npasswordManagerCommand=Polecenie niestandardowe\npasswordManagerCommandDescription=Niestandardowe polecenie do wykonania w celu pobrania haseł. Łańcuch zastępczy $KEY zostanie zastąpiony cytowanym kluczem hasła po wywołaniu. Powinno to wywołać CLI twojego menedżera haseł, aby wydrukować hasło na stdout, np. mypassmgr get $KEY.\nchooseTemplate=Wybierz szablon\nkeePassXcPlaceholder=Adres URL wpisu KeePassXC\nterminalEnvironment=Środowisko terminala\nterminalEnvironmentDescription=Jeśli chcesz użyć funkcji lokalnego środowiska WSL opartego na systemie Linux do dostosowania terminala, możesz użyć go jako środowiska terminala.\\n\\nWszelkie niestandardowe polecenia init terminala i konfiguracja multipleksera terminala będą wtedy uruchamiane w tej dystrybucji WSL.\nterminalInitScript=Skrypt inicjujący terminala\nterminalInitScriptDescription=Polecenia uruchamiane w środowisku terminala przed uruchomieniem połączenia. Możesz użyć tego do skonfigurowania środowiska terminala podczas uruchamiania.\nterminalMultiplexer=Multiplekser terminali\nterminalMultiplexerDescription=Multiplekser terminala do użycia jako alternatywa dla tabulatorów w terminalu. Zastąpi to niektóre cechy obsługi terminala, np. obsługę zakładek, funkcjonalnością multipleksera.\\n\\nWymaga zainstalowania w systemie odpowiedniego pliku wykonywalnego multipleksera.\nterminalMultiplexerWindowsDescription=Multiplekser terminala do użycia jako alternatywa dla tabulatorów w terminalu. Zastąpi to niektóre cechy obsługi terminala, np. obsługę zakładek, funkcjonalnością multipleksera.\\n\\nWymaga użycia środowiska terminalowego WSL w systemie Windows i zainstalowania pliku wykonywalnego multipleksera w systemie WSL.\nterminalAlwaysPauseOnExit=Zawsze wstrzymuj przy wyjściu\nterminalAlwaysPauseOnExitDescription=Jeśli opcja ta jest włączona, wyjście z sesji terminala zawsze spowoduje wyświetlenie monitu o ponowne uruchomienie lub zamknięcie sesji. Jeśli jest wyłączona, XPipe zrobi to tylko w przypadku nieudanych połączeń, które kończą się błędem.\nquerying=Zapytanie ...\nretrievedPassword=Uzyskano: $PASSWORD$\nrefreshOpenpubkey=Odśwież tożsamość openpubkey\nrefreshOpenpubkeyDescription=Uruchom odświeżanie opkssh, aby tożsamość openpubkey była ponownie ważna\nall=Wszystkie\nterminalPrompt=Monit terminala\nterminalPromptDescription=Narzędzie monitu terminala do użycia w terminalach zdalnych. Włączenie monitu terminala automatycznie ustawi i skonfiguruje narzędzie monitu w systemie docelowym podczas otwierania sesji terminala.\\n\\nNie modyfikuje to żadnych istniejących konfiguracji monitów ani plików profili w systemie. Spowoduje to wydłużenie czasu ładowania terminala po raz pierwszy podczas konfigurowania monitu w systemie zdalnym. Twój terminal może potrzebować dodatkowych czcionek do poprawnego wyświetlania monitu.\nterminalPromptConfiguration=Konfiguracja monitu terminala\nterminalPromptConfig=Plik konfiguracyjny\nterminalPromptConfigDescription=Niestandardowy plik konfiguracyjny do zastosowania w monicie. Ta konfiguracja zostanie automatycznie skonfigurowana w systemie docelowym podczas inicjalizacji terminala i będzie używana jako domyślna konfiguracja monitu.\\n\\nJeśli chcesz użyć istniejącego domyślnego pliku konfiguracyjnego w każdym systemie, możesz pozostawić to pole puste.\npasswordManagerKey=Klucz menedżera haseł\npasswordManagerKeyDescription=Identyfikator tajnego hasła w menedżerze haseł\npasswordManagerAgent=Agent menedżera haseł\ndockerComposeProject.displayName=Projekt Docker compose\ndockerComposeProject.displayDescription=Grupuj razem kontenery projektu kompilacji\nsshVerboseOutput=Włącz szczegółowe dane wyjściowe SSH\nsshVerboseOutputDescription=Spowoduje to wydrukowanie wielu informacji debugowania podczas łączenia się przez SSH. Przydatne do rozwiązywania problemów z połączeniami SSH.\ndontUseGateway=Nie używaj bramy\ndontUseGatewayDescription=Nie używaj hosta hypervisor jako bramy i połącz się bezpośrednio z IP\ncategoryColor=Kolor kategorii\ncategoryColorDescription=Domyślny kolor używany dla połączeń w tej kategorii\ncategorySync=Zsynchronizuj z repozytorium git\ncategorySyncDescription=Synchronizuj wszystkie połączenia automatycznie z repozytorium git. Wszystkie lokalne zmiany w połączeniach zostaną przesłane do repozytorium zdalnego.\ncategorySyncSpecial=Synchronizuj z repozytorium git\\n(Nie konfigurowalne dla kategorii specjalnej \"$NAME$\")\ncategoryDontAllowScripts=Wyłącz wszystkie modyfikacje\ncategoryDontAllowScriptsDescription=Wyłącz wykonywanie poleceń i innych operacji w systemach należących do tej kategorii, aby zapobiec wszelkim modyfikacjom. Spowoduje to wyłączenie wszystkich funkcji skryptów, poleceń środowiska powłoki, monitów i innych.\ncategoryConfirmAllModifications=Potwierdź wszystkie modyfikacje\ncategoryConfirmAllModificationsDescription=Potwierdź najpierw każdy rodzaj modyfikacji połączenia lub systemu plików. Może to zapobiec przypadkowym operacjom na ważnych systemach.\ncategoryDefaultIdentity=Tożsamość domyślna\ncategoryDefaultIdentityDescription=Jeśli często używasz określonej tożsamości w wielu systemach z tej kategorii, ustawienie domyślnej tożsamości pozwoli ci ją wstępnie wybrać podczas tworzenia nowych połączeń.\ncategoryConfigTitle=$NAME$ konfiguracja\nconfigure=Konfiguruj\naddConnection=Dodaj połączenie\nnoCompatibleConnection=Nie znaleziono zgodnego połączenia\nnoCompatibleIdentity=Nie znaleziono zgodnej tożsamości\nnewCategory=Nowa kategoria\ndockerComposeRestricted=Projekt compose jest ograniczony przez $NAME$ i nie może być modyfikowany zewnętrznie. Użyj $NAME$ do zarządzania tym projektem compose.\nrestricted=Ograniczony\ndisableSshPinCaching=Wyłącz buforowanie kodu PIN SSH\ndisableSshPinCachingDescription=XPipe automatycznie buforuje wszelkie kody PIN, które zostały wprowadzone dla klucza podczas korzystania z jakiejś formy uwierzytelniania sprzętowego.\\n\\nWyłączenie tej funkcji spowoduje konieczność ponownego wprowadzenia kodu PIN przy każdej próbie połączenia.\ngitSyncPull=Pociągnij, aby zsynchronizować zdalne zmiany git\nenpassVaultFile=Plik skarbca\nenpassVaultFileDescription=Lokalny plik sejfu Enpass.\nflat=Płaski\nrecursive=Rekursywny\nrdpAllowListBlocked=Wydaje się, że wybrana aplikacja RemoteApp nie znajduje się na liście zezwoleń RDP dla serwera.\npsonoServerUrl=Adres URL serwera\npsonoServerUrlDescription=Adres URL serwera backend psono\npsonoApiKey=Klucz API\npsonoApiKeyDescription=Klucz API do użycia, sformatowany jako uuid\npsonoApiSecretKey=Tajny klucz API\npsonoApiSecretKeyDescription=Tajny klucz API jako 64-bajtowy ciąg szesnastkowy\npassboltServerUrl=Adres URL serwera\npassboltServerUrlDescription=Adres URL serwera backend passbolt\npassboltPassphrase=Hasło\npassboltPassphraseDescription=Hasło dla klucza prywatnego skarbca\npassboltPrivateKey=Klucz prywatny\npassboltPrivateKeyDescription=Prywatny plik klucza gpg dla sejfu\nfocusWindowOnNotifications=Skoncentruj okno na powiadomieniach\nfocusWindowOnNotificationsDescription=Przenieś XPipe na pierwszy plan, gdy wyświetlane jest powiadomienie lub komunikat o błędzie, na przykład gdy połączenie lub tunel nieoczekiwanie się kończy.\ngitUsername=Niestandardowa nazwa użytkownika git\ngitUsernameDescription=Niestandardowy użytkownik do uwierzytelniania w zdalnym repozytorium git. Domyślnie XPipe użyje aktualnie skonfigurowanych poświadczeń git CLI.\\n\\nTo ustawienie zastąpi wszelkie domyślne poświadczenia, które są już skonfigurowane dla twojego lokalnego klienta git CLI.\ngitPassword=Niestandardowe hasło git / osobisty token dostępu\ngitPasswordDescription=Hasło lub osobisty token dostępu używany do uwierzytelniania. To, czy potrzebujesz hasła lub osobistego tokena dostępu, zależy od zdalnego dostawcy git. To ustawienie zastąpi wszelkie domyślne poświadczenia, które są już skonfigurowane dla twojego lokalnego klienta git CLI.\nsetReadOnly=Ustaw tylko do odczytu\nunsetReadOnly=Unset tylko do odczytu\nreadOnlyStoreError=Konfiguracja tego wpisu jest zamrożona. Wybierz inną nazwę, aby zapisać zmiany w nowej kopii.\ncategoryFreeze=Zamroź konfiguracje połączeń\ncategoryFreezeDescription=Oznacza konfiguracje połączeń jako tylko do odczytu. Oznacza to, że żadna istniejąca konfiguracja wpisu połączenia w tej kategorii nie może być modyfikowana. Można jednak dodawać nowe połączenia.\nupdateFail=Instalacja aktualizacji nie powiodła się\nupdateFailAction=Zainstaluj aktualizację ręcznie\nupdateFailActionDescription=Sprawdź najnowsze wersje w serwisie GitHub\nonePasswordPlaceholder=Nazwa elementu lub adres URL op://\ncomputeDirectorySizes=Oblicz rozmiary katalogów\ncomputeSize=Oblicz rozmiar\ncustomSpiceCommand=Polecenie niestandardowe\ncustomSpiceCommandDescription=Niestandardowe polecenie do wykonania w celu uruchomienia sesji SPICE. Łańcuch znaków zastępczych $FILE zostanie zastąpiony cytowaną ścieżką do pliku .vv po wywołaniu.\nvncClient=Klient VNC\nvncClientDescription=Klient VNC do uruchomienia podczas otwierania połączeń VNC w XPipe.\\n\\nMasz możliwość użycia zintegrowanego klienta VNC w XPipe lub alternatywnie uruchomienia zewnętrznego lokalnie zainstalowanego klienta VNC, jeśli szukasz większej personalizacji.\nintegratedXPipeVncClient=Zintegrowany klient XPipe VNC\ncustomVncCommand=Polecenie niestandardowe\ncustomVncCommandDescription=Niestandardowe polecenie do wykonania w celu uruchomienia sesji VNC. Łańcuch zastępczy $ADDRESS zostanie zastąpiony podanym adresem po wywołaniu.\nvncConnections=Połączenia VNC\npasswordManagerIdentity=Tożsamość menedżera haseł\npasswordManagerIdentity.displayName=Tożsamość menedżera haseł\npasswordManagerIdentity.displayDescription=Pobierz nazwę użytkownika i hasło tożsamości z menedżera haseł\npasswordCopied=Hasło połączenia skopiowane do schowka\nerrorOccurred=Wystąpił błąd\nactionMacro.displayName=Makro akcji\nactionMacro.displayDescription=Uruchom w akcji przy użyciu niestandardowych wyzwalaczy\nmacroAdd=Dodaj makro\nmacroName=Nazwa makra\nmacroNameDescription=Nadaj temu makru niestandardową nazwę\nactionId=Identyfikator akcji\nactionIdDescription=Akcja uruchamiana za pomocą tego makra\nmacroRefs=Powiązane połączenia\nmacroRefsDescription=Połączenia, za pomocą których można uruchomić akcję\nconnectionCopy=Kopia\nactionPickerTitle=Wybierz akcję\nactionPickerDescription=Kliknij coś, aby wykonać akcję. Zamiast wykonywać akcję, możesz tworzyć i edytować skróty do akcji w trybie wyboru skrótu akcji.\ncancelActionPicker=Anuluj wybór akcji\nactionShortcut=Skrót akcji\nactionShortcuts=Skróty akcji\nactionStore=Magazyn akcji\nactionStoreDescription=Wpis w sklepie, na którym chcesz uruchomić akcję\nactionStores=Magazyny akcji\nactionStoresDescription=Wpisy sklepu do uruchomienia akcji\nactionDesktopShortcut=Skrót pulpitu\nactionDesktopShortcutDescription=Utwórz skrót do tej akcji na pulpicie\nactionUrlShortcut=Skrót URL\nactionUrlShortcutDescription=Skopiuj adres URL, który może wywołać tę akcję po otwarciu\nactionUrlShortcutDisabled=Skrót URL (niedostępny)\nactionUrlShortcutDisabledDescription=Typ instalacji $TYPE$ nie obsługuje otwierania adresów URL\nactionApiCall=Żądanie API\nactionApiCallDescription=Wywołaj tę akcję z interfejsu API HTTP\nactionMacro=Makro akcji\nactionMacroDescription=Utwórz makro z zaawansowaną funkcjonalnością dla tej akcji\ncreateMacro=Utwórz makro\nactionConfiguration=Parametry\nactionConfigurationDescription=Parametry do przekazania do wykonywanej akcji\nconfirmAction=Potwierdź działanie\nactionConnections=Połączenia akcji\nactionConnectionsDescription=Połączenia, na których chcesz uruchomić akcję\nactionConnection=Połączenie akcji\nactionConnectionDescription=Połączenie, na którym chcesz uruchomić akcję\nappleContainerInstall.displayName=Kontenery Apple\nappleContainerInstall.displayDescription=Uzyskaj dostęp do instancji kontenera apple za pośrednictwem interfejsu CLI kontenera\nappleContainer.displayName=Kontener Apple\nappleContainer.displayDescription=Uzyskaj dostęp do instancji kontenera apple za pośrednictwem interfejsu CLI kontenera\nappleContainerHostDescription=Host, na którym znajduje się kontener Apple\nappleContainerDescription=Nazwa kontenera firmy apple\nappleContainers=Kontenery Apple\nchangeOrderIndexTitle=Zmień kolejność\norderIndex=Indeks\norderIndexDescription=Wyraźny indeks porządkujący ten wpis względem innych. Najniższe indeksy są wyświetlane na górze, najwyższe na dole\nmoveToFirst=Przenieś do pierwszego\nmoveToLast=Przejdź do ostatniego\ncategory=Kategoria\nincludeRoot=Uwzględnij root\nexcludeRoot=Wyklucz root\nfreezeConfiguration=Konfiguracja zamrożenia\nunfreezeConfiguration=Odblokuj konfigurację\nwaylandScalingTitle=Skalowanie Wayland\nactionApiUrl=$URL$ (Kopiuj treść json)\ncopyBody=Kopiuj treść żądania\ngitRepoTerminalOpen=Otwórz repozytorium w terminalu\ngitRepoTerminalOpenDescription=Spójrz na repozytorium samodzielnie za pomocą wiersza poleceń\ngitRepoOverwriteLocal=Nadpisz lokalne repozytorium\ngitRepoOverwriteLocalDescription=Zastąp wszystkie lokalne zmiany zmianami ze strony zdalnej\ngitRepoForcePush=Nadpisz zdalne repozytorium\ngitRepoForcePushDescription=Użyj git push --force, aby zastosować zmiany lokalne do zdalnych\ngitRepoDontWarn=Nie ostrzegaj więcej\ngitRepoDontWarnDescription=Jeśli jest to oczekiwane, spraw, aby XPipe ignorował ten błąd w przyszłości\ngitRepoTryAgain=Spróbuj ponownie\ngitRepoTryAgainDescription=Spróbuj ponownie wykonać tę samą operację\ngitRepoEnablePlain=Użyj zwykłej synchronizacji katalogów\ngitRepoEnablePlainDescription=Nie inicjuj repozytorium git, aby zsynchronizować zmiany w katalogu\ngitRepoCreateBare=Użyj synchronizacji git\ngitRepoCreateBareDescription=Zainicjuj nowe gołe repozytorium git w katalogu synchronizacji\ngitRepoDisable=Wyłącz na razie sejf git\ngitRepoDisableDescription=Nie wprowadzaj żadnych zmian podczas tej sesji\ngitRepoPullRefresh=Wyciągnij zmiany i odśwież\ngitRepoPullRefreshDescription=Scal zdalne zmiany i przeładuj dane\nbreakOutCategory=Wyróżnij kategorię\nmergeCategory=Kategoria scalania\nopenWinScp=Otwórz w WinSCP\nuninstallApplication=Odinstaluj\nuninstallApplicationDescription=Uruchamia skrypt instalacyjny .pkg w celu pełnego odinstalowania XPipe\nk8sEditPodTitle=Zastosuj zmiany\nk8sEditPodContent=Czy chcesz zastosować zmiany wprowadzone za pomocą polecenia kubectl apply? Aby zmiany zostały zastosowane, prawdopodobnie konieczne będzie ponowne uruchomienie systemu.\nvirshEditDomainTitle=Zastosuj zmiany\nvirshEditDomainContent=Czy chcesz zastosować zmiany w domenie? Prawdopodobnie wymagane jest ponowne uruchomienie, aby zmiany zostały zastosowane.\npkcs11Library=Biblioteka PKCS#11\npkcs11LibraryDescription=Ścieżka do dynamicznie połączonego pliku biblioteki\nsshAgentSocket=Niestandardowe gniazdo agenta SSH\nsshAgentSocketDescription=Niestandardowe gniazdo używane do komunikacji z agentem SSH. Tego niestandardowego agenta można użyć do połączenia, wybierając dla niego opcję agenta niestandardowego.\npublicKey=Identyfikator klucza publicznego\npublicKeyDescription=Opcjonalny klucz publiczny, aby wymusić na agencie oferowanie tylko pasującego klucza prywatnego\nactions=Działania\nhcloudServer.displayName=Serwer w chmurze Hetzner\nhcloudServer.displayDescription=Uzyskaj dostęp do serwera hostowanego w chmurze Hetzner przez SSH\nhcloudInstall.displayName=Hetzner Cloud CLI\nhcloudInstall.displayDescription=Uzyskaj dostęp do serwerów hostowanych w chmurze Hetzner za pośrednictwem hcloud\nhcloudContext.displayName=kontekst hcloud\nhcloudContext.displayDescription=Serwery dostępu do kontekstu hcloud\nmetrics=Metryka\nopenInVsCode=Otwórz w VsCode\naddCloud=Chmura ...\nhcloudToken=token hcloud\nhcloudTokenDescription=Token chmury Hetzner do użycia. Aby uzyskać więcej informacji, zapoznaj się z dokumentacją\nhcloudLogin=Logowanie do chmury Hetzner\nclearHcloudToken=Wyczyść token hcloud\nclearHcloudTokenDescription=Usuń istniejący token, aby móc zalogować się ponownie\nselectIdentity=Wybierz tożsamość\nenableMcpServer=Włącz serwer MCP\nenableMcpServerDescription=Włącza serwer XPipe MCP, umożliwiając zewnętrznym klientom MCP wysyłanie żądań do serwera MCP. Zobacz poniżej szczegóły konfiguracji.\\n\\nZwróć uwagę, że interfejs API HTTP nie musi być włączony dla funkcji MCP.\nenableMcpMutationTools=Włącz narzędzia mutacji MCP\nenableMcpMutationToolsDescription=Domyślnie na serwerze MCP włączone są tylko narzędzia tylko do odczytu. Ma to na celu zapewnienie, że nie zostaną wykonane żadne przypadkowe operacje, które mogą potencjalnie zmodyfikować system.\\n\\nJeśli planujesz wprowadzać zmiany w systemach za pośrednictwem klientów MCP, upewnij się, że klient MCP jest skonfigurowany do potwierdzania wszelkich potencjalnie destrukcyjnych działań przed włączeniem tej opcji. Wymaga ponownego połączenia wszystkich klientów MCP w celu zastosowania.\nmcpClientConfigurationDetails=Konfiguracja klienta MCP\nmcpClientConfigurationDetailsDescription=Użyj tych danych konfiguracyjnych, aby połączyć się z serwerem XPipe MCP z wybranego klienta MCP.\nswitchHostAddress=Zmień adres hosta\naddAnotherHostName=Dodaj kolejną nazwę hosta\naddNetwork=Skanowanie sieci ...\nnetworkScan=Skanowanie sieci\nnetworkScanStore=Host docelowy\nnetworkScanStoreDescription=Host, dla którego należy przeskanować sieć lokalną\nuseAsGateway=Użyj hosta jako bramy\nuseAsGatewayDescription=Czy użyć hosta docelowego jako bramy dla utworzonych połączeń?\nnetworkScanPorts=Porty do skanowania\nnetworkScanPortsDescription=Rozdzielana przecinkami lista portów do uwzględnienia w skanowaniu\nnetworkScanType=Typ połączenia\nnetworkScanTypeDescription=Typ serwerów, których należy szukać\nemptyDirectory=Ten katalog wygląda na pusty\nhcloudConfigFile=plik konfiguracyjny hcloud\nhcloudConfigFileDescription=Lokalizacja pliku konfiguracyjnego hcloud CLI .toml\npreferMonochromeIcons=Preferuj ikony monochromatyczne\npreferMonochromeIconsDescription=Po włączeniu, monochromatyczne zmienne ikony będą wybierane zamiast domyślnych kolorowych wersji ikony, zakładając, że dla ikony ze źródła dostępny jest oddzielny wariant ikony w trybie jasnym lub ciemnym.\\n\\nWymaga odświeżenia ikon do zastosowania.\nalwaysShowSshMotd=Zawsze pokazuj MOTD\nalwaysShowSshMotdDescription=Czy wyświetlać wiadomość dnia skonfigurowaną w zdalnym systemie po zalogowaniu w nowej sesji terminala. Zauważ, że zmiana tej opcji może zmienić zachowanie inicjalizacji połączeń SSH.\nmanageSubscription=Zarządzaj subskrypcją\nnoListeningServer=Brak serwera nasłuchującego\nnetworkScanResults=Wyniki skanowania\nnetworkScanResultsDescription=Lista znalezionych systemów w sieci\nlocalShellDialect=Powłoka lokalna\nlocalShellDialectDescription=Powłoka używana do operacji lokalnych. W przypadku, gdy normalna lokalna powłoka domyślna jest wyłączona lub uszkodzona do pewnego stopnia, opcja ta może być użyta do powrotu do innej alternatywy.\\n\\nNiektóre konfiguracje, takie jak niestandardowe wpisy PATH, mogą nie mieć zastosowania do powłoki awaryjnej, jeśli nie zostały jeszcze skonfigurowane w odpowiednich plikach profilu powłoki.\nagentSocketNotFound=Nie znaleziono aktywnego gniazda agenta\nagentSocket=Lokalizacja gniazda\nagentSocketDescription=Ścieżka do pliku gniazda agenta\nagentSocketNotConfigured=Nie skonfigurowano jeszcze niestandardowego gniazda\ndownloadInProgress=$NAME$ pobieranie w toku\nenableTerminalStartupBell=Włącz dzwonek uruchamiania terminala\nenableTerminalStartupBellDescription=Odtwórz polecenie sygnału dźwiękowego/dzwonka w nowej sesji terminala. Jeśli twój emulator terminala obsługuje dzwonki, może to ułatwić identyfikację nowo uruchomionych instancji terminala.\ninvalidSshGatewayChain=Nieprawidłowa konfiguracja łańcucha bram mieszanych z bramami skokowymi i bramami bez skoków.\nsyncFileExists=Zsynchronizowany plik $FILE$ już istnieje\nreplaceFile=Zastąp plik\nreplaceFileDescription=Zastąp istniejący plik tym\nrenameFile=Zmień nazwę pliku\nrenameFileDescription=Nadaj temu plikowi inną nazwę do synchronizacji\nnewFileName=Nowa nazwa pliku\nparentHostDoesNotSupportTunneling=Host nadrzędny $NAME$ nie obsługuje tunelowania\nconnectionNotesTemplate=Szablon notatek\nconnectionNotesTemplateDescription=Szablon markdown, który powinien być używany podczas dodawania nowego wpisu notatki do połączenia.\nconnectionNotesButton=Edytuj notatki\nrdpSmartSizing=Włącz inteligentny rozmiar\nrdpSmartSizingDescription=Po włączeniu, mstsc zmniejszy rozmiar pulpitu, jeśli okno jest zbyt małe, aby wyświetlić je w pełnej rozdzielczości. Współczynnik proporcji pulpitu jest zachowywany podczas skalowania w dół.\ndisableStartOnInit=Wyłącz automatyczne uruchamianie\nenableStartOnInit=Włącz automatyczne uruchamianie\nfileReadSudoTitle=Odczyt pliku Sudo\nfileReadSudoContent=Plik, który próbujesz odczytać, nie przyznaje bieżącemu użytkownikowi uprawnień do odczytu. Czy chcesz odczytać ten plik jako użytkownik root z sudo? Spowoduje to automatyczne podniesienie uprawnień do roota przy użyciu istniejących poświadczeń lub za pomocą monitu.\nnetbirdInstall.displayName=Instalacja Netbird\nnetbirdInstall.displayDescription=Połącz się z rówieśnikami w swojej sieci Netbird\nnetbirdProfile.displayName=Profil Netbird\nnetbirdProfile.displayDescription=Lista urządzeń równorzędnych w określonym profilu\nnetbirdPeer.displayName=Netbird peer\nnetbirdPeer.displayDescription=Połącz się z peerem przez SSH\nnetbirdPublicKey=Klucz publiczny\nnetbirdPublicKeyDescription=Wewnętrzny klucz publiczny urządzenia równorzędnego\nnetbirdHostName=Nazwa hosta\nnetbirdHostNameDescription=Nazwa hosta urządzenia równorzędnego w sieci\nvncRefSystem=Powiązany system\nvncRefSystemDescription=Wpis połączenia, z którym chcesz skojarzyć to połączenie VNC. Pozostaw puste, jeśli nie istnieje\nabstractHost.displayName=Streszczenie hosta\nabstractHost.displayDescription=Utwórz wpis dla hosta, który nie obsługuje połączeń powłoki\nabstractHostAddress=Adres hosta\nabstractHostAddressDescription=Adres hosta\nabstractHostGateway=Bramka\nabstractHostGatewayDescription=Opcjonalny system bramy, przez który można dotrzeć do tego hosta\nabstractHostConvert=Konwertuj na abstrakcyjny wpis hosta\nhostNoConnections=Brak dostępnych połączeń\nhostHasConnections=$COUNT$ dostępne połączenia\nhostHasConnection=$COUNT$ dostępne połączenie\nlargeFileWarningTitle=Edycja dużego pliku\nlargeFileWarningContent=Plik, który chcesz edytować, jest dość duży i zawiera adres $SIZE$. Czy naprawdę chcesz otworzyć ten plik w edytorze tekstu?\nrdpAskpassUser=Nazwa użytkownika RDP dla hosta $HOST$\nrdpAskpassPassword=Hasło dla użytkownika $USER$\ninPlaceKey=Klucz\ninPlaceKeyText=Zawartość klucza prywatnego\ninPlaceKeyTextDescription=Zawartość klucza prywatnego\nnetbirdSelfhosted=Samodzielnie hostowana instancja netbird\nnetbirdSelfhostedDescription=Podaj niestandardowy adres URL zamiast korzystać z wersji hostowanej w chmurze\nnetbirdManagementUrl=Adres URL zarządzania Netbird\nnetbirdManagementUrlDescription=Adres URL zarządzania Twojej samodzielnie hostowanej instancji\nnetbirdSetupKey=Klucz ustawień\nnetbirdSetupKeyDescription=Jeśli używasz kluczy ustawień, możesz użyć jednego do logowania\nnetbirdLogin=Zaloguj się do Netbird\naddProfile=Dodaj profil\nnetbirdProfileNameAsktext=Nazwa nowego profilu netbird\nopenSftp=Otwórz w sesji SFTP\ncapslockWarning=Masz włączony capslock\ninherit=Dziedzicz\nsshConfigStringSelected=Host docelowy\nsshConfigStringSelectedDescription=W przypadku wielu hostów, pierwszy z nich jest używany jako cel. Zmień kolejność hostów, aby zmienić cel\ntunnelToLocalhost=Tunel do localhost\ntunnelToLocalhostDescription=Automatycznie tuneluj zdalny port do localhost\ntags=Tagi\ntag=Znacznik\naddNewTag=Utwórz nowy tag\ncreateTag=Utwórz znacznik ...\ninPlacePublicKey=Klucz publiczny\ninPlacePublicKeyDescription=Powiązany klucz publiczny dla określonego klucza prywatnego\nsshKeygenTitle=Wygeneruj nowy klucz SSH\nsshKeygenAlgorithm=Algorytm\nsshKeygenAlgorithmDescription=Asymetryczny algorytm generowania klucza do użycia dla klucza\nrsaBits=Bity\nrsaBitsDescription=Liczba bitów w wygenerowanym kluczu\nsshKeygenComment=Komentarz\nsshKeygenCommentDescription=Opcjonalny komentarz dla tego klucza\nsshKeygenPassphrase=Hasło\nsshKeygenPassphraseDescription=Opcjonalne hasło dla tego klucza\ned25519SkResident=Utwórz klucz rezydenta\ned25519SkResidentDescription=Przechowuj klucz prywatny na sprzętowym kluczu bezpieczeństwa\ned25519SkResidentKeyName=Etykieta klucza rezydentnego\ned25519SkResidentKeyNameDescription=Nadaj kluczowi etykietę. Potrzebne w przypadku przechowywania wielu kluczy na kluczu bezpieczeństwa\ned25519SkPinRequired=Wymagaj kodu PIN\ned25519SkPinRequiredDescription=Wymagaj wprowadzenia kodu PIN podczas użytkowania\ned25519SkUserPresenceRequired=Wymagaj obecności użytkownika\ned25519SkUserPresenceRequiredDescription=Wymagaj obsługi dotykowej lub podobnej. Niektóre klucze bezpieczeństwa wymagają włączenia tej funkcji\ncopyPublicKey=Kopiuj klucz publiczny\ngeneratePublicKey=Wygeneruj klucz publiczny\npublicKeyGenerateNotice=Może być wygenerowany z klucza prywatnego\nidentityApplyTargetHost=Cel\nidentityApplyTargetHostDescription=System do zastosowania tożsamości do\nidentityApplyAuthorizedHost=Autoryzowany klucz SSH\nidentityApplyAuthorizedHostDescription=Klucz SSH jest dodawany do autoryzowanego pliku hosts\nidentityApplyAuthorizedHostButton=Dołącz klucz do pliku\napplyIdentityToHost=Zastosuj tożsamość do hosta ...\nidentityApplyMissingPublicKeyTitle=Brakujący klucz publiczny\nidentityApplyMissingPublicKeyContent=Klucz SSH tożsamości nie ma powiązanego klucza publicznego. Sprawdź konfigurację, aby uzyskać szczegółowe informacje.\nvalid=Ważny\nnotValid=Nieważne\nwarning=Ostrzeżenie\nidentityApplyTitle=Zastosuj tożsamość\nidentityApplyConfigPasswordEnabled=Autoryzacja hasłem włączona\nidentityApplyConfigPasswordEnabledDescription=Uwierzytelnianie hasłem jest nadal włączone w konfiguracji sshd\nidentityApplyConfigPasswordDisabled=Autoryzacja hasłem wyłączona\nidentityApplyConfigPasswordDisabledDescription=Uwierzytelnianie hasłem jest nadal wyłączone w konfiguracji sshd\nidentityApplyConfigKeyEnabled=Autoryzacja klucza włączona\nidentityApplyConfigKeyEnabledDescription=Uwierzytelnianie oparte na kluczach jest nadal włączone w konfiguracji sshd\nidentityApplyConfigKeyDisabled=Autoryzacja klucza wyłączona\nidentityApplyConfigKeyDisabledDescription=Uwierzytelnianie oparte na kluczach jest nadal wyłączone w konfiguracji sshd\nidentityApplyConfigRootDisabledWarning=Wyłączone logowanie do roota\nidentityApplyConfigRootDisabledWarningDescription=Logowanie użytkownika root nie jest włączone w konfiguracji sshd\nidentityApplyConfigAdminWarning=Skonfigurowane klucze administratora\nidentityApplyConfigAdminWarningDescription=Klucz może być dodany do administrators_authorized_keys zamiast tego dla użytkowników admin\nidentityApplyEditConfig=Edytuj konfigurację\nidentityApplyEditConfigDescription=Otwórz konfigurację sshd w edytorze, aby naprawić wszelkie błędy\nidentityApplyEditAuthorizedKeys=Edytuj autoryzowane klucze\nidentityApplyEditAuthorizedKeysDescription=Otwórz plik authorized_keys w edytorze, aby edytować lub usunąć inne klucze\nidentityApplyEditConfigButton=Otwórz sshd_config\nidentityApplyEditAuthorizedKeysButton=Otwórz authorized_keys\nidentityApplySetStoreIdentity=Zestaw tożsamości połączenia\nidentityApplySetStoreIdentityDescription=Tożsamość jest skonfigurowana do użycia przez połączenie\nidentityApplySetStoreIdentityButton=Zastosuj tożsamość\ngenerateKey=Wygeneruj klucz\ngroupSecretStrategy=Kontrola dostępu oparta na grupach\ngroupSecretStrategyDescription=Jak pobrać klucz tajny grupy używany do szyfrowania i deszyfrowania dla grupy. Wybrana metoda pobierania zostanie uruchomiona, gdy użytkownik zaloguje się do skarbca podczas uruchamiania.\\n\\nTo ustawienie jest konfigurowane dla poszczególnych grup. Aby zmienić to ustawienie dla innej grupy niż aktualnie aktywna, musisz zalogować się do skarbca jako członek tej grupy.\nfileSecret=Sekret oparty na plikach\ncommandSecret=Polecenie\nhttpRequestSecret=Odpowiedź HTTP\nfileSecretChoice=Lokalizacja pliku\nfileSecretChoiceDescription=Ścieżka do pliku zawierającego sekret szyfrowania grupy. Ponieważ plik ten może być sprawdzany na wszystkich platformach, możesz użyć ~ w ścieżce, aby odnieść się do katalogu domowego. Plik musi być dostępny na wszystkich systemach, z których odblokowujesz skarbiec, w przeciwnym razie logowanie nie powiedzie się.\ncommandSecretField=Skrypt pobierania\ncommandSecretFieldDescription=Polecenie, które zwróci tajny klucz szyfrowania dla bieżącej grupy. Polecenie jest uruchamiane w domyślnej powłoce systemu lokalnego, a klucz powinien zostać wypisany na stdout.\nhttpRequestSecretField=Identyfikator URI żądania\nhttpRequestSecretFieldDescription=Identyfikator URI, do którego należy wysłać żądanie HTTP. Sekret grupy jest pobierany z treści odpowiedzi HTTP.\nvaultAuthentication=Uwierzytelnianie skarbca\nvaultAuthenticationDescription=Jak uwierzytelnić / odblokować dane sejfu. Istnieje wiele różnych sposobów szyfrowania i odblokowywania danych skarbca, w zależności od tego, komu chcesz je udostępnić.\ngroupAuthFailed=Tajne uwierzytelnianie nie powiodło się\nuserAuthFailed=Uwierzytelnianie hasła nie powiodło się\nsavingChanges=Zapisywanie zmian\nawsDeviceScan=AWS\naws=AWS\nawsCliInstallTitle=Wymagane AWS CLI\nawsCliInstallContent=Integracja AWS wymaga zainstalowania AWS CLI w Twoim systemie lokalnym\nawsProfileCreateTitle=Nowy profil AWS\nawsProfileAccessKey=Klucz dostępu\nawsProfileName=Nazwa profilu\nawsProfileNameDescription=Wyświetlana nazwa nowego profilu\nawsProfileRegion=Region\nawsProfileRegionDescription=Region AWS powiązany z profilem\nawsProfileAccessKeyId=Identyfikator klucza dostępu\nawsProfileAccessKeyIdDescription=Identyfikator klucza dostępu użytkownika IAM\nawsProfileSecretAccessKey=Tajny klucz dostępu\nawsProfileSecretAccessKeyDescription=Powiązany tajny klucz dostępu\nawsInstall.displayName=Instalacja AWS CLI\nawsInstall.displayDescription=Połącz się z systemami AWS za pomocą AWS CLI\nawsProfile.displayName=Profil AWS CLI\nawsProfile.displayDescription=Uzyskaj dostęp do AWS za pośrednictwem określonego profilu\nawsInstanceId=Identyfikator wystąpienia\nawsInstanceIdDescription=Wewnętrzny identyfikator tej instancji\nawsInstanceUseSsm=Połącz przez SSM\nawsInstanceUseSsmDescription=Użyj narzędzia SSM, aby połączyć się z instancją przez SSH\nawsEc2Instance.displayName=Instancja AWS EC2\nawsEc2Instance.displayDescription=Połącz się z instancją EC2 przez SSH\nawsS3Group.displayName=Zasobniki S3\nawsS3Group.displayDescription=Uzyskaj dostęp do zasobników S3 profilu AWS\nawsS3Bucket.displayName=Wiadro S3\nawsS3Bucket.displayDescription=Uzyskaj dostęp do zasobnika S3 profilu AWS\nawsEc2Group.displayName=Instancje EC2\nawsEc2Group.displayDescription=Uzyskaj dostęp do instancji EC2 profilu AWS\nawsEc2InstanceSsmTerminal=Otwórz terminal SSM\ngenericS3Bucket.displayName=Ogólne zasobnik S3\ngenericS3Bucket.displayDescription=Uzyskaj dostęp do ogólnego zasobnika S3 za pośrednictwem interfejsu AWS CLI\naddFileSystem=System plików ...\ngenericS3BucketHost=Host\ngenericS3BucketHostDescription=Wpis hosta lub adres ręczny serwera S3\ngenericS3BucketPortDescription=Port, na którym nasłuchuje serwer S3\ngenericS3BucketAccessKeyId=Identyfikator klucza dostępu\ngenericS3BucketAccessKeyIdDescription=Identyfikator klucza dostępu użytkownika IAM\ngenericS3BucketSecretAccessKey=Tajny klucz dostępu\ngenericS3BucketSecretAccessKeyDescription=Powiązany tajny klucz dostępu\ngenericS3BucketHttps=Włącz HTTPS\ngenericS3BucketHttpsDescription=Użyj protokołu HTTPS, aby połączyć się z serwerem. Niektórzy dostawcy mogą wymagać HTTPS\ntunnelled=Tunel\nawsInstallSync=Synchronizacja konfiguracji\nawsInstallSyncDescription=Zsynchronizuj pliki konfiguracyjne AWS CLI ze skarbcem git\nawsInstallLocation=Lokalizacja danych użytkownika\nawsInstallLocationDescription=Ścieżka, z której pochodzą pliki konfiguracyjne AWS CLI\ninstanceActions=Działania instancji\nopenSplit=Otwórz w podzielonym terminalu\nterminalSplitStrategy=Kierunek widoku podzielonego\nterminalSplitStrategyDescription=Kontroluje sposób dzielenia kart terminala podczas korzystania z funkcji podzielonego widoku w trybie wsadowym w celu otwarcia wielu sesji terminala obok siebie.\nterminalSplitStrategyDisabledDescription=Kontroluje sposób dzielenia kart terminala podczas korzystania z funkcji podzielonego widoku w trybie wsadowym w celu otwarcia wielu sesji terminala obok siebie.\\n\\nTwoja bieżąca konfiguracja terminala nie obsługuje podzielonych widoków.\nhorizontal=Poziomo\nvertical=Pionowy\nbalanced=Zrównoważony\nclose=Zamknij\nhelpButton=$TOPIC$ łącze do dokumentacji\nquickAccess=Szybki dostęp\ntoggleEnabled=Przełącz stan\ncurrentPath=Bieżąca ścieżka\ndirectoryContents=Zawartość katalogu\ndirectoryOptions=Opcje katalogów\nchooseConnectionType=Wybierz typ połączenia\nbatchMode=Tryb wsadowy\ntoggleButton=Przycisk przełączania\ntailscaleUseSsh=Użyj autoryzacji SSH tailscale\ntailscaleUseSshDescription=Zaloguj się przez sam serwer SSH tailscale bez autoryzacji SSH\nportDescription=Port, na którym działa serwer SSH\nloginAs=Zaloguj się jako\nsshGatewayType=Typ bramki\nsshGatewayTypeDescription=Czy połączyć się z celem za pomocą tunelu lub opcji ProxyJump\ngatewayTunnel=Tunel bramy\nproxyJump=Skok proxy\ncommandTypeAsyncBackground=Uruchom odłączony w tle\ncommandTypeSyncBackground=Uruchom w tle i czekaj na zakończenie\ncommandTypeTerminalBackground=Otwórz w terminalu\nasyncBackgroundCommand=Polecenie tła\nsyncBackgroundCommand=Polecenie blokujące w tle\nterminalBackgroundCommand=Polecenie terminala\ntestingConnection=Testowanie połączenia ...\nopenManagementConsole=Otwarta konsola zarządzania\nopenLxcTerminal=Otwórz terminal LXC\nopenContainerConsole=Otwórz konsolę szeregową\nkeeper2fa=metoda 2FA\nkeeper2faDescription=Podstawowa metoda uwierzytelniania dwuskładnikowego skonfigurowana dla Twojego konta. Włącz tę opcję, jeśli Twoje konto Keeper wymaga uwierzytelniania dwuskładnikowego w celu uzyskania dostępu do haseł.\nkeeperTotpDuration=Czas trwania niestandardowego kodu 2FA\nkeeperTotpDurationDescription=Zastąp domyślny czas ważności kodu 2FA. Ma zastosowanie tylko wtedy, gdy zasady Twojej organizacji zezwalają na zmianę czasu trwania.\\n\\nMożliwe wartości to: $VALUES$\nkeeperOtherAuth=Inne (RSA SecurID, Duo Security, Keeper DNA itp.)\nextractReusableIdentities=Wyodrębnij tożsamości wielokrotnego użytku\nidentitiesAdded=Dodane tożsamości\nsyncMode=Tryb synchronizacji\nsyncModeDescription=Kontroluje sposób synchronizacji zmian.\\n\\nTryb natychmiastowy wypycha i ściąga zmiany tak szybko, jak to możliwe, tryb uruchamiania i zamykania synchronizuje wszystkie zmiany wprowadzone podczas sesji jednocześnie, a tryb ręczny synchronizuje tylko wtedy, gdy go zainicjujesz.\ntoggleTerminalDock=Przełącz terminal dokujący\nscriptDirectory=Lokalizacja katalogu\nscriptDirectoryDescription=Lokalny katalog zawierający pliki skryptów powłoki\nscriptSourceUrl=Adres URL repozytorium\nscriptSourceUrlDescription=Adres URL do zdalnego repozytorium git zawierającego pliki skryptów powłoki\nscriptCollectionSourceType=Typ źródła\nscriptCollectionSourceTypeDescription=Typ źródła, z którego powinny być ładowane skrypty powłoki\nscriptCollectionSourceEntry=Wpis źródłowy\nscriptCollectionSourceEntryDescription=Źródło, z którego powinny być ładowane skrypty powłoki\ngitRepository=Repozytorium git\nscriptCollectionSource.displayName=Źródło skryptu\nscriptCollectionSource.displayDescription=Automatycznie importuj skrypty powłoki z istniejącego źródła\ndirectorySource=Źródło katalogu\ngitRepositorySource=Źródło repozytorium git\nrefreshSource=Odśwież źródło\nscriptTextSourceUrl=Adres URL skryptu\nscriptTextSourceUrlDescription=Adres URL, z którego można pobrać plik skryptu\nscriptSourceType=Źródło skryptu\nscriptSourceTypeDescription=Skąd pobrać skrypt\nscriptSourceTypeInPlace=Skrypt w miejscu instalacji\nscriptSourceTypeUrl=Zewnętrzny adres URL\nscriptSourceTypeSource=Istniejące źródło\nimportScripts=Importuj skrypty\nscriptsContained=$NUMBER$ skrypty\nscriptSourceCollectionImportTitle=Importuj skrypty ze źródła ($SELECTED$/$COUNT$)\nnoScriptsFound=Nie znaleziono żadnych skryptów\ntunnel=Tunel\nnotInitialized=Nie zainicjowano\nselectCategory=Wybierz kategorię ...\nscriptSourceName=Nazwa skryptu\nscriptSourceNameDescription=Nazwa pliku skryptu w źródle\nworkspaceRestartTitle=Gotowy obszar roboczy\nworkspaceRestartContent=Skrót do nowego obszaru roboczego został utworzony na stronie $PATH$. Możesz przejść do skrótu lub ponownie uruchomić XPipe, aby automatycznie otworzyć nowy obszar roboczy.\nbrowseShortcut=Przeglądaj plik\nsyncModeInstant=Natychmiastowa synchronizacja\nsyncModeSession=Synchronizuj przy uruchomieniu i wyjściu\nsyncModeManual=Synchronizuj ręcznie\npushChanges=Naciśnij zmiany\npullChanges=Pull changes\nsourcedFrom=Pochodzi z $SOURCE$\ninPlaceScript=Skrypt w miejscu instalacji\ngeneric=Ogólny\nsyncToPlainDirectory=Synchronizuj ze zwykłym katalogiem\nsyncToPlainDirectoryDescription=Podczas synchronizacji z katalogiem lokalnym, możesz traktować ten katalog jako inne repozytorium git lub po prostu jako zwykły katalog. Jeśli włączone jest ustawienie zwykłego katalogu, katalog nie jest inicjowany jako repozytorium git.\nopenSpiceSession=Otwórz sesję SPICE\nterminalBehaviour=Zachowanie terminala\nnoScanPossible=Nie znaleziono obsługiwanych połączeń\nnetworkSwitchPorts=Porty sieciowe\nnswitchGroup.displayName=Porty sieciowe\nnswitchGroup.displayDescription=Lista dostępnych portów na urządzeniu sieciowym\nnswitchPort.displayName=Port sieciowy\nnswitchPort.displayDescription=Kontroluj pojedynczy port na przełączniku sieciowym\nenablePort=Włącz port\nshutdownPort=Zamknij port\nresetPort=Zresetuj port\nuseSystemDefault=Użyj domyślnych ustawień systemu\nportStatus=Stan portu\nclearCounters=Wyczyść liczniki\nshowStatus=Pokaż status\nshowAllPorts=Pokaż wszystkie porty\nactiveLicense=Licencja\nactiveLicenseDescription=Aktywuj klucz licencyjny XPipe\nauthenticatorApp=Aplikacja uwierzytelniająca\nsecurityKey=Klucz zabezpieczeń\nmcpAdditionalContext=Dodatkowy kontekst MCP\nmcpAdditionalContextDescription=Dodatkowe instrukcje do przekazania klientowi MCP. Użyj tego, aby kontrolować zachowanie agenta i zapewnić dodatkowy kontekst dla indywidualnej konfiguracji.\nmcpAdditionalContextSample=- Nie uruchamiaj ponownie żadnych usług i demonów automatycznie bez uprzedniego potwierdzenia\\n- Podczas konfigurowania interfejsu sieciowego zawsze używaj 192.168.1.1/24 jako bramy\nprefsRestartTitle=Wymagany restart\nprefsRestartContent=Niektóre opcje, które zmieniłeś, wymagają ponownego uruchomienia aplikacji. Czy chcesz teraz ponownie uruchomić XPipe?\nbashShell=Powłoka Bash\n"
  },
  {
    "path": "lang/strings/translations_pt.properties",
    "content": "delete=Elimina\nproperties=Propriedades\nusedDate=Usado $DATE$\nopenDir=Abrir diretório\nsortLastUsed=Ordena por data da última utilização\nsortAlphabetical=Ordena alfabeticamente por nome\nsortIndexed=Ordenar por índice de ordem\nrestartDescription=Um reinício pode muitas vezes ser uma solução rápida\nreportIssue=Relata um problema\nreportIssueDescription=Abre o relatório de problemas integrado\nusefulActions=Acções úteis\nstored=Guardado\ntroubleshootingOptions=Ferramentas de resolução de problemas\ntroubleshoot=Resolve problemas\nremote=Ficheiro remoto\naddShellStore=Adiciona o Shell ...\naddShellTitle=Adiciona uma ligação Shell\nsavedConnections=Ligações guardadas\nsave=Guarda\nclean=Limpa\nmoveTo=Passa para ...\naddDatabase=Base de dados ...\nbrowseInternalStorage=Navega no armazenamento interno\naddTunnel=Túnel ...\naddService=Serviço ...\naddScript=Script ...\naddHost=Anfitrião remoto ...\naddShell=Ambiente Shell ...\naddCommand=Comando ...\naddAutomatically=Adiciona automaticamente ...\naddOther=Adiciona outro ...\nconnectionAdd=Adiciona uma ligação\nscriptAdd=Adiciona um script\nscriptGroupAdd=Adiciona um grupo de scripts\nidentityAdd=Adicionar identidade\nnew=Novo\nselectType=Seleciona o tipo\nselectTypeDescription=Seleciona o tipo de ligação\nselectShellType=Tipo de shell\nselectShellTypeDescription=Seleciona o tipo de ligação Shell\nname=Nome do objeto\nstoreIntroHeader=Hub de ligação\nstoreIntroContent=Aqui podes gerir todas as tuas ligações shell locais e remotas num só lugar. Para começar, podes detetar rapidamente e de forma automática as ligações disponíveis e escolher as que queres adicionar.\nstoreIntroButton=Procura ligações ...\ndragAndDropFilesHere=Ou simplesmente arrasta e larga um ficheiro aqui\nconfirmDsCreationAbortTitle=Confirmação de abortar\nconfirmDsCreationAbortHeader=Queres abortar a criação da fonte de dados?\nconfirmDsCreationAbortContent=Perde todo o progresso da criação da fonte de dados.\nconfirmInvalidStoreTitle=Salta a validação\nconfirmInvalidStoreContent=Queres saltar a validação da ligação? Podes adicionar esta ligação mesmo que não possa ser validada e resolver os problemas de ligação mais tarde.\nexpand=Expande\naccessSubConnections=Acede às subligações\ncommon=Comum\ncolor=Cor\nalwaysConfirmElevation=Confirma sempre a elevação da permissão\nalwaysConfirmElevationDescription=Controla a forma de lidar com casos em que são necessárias permissões elevadas para executar um comando num sistema, por exemplo, com sudo.\\n\\nPor predefinição, quaisquer credenciais sudo são armazenadas em cache durante uma sessão e fornecidas automaticamente quando necessário. Se esta opção estiver activada, pede-te sempre para confirmares o acesso de elevação.\nallow=Permite\nask=Pergunta\ndeny=Recusar\nshare=Adiciona ao repositório git\nunshare=Remove do repositório git\nremove=Remove\ncreateNewCategory=Nova subcategoria\nprompt=Prompt\ncustomCommand=Comando personalizado\nother=Outro\nsetLock=Definir bloqueio\nselectConnection=Selecionar ligação\nselectEntry=Selecionar entrada\ncreateLock=Cria uma frase-chave\nchangeLock=Altera a frase-chave\ntest=Testa\nfinish=Termina\nerror=Ocorreu um erro\ndownloadStageDescription=Move os ficheiros transferidos para o diretório de transferências do sistema e abre-o.\nok=Ok\nsearch=Procura\nrepeatPassword=Repete a palavra-passe\naskpassAlertTitle=Askpass\nunsupportedOperation=Operação não suportada: $MSG$\nfileConflictAlertTitle=Resolve o conflito\nfileConflictAlertContent=Encontraste um conflito. O ficheiro $FILE$ já existe no sistema de destino.\\n\\nComo queres proceder?\nfileConflictAlertContentMultiple=Encontraste um conflito. O ficheiro $FILE$ já existe.\\n\\nComo queres proceder? Poderão existir mais conflitos que podes resolver automaticamente escolhendo uma opção que se aplique a todos.\nmoveAlertTitle=Confirmação de movimento\nmoveAlertHeader=Queres mover os ($COUNT$) elementos selecionados para $TARGET$?\ndeleteAlertTitle=Confirma a eliminação\ndeleteAlertHeader=Pretendes apagar os ($COUNT$) elementos selecionados?\nselectedElements=Elementos selecionados:\nmustNotBeEmpty=$VALUE$ não pode estar vazio\nvalueMustNotBeEmpty=O valor não pode estar vazio\ntransferDescription=Arrasta os ficheiros para aqui para descarregar\ndragLocalFiles=Arrasta as transferências a partir daqui\nnull=$VALUE$ não pode ser nulo\nroots=Raízes\nscripts=Scripts\nsearchFilter=Pesquisa ...\nrecent=Recentes\nshortcut=Atalho\nbrowserWelcomeEmptyHeader=Navegador de ficheiros\nbrowserWelcomeEmptyContent=Podes escolher à esquerda quais os sistemas a abrir no navegador de ficheiros. O XPipe lembrar-se-á dos sistemas e diretórios a que acedeste anteriormente e mostrá-los-á num menu de acesso rápido no futuro.\nbrowserWelcomeEmptyButton=Abre o navegador de ficheiros local\nbrowserWelcomeSystems=Estiveste recentemente ligado aos seguintes sistemas:\nbrowserWelcomeDocsHeader=Documentação\nbrowserWelcomeDocsContent=Se preferires uma abordagem mais guiada para te familiarizares com o XPipe, consulta o site da documentação.\nbrowserWelcomeDocsButton=Abre a documentação\nhostFeatureUnsupported=$FEATURE$ não está instalado no anfitrião\nmissingStore=$NAME$ não existe\nconnectionName=Nome da ligação\nconnectionNameDescription=Dá um nome personalizado a esta ligação\nopenFileTitle=Abre o ficheiro\nunknown=Desconhecido\nscanAlertTitle=Adiciona ligações\nscanAlertChoiceHeader=Destino\nscanAlertChoiceHeaderDescription=Escolhe onde procurar as ligações. Procura primeiro todas as ligações disponíveis.\nscanAlertHeader=Tipos de ligação\nscanAlertHeaderDescription=Seleciona os tipos de ligações que pretende adicionar automaticamente ao sistema.\nnoInformationAvailable=Não há informações disponíveis\nyes=Sim\nno=Não\nerrorOccured=Ocorreu um erro\nterminalErrorOccured=Ocorreu um erro no terminal\nerrorTypeOccured=Foi lançada uma exceção do tipo $TYPE$\npermissionsAlertTitle=Permissões necessárias\npermissionsAlertHeader=São necessárias permissões adicionais para efetuar esta operação.\npermissionsAlertContent=Segue a janela pop-up para dar ao XPipe as permissões necessárias no menu de definições.\nerrorDetails=Detalhes do erro\nupdateReadyAlertTitle=Atualizar pronto\nupdateReadyAlertHeader=Uma atualização para a versão $VERSION$ está pronta para ser instalada\nupdateReadyAlertContent=Instala a nova versão e reinicia o XPipe quando a instalação estiver concluída.\nerrorNoDetail=Não há detalhes de erro disponíveis\nerrorNoExceptionMessage=Foi lançado um erro do tipo $TYPE$\nupdateAvailableTitle=Atualização disponível\nupdateAvailableContent=Está disponível para instalação uma atualização do XPipe para a versão $VERSION$. Apesar de não ter sido possível iniciar o XPipe, podes tentar instalar a atualização para potencialmente corrigir o problema.\nclipboardActionDetectedTitle=Ação da área de transferência detectada\nclipboardActionDetectedContent=O XPipe detectou conteúdo na tua área de transferência que pode ser aberto. Queres abri-lo agora? Queres importar o conteúdo da tua área de transferência?\ninstall=Instala ...\nignore=Ignora\npossibleActions=Acções disponíveis\nreportError=Relata um erro\nreportOnGithub=Criar um relatório de problemas no GitHub\nreportOnGithubDescription=Abre um novo problema no repositório GitHub\nreportErrorDescription=Envia um relatório de erro com feedback opcional do utilizador e informações de diagnóstico\nignoreError=Ignora o erro\nignoreErrorDescription=Ignora este erro e continua como se nada tivesse acontecido\nprovideEmail=Como te podemos contactar (opcional, apenas se quiseres obter uma resposta). O teu relatório é anónimo por defeito, por isso podes fornecer informações de contacto, como um endereço de e-mail, aqui.\nadditionalErrorInfo=Fornece informações adicionais (opcional)\nadditionalErrorAttachments=Selecionar anexos (opcional)\ndataHandlingPolicies=Política de privacidade\nsendReport=Enviar relatório\nerrorHandler=Gestor de erros\nevents=Eventos\nvalidate=Valida\nstackTrace=Rastreio de pilha\npreviousStep=< Anterior\nnextStep=Seguinte >\nfinishStep=Conclui\nselect=Selecionar\nbrowseInternal=Navega internamente\ncheckOutUpdate=Verifica a atualização\nquit=Desiste\nnoTerminalSet=Não definiu automaticamente nenhuma aplicação de terminal. Podes fazê-lo manualmente no menu de definições.\nconnections=Ligações\nconnectionHub=Hub de ligação\nsettings=Definições\nexplorePlans=Licença\nhelp=Ajuda-te\nabout=Acerca de\ndeveloper=Criador\nbrowseFileTitle=Procurar ficheiro\nbrowser=Navegador de ficheiros\nselectFileFromComputer=Selecionar um ficheiro deste computador\nlinks=Ligações\nwebsite=Sítio Web\ndiscordDescription=Junta-te ao servidor Discord\nredditDescription=Junta-te ao subreddit XPipe\nsecurity=Segurança\nsecurityPolicy=Informações de segurança\nsecurityPolicyDescription=Lê a política de segurança detalhada\nprivacy=Política de privacidade\nprivacyDescription=Lê a política de privacidade da aplicação XPipe\nslackDescription=Junta-te ao espaço de trabalho do Slack\nsupport=Apoia\ngithubDescription=Consulta o repositório do GitHub\nopenSourceNotices=Avisos de código aberto\ncheckForUpdates=Verifica se há actualizações\ncheckForUpdatesDescription=Descarrega uma atualização, se existir uma\nlastChecked=Verificado pela última vez\nversion=Versão\nbuild=Constrói a versão\nruntimeVersion=Versão de tempo de execução\nvirtualMachine=Máquina virtual\nupdateReady=Instalar atualização\nupdateReadyPortable=Verifica a atualização\nupdateReadyDescription=Uma atualização foi descarregada e está pronta para ser instalada\nupdateReadyDescriptionPortable=Uma atualização está disponível para transferência\nupdateRestart=Reinicia para atualizar\nnever=Nunca mais\nupdateAvailableTooltip=Atualização disponível\nptbAvailableTooltip=Compilação de teste pública disponível\nvisitGithubRepository=Visita o repositório GitHub\nupdateAvailable=Atualização disponível: $VERSION$\ndownloadUpdate=Atualização da transferência\nlegalAccept=Aceito o Contrato de Licença de Utilizador Final\nconfirm=Confirma\nprint=Imprime\nwhatsNew=O que há de novo na versão $VERSION$ ($DATE$)\nantivirusNoticeTitle=Uma nota sobre programas antivírus\nupdateChangelogAlertTitle=Changelog\ngreetingsAlertTitle=Bem-vindo ao XPipe\neula=Contrato de licença de utilizador final\nnews=Notícias\nintroduction=Introdução\nprivacyPolicy=Política de privacidade\nagree=Concorda\ndisagree=Não concordas\ndirectories=Diretórios\nlogFile=Ficheiro de registo\nlogFiles=Ficheiros de registo\nlogFilesAttachment=Ficheiros de registo\nissueReporter=Relator de problemas\nopenCurrentLogFile=Ficheiros de registo\nopenCurrentLogFileDescription=Abre o ficheiro de registo da sessão atual\nopenLogsDirectory=Abre o diretório de registos\ninstallationFiles=Ficheiros de instalação\nopenInstallationDirectory=Ficheiros de instalação\nopenInstallationDirectoryDescription=Abre o diretório de instalação do XPipe\nlaunchDebugMode=Modo de depuração\nlaunchDebugModeDescription=Reinicia o XPipe em modo de depuração\nextensionInstallTitle=Descarrega\nextensionInstallDescription=Esta ação requer bibliotecas adicionais de terceiros que não são distribuídas pelo XPipe. Podes instalá-las automaticamente aqui. Os componentes são depois descarregados do sítio Web do fornecedor:\nextensionInstallLicenseNote=Ao efetuar a transferência e a instalação automática, concordas com os termos das licenças de terceiros:\nlicense=Licença\ninstallRequired=Instalação necessária\nrestore=Restaurar\nrestoreAllSessions=Repõe todas as sessões\nlimitedTouchscreenMode=Modo de ecrã tátil limitado\nlimitedTouchscreenModeDescription=Quando utilizas esta aplicação numa interface de ecrã tátil mais exótica, como um ecrã de telefone, alguns menus podem não funcionar corretamente. Quando esta opção está activada, a implementação do menu utiliza uma funcionalidade mais limitada para trabalhar com eventos de rato/toque enviados de forma esparsa.\nappearance=Aparece\ndisplay=Mostra\npersonalization=Personalização\ndisplayOptions=Opções de visualização\ntheme=Tema\nrdpConfiguration=Configuração do ambiente de trabalho remoto\nrdpClient=Cliente RDP\nrdpClientDescription=O programa cliente RDP a chamar ao iniciar ligações RDP.\\n\\nNota que vários clientes têm diferentes graus de capacidades e integrações. Alguns clientes não suportam a passagem automática de palavras-passe, pelo que tens de as preencher no arranque.\nlocalShell=Shell local\nthemeDescription=O teu tema de visualização preferido.\ndontAutomaticallyStartVmSshServer=Não inicia automaticamente o servidor SSH para VMs quando necessário\ndontAutomaticallyStartVmSshServerDescription=Qualquer ligação shell a uma VM em execução num hipervisor é feita através de SSH. O XPipe pode iniciar automaticamente o servidor SSH instalado quando necessário. Se não quiseres isto por razões de segurança, então podes simplesmente desativar este comportamento com esta opção.\nconfirmGitShareTitle=Sincronização Git\nconfirmGitShareContent=Pretendes adicionar o ficheiro selecionado ao teu repositório git vault? Isto irá copiar uma versão encriptada do ficheiro para o teu git vault e confirmar as tuas alterações. Terás então acesso ao ficheiro em todos os ambientes de trabalho sincronizados.\ngitShareFileTooltip=Adiciona um ficheiro ao diretório de dados do git vault para que seja sincronizado automaticamente.\\n\\nEsta ação só pode ser utilizada quando o git vault está ativado nas definições.\nperformanceMode=Modo de desempenho\nperformanceModeDescription=Desactiva todos os efeitos visuais que não são necessários para melhorar o desempenho da aplicação.\ndontAcceptNewHostKeys=Não aceita automaticamente novas chaves de anfitrião SSH\ndontAcceptNewHostKeysDescription=O XPipe aceitará automaticamente chaves de anfitrião por defeito de sistemas onde o teu cliente SSH não tem nenhuma chave de anfitrião conhecida já guardada. No entanto, se alguma chave de anfitrião conhecida tiver sido alterada, recusará a ligação a menos que aceites a nova chave.\\n\\nDesativar este comportamento permite-te verificar todas as chaves de anfitrião, mesmo que não haja conflito inicialmente.\nuiScale=Escala da IU\nuiScaleDescription=Um valor de escala personalizado que pode ser definido independentemente da escala de exibição de todo o sistema. Os valores estão em percentagem, pelo que, por exemplo, o valor de 150 resultará numa escala da IU de 150%.\neditorProgram=Programa editor\neditorProgramDescription=O editor de texto predefinido a utilizar quando edita qualquer tipo de dados de texto.\nwindowOpacity=Opacidade de uma janela\nwindowOpacityDescription=Altera a opacidade da janela para acompanhar o que está a acontecer em segundo plano.\nuseSystemFont=Utiliza o tipo de letra do sistema\nopenDataDir=Diretório de dados do cofre\nopenDataDirButton=Abre o diretório de dados\nopenDataDirDescription=Se quiseres sincronizar ficheiros adicionais, como chaves SSH, entre sistemas com o teu repositório git, podes colocá-los no diretório de dados de armazenamento. Quaisquer ficheiros aí referenciados terão os seus caminhos de ficheiro automaticamente adaptados em qualquer sistema sincronizado.\nupdates=Actualiza\nselectAll=Seleciona tudo\nadvanced=Avançado\nthirdParty=Avisos de fonte aberta\neulaDescription=Lê o Contrato de Licença de Utilizador Final da aplicação XPipe\nthirdPartyDescription=Vê as licenças de código aberto de bibliotecas de terceiros\nworkspaceLock=Palavra-passe principal\nenableGitStorage=Ativar a sincronização\nsharing=Partilha\ngitSync=Sincronização Git\nenableGitStorageDescription=Quando ativado, o XPipe inicializará um repositório git para o cofre local e confirmará quaisquer alterações nele. Tem em atenção que isto requer que o git esteja instalado e pode tornar as operações de carregamento e gravação mais lentas.\\n\\nTodas as categorias que devem ser sincronizadas devem ser marcadas explicitamente como sincronizadas.\nstorageGitRemote=URL de sincronização remota\nstorageGitRemoteDescription=Quando definido, o XPipe puxa automaticamente quaisquer alterações ao carregar e empurra quaisquer alterações para o repositório remoto ao guardar.\\n\\nIsto permite-te partilhar o teu cofre entre várias instalações XPipe. Suporta URLs HTTP e SSH, além de diretórios locais.\nvault=Cofre\nworkspaceLockDescription=Define uma palavra-passe personalizada para encriptar quaisquer informações sensíveis armazenadas no XPipe.\\n\\nIsto resulta numa maior segurança, uma vez que fornece uma camada adicional de encriptação para as informações sensíveis armazenadas. Ser-te-á pedido que introduzas a palavra-passe quando o XPipe for iniciado.\nuseSystemFontDescription=Controla se deve utilizar o tipo de letra predefinido do sistema ou o tipo de letra Inter, que está incluído no XPipe.\ntooltipDelay=Atraso da dica de ferramenta\ntooltipDelayDescription=A quantidade de milissegundos a aguardar até que uma dica de ferramenta seja apresentada.\nfontSize=Tamanho da letra\nwindowOptions=Opções de janela\nsaveWindowLocation=Guarda a localização da janela\nsaveWindowLocationDescription=Controla se as coordenadas da janela devem ser guardadas e restauradas nos reinícios.\nstartupShutdown=Arranque / Encerramento\nshowChildrenConnectionsInParentCategory=Mostra as categorias secundárias na categoria principal\nshowChildrenConnectionsInParentCategoryDescription=Incluir ou não todas as ligações localizadas em subcategorias quando é selecionada uma determinada categoria principal.\\n\\nSe esta opção estiver desactivada, as categorias comportam-se mais como pastas clássicas que apenas mostram o seu conteúdo direto sem incluir as subpastas.\ncondenseConnectionDisplay=Condensa o ecrã de ligação\ncondenseConnectionDisplayDescription=Faz com que cada ligação de nível superior ocupe menos espaço vertical para permitir uma lista de ligações mais condensada.\nopenConnectionSearchWindowOnConnectionCreation=Abre a janela de pesquisa de ligação na criação da ligação\nopenConnectionSearchWindowOnConnectionCreationDescription=Se abre ou não automaticamente a janela para procurar subconexões disponíveis ao adicionar uma nova ligação shell.\nworkflow=Fluxo de trabalho\nsystem=Sistema\napplication=Aplicação\nstorage=Armazenamento\nrunOnStartup=Executa no arranque\ncloseBehaviour=Comportamento de saída\ncloseBehaviourDescription=Controla a forma como o XPipe deve proceder ao fechar a sua janela principal.\nlanguage=Língua\nlanguageDescription=A língua de apresentação a utilizar. As traduções são melhoradas através das contribuições da comunidade. Podes ajudar o esforço de tradução submetendo correcções de tradução no GitHub.\nlightTheme=Tema de luz\ndarkTheme=Tema escuro\nexit=Sai do XPipe\ncontinueInBackground=Continua em segundo plano\nminimizeToTray=Minimiza para o tabuleiro\ncloseBehaviourAlertTitle=Define o comportamento de fecho\ncloseBehaviourAlertTitleHeader=Seleciona o que deve acontecer ao fechar a janela. Todas as ligações activas serão fechadas quando a aplicação for encerrada.\nstartupBehaviour=Comportamento de arranque\nstartupBehaviourDescription=Controla o comportamento predefinido da aplicação de ambiente de trabalho quando o XPipe é iniciado.\nclearCachesAlertTitle=Limpa a cache\nclearCachesAlertContent=Queres limpar todas as caches do XPipe? Isto elimina todos os dados de cache armazenados para melhorar a experiência do utilizador.\nstartGui=Inicia a GUI\nstartInTray=Iniciar no tabuleiro\nstartInBackground=Inicia em segundo plano\nclearCaches=Limpa as caches ...\nclearCachesDescription=Elimina todos os dados da cache\ncancel=Cancela\nnotAnAbsolutePath=Não é um caminho absoluto\nnotADirectory=Não é um diretório\nnotAnEmptyDirectory=Não é um diretório vazio\nautomaticallyCheckForUpdates=Verifica se há actualizações\nautomaticallyCheckForUpdatesDescription=Quando ativado, as informações de novas versões são obtidas automaticamente enquanto o XPipe está a ser executado após algum tempo. Continua a ter de confirmar explicitamente qualquer instalação de atualização.\nsendAnonymousErrorReports=Envia relatórios de erro anónimos\nsendUsageStatistics=Enviar estatísticas de utilização anónimas\nstorageDirectory=Diretório de armazenamento\nstorageDirectoryDescription=A localização onde o XPipe deve armazenar todas as informações de ligação. Ao alterar isto, os dados no diretório antigo não são copiados para o novo.\nlogLevel=Nível de registo\nappBehaviour=Comportamento da aplicação\nlogLevelDescription=O nível de registo que deve ser utilizado quando escreves ficheiros de registo.\ndeveloperMode=Modo de programador\ndeveloperModeDescription=Quando ativado, terás acesso a uma variedade de opções adicionais que são úteis para o desenvolvimento.\neditor=Editor\ncustom=Personaliza\npasswordManager=Gestor de palavras-passe\nexternalPasswordManager=Gestor de palavras-passe externas\npasswordManagerDescription=O gestor de palavras-passe instalado localmente com o qual te deves integrar.\\n\\nSe tiveres um gestor de palavras-passe instalado, podes configurar o XPipe para obter palavras-passe a partir dele, para que o XPipe não tenha de armazenar as próprias palavras-passe. Quando ativado, qualquer campo de palavra-passe para uma ligação pode ser configurado para utilizar o gestor de palavras-passe.\npasswordManagerCommandTest=Testa o gestor de palavras-passe\npasswordManagerCommandTestDescription=Podes testar aqui se a saída parece correta se tiveres configurado um gestor de senhas.\npreferTerminalTabs=Prefere abrir novos separadores\npreferTerminalTabsDescription=Controla se o XPipe tentará abrir novos separadores no terminal escolhido em vez de novas janelas. Nem todos os terminais suportam separadores.\ncustomRdpClientCommand=Comando personalizado\ncustomRdpClientCommandDescription=O comando a executar para iniciar o cliente RDP personalizado.\\n\\nA cadeia de caracteres de espaço $FILE será substituída pelo nome do arquivo .rdp absoluto entre aspas quando chamado. Lembra-te de colocar entre aspas o teu caminho executável se este contiver espaços.\ncustomEditorCommand=Comando do editor personalizado\ncustomEditorCommandDescription=O comando a executar para iniciar o editor personalizado.\\n\\nA string de espaço reservado $FILE será substituída pelo nome absoluto do ficheiro entre aspas quando chamado. Lembra-te de citar o caminho executável do teu editor se este contiver espaços.\neditorReloadTimeout=Tempo limite de recarga do editor\neditorReloadTimeoutDescription=A quantidade de milissegundos a esperar antes de ler um ficheiro depois de este ter sido atualizado. Isto evita problemas nos casos em que o teu editor é lento a escrever ou a libertar bloqueios de ficheiros.\nencryptAllVaultData=Encripta todos os dados do cofre\nencryptAllVaultDataDescription=Quando ativado, todas as partes dos dados de ligação do vault serão encriptadas com a sua chave de encriptação do vault do utilizador, em vez de apenas os segredos contidos nesses dados. Isto acrescenta outra camada de segurança para outros parâmetros, como nomes de utilizador, nomes de anfitrião, etc., que não são encriptados por predefinição no vault.\\n\\nEsta opção tornará o histórico do seu cofre git e os diffs inúteis, pois já não podes ver as alterações originais, apenas as alterações binárias.\nvaultSecurity=Segurança do cofre\ndeveloperDisableUpdateVersionCheck=Desativar a verificação da versão de atualização\ndeveloperDisableUpdateVersionCheckDescription=Controla se o verificador de actualizações ignora o número da versão quando procura uma atualização.\ndeveloperDisableGuiRestrictions=Desativar restrições GUI\ndeveloperDisableGuiRestrictionsDescription=Controla se algumas acções desactivadas ainda podem ser executadas a partir da interface do utilizador.\ndeveloperShowHiddenEntries=Mostra entradas ocultas\ndeveloperShowHiddenEntriesDescription=Quando ativado, as fontes de dados ocultas e internas serão mostradas.\ndeveloperShowHiddenProviders=Mostra fornecedores ocultos\ndeveloperShowHiddenProvidersDescription=Controla se os fornecedores de ligação e de fonte de dados ocultos e internos serão mostrados na caixa de diálogo de criação.\ndeveloperDisableConnectorInstallationVersionCheck=Desativar a verificação da versão do conetor\ndeveloperDisableConnectorInstallationVersionCheckDescription=Controla se o verificador de actualizações ignora o número da versão ao inspecionar a versão de um conetor XPipe instalado numa máquina remota.\nshellCommandTest=Teste de Comando Shell\nshellCommandTestDescription=Executa um comando na sessão shell utilizada internamente pelo XPipe.\nterminal=Terminal\nterminalType=Emulador de terminal\nterminalConfiguration=Configuração de terminal\nterminalCustomization=Personalização de terminais\neditorConfiguration=Configuração do editor\ndefaultApplication=Aplicação por defeito\ninitialSetup=Configuração inicial\nterminalTypeDescription=O terminal predefinido a utilizar para abrir ligações shell.\\n\\nO nível de suporte de recursos varia de acordo com o terminal, e cada um é marcado como recomendado ou não recomendado. A tua experiência de utilizador será melhor se utilizares um terminal recomendado.\nprogram=Programar\ncustomTerminalCommand=Comando de terminal personalizado\ncustomTerminalCommandDescription=O comando a executar para abrir o terminal personalizado com um determinado comando.\\n\\nO XPipe criará um script de shell de lançamento temporário para o teu terminal executar. A string de espaço reservado $CMD no comando que forneces será substituída pelo script de lançamento real quando chamado. Lembra-te de colocar entre aspas o caminho do executável do teu terminal se este contiver espaços.\nclearTerminalOnInit=Limpa o terminal no início\nclearTerminalOnInitDescription=Quando ativado, o XPipe executa um comando de limpeza depois de uma nova sessão de terminal ser iniciada para remover qualquer saída desnecessária que tenha sido impressa ao iniciar a sessão de terminal.\ndontCachePasswords=Não guardar em cache as palavras-passe solicitadas\ndontCachePasswordsDescription=Controla se as palavras-passe consultadas devem ser colocadas em cache internamente pelo XPipe para que não tenhas de as introduzir novamente na sessão atual.\\n\\nSe este comportamento estiver desativado, terás de voltar a introduzir quaisquer credenciais solicitadas sempre que estas forem requeridas pelo sistema.\ndenyTempScriptCreation=Recusa a criação de scripts temporários\ndenyTempScriptCreationDescription=Para realizar algumas das suas funcionalidades, o XPipe cria por vezes scripts de shell temporários num sistema de destino para permitir uma execução fácil de comandos simples. Estes não contêm qualquer informação sensível e são criados apenas para efeitos de implementação.\\n\\nSe este comportamento for desativado, o XPipe não criará quaisquer ficheiros temporários num sistema remoto. Esta opção é útil em contextos de alta segurança onde cada mudança no sistema de arquivos é monitorada. Se esta opção for desactivada, algumas funcionalidades, por exemplo, ambientes shell e scripts, não funcionarão como pretendido.\ndisableCertutilUse=Desativar a utilização do certutil no Windows\nuseLocalFallbackShell=Utiliza a shell de recurso local\nuseLocalFallbackShellDescription=Passa a usar outro shell local para lidar com operações locais. Seria o PowerShell no Windows e o bourne shell noutros sistemas.\\n\\nEsta opção pode ser usada no caso de o shell local padrão normal estar desativado ou quebrado em algum grau. Alguns recursos podem não funcionar como esperado quando esta opção está ativada.\ndisableCertutilUseDescription=Devido a várias falhas e bugs no cmd.exe, são criados scripts de shell temporários com o certutil, utilizando-o para descodificar a entrada base64, uma vez que o cmd.exe quebra em entradas não ASCII. O XPipe também pode usar o PowerShell para isso, mas será mais lento.\\n\\nIsso desabilita qualquer uso do certutil em sistemas Windows para realizar alguma funcionalidade e volta para o PowerShell. Isso pode agradar alguns AVs, pois alguns deles bloqueiam o uso do certutil.\ndisableTerminalRemotePasswordPreparation=Desativar a preparação da palavra-passe remota do terminal\ndisableTerminalRemotePasswordPreparationDescription=Em situações em que uma ligação shell remota que passa por vários sistemas intermédios deva ser estabelecida no terminal, pode ser necessário preparar quaisquer palavras-passe necessárias num dos sistemas intermédios para permitir o preenchimento automático de quaisquer prompts.\\n\\nSe não pretender que as palavras-passe sejam transferidas para qualquer sistema intermédio, pode desativar este comportamento. Qualquer senha intermediária necessária será então consultada no próprio terminal quando aberto.\nmore=Mais\ntranslate=Traduções\nallConnections=Todas as ligações\nallScripts=Todos os scripts\nallIdentities=Todas as identidades\nsynced=Sincronizado\npredefined=Predefinido\nsamples=Amostras\ngoodMorning=Bom dia\ngoodAfternoon=Boa tarde\ngoodEvening=Boa noite\naddVisual=Visual ...\naddDesktop=Desktop ...\nssh=SSH\nsshConfiguration=Configuração SSH\nsize=Tamanho\nattributes=Atribui\nmodified=Modificado\nowner=Proprietário\nupdateReadyTitle=Actualiza para $VERSION$ ready\ntemplates=Modelos\nretry=Repetir\nretryAll=Repetir tudo\nreplace=Substitui\nreplaceAll=Substitui tudo\nhibernateBehaviour=Comportamento de hibernação\nhibernateBehaviourDescription=Controla a forma como a aplicação se comporta quando o sistema é colocado em hibernação/sono.\noverview=Resumo\nhistory=História\nskipAll=Salta tudo\nnotes=Nota\naddNotes=Adiciona notas\norder=Reordenar\nkeepFirst=Guarda primeiro\nkeepLast=Mantém o último\npinToTop=Coloca o pin no topo\nunpinFromTop=Solta do topo\norderAheadOf=Encomenda antes de ...\nclearIndex=Repor o índice\nhttpServer=Servidor HTTP\nmcpServer=Servidor MCP\napiKey=Chave API\napiKeyDescription=A chave da API para autenticar os pedidos de API do daemon XPipe. Para mais informações sobre como autenticar, vê a documentação geral da API.\ndisableApiAuthentication=Desativar a autenticação da API\ndisableApiAuthenticationDescription=Desactiva todos os métodos de autenticação necessários para que qualquer pedido não autenticado seja tratado.\\n\\nA autenticação só deve ser desactivada para fins de desenvolvimento.\napi=API\nstoreIntroImportContent=Já estás a utilizar o XPipe noutro sistema? Sincroniza as tuas ligações existentes em vários sistemas através de um repositório git remoto. Também podes sincronizar mais tarde, a qualquer momento, se ainda não estiver configurado.\nstoreIntroImportButton=Sincroniza as ligações ...\nstoreIntroImportHeader=Importar ligações\nshowNonRunningChildren=Mostra as crianças que não estão a correr\nhttpApi=API HTTP\nisOnlySupportedLimit=só é suportado com uma licença profissional se tiver mais de $COUNT$ ligações\nareOnlySupportedLimit=só são suportados com uma licença profissional quando têm mais de $COUNT$ ligações\nenabled=Ativado\nenableGitStoragePtbDisabled=A sincronização do Git está desactivada para compilações de teste públicas para evitar a utilização com repositórios git de lançamento regulares e para desencorajar a utilização de uma compilação PTB como o teu condutor diário.\ncopyId=Copia o ID da API\nrequireDoubleClickForConnections=Exige duplo clique para ligações\nrequireDoubleClickForConnectionsDescription=Se estiver ativado, tens de fazer duplo clique nas ligações para as iniciar. Isto é útil se estiveres habituado a fazer duplo clique em coisas.\nclearTransferDescription=Limpar seleção\nselectTab=Selecionar separador\ncloseTab=Fecha o separador\ncloseOtherTabs=Fecha outros separadores\ncloseAllTabs=Fecha todos os separadores\ncloseLeftTabs=Fecha os separadores à esquerda\ncloseRightTabs=Fecha os separadores à direita\naddSerial=Ligação em série ...\nconnect=Liga-te\nworkspaces=Espaços de trabalho\nmanageWorkspaces=Gere espaços de trabalho\naddWorkspace=Adiciona um espaço de trabalho ...\nworkspaceAdd=Adiciona um novo espaço de trabalho\nworkspaceAddDescription=Workspaces são configurações distintas para executar o XPipe. Cada espaço de trabalho tem um diretório de dados onde todos os dados são armazenados localmente. Isto inclui dados de ligação, definições e muito mais.\\n\\nSe utilizares a funcionalidade de sincronização, também podes optar por sincronizar cada espaço de trabalho com um repositório git diferente.\nworkspaceName=Nome do espaço de trabalho\nworkspaceNameDescription=O nome de apresentação do espaço de trabalho\nworkspacePath=Caminho do espaço de trabalho\nworkspacePathDescription=A localização do diretório de dados do espaço de trabalho\nworkspaceCreationAlertTitle=Criação de espaço de trabalho\ndeveloperForceSshTty=Força o SSH TTY\ndeveloperForceSshTtyDescription=Faz com que todas as ligações SSH atribuam um pty para testar o suporte para um stderr e um pty em falta.\ndeveloperDisableSshTunnelGateways=Desativar o túnel de gateway SSH\ndeveloperDisableSshTunnelGatewaysDescription=Não utilizes sessões de túnel para gateways e, em vez disso, liga-te diretamente ao sistema.\nttyWarning=A ligação atribuiu à força um pty/tty e não fornece um fluxo stderr separado.\\n\\nIsto pode levar a alguns problemas.\\n\\nSe puderes, tenta fazer com que o comando de ligação não atribua um pty.\nxshellSetup=Configuração do Xshell\ntermiusSetup=Configuração do Termius\ntryPtbDescription=Experimenta as novas funcionalidades nas primeiras versões de programador do XPipe\nconfirmVaultUnencryptTitle=Confirma a descriptografia do cofre\nconfirmVaultUnencryptContent=Queres mesmo desativar a encriptação avançada da abóbada? Isto irá remover a encriptação adicional dos dados armazenados e irá substituir os dados existentes.\nenableHttpApi=Ativar a API HTTP\nenableHttpApiDescription=Habilita a API, permitindo que programas externos chamem o daemon XPipe para executar ações com suas conexões gerenciadas.\nchooseCustomIcon=Escolhe um ícone personalizado\ngitVault=Cofre do Git\nfileBrowser=Navegador de ficheiros\nconfirmAllDeletions=Confirma todas as eliminações\nconfirmAllDeletionsDescription=Mostra ou não uma caixa de diálogo de confirmação para todas as operações de eliminação. Por predefinição, apenas as diretorias requerem uma confirmação.\nyesterday=Ontem\ngreen=Verde\nyellow=Amarelo\nblue=Azul\nred=Vermelho\ncyan=Ciano\npurple=Púrpura\nasktextAlertTitle=Prompt\nfileWriteSudoTitle=Escreve um ficheiro Sudo\nfileWriteSudoContent=O ficheiro que estás a tentar escrever não concede permissões de escrita ao teu utilizador. Queres escrever este ficheiro como root com sudo? Isto irá elevar-te automaticamente para root com as credenciais existentes ou através de um prompt.\ndontAllowTerminalRestart=Não permitir o reinício do terminal\ndontAllowTerminalRestartDescription=Por defeito, as sessões de terminal podem ser reiniciadas depois de terminarem a partir do terminal. Para permitir isso, o XPipe aceitará essas solicitações externas do terminal para iniciar a sessão novamente\\n\\nO XPipe não tem qualquer controlo sobre o terminal e sobre a origem desta chamada, pelo que as aplicações locais maliciosas também podem utilizar esta funcionalidade para iniciar ligações através do XPipe. Desativar esta funcionalidade evita este cenário.\nopenDocumentation=Abre a documentação\nopenDocumentationDescription=Visita a página de documentação do XPipe para esta questão\nrenameAll=Renomeia tudo\nlogging=Registo\nenableTerminalLogging=Ativar o registo de terminal\nenableTerminalLoggingDescription=Ativa o registo do lado do cliente para todas as sessões de terminal. Todas as entradas e saídas da sessão de terminal são gravadas em um arquivo de log de sessão. Nota que qualquer informação sensível, como pedidos de palavra-passe, não é registada.\nterminalLoggingDirectory=Registos de sessões de terminal\nterminalLoggingDirectoryDescription=Todos os registos são armazenados no diretório de dados do XPipe no teu sistema local.\nopenSessionLogs=Abre os registos da sessão\nsessionLogging=Registo de terminal\nsessionActive=Está a decorrer uma sessão em segundo plano para esta ligação.\\n\\nPara parar esta sessão manualmente, clica no indicador de estado.\nskipValidation=Salta a validação\nscriptsIntroHeader=Sobre scripts\nscriptsIntroContent=Podes executar scripts no shell init, no navegador de ficheiros e a pedido. Podes criar scripts dentro do XPipe ou importar scripts existentes do teu sistema local ou de um repositório git remoto.\nscriptsIntroBottomHeader=Utilizar scripts\nscriptsIntroBottomContent=Há uma variedade de exemplos de scripts para começares. Podes clicar no botão de edição dos scripts individuais para veres como são implementados. Primeiro, os scripts têm de ser activados para serem executados e aparecerem nos menus; para isso, há uma opção em cada script.\nscriptsIntroBottomButton=Começa a trabalhar\nscriptSourcesIntroHeader=Fontes de script\nscriptSourcesIntroContent=Podes adicionar fontes de script personalizadas para teres acesso instantâneo a toda uma coleção de scripts de shell. Tanto as fontes locais como os repositórios git remotos são suportados como fontes. Todos os scripts detectados da fonte ficarão disponíveis automaticamente.\nscriptSourcesIntroButton=Acrescenta a fonte ...\ncheckForSecurityUpdates=Verifica se existem actualizações de segurança\ncheckForSecurityUpdatesDescription=O XPipe pode verificar potenciais actualizações de segurança separadamente das actualizações de funcionalidades normais. Quando esta opção está activada, pelo menos as actualizações de segurança importantes serão recomendadas para instalação, mesmo que a verificação de atualização normal esteja desactivada.\\n\\nSe desativar esta definição, não será efectuado qualquer pedido de versão externa e não serás notificado sobre quaisquer actualizações de segurança.\nclickToDock=Clica para acoplar o terminal\nterminalStarting=Aguarda o arranque do terminal ...\npinTab=Separador de pinos\nunpinTab=Desfixar separador\npinned=Fixado\nenableConnectionHubTerminalDocking=Ativar a ligação do terminal do hub de ligação\nenableConnectionHubTerminalDockingDescription=Podes acoplar as janelas de terminal à janela da aplicação XPipe no hub de ligação para simular um terminal de certa forma integrado. As janelas de terminal são então geridas pelo XPipe para caberem sempre na doca.\nenableFileBrowserTerminalDocking=Ativar a ancoragem do terminal do navegador de ficheiros\nenableFileBrowserTerminalDockingDescription=Podes acoplar as janelas de terminal à janela da aplicação XPipe no navegador de ficheiros para simular um terminal de certa forma integrado. As janelas de terminal são então geridas pelo XPipe para caberem sempre na doca.\ndownloadsDirectory=Diretório de transferências personalizado\ndownloadsDirectoryDescription=O diretório personalizado para colocar os arquivos baixados ao clicar no botão mover para downloads. Por defeito, o XPipe utiliza o diretório de downloads do utilizador.\npinLocalMachineOnStartup=Fixa o separador da máquina local no arranque\npinLocalMachineOnStartupDescription=Abre automaticamente um separador da máquina local e fixa-o. Isto é útil se estiveres a utilizar frequentemente um navegador de ficheiros dividido com a máquina local e o sistema de ficheiros remoto abertos.\nterminalErrorDescription=Este erro é terminal e o XPipe não pode continuar sem o corrigir.\ngroupName=Nome do grupo\nchmodPermissions=Novas permissões\neditFilesWithDoubleClick=Edita ficheiros com um duplo clique\neditFilesWithDoubleClickDescription=Quando ativado, fazer duplo clique em ficheiros abre-os diretamente no teu editor de texto em vez de mostrar o menu de contexto.\ncensorMode=Modo de censura\ncensorModeDescription=Desfoca qualquer informação como nomes de anfitrião, nomes de utilizador, nomes de ligação e muito mais.\\n\\nIsto é útil se pretenderes fazer uma captura de ecrã ou uma partilha de ecrã do XPipe e não quiseres divulgar qualquer informação.\naddIdentity=Identidade ...\nidentities=Identificações\naddMacro=Ação ...\nidentitiesIntroHeader=Sobre identidades\nidentitiesIntroContent=Se estiveres a reutilizar combinações comuns de nomes de utilizador, palavras-passe e chaves, poderá fazer sentido criar identidades reutilizáveis. Isto permite-te referenciá-las rapidamente quando adicionas novas ligações.\nidentitiesIntroBottomHeader=Partilhar identidades\nidentitiesIntroBottomContent=Podes adicionar identidades localmente ou também sincronizá-las no repositório git quando este estiver ativado. Isto permite-te partilhar seletivamente identidades em vários sistemas e com outros membros da equipa.\nidentitiesIntroBottomButton=Sincronização de configuração\nidentitiesIntroButton=Cria uma identidade\nuserName=Nome de utilizador\nuserAuth=Autenticação por palavra-passe baseada no utilizador\ngroupAuth=Autenticação secreta baseada em grupo\nteam=Equipa\nteamSettings=Definições da equipa\nteamVaults=Cofres de equipa\nvaultTypeNameDefault=Cofre por defeito\nvaultTypeNameLegacy=Cofre pessoal legado\nvaultTypeNamePersonal=Cofre pessoal\nvaultTypeNameTeam=Cofre da equipa\nteamVaultsDescription=As abóbadas de equipa permitem que vários utilizadores e grupos tenham acesso seguro a uma abóbada partilhada. Pode configurar as ligações e identidades para serem partilhadas por todos os utilizadores ou apenas estarem disponíveis para utilizadores e grupos individuais, encriptando-as com a sua própria chave. Outros usuários do vault não podem acessar conexões e identidades pessoais e baseadas em grupos se não tiverem acesso à chave.\nvaultTypeContentDefault=Atualmente, está a utilizar uma abóbada predefinida sem utilizador e com uma frase-chave personalizada definida. Os segredos são encriptados com a chave local do vault. Podes atualizar para um cofre pessoal criando uma conta de utilizador do cofre. Isto permite-lhe encriptar os segredos do cofre com a sua própria frase-chave pessoal que tem de introduzir em cada início de sessão para desbloquear o cofre.\nvaultTypeContentLegacy=Atualmente, está a utilizar um cofre pessoal antigo para o seu utilizador. Os segredos são encriptados com a tua frase-chave pessoal. Esta compatibilidade antiga tem funcionalidades limitadas e não pode ser actualizada para um cofre de equipa no local.\nvaultTypeContentPersonal=Atualmente, estás a utilizar um cofre pessoal para o teu utilizador. Os segredos são encriptados com a sua frase-chave pessoal. Pode atualizar para um cofre de equipa adicionando utilizadores adicionais do cofre ou adicionando uma configuração de acesso baseada em grupo.\nvaultTypeContentTeam=Atualmente, está a utilizar um cofre de equipa, que permite que vários utilizadores tenham acesso seguro a um cofre partilhado. Pode configurar as ligações e identidades para serem partilhadas por todos os utilizadores ou apenas estarem disponíveis para o seu utilizador pessoal ou grupo, encriptando-as com a sua chave pessoal ou de grupo. Outros utilizadores do vault não podem aceder às suas ligações e identidades pessoais e baseadas em grupos se não tiverem acesso à chave.\ngroupManagement=Gestão de grupos\ngroupManagementEmpty=Gestão de grupos\ngroupManagementDescription=Gere os grupos de abóbadas existentes ou cria novos grupos. Cada grupo de abóbadas tem a sua própria chave secreta individual, que é utilizada para encriptar ligações e identidades que só devem estar disponíveis para o grupo e não para outros.\ngroupManagementEmptyDescription=Gere os grupos de abóbadas existentes ou cria novos grupos. Cada grupo de abóbadas tem a sua própria chave secreta individual, que é utilizada para encriptar ligações e identidades que só devem estar disponíveis para o grupo e não para outros.\\n\\nAs contas baseadas em grupos para uma equipa são suportadas no plano profissional.\nuserManagement=Gestão de utilizadores\nuserManagementEmpty=Gestão de utilizadores\nuserManagementDescription=Gere os utilizadores da abóbada existentes ou cria novos utilizadores. Cada utilizador da abóbada tem a sua própria palavra-passe individual, que é utilizada para encriptar ligações e identidades que só devem estar disponíveis para o utilizador e não para outros.\nuserManagementEmptyDescription=Gere os utilizadores da abóbada existentes ou cria novos utilizadores. Cada utilizador da abóbada tem a sua própria palavra-passe individual, que é utilizada para encriptar ligações e identidades que só devem estar disponíveis para o utilizador e não para outros. Cria um utilizador para ti próprio para poderes encriptar ligações e identidades com a tua chave pessoal.\\n\\nÉ suportada uma única conta de utilizador na edição comunitária. São suportadas várias contas de utilizador para uma equipa no plano profissional.\nuserIntroHeader=Gestão de utilizadores\nuserIntroContent=Cria a primeira conta de utilizador para ti próprio para começar. Isto permite-te bloquear este espaço de trabalho com uma palavra-passe.\naddReusableIdentity=Adiciona uma identidade reutilizável\nusers=Utilizadores\nsyncVault=Sincronização de cofres\nsyncVaultDescription=Para sincronizar o seu vault com vários sistemas ou com vários membros da equipa, active a sincronização git para este vault.\nenableGitSync=Ativar a sincronização do git\nbrowseVault=Dados do cofre\nbrowseVaultDescription=Podes dar uma vista de olhos ao diretório do vault no teu gestor de ficheiros nativo. Nota que as edições externas não são recomendadas e podem causar uma variedade de problemas.\nbrowseVaultButton=Navega no cofre\nvaultUsers=Utilizadores de cofres\ncreateHeapDump=Cria um despejo de heap\ncreateHeapDumpDescription=Despeja o conteúdo da memória num ficheiro para resolver problemas de utilização da memória\ninitializingApp=Carregamento de ligações\ncheckingLicense=Verificação da licença\nloadingGit=Sincronização com o repositório git\nloadingGpg=Iniciando o daemon GnuPG para o git\nloadingSettings=Carregamento de definições\nloadingConnections=Carregamento de ligações\nunlockingVault=Desbloquear o cofre\nloadingUserInterface=Carregamento da interface do utilizador\nptbNotice=Aviso para a compilação de teste pública\nuserDeletionTitle=Eliminação do utilizador\nuserDeletionContent=Pretendes eliminar este utilizador da abóbada? Isto irá reencriptar todas as tuas identidades pessoais e segredos de ligação utilizando a chave da abóbada que está disponível para todos os utilizadores. Isto demorará algum tempo e o XPipe será reiniciado para aplicar as alterações ao utilizador.\ngroupDeletionTitle=Eliminação de grupos\ngroupDeletionContent=Pretendes eliminar este grupo da abóbada? Isto irá reencriptar todas as identidades apenas de grupo e segredos de ligação utilizando a chave do vault que está disponível para todos os utilizadores. Isto demorará algum tempo e o XPipe será reiniciado para aplicar as alterações ao grupo.\nkillTransfer=Elimina a transferência\ndestination=Destino\nconfiguration=Configuração\nnewFile=Novo ficheiro\nnewLink=Nova ligação\nlinkName=Nome da ligação\nscanConnections=Procura ligações disponíveis ...\nobserve=Começa a observar\nstopObserve=Pára de observar\ncreateShortcut=Cria um atalho no ambiente de trabalho\nbrowseFiles=Procurar ficheiros\nclone=Clone\ntargetPath=Caminho de destino\nnewDirectory=Novo diretório\ncopyShareLink=Copia a ligação\nselectStore=Selecionar loja\nsaveSource=Guardar para mais tarde\nexecute=Executa\ndeleteChildren=Remove todas as crianças\nscriptGroupDescriptionDescription=Dá a este grupo uma descrição opcional\nabstractHostDescriptionDescription=Dá a este anfitrião uma descrição opcional\nselectSource=Selecionar fonte\ncommandLineRead=Atualização\ncommandLineWrite=Escreve\nadditionalOptions=Opções adicionais\ninput=Introduzir\nmachine=Máquina\nopen=Abre\nedit=Edita\nscriptContents=Conteúdo do guião\nscriptContentsDescription=Os comandos de script a executar\nsnippets=Dependências de scripts\nsnippetsDescription=Outros scripts para executar primeiro\nsnippetsDependenciesDescription=Todos os possíveis scripts que devem ser executados, se aplicável\nisDefault=Corre no init em todas as shells compatíveis\nbringToShells=Traz para todos os shells compatíveis\nisDefaultGroup=Executa todos os scripts de grupo no shell init\nexecutionType=Tipo de execução\nexecutionTypeDescription=Em que contextos podes utilizar este script\nminimumShellDialect=Tipo de shell\nminimumShellDialectDescription=O tipo de shell em que deves executar este script\ndumbOnly=Estúpido\nterminalOnly=Terminal\nboth=Ambos\nshouldElevate=Deve elevar\nshouldElevateDescription=Se deves executar este script com permissões elevadas\nscript.displayName=Script de shell\nscript.displayDescription=Cria um script de shell reutilizável\nscriptGroup.displayName=Grupo de scripts\nscriptGroup.displayDescription=Agrupa scripts e organiza-os dentro de\nscriptGroup=Agrupa\nscriptGroupDescription=O grupo ao qual atribuir este guião\nscriptGroupGroupDescription=O grupo pai opcional ao qual atribuir este grupo de scripts\nopenInNewTab=Abre num novo separador\nexecuteInBackground=em segundo plano\nexecuteInTerminal=em $TERM$\nback=Volta atrás\nbrowseInWindowsExplorer=Navega no Windows Explorer\nbrowseInDefaultFileManager=Navega no gestor de ficheiros predefinido\nbrowseInFinder=Navega no localizador\ncopy=Copia\npaste=Cola\ncopyLocation=Copia a localização\nabsolutePaths=Caminhos absolutos\nabsoluteLinkPaths=Caminhos de ligação absolutos\nabsolutePathsQuoted=Caminhos absolutos entre aspas\nfileNames=Nomes de ficheiros\nlinkFileNames=Liga nomes de ficheiros\nfileNamesQuoted=Nomes de ficheiros (Citado)\ndeleteFile=Elimina $FILE$\neditWithEditor=Edita com $EDITOR$\nfollowLink=Segue a ligação\ngoForward=Avança\nshowDetails=Mostra os detalhes\nshowDetailsDescription=Mostra o rastreio da pilha de erros\nopenFileWith=Abre com ...\nopenWithDefaultApplication=Abre com a aplicação predefinida\nrename=Renomear\nrun=Executa\nopenInTerminal=Abre no terminal\nfile=Ficheiro\ndirectory=Diretório\nsymbolicLink=Ligação simbólica\ndesktopEnvironment.displayName=Ambiente de trabalho\ndesktopEnvironment.displayDescription=Cria uma configuração reutilizável do ambiente de trabalho remoto\ndesktopHost=Anfitrião de ambiente de trabalho\ndesktopHostDescription=A ligação ao ambiente de trabalho a utilizar como base\ndesktopShellDialect=Dialeto de shell\ndesktopShellDialectDescription=O dialeto da shell a utilizar para executar scripts e aplicações\ndesktopSnippets=Trechos de script\ndesktopSnippetsDescription=Lista de snippets de script reutilizáveis para executar primeiro\ndesktopInitScript=Script de inicialização\ndesktopInitScriptDescription=Comandos de inicialização específicos para este ambiente\ndesktopTerminal=Aplicação terminal\ndesktopTerminalDescription=O terminal a utilizar no ambiente de trabalho para iniciar scripts\ndesktopApplication.displayName=Aplicação de ambiente de trabalho\ndesktopApplication.displayDescription=Executa uma aplicação num ambiente de trabalho remoto\ndesktopBase=Computador de secretária\ndesktopBaseDescription=O ambiente de trabalho onde executar esta aplicação\ndesktopEnvironmentBase=Ambiente de trabalho\ndesktopEnvironmentBaseDescription=O ambiente de trabalho para executar esta aplicação\ndesktopApplicationPath=Caminho da aplicação\ndesktopApplicationPathDescription=O caminho do executável a executar\ndesktopApplicationArguments=Argumentos\ndesktopApplicationArgumentsDescription=Os argumentos opcionais a transmitir à aplicação\ndesktopCommand.displayName=Comando do ambiente de trabalho\ndesktopCommand.displayDescription=Executa um comando num ambiente de trabalho remoto\ndesktopCommandScript=Comandos\ndesktopCommandScriptDescription=Os comandos a executar no ambiente\nservice.displayName=Serviço\nservice.displayDescription=Encaminhar um serviço remoto para a tua máquina local\nserviceLocalPort=Porta local explícita\nserviceLocalPortDescription=A porta local para a qual reencaminhar, caso contrário é utilizada uma porta aleatória\nserviceRemotePort=Porta remota\nserviceRemotePortDescription=A porta em que o serviço está a ser executado\nserviceHost=Anfitrião de serviço\nserviceHostDescription=O anfitrião em que o serviço está a ser executado\nopenWebsite=Abre o sítio Web\ncustomServiceGroup.displayName=Grupo de serviços\ncustomServiceGroup.displayDescription=Agrupa vários serviços numa categoria\ninitScript=Script de inicialização - Executa na inicialização do shell\nshellScript=Script de sessão de shell - Torna o script disponível para ser executado durante uma sessão de shell\nrunnableScript=Script executável - Permite que o script seja executado diretamente a partir do hub de ligação\nfileScript=Script de ficheiro - Permite que o script seja chamado para ficheiros selecionados no navegador de ficheiros\nrunScript=Executa o script\ncopyUrl=Copia o URL\nfixedServiceGroup.displayName=Grupo de serviços\nfixedServiceGroup.displayDescription=Lista os serviços disponíveis num sistema\nmappedService.displayName=Serviço\nmappedService.displayDescription=Interage com um serviço exposto por um contentor\ncustomService.displayName=Serviço\ncustomService.displayDescription=Abre automaticamente ou faz o túnel de uma porta de serviço remoto na tua máquina local\nfixedService.displayName=Serviço\nfixedService.displayDescription=Utiliza um serviço predefinido\nnoServices=Não há serviços disponíveis\nhasServices=$COUNT$ serviços disponíveis\nhasService=$COUNT$ serviço disponível\nnoConnections=Não há ligações disponíveis\nhasConnections=$COUNT$ ligações disponíveis\nhasConnection=$COUNT$ ligação disponível\nopenHttp=Abre o serviço HTTP\nopenHttps=Abre o serviço HTTPS\nnoScriptsAvailable=Não há scripts habilitados e compatíveis disponíveis\nscriptsDisabled=Scripts desactivados\nchangeIcon=Altera o ícone\ninit=Init\nshell=Shell\nhub=Hub\nscript=guião\ngenericScript=Genérico\ngradleTasks=Tarefas Gradle\nrunTask=Executa uma tarefa\narchiveName=Nome do arquivo\ncompress=Comprimir\ncompressContents=Comprimir conteúdos\nuntarHere=Untar aqui\nuntarDirectory=Untar para $DIR$\nunzipDirectory=Descompacta para $DIR$\nunzipHere=Descompacta aqui\nrequiresRestart=Requer um reinício para ser aplicado.\ndownload=Descarrega\nservicePath=Caminho de serviço\nservicePathDescription=O subcaminho opcional ao abrir o URL num browser\nactive=Ativa\ninactive=Inativo\nstarting=Iniciar\nremotePort=Porta remota\nremotePortNumber=Porta remota $PORT$\nuserIdentity=Identidade pessoal\nglobalIdentity=Identidade global\nidentityChoice=Identidade do utilizador\nidentityChoiceDescription=Escolhe uma identidade predefinida ou especifica os detalhes de início de sessão apenas para esta ligação\ndefineNewIdentityOrSelect=Introduzir um novo texto ou selecionar um existente\nlocalIdentity.displayName=Identidade local\nlocalIdentity.displayDescription=Cria uma identidade reutilizável para este ambiente de trabalho local\nsyncedIdentity.displayName=Identidade sincronizada\nsyncedIdentity.displayDescription=Cria uma identidade reutilizável que é sincronizada entre sistemas\nlocalIdentity=Identidade local\nkeyNotSynced=O ficheiro chave ainda não está sincronizado com o repositório git. Utiliza o botão adicionar ao git para o ficheiro chave para o adicionar.\nusernameDescription=O nome de utilizador para iniciar sessão\nidentity.displayName=Identifica-te\nidentity.displayDescription=Cria uma identidade reutilizável para ligações\nlocal=Local\nshared=Global\nuserDescription=O nome de utilizador ou identidade predefinida para iniciar sessão como\nidentityAccessLevel=Nível de acesso\nidentityPerUser=Acesso à identidade pessoal\nidentityPerUserDescription=Restringe o acesso a esta identidade e às ligações associadas apenas ao teu utilizador do vault\nidentityPerUserDisabled=Acesso à identidade pessoal (desativado)\nidentityPerUserDisabledDescription=Restringe o acesso a esta identidade e às ligações associadas apenas ao teu utilizador do cofre (requer que a equipa seja configurada)\nidentityPerGroup=Acesso à identidade só de grupo\nidentityPerGroupDescription=Restringe o acesso a esta identidade e às suas ligações associadas apenas a este grupo de abóbadas\nlibrary=Biblioteca\nlocation=Localização\nkeyAuthentication=Autenticação baseada em chaves\nkeyAuthenticationDescription=O método de autenticação a utilizar se for necessária uma autenticação baseada em chaves\nlocationDescription=O caminho do ficheiro da tua chave privada correspondente\nkeyFile=Ficheiro de chave local\nkeyPassword=Palavra-passe\nkey=Chave\nyubikeyPiv=Yubikey PIV\npageant=Concurso\ngpgAgent=Agente GPG\ncustomPkcs11Library=Biblioteca PKCS#11 personalizada\nsshAgent=Agente OpenSSH\nnone=Não tens\nindex=Índice ...\notherExternal=Outro agente externo\nsync=Sincroniza\nvaultSync=Sincronização de cofre\ncustomUsername=Nome de utilizador\ncustomUsernameDescription=O utilizador alternativo opcional para iniciar sessão como\ncustomUsernamePassword=Palavra-passe\ncustomUsernamePasswordDescription=A palavra-passe do utilizador a utilizar quando é necessária a autenticação sudo\nshowInternalPods=Mostra pods internos\nshowAllNamespaces=Mostra todos os namespaces\nshowInternalContainers=Mostra os contentores internos\nrefresh=Actualiza\nvmwareGui=Inicia a GUI\nmonitorVm=Monitor VM\naddCluster=Adiciona um cluster ...\nshowNonRunningInstances=Mostra instâncias não em execução\nvmwareGuiDescription=Se inicia uma máquina virtual em segundo plano ou numa janela.\nvmwareEncryptionPassword=Palavra-passe de encriptação\nvmwareEncryptionPasswordDescription=A palavra-passe opcional utilizada para encriptar a VM.\nvmPasswordDescription=A palavra-passe necessária para o utilizador convidado.\nvmPassword=Palavra-passe do utilizador\nvmUser=Utilizador convidado\nrunTempContainer=Executa um contentor temporário\nvmUserDescription=O nome de utilizador do teu principal utilizador convidado\ndockerTempRunAlertTitle=Executa um contentor temporário\ndockerTempRunAlertHeader=Executa um processo shell num contentor temporário que será automaticamente removido quando for parado.\nimageName=Nome da imagem\nimageNameDescription=O identificador de imagem de contentor a utilizar\ncontainerName=Nome do contentor\ncontainerNameDescription=O nome personalizado opcional do contentor\nvm=Máquina virtual\nvmDescription=O ficheiro de configuração associado.\nvmwareScan=Hipervisores de ambiente de trabalho VMware\nvmwareMachine.displayName=Máquina virtual VMware\nvmwareMachine.displayDescription=Liga-te a uma máquina virtual através de SSH\nvmwareInstallation.displayName=Instalação do hipervisor de ambiente de trabalho VMware\nvmwareInstallation.displayDescription=Interage com as VMs instaladas através do seu CLI\nstart=Começa\nstop=Pára\npause=Pausa\nrdpTunnelHost=Anfitrião de destino\nrdpTunnelHostDescription=A ligação SSH para encapsular a ligação RDP\nrdpTunnelUsername=Nome de utilizador\nrdpTunnelUsernameDescription=O utilizador personalizado para iniciar sessão, utiliza o utilizador SSH se ficar vazio\nrdpFileLocation=Localização do ficheiro\nrdpFileLocationDescription=O caminho do ficheiro .rdp\nrdpPasswordAuthentication=Autenticação de palavra-passe\nrdpFiles=Ficheiros RDP\nrdpPasswordAuthenticationDescription=A palavra-passe a preencher ou a copiar para a área de transferência, consoante o suporte do cliente\nrdpFile.displayName=Ficheiro RDP\nrdpFile.displayDescription=Liga-se a um sistema através de um ficheiro .rdp existente\nrequiredSshServerAlertTitle=Configura o servidor SSH\nrequiredSshServerAlertHeader=Não é possível encontrar um servidor SSH instalado na VM.\nrequiredSshServerAlertContent=Para se conectar à VM, o XPipe está procurando um servidor SSH em execução, mas nenhum servidor SSH disponível foi detectado para a VM.\ncomputerName=Nome do computador\npssComputerNameDescription=O nome do computador ao qual te deves ligar\ncredentialUser=Utilizador de credenciais\ncredentialUserDescription=O utilizador para iniciar sessão como.\ncredentialPassword=Palavra-passe de credencial\ncredentialPasswordDescription=A palavra-passe do utilizador.\nsshConfig=Ficheiros de configuração SSH\nautostart=Liga automaticamente no arranque do XPipe\nacceptHostKey=Aceita a chave do anfitrião\nmodifyHostKeyPermissions=Modifica as permissões da chave do anfitrião\nattachContainer=Anexa\ncontainerLogs=Mostra os registos\nopenSftpClient=Abre num cliente SFTP externo\nopenTermius=Abre em Termius\nshowInternalInstances=Mostra instâncias internas\neditPod=Editar pod\nacceptHostKeyDescription=Confia na nova chave de anfitrião e continua\nmodifyHostKeyPermissionsDescription=Tenta remover as permissões do ficheiro original para que o OpenSSH fique satisfeito\npsSession.displayName=Sessão remota do PowerShell\npsSession.displayDescription=Liga-te através de New-PSSession e Enter-PSSession\nsshLocalTunnel.displayName=Túnel SSH local\nsshLocalTunnel.displayDescription=Estabelece um túnel SSH para um host remoto\nsshRemoteTunnel.displayName=Túnel SSH remoto\nsshRemoteTunnel.displayDescription=Estabelece um túnel SSH reverso a partir de um host remoto\nsshDynamicTunnel.displayName=Túnel SSH dinâmico\nsshDynamicTunnel.displayDescription=Estabelece um proxy SOCKS através de uma ligação SSH\nshellEnvironmentGroup.displayName=Ambientes de shell\nshellEnvironmentGroup.displayDescription=Ambientes de shell\nshellEnvironment.displayName=Ambiente de shell\nshellEnvironment.displayDescription=Cria um ambiente de arranque de shell personalizado\nshellEnvironment.informationFormat=$TYPE$ ambiente\nshellEnvironment.elevatedInformationFormat=$ELEVATION$ $TYPE$ ambiente\nenvironmentConnectionDescription=A ligação de base para criar um ambiente para\nenvironmentScriptDescription=O script de inicialização personalizado opcional a ser executado no shell\nenvironmentSnippets=Scripts de shell\ncommandSnippetsDescription=Os scripts de shell predefinidos opcionais a serem executados primeiro\nenvironmentSnippetsDescription=Os scripts de shell predefinidos opcionais a serem executados na inicialização\nshellTypeDescription=O tipo de shell explícito a lançar\noriginPort=Porta de origem\noriginAddress=Endereço de origem\nremoteAddress=Endereço remoto\nremoteSourceAddress=Endereço de origem remota\nremoteSourcePort=Porta de origem remota\noriginDestinationPort=Porta de destino de origem\noriginDestinationAddress=Endereço de destino de origem\norigin=Origem\nremoteHost=Anfitrião remoto\naddress=Aborda\nproxmox.displayName=Proxmox\nproxmox.displayDescription=Liga-te a sistemas num ambiente virtual Proxmox\nproxmoxVm.displayName=Proxmox VM\nproxmoxVm.displayDescription=Conecta-se a uma máquina virtual em um Proxmox VE via SSH\nproxmoxContainer.displayName=Contentor Proxmox\nproxmoxContainer.displayDescription=Liga-te a um contentor num Proxmox VE\nsshDynamicTunnel.hostDescription=O sistema a utilizar como proxy SOCKS\nsshDynamicTunnel.bindingDescription=A que endereços ligar o túnel\nsshRemoteTunnel.hostDescription=O sistema a partir do qual inicia o túnel remoto para a origem\nsshRemoteTunnel.bindingDescription=A que endereços ligar o túnel\nsshLocalTunnel.hostDescription=O sistema para abrir o túnel para\nsshLocalTunnel.bindingDescription=A que endereços ligar o túnel\nsshLocalTunnel.localAddressDescription=O endereço local a associar\nsshLocalTunnel.remoteAddressDescription=O endereço remoto a associar\ncmd.displayName=Comanda\ncmd.displayDescription=Executa um comando arbitrário num sistema\nk8sPod.displayName=Pod de Kubernetes\nk8sPod.displayDescription=Liga-te a um pod e aos seus contentores através do kubectl\nk8sContainer.displayName=Contentor Kubernetes\nk8sContainer.displayDescription=Abre uma shell para um contentor\nk8sCluster.displayName=Cluster Kubernetes\nk8sCluster.displayDescription=Liga-te a um cluster e aos seus pods através do kubectl\nsshTunnelGroup.displayName=Túneis SSH\nsshTunnelGroup.displayCategory=Todos os tipos de túneis SSH\nlocal.displayName=Máquina local\nlocal.displayDescription=O shell da máquina local\ncygwin=Cygwin\nmsys2=MSYS2\ngitWindows=Git para Windows\ngitForWindows.displayName=Git para Windows\ngitForWindows.displayDescription=Aceder ao teu ambiente local do Git For Windows\nmsys2.displayName=MSYS2\nmsys2.displayDescription=Acesso a shells do teu ambiente MSYS2\ncygwin.displayName=Cygwin\ncygwin.displayDescription=Aceder a shells do teu ambiente Cygwin\nnamespace=Espaço de nome\ngitVaultIdentityStrategy=Identidade SSH do Git\ngitVaultIdentityStrategyDescription=Se optaste por utilizar um URL git SSH como remoto e o teu repositório remoto requer uma identidade SSH, define esta opção.\\n\\nCaso tenhas fornecido um url HTTP, podes ignorar esta opção.\ndockerContainers=Contentores Docker\ndockerCmd.displayName=cliente CLI do docker\ndockerCmd.displayDescription=Acede a contentores Docker através do cliente docker CLI\nwslCmd.displayName=Instalação WSL\nwslCmd.displayDescription=Acede a instâncias WSL através do cliente wsl CLI\nk8sCmd.displayName=cliente kubectl\nk8sCmd.displayDescription=Aceder a clusters Kubernetes através de kubectl\nk8sClusters=Clusters Kubernetes\nshells=Conchas disponíveis\ninspectContainer=Inspecciona\ninspectContext=Inspecciona\nk8sClusterNameDescription=O nome do contexto em que o cluster se encontra.\npod=Pod\npodName=Nome do pod\nk8sClusterContext=Contexto\nk8sClusterContextDescription=O nome do contexto em que o cluster se encontra\nk8sClusterNamespace=Espaço de nome\nk8sClusterNamespaceDescription=O espaço de nomes personalizado ou o espaço de nomes predefinido, se estiver vazio\nk8sConfigLocation=Ficheiro de configuração\nk8sConfigLocationDescription=O ficheiro kubeconfig personalizado ou o ficheiro predefinido se for deixado vazio\ninspectPod=Inspecciona\nshowAllContainers=Mostra contentores não em execução\nshowAllPods=Mostra pods não em execução\nk8sPodHostDescription=O anfitrião no qual o pod está localizado\nk8sContainerDescription=O nome do contentor Kubernetes\nk8sPodDescription=O nome do pod Kubernetes\npodDescription=O pod no qual o contentor está localizado\nk8sClusterHostDescription=O anfitrião através do qual o cluster deve ser acedido. Tem de ter o kubectl instalado e configurado para poder aceder ao cluster.\nconnection=Ligação\nshellCommand.displayName=Comando shell personalizado\nshellCommand.displayDescription=Abre uma shell padrão através de um comando personalizado\nssh.displayName=Ligação SSH\nssh.displayDescription=Liga-te a um sistema remoto através do cliente de linha de comandos SSH\nsshConfig.displayName=Ficheiro de configuração SSH\nsshConfig.displayDescription=Liga-te a anfitriões definidos num ficheiro de configuração SSH\nsshConfigHost.displayName=Anfitrião do ficheiro de configuração SSH\nsshConfigHost.displayDescription=Liga-te a um anfitrião definido num ficheiro de configuração SSH\nsshConfigHost.password=Palavra-passe\nsshConfigHost.passwordDescription=Fornece a palavra-passe opcional para o início de sessão do utilizador.\nsshConfigHost.identityPassphrase=Frase-chave\nsshConfigHost.identityPassphraseDescription=Fornece a frase-passe opcional para a tua chave.\nshellCommand.hostDescription=O anfitrião para executar o comando em\nshellCommand.commandDescription=O comando que abre uma shell\ncommandType=Tipo de comando\ncommandTypeDescription=Como executar o comando\ncommandDescription=Os comandos personalizados a executar no anfitrião\ncommandHostDescription=O anfitrião para executar o comando\ncommandDataFlowDescription=Como este comando lida com a entrada e saída\ncommandElevationDescription=Executa este comando com permissões elevadas\ncommandShellTypeDescription=A shell a utilizar para este comando\nlimitedSystem=Este é um sistema limitado ou incorporado\nlimitedSystemDescription=Não tentes identificar o tipo de shell, necessário para sistemas incorporados limitados ou dispositivos IOT\nsshForwardX11=Encaminha o X11\nsshForwardX11Description=Ativa o reencaminhamento X11 para a ligação\ncustomAgent=Agente personalizado\nidentityAgent=Agente de identidade\nssh.proxyDescription=O anfitrião proxy opcional a utilizar quando estabelece a ligação SSH. Tem de ter um cliente ssh instalado.\nusage=Usa\nwslHostDescription=O host no qual a instância WSL está localizada. Deve ter o wsl instalado.\nwslDistributionDescription=O nome da instância WSL\nwslUsernameDescription=O nome de utilizador explícito para iniciar sessão. Se não for especificado, será utilizado o nome de utilizador predefinido.\nwslPasswordDescription=A palavra-passe do utilizador que pode ser utilizada para comandos sudo.\ndockerHostDescription=O host no qual o contêiner do docker está localizado. Deve ter o docker instalado.\ndockerContainerDescription=O nome do contentor do docker\nlocalMachine=Máquina local\nrootScan=Ambiente de shell Sudo\nloginEnvironmentScan=Ambiente de início de sessão personalizado\nk8sScan=Cluster Kubernetes\noptions=Opções\ndockerRunningScan=Executa contentores docker\ndockerAllScan=Todos os contentores do docker\nwslScan=Instâncias WSL\nsshScan=Ligações de configuração SSH\nrunAsUser=Executa como utilizador\nrunAsUserDescription=Inicia este ambiente de shell como um utilizador diferente\ndefault=Por defeito\nadministrator=Administrador\nwslHost=Anfitrião WSL\ntimeout=Tempo limite\ninstallLocation=Local de instalação\ninstallLocationDescription=O local onde o teu ambiente $NAME$ está instalado\nwsl.displayName=Subsistema Windows para Linux\nwsl.displayDescription=Liga-te a uma instância WSL em execução no Windows\ndocker.displayName=Contentor Docker\ndocker.displayDescription=Liga-te a um contentor docker\nport=Porta\nuser=Utilizador\npassword=Palavra-passe\nmethod=Método\nuri=URL\nproxy=Proxy\ndistribution=Distribuição\nusername=Nome de utilizador\nshellType=Tipo de shell\nbrowseFile=Procurar ficheiro\nopenShell=Abre a shell no terminal\nopenCommand=Executa o comando no terminal\neditFile=Editar ficheiro\ndescription=Descrição\nfurtherCustomization=Personalização adicional\nfurtherCustomizationDescription=Para mais opções de configuração, utiliza os ficheiros de configuração ssh\nbrowse=Procura\nconfigHost=Apresenta\nconfigHostDescription=O host no qual a configuração está localizada\nconfigLocation=Localização da configuração\nconfigLocationDescription=O caminho do ficheiro de configuração\ngateway=Gateway\ngatewayDescription=O gateway opcional a utilizar quando estabelece uma ligação\nconnectionInformation=Informações de ligação\nconnectionInformationDescription=A que sistema te deves ligar\npasswordAuthentication=Autenticação de palavra-passe\npasswordAuthenticationDescription=A palavra-passe opcional a utilizar para autenticar\nsshConfigString.displayName=Ligação SSH baseada em configurações\nsshConfigString.displayDescription=Cria uma ligação SSH totalmente personalizada no formato de configuração SSH\nsshConfigStringContent=Configuração\nsshConfigStringContentDescription=Opções SSH para a ligação no formato de configuração OpenSSH\nvnc.displayName=Ligação VNC através de SSH\nvnc.displayDescription=Abre uma sessão VNC através de uma ligação em túnel\nbinding=Encadernação\nvncPortDescription=A porta em que o servidor VNC está escutando\nrdpPortDescription=A porta em que o servidor RDP está a escutar\nvncUsername=Nome de utilizador\nvncUsernameDescription=O nome de utilizador opcional do VNC\nvncPassword=Palavra-passe\nvncPasswordDescription=A palavra-passe VNC\nx11WslInstance=Instância X11 Forward WSL\nx11WslInstanceDescription=A distribuição local do Subsistema Windows para Linux a utilizar como um servidor X11 quando utiliza o reencaminhamento X11 numa ligação SSH. Esta distribuição deve ser uma distribuição WSL2.\nopenAsRoot=Abre como root\nopenInWSL=Abre em WSL\nlaunch=Lança\nsshTrustKeyContent=A chave do anfitrião não é conhecida e activaste a verificação manual da chave do anfitrião. $CONTENT$\nsshTrustKeyTitle=Chave de anfitrião desconhecida\nrdpTunnel.displayName=Ligação RDP através de SSH\nrdpTunnel.displayDescription=Liga-se via RDP através de uma ligação em túnel\nrdpEnableDesktopIntegration=Permite a integração do ambiente de trabalho\nrdpEnableDesktopIntegrationDescription=Executa aplicações remotas assumindo que a lista de permissões do RDP permite isso\nrdpSetupAdminTitle=Necessita de configuração RDP\nrdpSetupAllowTitle=Aplicação remota RDP\nrdpSetupAllowContent=Iniciar aplicações remotas diretamente não é atualmente permitido neste sistema. Queres activá-lo? Isto permitir-te-á executar as tuas aplicações remotas diretamente a partir do XPipe, desactivando a lista de permissões para aplicações remotas RDP.\nrdpServerEnableTitle=Servidor RDP\nrdpServerEnableContent=O servidor RDP está desativado no sistema de destino. Pretende activá-lo no registo de modo a permitir ligações RDP remotas?\nrdp=RDP\nrdpScan=Túnel RDP sobre SSH\nwslX11SetupTitle=Configuração do WSL X11\nwslX11SetupContent=O XPipe pode utilizar a tua distribuição WSL local para atuar como um servidor de visualização X11. Gostarias de configurar o X11 em $DIST$? Isto irá instalar os pacotes básicos do X11 na distribuição WSL e pode demorar um pouco. Podes também alterar a distribuição que é usada no menu de definições.\ncommand=Comanda\ncommandGroup=Grupo de comandos\nvncSystem=Sistema de destino VNC\nvncSystemDescription=O sistema real com o qual interage. Geralmente é o mesmo que o host do túnel\nvncHost=Anfitrião VNC de destino\nvncHostDescription=O sistema no qual o servidor VNC está sendo executado\nvncDirectHost=Apresenta\nvncDirectHostDescription=A entrada do host ou o endereço manual do servidor no qual o servidor VNC está sendo executado\nrdpDirectHost=Apresenta\nrdpDirectHostDescription=A entrada do anfitrião ou o endereço manual do servidor no qual o servidor RDP está a ser executado\ngitVaultTitle=Cofre do Git\ngitVaultForcePushContent=Queres forçar o push para o repositório remoto? Isto irá substituir completamente todos os conteúdos do repositório remoto pelo teu repositório local, incluindo o histórico.\ngitVaultOverwriteLocalContent=Queres substituir as alterações do teu cofre local? Isto irá aplicar todas as alterações remotas ao teu repositório local.\nrdpSimple.displayName=Ligação direta RDP\nrdpSimple.displayDescription=Liga-te a um anfitrião através de RDP\nrdpUsername=Nome de utilizador\nrdpUsernameDescription=O utilizador com o qual deves iniciar sessão. Pode incluir um prefixo de domínio\naddressDescription=Onde te deves ligar\nrdpAdditionalOptions=Opções adicionais de RDP\nrdpAdditionalOptionsDescription=Opções RDP brutas a incluir, formatadas da mesma forma que nos ficheiros .rdp\nproxmoxVncConfirmTitle=Acesso VNC\nproxmoxVncConfirmContent=Queres ativar o acesso VNC para a VM? Isso habilitará o acesso direto do cliente VNC no arquivo de configuração da VM e reiniciará a máquina virtual.\ndockerContext.displayName=Contexto do Docker\ndockerContext.displayDescription=Interage com contentores localizados num contexto específico\nvmActions=Acções VM\ndockerContextActions=Acções de contexto\nk8sPodActions=Acções de pod\nopenVnc=Ativar o acesso VNC\naddVnc=Adiciona uma ligação VNC\ncommandGroup.displayName=Grupo de comandos\ncommandGroup.displayDescription=Agrupa os comandos disponíveis para um sistema\nserial.displayName=Ligação em série\nserial.displayDescription=Abre uma ligação de série num terminal\nserialPort=Porta de série\nserialPortDescription=A porta de série / dispositivo a ligar\nbaudRate=Taxa de transmissão\ndataBits=Bits de dados\nstopBits=Bits de paragem\nparity=Paridade\nflowControlWindow=Controlo de fluxo\nserialImplementation=Implementação em série\nserialImplementationDescription=A ferramenta a utilizar para ligar à porta série\nserialHost=Apresenta\nserialHostDescription=O sistema para aceder à porta série em\nserialPortConfiguration=Configuração da porta de série\nserialPortConfigurationDescription=Parâmetros de configuração do dispositivo de série ligado\nserialInformation=Informação de série\nopenXShell=Abre no XShell\ntsh.displayName=Teletransporte\ntsh.displayDescription=Liga-te aos teus nós de teletransporte via tsh\ntshNode.displayName=Nó de teletransporte\ntshNode.displayDescription=Liga-te a um nó de teletransporte num cluster\nteleportCluster=Cluster\nteleportClusterDescription=O cluster em que o nó se encontra\nteleportProxy=Proxy\nteleportProxyDescription=O servidor proxy utilizado para ligar ao nó\nteleportHost=Apresenta\nteleportHostDescription=O nome do anfitrião do nó\nteleportUser=Utilizador\nteleportUserDescription=O utilizador para iniciar sessão como\nlogin=Acede\nhyperVInstall.displayName=Hyper-V\nhyperVInstall.displayDescription=Conecta-se a VMs gerenciadas pelo Hyper-V\nhyperVVm.displayName=VM Hyper-V\nhyperVVm.displayDescription=Liga-te a uma VM Hyper-V através de SSH ou PSSession\ntrustHost=Anfitrião de confiança\ntrustHostDescription=Adiciona ComputerName à lista de anfitriões de confiança\ncopyIp=Copia o IP\nvncDirect.displayName=Ligação VNC direta\nvncDirect.displayDescription=Liga-te diretamente a um sistema através do VNC\neditConfiguration=Edita a configuração\nviewInDashboard=Vê no painel de controlo\nsetDefault=Definir predefinição\nremoveDefault=Remove a predefinição\nconnectAsOtherUser=Liga-te como outro utilizador\nprovideUsername=Fornece um nome de utilizador alternativo para iniciar sessão\nvmIdentity=Identidade do convidado\nvmIdentityDescription=O método de autenticação de identidade SSH a utilizar para estabelecer ligação, se necessário\nvmPort=Porta\nvmPortDescription=A porta a que te deves ligar via SSH\nforwardAgent=Agente de encaminhamento\nforwardAgentDescription=Torna as identidades do agente SSH disponíveis no sistema remoto\nvirshUri=URI\nvirshUriDescription=O URI do hipervisor, também são suportados aliases\nvirshDomain.displayName=domínio libvirt\nvirshDomain.displayDescription=Liga-te a um domínio libvirt\nvirshHypervisor.displayName=hipervisor libvirt\nvirshHypervisor.displayDescription=Liga-te a um controlador de hipervisor suportado pela libvirt\nvirshInstall.displayName=cliente de linha de comando libvirt\nvirshInstall.displayDescription=Conecta-se a todos os hipervisores libvirt disponíveis via virsh\naddHypervisor=Adiciona um hipervisor\ninteractiveTerminal=Terminal interativo\neditDomain=Editar domínio\nlibvirt=domínios libvirt\ncustomIp=IP personalizado\ncustomIpDescription=Substitui a deteção de IP da VM local predefinida se utilizares uma rede avançada\nautomaticallyDetect=Detecta automaticamente\nuserAddDialogTitle=Criação de utilizadores\ngroupAddDialogTitle=Criação de grupos\npassphrase=Palavra-passe\nrepeatPassphrase=Repete a frase-chave\ngroupSecret=Segredo de grupo\nrepeatGroupSecret=Repete o segredo do grupo\nvaultGroup=Grupo de cofres\nloginAlertTitle=Início de sessão necessário\nloginAlertHeader=Desbloqueia o cofre para aceder às tuas ligações pessoais\nvaultUser=Utilizador do cofre\nme=Eu\naddGroup=Adiciona um grupo ...\naddGroupDescription=Cria um novo grupo para esta caixa-forte\naddUser=Adicionar utilizador ...\naddUserDescription=Cria um novo utilizador para esta caixa-forte\nskip=Salta\nuserChangePasswordAlertTitle=Alteração da palavra-passe\ngroupChangeSecretAlertTitle=Mudança de segredo\ndocs=Documentação\nlxd.displayName=Contentor LXD\nlxd.displayDescription=Liga-te a um contentor LXD através do lxc\nlxdCmd.displayName=Cliente CLI LXD\nlxdCmd.displayDescription=Acede aos contentores LXD através do cliente lxc CLI\npodman.displayName=Contentor Podman\npodman.displayDescription=Liga-te a um contentor Podman\nincusInstall.displayName=Gestor de máquinas Incus\nincusInstall.displayDescription=Acede aos contentores da incus através do cliente CLI da incus\nincusContainer.displayName=Contentor Incus\nincusContainer.displayDescription=Liga-te a um contentor incus\npodmanCmd.displayName=Podman CLI\npodmanCmd.displayDescription=Acede aos contentores Podman através do cliente CLI\nlxdHostDescription=O host no qual o contêiner LXD está localizado. Deve ter o lxc instalado.\nlxdContainerDescription=O nome do contentor LXD\npodmanContainers=Contentores Podman\nlxdContainers=Contentores LXD\nincusContainers=Contentores Incus\ncontainer=Contentor\nhost=Apresenta\ncontainerActions=Acções de contentor\nserialConsole=Consola de série\neditRunConfiguration=Edita a configuração de execução\ncommunityDescription=Uma ferramenta de ligação perfeita para os teus casos de utilização pessoal.\nupgradeDescription=Gestão profissional das ligações para toda a tua infraestrutura de servidores.\ndiscoverPlans=Descobre as opções de atualização\nextendProfessional=Actualiza para as funcionalidades profissionais mais recentes\ncommunityItem1=Ligações ilimitadas a sistemas e ferramentas não comerciais\ncommunityItem2=Integração perfeita com os teus terminais e editores instalados\ncommunityItem3=Navegador de ficheiros remoto com todas as funcionalidades\ncommunityItem4=Poderoso sistema de scripting para todos os shells\ncommunityItem5=Integração do Git para sincronização e partilha de informações de ligação\nupgradeItem1=Inclui todas as funcionalidades da edição comunitária\nupgradeItem2=O plano Homelab suporta hipervisores ilimitados e funcionalidades SSH avançadas\nupgradeItem3=O plano Professional suporta adicionalmente sistemas operativos e ferramentas empresariais\nupgradeItem4=O plano Enterprise inclui total flexibilidade para o teu caso de utilização individual\nupgrade=Actualiza-te\nupgradeTitle=Planos disponíveis\nstatus=Estado\ntype=Digita\nlicenseAlertTitle=Licença necessária\nuseCommunity=Continua com a comunidade\npreviewDescription=Experimenta as novas funcionalidades durante algumas semanas após o lançamento.\ntryPreview=Ativar pré-visualização\npreviewItem1=Acesso total às funcionalidades profissionais recém-lançadas durante 2 semanas após o lançamento\npreviewItem2=Experimenta novas funcionalidades sem qualquer compromisso\nlicensedTo=Licenciado para\nemail=Endereço de correio eletrónico\napply=Aplica\nclear=Limpar\nactivate=Ativar\nvalidUntil=Válido até\nlicenseActivated=Licença activada\nrestart=Reinicia\nlockVault=Fecha o cofre\nrestartApp=Reinicia o XPipe\nfree=Gratuito\nupgradeInfo=Podes encontrar informações sobre a atualização para uma licença abaixo.\nupgradeInfoPreview=Podes encontrar informações sobre a atualização para uma licença abaixo ou experimentar a pré-visualização.\nenterLicenseKey=Introduzir a chave de licença para atualizar\nisOnlySupported=só é suportado com, pelo menos, uma licença $TYPE$\nareOnlySupported=só são suportados com, pelo menos, uma licença $TYPE$\nlegacyLicense=Esta licença inclui apenas as novas funcionalidades Professional lançadas no prazo de um ano após a compra.\npreviewExpiredLicense=Esta funcionalidade esteve recentemente disponível gratuitamente numa pré-visualização, mas este período já expirou.\nopenApiDocs=Documentação API\nopenApiDocsDescription=A documentação da API HTTP está disponível online, incluindo uma especificação OpenAPI .yaml. Podes abri-la no teu browser da Web ou no teu cliente HTTP preferido.\nopenApiDocsButton=Abre documentos\npythonApi=API Python\npersonalConnection=Esta ligação e todos os seus filhos só estão disponíveis para o teu utilizador, uma vez que dependem de uma identidade pessoal.\ndeveloperPrintInitFiles=Imprime a execução do ficheiro de inicialização\ndeveloperPrintInitFilesDescription=Imprime todos os scripts shell init que são executados quando um terminal é iniciado.\ndeveloperShowSensitiveCommands=Regista comandos sensíveis\ndeveloperShowSensitiveCommandsDescription=Inclui comandos sensíveis na saída do registo para depuração.\ncheckingForUpdates=Verificação de actualizações\ncheckingForUpdatesDescription=Obter informações sobre a última versão\ndownloadingUpdate=Recuperar a versão (Versão $VERSION$)\ndownloadingUpdateDescription=Descarregar o pacote de lançamento\nupdateNag=Não actualizas o XPipe há algum tempo. Podes estar a perder as novas funcionalidades e correcções das versões mais recentes.\nupdateNagTitle=Lembrete de atualização\nupdateNagButton=Ver lançamentos\nrefreshServices=Atualizar serviços\nserviceProtocolType=Tipo de protocolo de serviço\nserviceProtocolTypeDescription=Controla a forma de abrir o serviço\nserviceCommand=O comando a executar quando o serviço estiver ativo\nserviceCommandDescription=O marcador de posição $PORT será substituído pela porta local com túnel real\nvalue=Valoriza\nshowAdvancedOptions=Mostra opções avançadas\nsshAdditionalConfigOptions=Opções de configuração adicionais\nremoteFileManager=Gestor de ficheiros remoto\nclearUserData=Eliminar dados do utilizador\nclearUserDataDescription=Elimina todos os dados de configuração do utilizador, incluindo as ligações\nclearUserDataTitle=Eliminação de dados do utilizador\nclearUserDataContent=Isto irá eliminar todos os dados do utilizador local para o xpipe e reiniciar. Se te preocupas com as tuas ligações, certifica-te de que as sincronizas primeiro com um repositório git.\nundefined=Não definido\ncopyAddress=Copia o endereço\nnetbirdDeviceScan=Ligações Netbird\nnetbirdId=Chave pública de par\nnetbirdIdDescription=O ID da chave pública interna do Netbird do par\ntailscaleDeviceScan=Ligações Tailscale\ntailscaleInstall.displayName=Instalação do Tailscale\ntailscaleInstall.displayDescription=Liga-te a dispositivos na tua rede de cauda através de SSH\ntailscaleDevice.displayName=Dispositivo Tailscale\ntailscaleDevice.displayDescription=Liga-te a um dispositivo na tua rede de cauda através de SSH\ntailscaleId=ID do dispositivo\ntailscaleIdDescription=O ID interno do dispositivo Tailscale\ntailscaleHostName=Nome do anfitrião\ntailscaleHostNameDescription=O nome de anfitrião do dispositivo na rede de cauda\ntailscaleUsername=Nome de utilizador\ntailscaleUsernameDescription=O utilizador para iniciar sessão como\ntailscalePassword=Palavra-passe\ntailscalePasswordDescription=A palavra-passe de utilizador opcional que pode ser utilizada para o sudo\nscriptName=Nome do script\nscriptNameDescription=Dá um nome personalizado a este script\nscriptGroupName=Nome do grupo de scripts\nscriptGroupNameDescription=Atribui um nome personalizado a este grupo de scripts\nidentityName=Nome da identidade\nidentityNameDescription=Dá um nome personalizado a esta identidade\ntailscaleTailnet.displayName=Tailnet\ntailscaleTailnet.displayDescription=Liga-te a uma rede de cauda específica com a tua conta\nputtyConnections=Ligações PuTTY\nkittyConnections=Ligações KiTTY\nicons=Ícones\ncustomIcons=Ícones personalizados\niconSources=Fontes de ícones\niconSourcesDescription=Podes adicionar as tuas próprias fontes de ícones aqui. O XPipe irá recolher quaisquer ficheiros .svg na localização adicionada e adicioná-los ao conjunto de ícones disponíveis.\\n\\nTanto diretórios locais quanto repositórios git remotos são suportados como locais de ícones.\nrefreshSources=Atualizar ícones\nrefreshSourcesDescription=Actualiza todos os ícones a partir das fontes disponíveis\naddDirectoryIconSource=Acrescentar fonte de diretório ...\naddDirectoryIconSourceDescription=Adicionar ícones de um diretório local\naddGitIconSource=Adiciona a fonte git ...\naddGitIconSourceDescription=Adiciona ícones localizados num repositório git remoto\nrepositoryUrl=URL do repositório Git\niconDirectory=Diretório de ícones\naddUnsupportedKexMethod=Adiciona um método de troca de chaves não suportado\naddUnsupportedKexMethodDescription=Permite que o método de troca de chaves $VAL$ seja utilizado para esta ligação\naddUnsupportedHostKeyType=Adiciona um tipo de chave de anfitrião não suportado\naddUnsupportedHostKeyTypeDescription=Permite que o tipo de chave de anfitrião $VAL$ seja utilizado para esta ligação\naddUnsupportedMacType=Adiciona um tipo de MAC não suportado\naddUnsupportedMacTypeDescription=Permite que o tipo de MAC $VAL$ seja utilizado para esta ligação\nrunSilent=silenciosamente em segundo plano\nrunInFileBrowser=no navegador de ficheiros\nrunInConnectionHub=num hub de ligação\ncommandOutput=Saída de comando\niconSourceDeletionTitle=Apaga o ícone de origem\niconSourceDeletionContent=Queres apagar esta fonte de ícones e todos os ícones associados a ela?\nrefreshIcons=Atualizar ícones\nrefreshIconsDescription=Recuperar, renderizar e armazenar em cache todos os mais de 1000 ícones disponíveis de fontes externas para arquivos .png. Isso pode demorar um pouco ...\nvaultUserLegacy=Utilizador do Vault (modo de compatibilidade legado limitado)\nupgradeInstructions=Instruções de atualização\nexternalActionTitle=Pedido de ação externa\nexternalActionContent=Foi solicitada uma ação externa. Pretendes permitir o lançamento de acções a partir do exterior do XPipe?\nnoScriptStateAvailable=Actualiza para determinar a compatibilidade do script ...\ndocumentationDescription=Verifica a documentação\ncustomEditorCommandInTerminal=Executa um comando personalizado num terminal\ncustomEditorCommandInTerminalDescription=Se o teu editor for baseado em terminal, podes ativar esta opção para abrir automaticamente um terminal e executar o comando na sessão do terminal.\\n\\nPodes utilizar esta opção para editores como o vi, vim, nvim e outros.\ndisableHttpsTlsCheck=Desativar a verificação do certificado de pedido HTTPS\ndisableHttpsTlsCheckDescription=Se a tua organização estiver a desencriptar o teu tráfego HTTPS nas firewalls utilizando a interceção SSL, quaisquer verificações de atualização ou de licença falharão devido à não correspondência dos certificados. Podes resolver este problema activando esta opção e desactivando a validação de certificados TLS.\nconnectionsSelected=$NUMBER$ ligações selecionadas\naddConnections=Adiciona ligações\nbrowseDirectory=Navega no diretório\nopenTerminal=Abre o terminal\ndocumentation=Documentação\nreport=Relata um erro\nkeePassXcNotAssociated=Ligação KeePassXC\nkeePassXcNotAssociatedDescription=XPipe não está associado à tua base de dados local KeePassXC. Clica em baixo para executar o passo único de associação do XPipe à base de dados KeePassXC para que o XPipe possa consultar as palavras-passe.\nkeePassXcAssociateMore=Liga mais bases de dados\nkeePassXcAssociateMoreDescription=Podes estar ligado a várias bases de dados KeePassXC ao mesmo tempo\nkeePassXcAssociated=Ligações KeePassXC\nkeePassXcAssociatedDescription=O XPipe está ligado às seguintes bases de dados locais do KeePassXC:\nkeePassXcNotAssociatedButton=Liga a base de dados\nidentifier=Identificador\npasswordManagerCommand=Comando personalizado\npasswordManagerCommandDescription=O comando personalizado a executar para obter palavras-passe. A string de espaço reservado $KEY será substituída pela chave de senha citada quando chamada. Isto deve chamar o teu gestor de senhas CLI para imprimir a senha para stdout, por exemplo, mypassmgr get $KEY.\nchooseTemplate=Selecionar modelo\nkeePassXcPlaceholder=URL de entrada do KeePassXC\nterminalEnvironment=Ambiente de terminal\nterminalEnvironmentDescription=Caso pretenda utilizar caraterísticas de um ambiente WSL local baseado em Linux para a personalização do terminal, pode utilizá-lo como ambiente de terminal.\\n\\nQuaisquer comandos personalizados de inicialização de terminal e configuração de multiplexador de terminal serão então executados nesta distribuição WSL.\nterminalInitScript=Script de inicialização do terminal\nterminalInitScriptDescription=Comandos a serem executados no ambiente do terminal antes de a ligação ser iniciada. Podes utilizar isto para configurar o ambiente de terminal no arranque.\nterminalMultiplexer=Multiplexador de terminais\nterminalMultiplexerDescription=O multiplexador de terminal a utilizar como alternativa aos separadores num terminal. Substitui certas caraterísticas de manuseamento do terminal, por exemplo, o manuseamento de separadores, pela funcionalidade do multiplexador.\\n\\nRequer que o respetivo executável do multiplexador esteja instalado no sistema.\nterminalMultiplexerWindowsDescription=O multiplexador de terminal a utilizar como alternativa aos separadores num terminal. Substitui certas caraterísticas de manuseamento do terminal, por exemplo, o manuseamento de separadores, pela funcionalidade do multiplexador.\\n\\nRequer a utilização de um ambiente de terminal WSL no Windows e que o executável do multiplexador seja instalado no sistema WSL.\nterminalAlwaysPauseOnExit=Faz sempre uma pausa ao sair\nterminalAlwaysPauseOnExitDescription=Quando ativado, sair de uma sessão de terminal irá sempre pedir-te para reiniciar ou fechar a sessão. Se estiver desativado, o XPipe só o fará no caso de ligações falhadas que saiam com um erro.\nquerying=Consulta ...\nretrievedPassword=Obtido: $PASSWORD$\nrefreshOpenpubkey=Actualiza a identidade openpubkey\nrefreshOpenpubkeyDescription=Executa opkssh refresh para tornar a identidade openpubkey válida novamente\nall=Tudo\nterminalPrompt=Prompt de terminal\nterminalPromptDescription=A ferramenta de prompt de terminal a ser usada nos teus terminais remotos. Ao ativar uma linha de comandos de terminal, define e configura automaticamente a ferramenta de linha de comandos no sistema de destino quando abre uma sessão de terminal.\\n\\nIsso não modifica nenhuma configuração de prompt existente ou arquivos de perfil em um sistema. Isso aumentará o tempo de carregamento do terminal pela primeira vez enquanto o prompt estiver sendo configurado no sistema remoto. Seu terminal pode precisar de fontes adicionais para exibir o prompt corretamente.\nterminalPromptConfiguration=Configuração da linha de comandos do terminal\nterminalPromptConfig=Ficheiro de configuração\nterminalPromptConfigDescription=O arquivo de configuração personalizado para aplicar ao prompt. Esta configuração será automaticamente definida no sistema alvo quando o terminal for inicializado e usado como a configuração padrão do prompt.\\n\\nSe quiseres usar o ficheiro de configuração predefinido existente em cada sistema, podes deixar este campo vazio.\npasswordManagerKey=Chave do gestor de senhas\npasswordManagerKeyDescription=O identificador do segredo no gestor de senhas\npasswordManagerAgent=Agente de gestão de palavras-passe\ndockerComposeProject.displayName=Projeto Docker compose\ndockerComposeProject.displayDescription=Agrupa contentores de um projeto de composição\nsshVerboseOutput=Habilita a saída verbosa do SSH\nsshVerboseOutputDescription=Imprime muitas informações de depuração ao se conectar via SSH. Útil para solucionar problemas com conexões SSH.\ndontUseGateway=Não uses o gateway\ndontUseGatewayDescription=Não utilizes o anfitrião do hipervisor como gateway e liga-te diretamente ao IP\ncategoryColor=Categoria cor\ncategoryColorDescription=A cor predefinida a utilizar para ligações dentro desta categoria\ncategorySync=Sincroniza com o repositório git\ncategorySyncDescription=Sincroniza todas as ligações automaticamente com o repositório git. Todas as alterações locais às ligações serão enviadas para o repositório remoto.\ncategorySyncSpecial=Sincroniza com o repositório git\\n(Não configurável para a categoria especial \"$NAME$\")\ncategoryDontAllowScripts=Desabilita todas as modificações\ncategoryDontAllowScriptsDescription=Desabilita qualquer execução de comando e outras operações em sistemas dentro desta categoria para evitar quaisquer modificações. Isto irá desativar todas as funcionalidades de scripting, comandos de ambiente shell, prompts e muito mais.\ncategoryConfirmAllModifications=Confirma todas as modificações\ncategoryConfirmAllModificationsDescription=Confirma primeiro qualquer tipo de modificação de uma ligação ou de um sistema de ficheiros. Isto pode evitar operações acidentais em sistemas importantes.\ncategoryDefaultIdentity=Identidade por defeito\ncategoryDefaultIdentityDescription=Se utilizares frequentemente uma determinada identidade em muitos dos sistemas desta categoria, a definição de uma identidade predefinida permitir-te-á pré-seleccioná-la quando criares novas ligações.\ncategoryConfigTitle=$NAME$ configuração\nconfigure=Configura\naddConnection=Adiciona uma ligação\nnoCompatibleConnection=Não encontraste uma ligação compatível\nnoCompatibleIdentity=Não encontraste uma identidade compatível\nnewCategory=Nova categoria\ndockerComposeRestricted=O projeto compose é restrito por $NAME$ e não pode ser modificado externamente. Utiliza $NAME$ para gerir este projeto de composição.\nrestricted=Restrito\ndisableSshPinCaching=Desativar o armazenamento em cache do PIN SSH\ndisableSshPinCachingDescription=O XPipe coloca automaticamente em cache quaisquer PINs que tenham sido introduzidos para uma chave quando utiliza alguma forma de autenticação baseada em hardware.\\n\\nSe desactivares esta opção, terás de voltar a introduzir o PIN em cada tentativa de ligação.\ngitSyncPull=Puxa para sincronizar alterações remotas do git\nenpassVaultFile=Ficheiro de cofre\nenpassVaultFileDescription=O ficheiro local do cofre do Enpass.\nflat=Plano\nrecursive=Recursivo\nrdpAllowListBlocked=A RemoteApp selecionada não parece estar incluída na lista de permissões RDP do servidor.\npsonoServerUrl=URL do servidor\npsonoServerUrlDescription=URL do servidor backend psono\npsonoApiKey=Chave API\npsonoApiKeyDescription=A chave API a utilizar, formatada como um uuid\npsonoApiSecretKey=Chave secreta da API\npsonoApiSecretKeyDescription=A chave secreta da API como uma cadeia hexadecimal de 64 bytes\npassboltServerUrl=URL do servidor\npassboltServerUrlDescription=URL do servidor backend passbolt\npassboltPassphrase=Palavra-passe\npassboltPassphraseDescription=A frase-passe da chave privada da caixa-forte\npassboltPrivateKey=Chave privada\npassboltPrivateKeyDescription=O ficheiro de chave gpg privado para o cofre\nfocusWindowOnNotifications=Foca a janela nas notificações\nfocusWindowOnNotificationsDescription=Coloca o XPipe em primeiro plano quando é apresentada uma notificação ou uma mensagem de erro, por exemplo, quando uma ligação ou um túnel termina inesperadamente.\ngitUsername=Nome de utilizador personalizado do git\ngitUsernameDescription=O utilizador personalizado para autenticar no repositório remoto git. Por defeito, o XPipe irá utilizar as credenciais atualmente configuradas do git CLI.\\n\\nEsta definição irá sobrepor-se a quaisquer credenciais predefinidas que já estejam configuradas para o teu cliente git CLI local.\ngitPassword=Password git personalizada / token de acesso pessoal\ngitPasswordDescription=A senha ou o token de acesso pessoal a ser usado para autenticar. A necessidade de uma senha ou token de acesso pessoal depende do provedor remoto do git. Esta definição irá substituir quaisquer credenciais padrão que já estejam configuradas para o teu cliente git CLI local.\nsetReadOnly=Define como só de leitura\nunsetReadOnly=Não definido apenas para leitura\nreadOnlyStoreError=A configuração desta entrada está congelada. Escolhe um nome diferente para guardar as tuas alterações numa nova cópia.\ncategoryFreeze=Congela as configurações de ligação\ncategoryFreezeDescription=Marca as configurações de ligação como só de leitura. Isto significa que nenhuma configuração de entrada de ligação existente nesta categoria pode ser modificada. No entanto, podem ser adicionadas novas ligações.\nupdateFail=A instalação da atualização não foi bem sucedida\nupdateFailAction=Instalar a atualização manualmente\nupdateFailActionDescription=Verifica as últimas versões no GitHub\nonePasswordPlaceholder=Nome do item ou URL op://\ncomputeDirectorySizes=Calcula o tamanho dos diretórios\ncomputeSize=Calcula o tamanho\ncustomSpiceCommand=Comando personalizado\ncustomSpiceCommandDescription=O comando personalizado a executar para iniciar sessões SPICE. A string de espaço reservado $FILE será substituída pelo caminho do ficheiro entre aspas para o ficheiro .vv quando chamado.\nvncClient=Cliente VNC\nvncClientDescription=O cliente VNC a lançar ao abrir ligações VNC no XPipe.\\n\\nTens a opção de utilizar o cliente VNC integrado no XPipe ou, em alternativa, lançar um cliente VNC externo instalado localmente se procuras mais personalização.\nintegratedXPipeVncClient=Cliente VNC XPipe integrado\ncustomVncCommand=Comando personalizado\ncustomVncCommandDescription=O comando personalizado a ser executado para iniciar sessões VNC. A string de espaço reservado $ADDRESS será substituída pelo endereço citado quando chamado.\nvncConnections=Ligações VNC\npasswordManagerIdentity=Identidade do gestor de palavras-passe\npasswordManagerIdentity.displayName=Identidade do gestor de palavras-passe\npasswordManagerIdentity.displayDescription=Recuperar o nome de utilizador e a palavra-passe de uma identidade a partir do teu gestor de palavras-passe\npasswordCopied=Palavra-passe de ligação copiada para a área de transferência\nerrorOccurred=Ocorreu um erro\nactionMacro.displayName=Macro de ação\nactionMacro.displayDescription=Executa em ação utilizando accionadores personalizados\nmacroAdd=Adiciona macro\nmacroName=Nome da macro\nmacroNameDescription=Dá um nome personalizado a esta macro\nactionId=ID da ação\nactionIdDescription=A ação a executar com esta macro\nmacroRefs=Ligações associadas\nmacroRefsDescription=As ligações com as quais executa a ação\nconnectionCopy=Copia\nactionPickerTitle=Ação de seleção\nactionPickerDescription=Clica em algo para executar uma ação. Em vez de executar a ação, podes criar e editar atalhos para a ação no modo de seleção de atalhos de ação.\ncancelActionPicker=Cancelar ação de seleção\nactionShortcut=Atalho de ação\nactionShortcuts=Atalhos de ação\nactionStore=Loja de acções\nactionStoreDescription=A entrada de loja para executar a ação\nactionStores=Armazenamento de acções\nactionStoresDescription=As entradas de loja para executar a ação\nactionDesktopShortcut=Atalho para o ambiente de trabalho\nactionDesktopShortcutDescription=Cria um atalho para esta ação no teu ambiente de trabalho\nactionUrlShortcut=Atalho URL\nactionUrlShortcutDescription=Copia um URL que pode desencadear estas acções quando aberto\nactionUrlShortcutDisabled=Atalho URL (Indisponível)\nactionUrlShortcutDisabledDescription=O tipo de instalação $TYPE$ não suporta a abertura de URLs\nactionApiCall=Pedido de API\nactionApiCallDescription=Chama esta ação a partir da API HTTP\nactionMacro=Macro de ação\nactionMacroDescription=Cria uma macro com funcionalidade avançada para esta ação\ncreateMacro=Cria uma macro\nactionConfiguration=Parâmetros\nactionConfigurationDescription=Os parâmetros a passar para a ação executada\nconfirmAction=Confirmação de ação\nactionConnections=Ligações de ação\nactionConnectionsDescription=As ligações para executar a ação\nactionConnection=Ligação de ação\nactionConnectionDescription=A ligação para executar a ação\nappleContainerInstall.displayName=Contentores Apple\nappleContainerInstall.displayDescription=Acede a instâncias de contentores da apple através do CLI do contentor\nappleContainer.displayName=Contentor Apple\nappleContainer.displayDescription=Acede a instâncias de contentores da apple através do CLI do contentor\nappleContainerHostDescription=O anfitrião no qual o contentor da apple está localizado\nappleContainerDescription=O nome do contentor da apple\nappleContainers=Contentores Apple\nchangeOrderIndexTitle=Alterar a ordem\norderIndex=Índice\norderIndexDescription=Índice explícito para ordenar esta entrada em relação a outras. Os índices mais baixos são apresentados em cima, os mais altos em baixo\nmoveToFirst=Move para o primeiro\nmoveToLast=Move para o último\ncategory=Categoria\nincludeRoot=Inclui a raiz\nexcludeRoot=Excluir raiz\nfreezeConfiguration=Congela a configuração\nunfreezeConfiguration=Descongela a configuração\nwaylandScalingTitle=Escala de Wayland\nactionApiUrl=$URL$ (Copia o corpo json)\ncopyBody=Copia o corpo do pedido\ngitRepoTerminalOpen=Abre o repositório no terminal\ngitRepoTerminalOpenDescription=Dá uma vista de olhos ao repositório com a linha de comandos\ngitRepoOverwriteLocal=Substitui o repositório local\ngitRepoOverwriteLocalDescription=Substitui todas as alterações locais por alterações do remoto\ngitRepoForcePush=Substitui o repositório remoto\ngitRepoForcePushDescription=Usa git push --force para aplicar as tuas alterações locais ao remoto\ngitRepoDontWarn=Não avises mais\ngitRepoDontWarnDescription=Se isto for esperado, faz com que o XPipe ignore este erro no futuro\ngitRepoTryAgain=Tenta novamente\ngitRepoTryAgainDescription=Tenta repetir a mesma operação\ngitRepoEnablePlain=Utiliza a sincronização de diretórios simples\ngitRepoEnablePlainDescription=Não inicializa um repositório git para sincronizar as alterações no diretório\ngitRepoCreateBare=Usa o git sync\ngitRepoCreateBareDescription=Inicializa um novo repositório git simples no diretório de sincronização\ngitRepoDisable=Desactiva o git vault por agora\ngitRepoDisableDescription=Não faças nenhuma alteração durante esta sessão\ngitRepoPullRefresh=Puxa as alterações e actualiza\ngitRepoPullRefreshDescription=Junta alterações remotas e recarrega dados\nbreakOutCategory=Categoria de rutura\nmergeCategory=Junta a categoria\nopenWinScp=Abre no WinSCP\nuninstallApplication=Desinstalar\nuninstallApplicationDescription=Executa o .pkg um script de instalação para desinstalar completamente o XPipe\nk8sEditPodTitle=Aplicar alterações\nk8sEditPodContent=Queres aplicar as alterações feitas através do comando kubectl apply? É provável que seja necessário reiniciar o sistema para que as alterações sejam aplicadas.\nvirshEditDomainTitle=Aplicar alterações\nvirshEditDomainContent=Pretendes aplicar as alterações ao domínio? É provável que seja necessário reiniciar o computador para que as alterações sejam aplicadas.\npkcs11Library=Biblioteca PKCS#11\npkcs11LibraryDescription=O caminho do ficheiro da biblioteca ligada dinamicamente\nsshAgentSocket=Soquete de agente SSH personalizado\nsshAgentSocketDescription=O socket personalizado a utilizar para comunicar com o agente SSH. Este agente personalizado pode ser utilizado para uma ligação selecionando a opção de agente personalizado para o mesmo.\npublicKey=Identificador de chave pública\npublicKeyDescription=A chave pública opcional para forçar o agente a oferecer apenas a chave privada correspondente\nactions=Acções\nhcloudServer.displayName=Servidor de nuvem Hetzner\nhcloudServer.displayDescription=Acede a um servidor alojado na nuvem Hetzner através de SSH\nhcloudInstall.displayName=Hetzner Cloud CLI\nhcloudInstall.displayDescription=Acede a servidores alojados na nuvem Hetzner através de hcloud\nhcloudContext.displayName=contexto hcloud\nhcloudContext.displayDescription=Acede aos servidores de um contexto hcloud\nmetrics=Métrica\nopenInVsCode=Abre em VsCode\naddCloud=Nuvem ...\nhcloudToken=token hcloud\nhcloudTokenDescription=O token de nuvem Hetzner a utilizar. Para mais informações, consulta a documentação\nhcloudLogin=Início de sessão na nuvem da Hetzner\nclearHcloudToken=Limpa o token hcloud\nclearHcloudTokenDescription=Elimina o token existente para que possas iniciar sessão novamente\nselectIdentity=Selecionar identidade\nenableMcpServer=Ativar o servidor MCP\nenableMcpServerDescription=Ativa o servidor XPipe MCP, permitindo que os clientes MCP externos enviem pedidos para o servidor MCP. Vê abaixo os detalhes de configuração.\\n\\nNota que a API HTTP não tem de ser activada para a funcionalidade MCP.\nenableMcpMutationTools=Habilita as ferramentas de mutação da CIM\nenableMcpMutationToolsDescription=Por defeito, apenas as ferramentas só de leitura estão activadas no servidor MCP. Isso é para garantir que nenhuma operação acidental possa ser feita e potencialmente modificar um sistema.\\n\\nSe planeja fazer alterações nos sistemas por meio de clientes MCP, certifica-se de verificar se o cliente MCP está configurado para confirmar quaisquer ações potencialmente destrutivas antes de ativar esta opção. Requer uma reconexão de quaisquer clientes MCP para aplicar.\nmcpClientConfigurationDetails=Configuração do cliente MCP\nmcpClientConfigurationDetailsDescription=Utiliza estes dados de configuração para estabelecer ligação ao servidor XPipe MCP a partir do cliente MCP da tua escolha.\nswitchHostAddress=Altera o endereço do anfitrião\naddAnotherHostName=Adiciona outro nome de anfitrião\naddNetwork=Verificação de rede ...\nnetworkScan=Pesquisa de rede\nnetworkScanStore=Anfitrião de destino\nnetworkScanStoreDescription=O anfitrião que procura na rede local\nuseAsGateway=Utiliza o anfitrião como gateway\nuseAsGatewayDescription=Se deves utilizar o anfitrião de destino como gateway para as ligações criadas\nnetworkScanPorts=Portas a analisar\nnetworkScanPortsDescription=A lista de portas separada por vírgulas a incluir no exame\nnetworkScanType=Tipo de ligação\nnetworkScanTypeDescription=O tipo de servidores que deves procurar\nemptyDirectory=Este diretório parece estar vazio\nhcloudConfigFile=ficheiro de configuração hcloud\nhcloudConfigFileDescription=A localização do ficheiro de configuração .toml do hcloud CLI\npreferMonochromeIcons=Prefere ícones monocromáticos\npreferMonochromeIconsDescription=Quando ativado, as variáveis de ícone monocromáticas serão escolhidas em vez das versões coloridas predefinidas de um ícone, assumindo que uma variante de ícone de modo claro ou escuro separada está disponível para um ícone a partir de uma fonte.\\n\\nRequer uma atualização dos ícones a aplicar.\nalwaysShowSshMotd=Mostra sempre o MOTD\nalwaysShowSshMotdDescription=Mostra ou não a mensagem do dia configurada em um sistema remoto no login em uma nova sessão de terminal. Nota que alterar isto pode alterar o comportamento de inicialização das ligações SSH.\nmanageSubscription=Gerir subscrição\nnoListeningServer=Nenhum servidor de escuta\nnetworkScanResults=Resultados da pesquisa\nnetworkScanResultsDescription=A lista de sistemas encontrados na rede\nlocalShellDialect=Shell local\nlocalShellDialectDescription=A shell que é usada para operações locais. No caso de a shell local normal por omissão estar desactivada ou avariada de alguma forma, esta opção pode ser usada para voltar a outra alternativa.\\n\\nAlgumas configurações como entradas PATH personalizadas podem não se aplicar com a shell de recurso se ainda não estiverem configuradas nos respectivos ficheiros de perfil da shell.\nagentSocketNotFound=Não foi encontrada nenhuma tomada de agente ativa\nagentSocket=Localização de sockets\nagentSocketDescription=O caminho do ficheiro do socket do agente\nagentSocketNotConfigured=Ainda não foi configurado nenhum socket personalizado\ndownloadInProgress=$NAME$ transferência em curso\nenableTerminalStartupBell=Ativar a campainha de arranque do terminal\nenableTerminalStartupBellDescription=Reproduz um comando de bipe/sino em uma nova sessão de terminal. Se o teu emulador de terminal suportar sinos, isto pode ser utilizado para facilitar a identificação de instâncias de terminal recém-iniciadas.\ninvalidSshGatewayChain=Configuração inválida de cadeia de gateways mistas com gateways de salto e gateways sem salto.\nsyncFileExists=O ficheiro sincronizado $FILE$ já existe\nreplaceFile=Substitui o ficheiro\nreplaceFileDescription=Substitui o ficheiro existente por este\nrenameFile=Mudar o nome do ficheiro\nrenameFileDescription=Dá a este ficheiro um nome diferente para sincronizar\nnewFileName=Novo nome de ficheiro\nparentHostDoesNotSupportTunneling=O host pai $NAME$ não suporta tunelamento\nconnectionNotesTemplate=Modelo de notas\nconnectionNotesTemplateDescription=O modelo markdown que deve ser utilizado quando adiciona uma nova entrada de notas a uma ligação.\nconnectionNotesButton=Editar notas\nrdpSmartSizing=Ativar o dimensionamento inteligente\nrdpSmartSizingDescription=Quando ativado, o mstsc reduz o tamanho do ambiente de trabalho se a janela for demasiado pequena para ser apresentada na sua resolução total. A relação de aspeto da área de trabalho é preservada quando reduzida.\ndisableStartOnInit=Desativar o arranque automático\nenableStartOnInit=Ativar o arranque automático\nfileReadSudoTitle=Leitura de ficheiros Sudo\nfileReadSudoContent=O ficheiro que estás a tentar ler não te concede permissões de leitura ao utilizador atual. Queres ler este ficheiro como utilizador root com sudo? Isto irá elevar-te automaticamente para root com as credenciais existentes ou através de um prompt.\nnetbirdInstall.displayName=Instalação do Netbird\nnetbirdInstall.displayDescription=Liga-se a pares na tua rede Netbird\nnetbirdProfile.displayName=Perfil Netbird\nnetbirdProfile.displayDescription=Lista os pares de um perfil específico\nnetbirdPeer.displayName=Pares Netbird\nnetbirdPeer.displayDescription=Liga-te a um par através de SSH\nnetbirdPublicKey=Chave pública\nnetbirdPublicKeyDescription=A chave pública interna do par\nnetbirdHostName=Nome do anfitrião\nnetbirdHostNameDescription=O nome do anfitrião do par na rede\nvncRefSystem=Sistema associado\nvncRefSystemDescription=A entrada de ligação a associar a esta ligação VNC. Deixa em branco se não houver nenhuma\nabstractHost.displayName=Abrir o host\nabstractHost.displayDescription=Cria uma entrada para um anfitrião que não suporta ligações shell\nabstractHostAddress=Endereço do anfitrião\nabstractHostAddressDescription=O endereço do anfitrião\nabstractHostGateway=Gateway\nabstractHostGatewayDescription=O sistema de gateway opcional através do qual podes aceder a este anfitrião\nabstractHostConvert=Converte para uma entrada de anfitrião abstrata\nhostNoConnections=Não há ligações disponíveis\nhostHasConnections=$COUNT$ ligações disponíveis\nhostHasConnection=$COUNT$ ligação disponível\nlargeFileWarningTitle=Edita um ficheiro grande\nlargeFileWarningContent=O ficheiro que queres editar é bastante grande, com $SIZE$. Queres mesmo abrir este ficheiro no teu editor de texto?\nrdpAskpassUser=Nome de utilizador RDP para o anfitrião $HOST$\nrdpAskpassPassword=Palavra-passe para o utilizador $USER$\ninPlaceKey=Chave\ninPlaceKeyText=Conteúdo da chave privada\ninPlaceKeyTextDescription=O conteúdo da chave privada\nnetbirdSelfhosted=Instancia netbird auto-hospedada\nnetbirdSelfhostedDescription=Fornece um URL personalizado em vez de usar a versão hospedada na nuvem\nnetbirdManagementUrl=URL de gestão do Netbird\nnetbirdManagementUrlDescription=O URL de gestão da tua instância auto-hospedada\nnetbirdSetupKey=Tecla de configuração\nnetbirdSetupKeyDescription=Se estiveres a utilizar chaves de configuração, podes utilizar uma para iniciar sessão\nnetbirdLogin=Início de sessão Netbird\naddProfile=Adicionar perfil\nnetbirdProfileNameAsktext=Nome do novo perfil Netbird\nopenSftp=Abre uma sessão SFTP\ncapslockWarning=Tens o capslock ativado\ninherit=Herdar\nsshConfigStringSelected=Anfitrião de destino\nsshConfigStringSelectedDescription=Para vários hosts, o primeiro é usado como alvo. Reordena os anfitriões para alterar o destino\ntunnelToLocalhost=Faz um túnel para o localhost\ntunnelToLocalhostDescription=Faz o tunelamento automático da porta remota para o localhost\ntags=Etiquetas\ntag=Etiqueta\naddNewTag=Cria uma nova etiqueta\ncreateTag=Criar etiqueta ...\ninPlacePublicKey=Chave pública\ninPlacePublicKeyDescription=A chave pública associada à chave privada especificada\nsshKeygenTitle=Gera uma nova chave SSH\nsshKeygenAlgorithm=Algoritmo\nsshKeygenAlgorithmDescription=O algoritmo de geração de chaves assimétricas a utilizar para a chave\nrsaBits=Bits\nrsaBitsDescription=Número de bits na chave gerada\nsshKeygenComment=Comenta\nsshKeygenCommentDescription=O comentário opcional para esta chave\nsshKeygenPassphrase=Frase-chave\nsshKeygenPassphraseDescription=A frase-passe opcional para esta chave\ned25519SkResident=Cria uma chave residente\ned25519SkResidentDescription=Armazena a chave privada na chave de segurança do hardware\ned25519SkResidentKeyName=Etiqueta de chave residente\ned25519SkResidentKeyNameDescription=Atribui uma etiqueta à chave. Necessário para armazenar várias chaves na chave de segurança\ned25519SkPinRequired=Requerer PIN\ned25519SkPinRequiredDescription=Exige a introdução do PIN aquando da utilização\ned25519SkUserPresenceRequired=Exige a presença do utilizador\ned25519SkUserPresenceRequiredDescription=Exige toque ou algo semelhante na utilização. Algumas chaves de segurança exigem que isto esteja ativado\ncopyPublicKey=Copia a chave pública\ngeneratePublicKey=Gera uma chave pública\npublicKeyGenerateNotice=Pode ser gerado a partir da chave privada\nidentityApplyTargetHost=Destino\nidentityApplyTargetHostDescription=O sistema ao qual aplicar a identidade\nidentityApplyAuthorizedHost=Chave SSH autorizada\nidentityApplyAuthorizedHostDescription=A chave SSH é adicionada ao ficheiro de hosts autorizados\nidentityApplyAuthorizedHostButton=Acrescenta uma chave ao ficheiro\napplyIdentityToHost=Aplica a identidade ao anfitrião ...\nidentityApplyMissingPublicKeyTitle=Chave pública em falta\nidentityApplyMissingPublicKeyContent=A chave SSH da identidade não tem uma chave pública associada. Verifica a configuração para obteres detalhes.\nvalid=Válido\nnotValid=Não é válido\nwarning=Aviso\nidentityApplyTitle=Aplicar identidade\nidentityApplyConfigPasswordEnabled=Autenticação de palavra-passe activada\nidentityApplyConfigPasswordEnabledDescription=A autenticação por palavra-passe ainda está activada na configuração do sshd\nidentityApplyConfigPasswordDisabled=Autenticação de palavra-passe desactivada\nidentityApplyConfigPasswordDisabledDescription=A autenticação por palavra-passe ainda está desactivada na configuração do sshd\nidentityApplyConfigKeyEnabled=Autenticação de chaves activada\nidentityApplyConfigKeyEnabledDescription=A autenticação baseada em chave ainda está activada na configuração do sshd\nidentityApplyConfigKeyDisabled=Autenticação de chaves desactivada\nidentityApplyConfigKeyDisabledDescription=A autenticação baseada em chaves ainda está desactivada na configuração do sshd\nidentityApplyConfigRootDisabledWarning=Início de sessão de raiz desativado\nidentityApplyConfigRootDisabledWarningDescription=O início de sessão do utilizador raiz não está ativado na configuração do sshd\nidentityApplyConfigAdminWarning=Chaves de administrador configuradas\nidentityApplyConfigAdminWarningDescription=A chave poderá ter de ser adicionada a administrators_authorized_keys para os utilizadores administradores\nidentityApplyEditConfig=Editar configuração\nidentityApplyEditConfigDescription=Abre a configuração do sshd no editor para corrigir quaisquer problemas\nidentityApplyEditAuthorizedKeys=Edita chaves autorizadas\nidentityApplyEditAuthorizedKeysDescription=Abre o ficheiro authorized_keys no editor para editar ou remover outras chaves\nidentityApplyEditConfigButton=Abre sshd_config\nidentityApplyEditAuthorizedKeysButton=Abre authorized_keys\nidentityApplySetStoreIdentity=Conjunto de identidades de ligação\nidentityApplySetStoreIdentityDescription=A identidade está configurada para ser utilizada pela ligação\nidentityApplySetStoreIdentityButton=Aplicar identidade\ngenerateKey=Gera uma chave\ngroupSecretStrategy=Controlo de acesso baseado em grupos\ngroupSecretStrategyDescription=Como recuperar o segredo do grupo usado para encriptação e desencriptação para o grupo. O método de recuperação que escolheres será executado quando um utilizador iniciar sessão no vault no arranque.\\n\\nEsta definição é configurada por grupo. Para alterar esta definição para um grupo diferente do que está atualmente ativo, terá de iniciar sessão no vault como membro desse grupo.\nfileSecret=Segredo baseado em ficheiros\ncommandSecret=Comanda\nhttpRequestSecret=Resposta HTTP\nfileSecretChoice=Localização do ficheiro\nfileSecretChoiceDescription=O caminho para o ficheiro que contém o segredo de encriptação do grupo. Uma vez que este ficheiro pode ser consultado em todas as plataformas, podes usar ~ no caminho para te referires ao diretório home. O ficheiro tem de estar disponível em todos os sistemas a partir dos quais desbloqueia a abóbada, caso contrário o início de sessão falhará.\ncommandSecretField=Script de recuperação\ncommandSecretFieldDescription=O comando que devolverá a chave de encriptação secreta para o grupo atual. O comando é executado no shell padrão do sistema local e a chave deve ser impressa no stdout.\nhttpRequestSecretField=URI de pedido\nhttpRequestSecretFieldDescription=O URI para o qual envia um pedido HTTP. O segredo do grupo é retirado do corpo da resposta HTTP.\nvaultAuthentication=Autenticação de cofre\nvaultAuthenticationDescription=Como autenticar / desbloquear os dados da abóbada. Existem várias formas diferentes de encriptar e desbloquear os dados da abóbada, dependendo de com quem pretende partilhar os dados da abóbada.\ngroupAuthFailed=A autenticação secreta falhou\nuserAuthFailed=A autenticação da palavra-passe falhou\nsavingChanges=Guardar alterações\nawsDeviceScan=AWS\naws=AWS\nawsCliInstallTitle=Necessita do AWS CLI\nawsCliInstallContent=A integração do AWS requer que o AWS CLI seja instalado no teu sistema local\nawsProfileCreateTitle=Novo perfil AWS\nawsProfileAccessKey=Chave de acesso\nawsProfileName=Nome do perfil\nawsProfileNameDescription=O nome de apresentação do novo perfil\nawsProfileRegion=Região\nawsProfileRegionDescription=A região AWS associada ao perfil\nawsProfileAccessKeyId=ID da chave de acesso\nawsProfileAccessKeyIdDescription=O ID da chave de acesso do utilizador IAM\nawsProfileSecretAccessKey=Chave de acesso secreta\nawsProfileSecretAccessKeyDescription=A chave de acesso secreta associada\nawsInstall.displayName=Instalação do AWS CLI\nawsInstall.displayDescription=Liga-te aos teus sistemas AWS através do AWS CLI\nawsProfile.displayName=Perfil CLI do AWS\nawsProfile.displayDescription=Acede ao AWS através de um perfil específico\nawsInstanceId=ID da instância\nawsInstanceIdDescription=O ID interno desta instância\nawsInstanceUseSsm=Liga-te através do SSM\nawsInstanceUseSsmDescription=Usa a ferramenta SSM para se conectar à instância via SSH\nawsEc2Instance.displayName=Instância EC2 do AWS\nawsEc2Instance.displayDescription=Liga-te a uma instância EC2 através de SSH\nawsS3Group.displayName=Baldes S3\nawsS3Group.displayDescription=Aceder a buckets S3 de um perfil AWS\nawsS3Bucket.displayName=Balde S3\nawsS3Bucket.displayDescription=Aceder a um bucket S3 de um perfil AWS\nawsEc2Group.displayName=Instâncias EC2\nawsEc2Group.displayDescription=Aceder a instâncias EC2 de um perfil AWS\nawsEc2InstanceSsmTerminal=Abre o terminal SSM\ngenericS3Bucket.displayName=Balde S3 genérico\ngenericS3Bucket.displayDescription=Aceder a um bucket S3 genérico através do AWS CLI\naddFileSystem=Sistema de ficheiros ...\ngenericS3BucketHost=Apresenta\ngenericS3BucketHostDescription=A entrada do anfitrião ou o endereço manual do servidor S3\ngenericS3BucketPortDescription=A porta em que o servidor S3 está a escutar\ngenericS3BucketAccessKeyId=ID da chave de acesso\ngenericS3BucketAccessKeyIdDescription=O ID da chave de acesso do utilizador IAM\ngenericS3BucketSecretAccessKey=Chave de acesso secreta\ngenericS3BucketSecretAccessKeyDescription=A chave de acesso secreta associada\ngenericS3BucketHttps=Ativar HTTPS\ngenericS3BucketHttpsDescription=Utiliza HTTPS para ligar ao servidor. Alguns fornecedores podem exigir HTTPS\ntunnelled=Com túnel\nawsInstallSync=Sincronização de configuração\nawsInstallSyncDescription=Sincroniza os ficheiros de configuração do AWS CLI com o cofre do git\nawsInstallLocation=Localização dos dados do utilizador\nawsInstallLocationDescription=O caminho a partir do qual os ficheiros de configuração do AWS CLI são obtidos\ninstanceActions=Acções de instância\nopenSplit=Abre em terminal dividido\nterminalSplitStrategy=Direção da vista dividida\nterminalSplitStrategyDescription=Controla a forma como os separadores do terminal são divididos quando se utiliza a funcionalidade de vista dividida no modo de lote para abrir várias sessões de terminal lado a lado.\nterminalSplitStrategyDisabledDescription=Controla a forma como os separadores do terminal são divididos quando se utiliza a funcionalidade de vista dividida no modo de lote para abrir várias sessões de terminal lado a lado.\\n\\nA sua configuração atual do terminal não suporta vistas divididas.\nhorizontal=Horizontal\nvertical=Vertical\nbalanced=Equilibrado\nclose=Fecha\nhelpButton=$TOPIC$ ligação de documentação\nquickAccess=Acesso rápido\ntoggleEnabled=Estado de alternância\ncurrentPath=Caminho atual\ndirectoryContents=Conteúdo do diretório\ndirectoryOptions=Opções de diretório\nchooseConnectionType=Escolhe o tipo de ligação\nbatchMode=Modo de lote\ntoggleButton=Botão de alternância\ntailscaleUseSsh=Utiliza a autenticação SSH da escala de cauda\ntailscaleUseSshDescription=Inicia sessão através do próprio servidor SSH Tailscale sem qualquer autenticação SSH\nportDescription=A porta em que o servidor SSH está a ser executado\nloginAs=Inicia sessão como\nsshGatewayType=Tipo de gateway\nsshGatewayTypeDescription=Se te deves ligar ao alvo através de um túnel ou com a opção ProxyJump\ngatewayTunnel=Túnel de gateway\nproxyJump=Salto de proxy\ncommandTypeAsyncBackground=Executa a desanexação em segundo plano\ncommandTypeSyncBackground=Corre em segundo plano e espera pela conclusão\ncommandTypeTerminalBackground=Abre no terminal\nasyncBackgroundCommand=Comando de fundo\nsyncBackgroundCommand=Bloqueia o comando de fundo\nterminalBackgroundCommand=Comando de terminal\ntestingConnection=Testar a ligação ...\nopenManagementConsole=Abre a consola de gestão\nopenLxcTerminal=Abre o terminal LXC\nopenContainerConsole=Abre a consola de série\nkeeper2fa=método 2FA\nkeeper2faDescription=O método primário de autenticação de dois fatores que está configurado para a tua conta. Ative isso se sua conta do Keeper exigir autenticação de dois fatores para acessar senhas.\nkeeperTotpDuration=Duração do código 2FA personalizado\nkeeperTotpDurationDescription=Substitui a duração predefinida de quanto tempo um código 2FA é válido. Só se aplica se a política da tua organização permitir alterar a duração.\\n\\nOs valores possíveis são: $VALUES$\nkeeperOtherAuth=Outros (RSA SecurID, Duo Security, Keeper DNA, etc.)\nextractReusableIdentities=Extrai identidades reutilizáveis\nidentitiesAdded=Identificações adicionadas\nsyncMode=Modo de sincronização\nsyncModeDescription=Controla a forma como as alterações devem ser sincronizadas.\\n\\nO modo instantâneo envia e recebe as alterações assim que possível, o modo de arranque e de saída sincroniza todas as alterações efectuadas durante uma sessão de uma só vez e o modo manual só sincroniza quando tu o inicias.\ntoggleTerminalDock=Alterna a doca do terminal\nscriptDirectory=Localização do diretório\nscriptDirectoryDescription=O diretório local que contém ficheiros de scripts da shell\nscriptSourceUrl=URL do repositório\nscriptSourceUrlDescription=O URL para um repositório git remoto que contém ficheiros de scripts de shell\nscriptCollectionSourceType=Tipo de fonte\nscriptCollectionSourceTypeDescription=O tipo de fonte de onde os scripts de shell devem ser carregados\nscriptCollectionSourceEntry=Entrada de origem\nscriptCollectionSourceEntryDescription=A fonte de onde os scripts de shell devem ser carregados\ngitRepository=Repositório Git\nscriptCollectionSource.displayName=Fonte do script\nscriptCollectionSource.displayDescription=Importa automaticamente scripts de shell de uma fonte existente\ndirectorySource=Fonte de diretório\ngitRepositorySource=Fonte do repositório Git\nrefreshSource=Atualizar fonte\nscriptTextSourceUrl=URL do guião\nscriptTextSourceUrlDescription=O URL para recuperar o ficheiro de script\nscriptSourceType=Fonte do script\nscriptSourceTypeDescription=De onde obter o guião\nscriptSourceTypeInPlace=Script no local\nscriptSourceTypeUrl=URL externo\nscriptSourceTypeSource=Fonte existente\nimportScripts=Importar scripts\nscriptsContained=$NUMBER$ roteiros\nscriptSourceCollectionImportTitle=Importar scripts da fonte ($SELECTED$/$COUNT$)\nnoScriptsFound=Não encontraste scripts\ntunnel=Túnel\nnotInitialized=Não inicializado\nselectCategory=Selecionar categoria ...\nscriptSourceName=Nome do script\nscriptSourceNameDescription=O nome do ficheiro do script na fonte\nworkspaceRestartTitle=Prepara o espaço de trabalho\nworkspaceRestartContent=Foi criado um atalho para o novo espaço de trabalho em $PATH$. Podes navegar para o atalho ou reiniciar o XPipe agora para abrir automaticamente o novo espaço de trabalho.\nbrowseShortcut=Procurar ficheiro\nsyncModeInstant=Sincroniza instantaneamente\nsyncModeSession=Sincroniza no arranque e na saída\nsyncModeManual=Sincroniza manualmente\npushChanges=Alterações push\npullChanges=Alterações pull\nsourcedFrom=Obtido de $SOURCE$\ninPlaceScript=Script no local\ngeneric=Genérico\nsyncToPlainDirectory=Sincroniza com o diretório simples\nsyncToPlainDirectoryDescription=Ao sincronizar com um diretório local, podes tratar este diretório como outro repositório git ou apenas como um diretório simples. Se a definição de diretório simples estiver activada, o diretório não é inicializado como um repositório git.\nopenSpiceSession=Abre uma sessão SPICE\nterminalBehaviour=Comportamento do terminal\nnoScanPossible=Não foram encontradas ligações suportadas\nnetworkSwitchPorts=Portas de rede\nnswitchGroup.displayName=Portas de rede\nnswitchGroup.displayDescription=Lista as portas disponíveis num dispositivo de rede\nnswitchPort.displayName=Porta de rede\nnswitchPort.displayDescription=Controla uma porta individual num dispositivo de comutação de rede\nenablePort=Ativar porta\nshutdownPort=Fecha a porta\nresetPort=Redefinir porta\nuseSystemDefault=Utiliza a predefinição do sistema\nportStatus=Estado do porto\nclearCounters=Limpa os contadores\nshowStatus=Mostra o estado\nshowAllPorts=Mostra todas as portas\nactiveLicense=Licença\nactiveLicenseDescription=Ativar uma chave de licença XPipe\nauthenticatorApp=Aplicação de autenticação\nsecurityKey=Chave de segurança\nmcpAdditionalContext=Contexto adicional do MCP\nmcpAdditionalContextDescription=Instruções adicionais para passar para o cliente MCP. Utiliza isto para controlar o comportamento do agente e fornecer contexto adicional para a sua configuração individual.\nmcpAdditionalContextSample=- Não reinicies automaticamente quaisquer serviços e daemons sem confirmar primeiro\\n- Ao configurar uma interface de rede, utiliza sempre 192.168.1.1/24 como gateway\nprefsRestartTitle=Reiniciar necessário\nprefsRestartContent=Algumas opções que alteraste requerem um reinício da aplicação para serem aplicadas. Queres reiniciar o XPipe agora?\nbashShell=Concha Bash\n"
  },
  {
    "path": "lang/strings/translations_ru.properties",
    "content": "delete=Удалить\nproperties=Свойства\nusedDate=Используется $DATE$\nopenDir=Открытый каталог\nsortLastUsed=Сортировка по дате последнего использования\nsortAlphabetical=Сортировка по алфавиту по имени\nsortIndexed=Сортировка по порядковому индексу\nrestartDescription=Перезагрузка часто может быть быстрым решением проблемы\nreportIssue=Сообщить о проблеме\nreportIssueDescription=Откройте интегрированный отчет о проблемах\nusefulActions=Полезные действия\nstored=Сохраненный\ntroubleshootingOptions=Инструменты для устранения неполадок\ntroubleshoot=Устранение неполадок\nremote=Удаленный файл\naddShellStore=Добавь оболочку ...\naddShellTitle=Добавить подключение к оболочке\nsavedConnections=Сохраненные соединения\nsave=Сохранить\nclean=Очистить\nmoveTo=Перейти к ...\naddDatabase=База данных ...\nbrowseInternalStorage=Просмотр внутреннего хранилища\naddTunnel=Туннель ...\naddService=Сервис ...\naddScript=Скрипт ...\naddHost=Удаленный хост ...\naddShell=Shell Environment ...\n#custom\naddCommand=Команда ...\naddAutomatically=Добавь автоматически ...\naddOther=Добавьте другие ...\nconnectionAdd=Добавить соединение\nscriptAdd=Добавить скрипт\nscriptGroupAdd=Добавить группу скриптов\nidentityAdd=Добавить личность\nnew=Новый\nselectType=Выберите тип\nselectTypeDescription=Выберите тип соединения\nselectShellType=Тип оболочки\nselectShellTypeDescription=Выберите тип соединения с оболочкой\nname=Имя\nstoreIntroHeader=Концентратор соединений\nstoreIntroContent=Здесь ты можешь управлять всеми своими локальными и удаленными shell-соединениями в одном месте. Для начала ты можешь быстро обнаружить доступные соединения в автоматическом режиме и выбрать, какие из них добавить.\nstoreIntroButton=Поиск соединений ...\ndragAndDropFilesHere=Или просто перетащи сюда файл\nconfirmDsCreationAbortTitle=Подтвердить прерывание\nconfirmDsCreationAbortHeader=Хочешь прервать создание источника данных?\nconfirmDsCreationAbortContent=Весь прогресс создания источника данных будет потерян.\nconfirmInvalidStoreTitle=Проверка пропуска\nconfirmInvalidStoreContent=Хочешь пропустить проверку соединения? Ты можешь добавить это соединение, даже если его не удалось проверить, и устранить проблемы с подключением позже.\nexpand=Развернуть\naccessSubConnections=Подключения доступа\ncommon=Общий\ncolor=Цвет\nalwaysConfirmElevation=Всегда подтверждай повышение разрешения\nalwaysConfirmElevationDescription=Управляет тем, как обрабатывать случаи, когда для выполнения команды в системе требуются повышенные права, например, с помощью sudo.\\n\\nПо умолчанию любые учетные данные sudo кэшируются во время сессии и автоматически предоставляются при необходимости. Если эта опция включена, то система будет каждый раз просить тебя подтвердить повышенный доступ.\nallow=Разрешить\n#custom\nask=Спросить\ndeny=Запретить\nshare=Добавить в git-репозиторий\nunshare=Удалить из git-репозитория\nremove=Удалить\ncreateNewCategory=Новая подкатегория\nprompt=Prompt\ncustomCommand=Пользовательская команда\nother=Другие\nsetLock=Установить блокировку\nselectConnection=Выберите соединение\nselectEntry=Выберите запись\ncreateLock=Создание парольной фразы\nchangeLock=Изменение парольной фразы\ntest=Тест\n#custom\nfinish=Закончить\nerror=Произошла ошибка\ndownloadStageDescription=Перемести скачанные файлы в системный каталог загрузок и открой его.\nok=Ок\nsearch=Поиск\nrepeatPassword=Повторять пароль\n#custom\naskpassAlertTitle=Запрос пароля\nunsupportedOperation=Неподдерживаемая операция: $MSG$\nfileConflictAlertTitle=Разрешить конфликт\nfileConflictAlertContent=Возник конфликт. Файл $FILE$ уже существует в целевой системе.\\n\\nКак бы ты хотел продолжить?\nfileConflictAlertContentMultiple=Возник конфликт. Файл $FILE$ уже существует.\\n\\nКак бы ты хотел продолжить? Возможно, есть еще конфликты, которые ты можешь автоматически разрешить, выбрав опцию, применимую ко всем.\nmoveAlertTitle=Подтвердить перемещение\nmoveAlertHeader=Ты хочешь переместить ($COUNT$) выбранные элементы в $TARGET$?\ndeleteAlertTitle=Подтвердите удаление\ndeleteAlertHeader=Хочешь удалить ($COUNT$) выбранные элементы?\nselectedElements=Выбранные элементы:\nmustNotBeEmpty=$VALUE$ не должен быть пустым\nvalueMustNotBeEmpty=Значение не должно быть пустым\ntransferDescription=Перетащите файлы сюда, чтобы скачать\ndragLocalFiles=Перетаскивание загрузок отсюда\nnull=$VALUE$ должен быть не нулевым\nroots=Корни\nscripts=Скрипты\nsearchFilter=Поиск ...\nrecent=Последние\nshortcut=Ярлык\nbrowserWelcomeEmptyHeader=Браузер файлов\nbrowserWelcomeEmptyContent=Слева ты можешь выбрать, какие системы открывать в браузере файлов. XPipe запомнит, к каким системам и каталогам ты обращался ранее, и в будущем будет показывать их здесь в меню быстрого доступа.\nbrowserWelcomeEmptyButton=Открыть браузер локальных файлов\nbrowserWelcomeSystems=Недавно ты был подключен к следующим системам:\nbrowserWelcomeDocsHeader=Документация\nbrowserWelcomeDocsContent=Если ты предпочитаешь более экскурсионный подход к знакомству с XPipe, загляни на сайт документации.\n#custom\nbrowserWelcomeDocsButton=Открыть документацию\nhostFeatureUnsupported=$FEATURE$ не установлен на хосте\nmissingStore=$NAME$ не существует\nconnectionName=Имя подключения\nconnectionNameDescription=Дайте этому соединению пользовательское имя\nopenFileTitle=Открытый файл\nunknown=Неизвестный\nscanAlertTitle=Добавить соединения\nscanAlertChoiceHeader=Цель\nscanAlertChoiceHeaderDescription=Выбери, где искать соединения. Сначала будут искаться все доступные соединения.\nscanAlertHeader=Типы соединений\nscanAlertHeaderDescription=Выбери типы соединений, которые ты хочешь автоматически добавлять для системы.\nnoInformationAvailable=Нет информации\nyes=Да\nno=Нет\nerrorOccured=Произошла ошибка\nterminalErrorOccured=Произошла ошибка терминала\nerrorTypeOccured=Было выброшено исключение типа $TYPE$\npermissionsAlertTitle=Необходимые разрешения\npermissionsAlertHeader=Для выполнения этой операции требуются дополнительные разрешения.\npermissionsAlertContent=Проследи за всплывающим окном, чтобы дать XPipe необходимые разрешения в меню настроек.\nerrorDetails=Детали ошибки\nupdateReadyAlertTitle=Готовность к обновлению\nupdateReadyAlertHeader=Обновление до версии $VERSION$ готово к установке\nupdateReadyAlertContent=Это позволит установить новую версию и перезапустить XPipe после завершения установки.\nerrorNoDetail=Никаких подробностей об ошибке нет\nerrorNoExceptionMessage=Возникла ошибка типа $TYPE$\nupdateAvailableTitle=Обновление доступно\nupdateAvailableContent=Обновление XPipe до версии $VERSION$ доступно для установки. Даже если XPipe не удалось запустить, ты можешь попытаться установить обновление, чтобы потенциально устранить проблему.\nclipboardActionDetectedTitle=Обнаружено действие буфера обмена\nclipboardActionDetectedContent=XPipe обнаружил в твоем буфере обмена содержимое, которое можно открыть. Хочешь ли ты открыть его прямо сейчас? Хочешь импортировать содержимое буфера обмена?\n#custom\ninstall=Установить ...\nignore=Игнорируй\npossibleActions=Доступные действия\nreportError=Ошибка в отчете\nreportOnGithub=Создать отчет о проблеме на GitHub\nreportOnGithubDescription=Открой новую проблему в репозитории GitHub\nreportErrorDescription=Отправить отчет об ошибке с дополнительной информацией для пользователя и диагностикой\nignoreError=Игнорировать ошибку\nignoreErrorDescription=Игнорируй эту ошибку и продолжай как ни в чем не бывало\nprovideEmail=Как мы можем с тобой связаться (необязательно, только если ты хочешь получить ответ). По умолчанию твой отчет анонимен, поэтому здесь ты можешь указать контактную информацию вроде адреса электронной почты.\nadditionalErrorInfo=Предоставь дополнительную информацию (необязательно)\nadditionalErrorAttachments=Выберите вложения (необязательно)\ndataHandlingPolicies=Политика конфиденциальности\nsendReport=Отправить отчет\nerrorHandler=Обработчик ошибок\nevents=События\nvalidate=Проверьте\nstackTrace=Трассировка стека\npreviousStep=< Предыдущий\nnextStep=Следующая >\n#custom\nfinishStep=Закончить\n#custom\nselect=Выбрать\nbrowseInternal=Обзор внутренних\ncheckOutUpdate=Проверить обновление\n#custom\nquit=Выйти\nnoTerminalSet=Ни одно терминальное приложение не было установлено автоматически. Ты можешь сделать это вручную в меню настроек.\nconnections=Подключения\nconnectionHub=Концентратор соединений\nsettings=Настройки\nexplorePlans=Лицензия\nhelp=Справка\n#custom\nabout=О XPipe\ndeveloper=Разработчик\nbrowseFileTitle=Просмотр файла\nbrowser=Браузер файлов\nselectFileFromComputer=Выберите файл с этого компьютера\nlinks=Ссылки\nwebsite=Сайт\ndiscordDescription=Присоединяйтесь к серверу Discord\nredditDescription=Присоединяйся к сабреддиту XPipe\nsecurity=Безопасность\nsecurityPolicy=Информация о безопасности\nsecurityPolicyDescription=Прочитайте подробную политику безопасности\nprivacy=Политика конфиденциальности\nprivacyDescription=Прочитайте политику конфиденциальности для приложения XPipe\nslackDescription=Присоединяйтесь к рабочему пространству Slack\n#custom\nsupport=Поддержать\ngithubDescription=Посмотри репозиторий GitHub\nopenSourceNotices=Уведомления об открытом исходном коде\n#custom\ncheckForUpdates=Проверить наличие обновлений\n#custom\ncheckForUpdatesDescription=Загрузить обновление, если оно есть\nlastChecked=Последняя проверка\nversion=Версия\nbuild=Версия сборки\nruntimeVersion=Версия для выполнения\nvirtualMachine=Виртуальная машина\nupdateReady=Установить обновление\nupdateReadyPortable=Проверить обновление\nupdateReadyDescription=Обновление было загружено и готово к установке\nupdateReadyDescriptionPortable=Обновление доступно для загрузки\nupdateRestart=Перезагрузка для обновления\nnever=Никогда\nupdateAvailableTooltip=Обновление доступно\nptbAvailableTooltip=Доступна публичная тестовая сборка\nvisitGithubRepository=Посетите репозиторий GitHub\nupdateAvailable=Доступно обновление: $VERSION$\ndownloadUpdate=Скачать обновление\nlegalAccept=Я принимаю лицензионное соглашение с конечным пользователем\n#custom\nconfirm=Подтвердить\n#custom\nprint=Напечатать\nwhatsNew=Что нового в версии $VERSION$ ($DATE$)\nantivirusNoticeTitle=Заметка об антивирусных программах\n#custom\nupdateChangelogAlertTitle=История изменений\ngreetingsAlertTitle=Добро пожаловать в XPipe\neula=Лицензионное соглашение с конечным пользователем\nnews=Новости\nintroduction=Введение\nprivacyPolicy=Политика конфиденциальности\n#custom\nagree=Согласиться\n#custom\ndisagree=Не согласиться\ndirectories=Директории\n#custom\nlogFile=Файл журнала\n#custom\nlogFiles=Файлы журналов\n#custom\nlogFilesAttachment=Вложить файлы журналов\nissueReporter=Репортер проблем\n#custom\nopenCurrentLogFile=Файл журнала\nopenCurrentLogFileDescription=Открыть файл журнала текущей сессии\nopenLogsDirectory=Открытая директория журналов\ninstallationFiles=Установочные файлы\nopenInstallationDirectory=Установочные файлы\nopenInstallationDirectoryDescription=Открыть каталог установки XPipe\nlaunchDebugMode=Режим отладки\nlaunchDebugModeDescription=Перезапустите XPipe в режиме отладки\nextensionInstallTitle=Скачать\nextensionInstallDescription=Для выполнения этого действия требуются дополнительные сторонние библиотеки, которые не распространяются XPipe. Ты можешь автоматически установить их здесь. Затем компоненты загружаются с сайта производителя:\nextensionInstallLicenseNote=Выполняя загрузку и автоматическую установку, ты соглашаешься с условиями лицензий третьих сторон:\nlicense=Лицензия\ninstallRequired=Требуется установка\n#custom\nrestore=Восстановить\nrestoreAllSessions=Восстановить все сессии\nlimitedTouchscreenMode=Ограниченный режим сенсорного экрана\nlimitedTouchscreenModeDescription=При использовании этого приложения на более экзотическом сенсорном интерфейсе, например на экране телефона, некоторые меню могут работать некорректно. Когда эта опция включена, реализация меню использует более ограниченную функциональность для работы с редкими событиями от мыши/тач.\nappearance=Внешний вид\ndisplay=Отображение\npersonalization=Персонализация\ndisplayOptions=Параметры отображения\ntheme=Тема\nrdpConfiguration=Настройка удаленного рабочего стола\nrdpClient=RDP-клиент\nrdpClientDescription=Программа-клиент RDP, которую нужно вызывать при запуске RDP-соединений.\\n\\nУчти, что разные клиенты имеют разную степень возможностей и интеграции. Некоторые клиенты не поддерживают автоматическую передачу паролей, поэтому тебе все равно придется заполнять их при запуске.\nlocalShell=Локальная оболочка\nthemeDescription=Твоя предпочтительная тема отображения.\ndontAutomaticallyStartVmSshServer=Не запускай автоматически SSH-сервер для виртуальных машин, когда это необходимо\ndontAutomaticallyStartVmSshServerDescription=Любое shell-подключение к ВМ, запущенной в гипервизоре, осуществляется через SSH. XPipe может автоматически запускать установленный SSH-сервер, когда это необходимо. Если тебе это не нужно по соображениям безопасности, то ты можешь просто отключить такое поведение с помощью этой опции.\n#custom\nconfirmGitShareTitle=Синхронизация git\nconfirmGitShareContent=Хочешь добавить выбранный файл в свой репозиторий git vault? Это скопирует зашифрованную версию файла в твое git-хранилище и зафиксирует твои изменения. После этого ты получишь доступ к файлу на всех синхронизированных рабочих столах.\ngitShareFileTooltip=Добавь файл в каталог данных git vault, чтобы он автоматически синхронизировался.\\n\\nЭто действие можно использовать, только если в настройках включено git vault.\nperformanceMode=Режим производительности\nperformanceModeDescription=Отключи все визуальные эффекты, которые не нужны, чтобы повысить производительность приложения.\ndontAcceptNewHostKeys=Не принимай новые ключи хоста SSH автоматически\ndontAcceptNewHostKeysDescription=XPipe по умолчанию автоматически принимает хост-ключи от систем, в которых у твоего SSH-клиента нет уже сохраненного известного хост-ключа. Однако если какой-либо известный ключ хоста изменился, он откажется подключаться, пока ты не примешь новый.\\n\\nОтключение этого поведения позволяет тебе проверять все хост-ключи, даже если изначально конфликта нет.\n#custom\nuiScale=Шкала масштаба пользовательского интерфейса\nuiScaleDescription=Пользовательское значение масштабирования, которое может быть установлено независимо от общесистемного масштаба отображения. Значения указываются в процентах, поэтому, например, значение 150 приведет к масштабированию пользовательского интерфейса на 150%.\neditorProgram=Программа-редактор\neditorProgramDescription=Текстовый редактор по умолчанию, который используется при редактировании любого вида текстовых данных.\nwindowOpacity=Непрозрачность окна\nwindowOpacityDescription=Изменяет непрозрачность окна, чтобы следить за тем, что происходит на заднем плане.\nuseSystemFont=Использовать системный шрифт\nopenDataDir=Каталог данных хранилища\nopenDataDirButton=Открытый каталог данных\nopenDataDirDescription=Если ты хочешь синхронизировать дополнительные файлы, например SSH-ключи, между системами с твоим git-репозиторием, ты можешь поместить их в каталог данных хранилища. У любых файлов, на которые там ссылаются, пути к файлам будут автоматически адаптированы на любой синхронизированной системе.\nupdates=Обновления\nselectAll=Выберите все\nadvanced=Продвинутый\n#custom\nthirdParty=Лицензия с открытым исходным кодом\neulaDescription=Прочитай лицензионное соглашение с конечным пользователем для приложения XPipe\nthirdPartyDescription=Просмотр лицензий с открытым исходным кодом сторонних библиотек\nworkspaceLock=Мастер-пароль\nenableGitStorage=Включить синхронизацию\nsharing=Общий доступ\n#custom\ngitSync=Синхронизация git\nenableGitStorageDescription=Если эта функция включена, XPipe инициализирует git-репозиторий для локального хранилища и фиксирует в нем все изменения. Учти, что для этого необходимо установить git, и это может замедлить операции загрузки и сохранения.\\n\\nВсе категории, которые должны быть синхронизированы, должны быть явно помечены как синхронизированные.\nstorageGitRemote=URL-адрес удаленной синхронизации\nstorageGitRemoteDescription=Если установить этот параметр, XPipe будет автоматически вытаскивать любые изменения при загрузке и выталкивать их в удаленный репозиторий при сохранении.\\n\\nЭто позволяет тебе делиться своим хранилищем между несколькими установками XPipe. Поддерживаются HTTP- и SSH-адреса, а также локальные директории.\nvault=Хранилище\nworkspaceLockDescription=Устанавливает пользовательский пароль для шифрования любой конфиденциальной информации, хранящейся в XPipe.\\n\\nЭто повышает безопасность, так как обеспечивает дополнительный уровень шифрования хранимой тобой конфиденциальной информации. После этого тебе будет предложено ввести пароль при запуске XPipe.\nuseSystemFontDescription=Контролирует, использовать ли системный шрифт по умолчанию или шрифт Inter, который входит в состав XPipe.\ntooltipDelay=Задержка всплывающей подсказки\ntooltipDelayDescription=Количество миллисекунд, которое нужно подождать до появления всплывающей подсказки.\nfontSize=Размер шрифта\nwindowOptions=Параметры окна\nsaveWindowLocation=Сохранить местоположение окна\nsaveWindowLocationDescription=Контролирует, нужно ли сохранять и восстанавливать координаты окна при перезагрузке.\nstartupShutdown=Запуск / выключение\nshowChildrenConnectionsInParentCategory=Показывать дочерние категории в родительской категории\nshowChildrenConnectionsInParentCategoryDescription=Включать или не включать все соединения, расположенные в подкатегориях, при выборе определенной родительской категории.\\n\\nЕсли эта опция отключена, то категории будут вести себя скорее как классические папки, в которых отображается только их непосредственное содержимое без включения вложенных папок.\ncondenseConnectionDisplay=Сжатое отображение соединения\ncondenseConnectionDisplayDescription=Сделай так, чтобы каждое соединение верхнего уровня занимало меньше места по вертикали, чтобы список соединений был более сжатым.\nopenConnectionSearchWindowOnConnectionCreation=Открыть окно поиска соединения при его создании\nopenConnectionSearchWindowOnConnectionCreationDescription=Нужно ли автоматически открывать окно для поиска доступных подсоединений при добавлении нового соединения оболочки.\nworkflow=Рабочий процесс\nsystem=Система\napplication=Приложение\nstorage=Хранилище\n#custom\nrunOnStartup=Запуск при старте\n#custom\ncloseBehaviour=Поведение при выходе из XPipe\ncloseBehaviourDescription=Управляет тем, как XPipe должен действовать после закрытия своего главного окна.\nlanguage=Язык\nlanguageDescription=Язык отображения, который нужно использовать. Переводы улучшаются благодаря вкладу сообщества. Ты можешь помочь усилиям по переводу, отправляя исправления перевода на GitHub.\n#custom\nlightTheme=Светлая тема\ndarkTheme=Темная тема\nexit=Выйти из XPipe\ncontinueInBackground=Продолжение в фоновом режиме\nminimizeToTray=Минимизировать в трей\ncloseBehaviourAlertTitle=Установить поведение при закрытии\ncloseBehaviourAlertTitleHeader=Выбери, что должно произойти при закрытии окна. Все активные соединения будут закрыты при завершении работы приложения.\nstartupBehaviour=Поведение при запуске\nstartupBehaviourDescription=Управляет поведением приложения рабочего стола по умолчанию при запуске XPipe.\nclearCachesAlertTitle=Очистить кэш\nclearCachesAlertContent=Хочешь очистить все кэши XPipe? Это удалит все данные кэша, которые хранятся для улучшения работы пользователя.\nstartGui=Запуск графического интерфейса\nstartInTray=Запуск в трее\nstartInBackground=Запуск в фоновом режиме\n#custom\nclearCaches=Очистить кэш ...\nclearCachesDescription=Удалите все данные кэша\ncancel=Отмена\nnotAnAbsolutePath=Не абсолютный путь\nnotADirectory=Не каталог\nnotAnEmptyDirectory=Не пустая директория\n#custom\nautomaticallyCheckForUpdates=Проверять наличие обновлений\nautomaticallyCheckForUpdatesDescription=Если эта функция включена, информация о новых релизах автоматически подхватывается во время работы XPipe через некоторое время. При этом тебе все равно придется явно подтверждать установку любого обновления.\nsendAnonymousErrorReports=Отправлять анонимные сообщения об ошибках\nsendUsageStatistics=Отправлять анонимную статистику использования\nstorageDirectory=Каталог хранилищ\nstorageDirectoryDescription=Место, где XPipe должен хранить всю информацию о соединениях. При его изменении данные из старой директории не копируются в новую.\nlogLevel=Уровень журнала\nappBehaviour=Поведение приложения\nlogLevelDescription=Уровень журнала, который следует использовать при записи лог-файлов.\ndeveloperMode=Режим разработчика\ndeveloperModeDescription=Когда эта функция включена, ты получишь доступ к множеству дополнительных опций, полезных для разработки.\neditor=Редактор\n#custom\ncustom=Пользовательские\npasswordManager=Менеджер паролей\nexternalPasswordManager=Внешний менеджер паролей\npasswordManagerDescription=Локально установленный менеджер паролей, с которым нужно интегрироваться.\\n\\nЕсли у тебя установлен менеджер паролей, ты можешь настроить XPipe на получение паролей из него, чтобы XPipe не приходилось хранить пароли самому. Если эта функция включена, любое поле пароля для соединения можно настроить на использование менеджера паролей.\npasswordManagerCommandTest=Тестовый менеджер паролей\npasswordManagerCommandTestDescription=Здесь ты можешь проверить, правильно ли выглядит вывод, если ты установил менеджер паролей.\npreferTerminalTabs=Предпочитает открывать новые вкладки\npreferTerminalTabsDescription=Контролирует, будет ли XPipe пытаться открывать новые вкладки в выбранном тобой терминале вместо новых окон. Не каждый терминал поддерживает вкладки.\ncustomRdpClientCommand=Пользовательская команда\ncustomRdpClientCommandDescription=Команда, которую нужно выполнить, чтобы запустить пользовательский RDP-клиент.\\n\\nСтрока-заполнитель $FILE при вызове будет заменена на заключенное в кавычки абсолютное имя файла .rdp. Не забудь взять в кавычки путь к исполняемому файлу, если он содержит пробелы.\ncustomEditorCommand=Пользовательская команда редактора\ncustomEditorCommandDescription=Команда, которую нужно выполнить, чтобы запустить пользовательский редактор.\\n\\nСтрока-заполнитель $FILE при вызове будет заменена абсолютным именем файла, заключенным в кавычки. Не забудь взять в кавычки путь к исполняемому файлу редактора, если он содержит пробелы.\neditorReloadTimeout=Таймаут перезагрузки редактора\neditorReloadTimeoutDescription=Количество миллисекунд, которое нужно подождать перед чтением файла после его обновления. Это позволяет избежать проблем в тех случаях, когда твой редактор медленно записывает или снимает блокировки файлов.\nencryptAllVaultData=Зашифруй все данные хранилища\nencryptAllVaultDataDescription=Если эта функция включена, каждая часть данных соединения хранилища будет зашифрована твоим ключом шифрования, а не только секреты, содержащиеся в этих данных. Это добавляет еще один уровень безопасности для других параметров, таких как имена пользователей, имена хостов и т.д., которые по умолчанию не шифруются в хранилище.\\n\\nЭта опция сделает историю git-хранилища и дифы бесполезными, так как ты больше не сможешь увидеть оригинальные изменения, только бинарные.\nvaultSecurity=Защита хранилища\ndeveloperDisableUpdateVersionCheck=Отключить проверку версий обновлений\ndeveloperDisableUpdateVersionCheckDescription=Контролирует, будет ли программа проверки обновлений игнорировать номер версии при поиске обновления.\ndeveloperDisableGuiRestrictions=Отключить ограничения графического интерфейса\ndeveloperDisableGuiRestrictionsDescription=Контролирует, могут ли некоторые отключенные действия по-прежнему выполняться из пользовательского интерфейса.\ndeveloperShowHiddenEntries=Показать скрытые записи\ndeveloperShowHiddenEntriesDescription=Если включить эту функцию, будут показаны скрытые и внутренние источники данных.\ndeveloperShowHiddenProviders=Показать скрытых провайдеров\ndeveloperShowHiddenProvidersDescription=Контролирует, будут ли в диалоге создания показываться скрытые и внутренние провайдеры соединений и источников данных.\ndeveloperDisableConnectorInstallationVersionCheck=Отключить проверку версии коннектора\ndeveloperDisableConnectorInstallationVersionCheckDescription=Контролирует, будет ли программа проверки обновлений игнорировать номер версии при проверке версии коннектора XPipe, установленного на удаленной машине.\nshellCommandTest=Тест на знание команд оболочки\nshellCommandTestDescription=Выполни команду в сессии оболочки, используемой внутри XPipe.\nterminal=Терминал\nterminalType=Эмулятор терминала\nterminalConfiguration=Конфигурация терминала\nterminalCustomization=Настройка терминала\neditorConfiguration=Конфигурация редактора\ndefaultApplication=Приложение по умолчанию\ninitialSetup=Начальная настройка\nterminalTypeDescription=Терминал по умолчанию, который используется для открытия shell-соединений.\\n\\nУровень поддержки разных функций зависит от терминала, и каждый из них помечен как рекомендуемый или не рекомендуемый. Твой пользовательский опыт будет наилучшим при использовании рекомендуемого терминала.\nprogram=Программа\ncustomTerminalCommand=Пользовательская команда терминала\ncustomTerminalCommandDescription=Команда, которую нужно выполнить, чтобы открыть пользовательский терминал с заданной командой.\\n\\nXPipe создаст временный скрипт оболочки запуска для твоего терминала, который будет выполняться. Строка-заполнитель $CMD в команде, которую ты предоставишь, при вызове будет заменена реальным скриптом запуска. Не забудь взять в кавычки путь к исполняемому файлу твоего терминала, если он содержит пробелы.\nclearTerminalOnInit=Очистить терминал при запуске\nclearTerminalOnInitDescription=Если эта функция включена, то после запуска новой терминальной сессии XPipe будет выполнять команду clear, чтобы удалить ненужный вывод, который был напечатан при запуске терминальной сессии.\ndontCachePasswords=Не кэшируй введенные пароли\ndontCachePasswordsDescription=Контролирует, нужно ли кэшировать запрашиваемые пароли внутри XPipe, чтобы тебе не пришлось вводить их снова в текущей сессии.\\n\\nЕсли это поведение отключено, тебе придется заново вводить запрашиваемые учетные данные каждый раз, когда они потребуются системе.\ndenyTempScriptCreation=Запрет на создание временных скриптов\ndenyTempScriptCreationDescription=Для реализации некоторых своих функций XPipe иногда создает временные shell-скрипты на целевой системе, чтобы обеспечить легкое выполнение простых команд. Они не содержат никакой конфиденциальной информации и создаются просто в целях реализации.\\n\\nЕсли отключить это поведение, XPipe не будет создавать никаких временных файлов на удаленной системе. Эта опция полезна в условиях повышенной безопасности, когда отслеживается каждое изменение файловой системы. Если эта опция отключена, некоторые функции, например, окружения оболочки и скрипты, не будут работать так, как задумано.\ndisableCertutilUse=Отключить использование certutil в Windows\nuseLocalFallbackShell=Использовать локальную резервную оболочку\nuseLocalFallbackShellDescription=Переключись на использование другой локальной оболочки для выполнения локальных операций. Это может быть PowerShell в Windows и bourne shell в других системах.\\n\\nЭту опцию можно использовать в том случае, если обычная локальная оболочка по умолчанию отключена или в какой-то степени сломана. Однако при включении этой опции некоторые функции могут работать не так, как ожидалось.\ndisableCertutilUseDescription=Из-за ряда недостатков и ошибок в cmd.exe временные shell-скрипты создаются с помощью certutil, используя его для декодирования ввода base64, так как cmd.exe ломается при вводе не ASCII. XPipe также может использовать для этого PowerShell, но это будет медленнее.\\n\\nТаким образом, на Windows-системах отменяется использование certutil для реализации некоторой функциональности, и вместо него используется PowerShell. Это может порадовать некоторые антивирусы, так как некоторые из них блокируют использование certutil.\ndisableTerminalRemotePasswordPreparation=Отключить подготовку удаленного пароля терминала\ndisableTerminalRemotePasswordPreparationDescription=В ситуациях, когда в терминале необходимо установить удаленное shell-соединение, проходящее через несколько промежуточных систем, может возникнуть необходимость подготовить все необходимые пароли на одной из промежуточных систем, чтобы обеспечить автоматическое заполнение любых подсказок.\\n\\nЕсли ты не хочешь, чтобы пароли когда-либо передавались в какую-либо промежуточную систему, ты можешь отключить это поведение. Тогда любой требуемый промежуточный пароль будет запрашиваться в самом терминале при его открытии.\nmore=Подробнее\ntranslate=Переводы\nallConnections=Все соединения\nallScripts=Все скрипты\n#custom\nallIdentities=Все учетные данные\n#custom\nsynced=Синхронизируемые\npredefined=Предопределенный\nsamples=Образцы\ngoodMorning=Доброе утро\ngoodAfternoon=Добрый день\ngoodEvening=Добрый вечер\naddVisual=Visual ...\n#custom\naddDesktop=Рабочий стол ...\nssh=SSH\nsshConfiguration=Конфигурация SSH\nsize=Размер\nattributes=Атрибуты\nmodified=Изменено\nowner=Владелец\nupdateReadyTitle=Обновление на $VERSION$ готово\ntemplates=Шаблоны\n#custom\nretry=Повторить\n#custom\nretryAll=Повторите всё\n#custom\nreplace=Заменить\nreplaceAll=Заменить все\nhibernateBehaviour=Поведение в спящем режиме\nhibernateBehaviourDescription=Управляет поведением приложения, когда твоя система переходит в гибернацию/в спящий режим.\noverview=Обзор\nhistory=История\nskipAll=Пропустить все\nnotes=Заметки\n#custom\naddNotes=Добавить заметку\n#custom\norder=Порядок\n#custom\nkeepFirst=Установить первым\n#custom\nkeepLast=Установить последним\n#custom\npinToTop=Закрепить сверху\n#custom\nunpinFromTop=Открепить сверху\norderAheadOf=Заказать заранее ...\nclearIndex=Индекс сброса\nhttpServer=HTTP-сервер\nmcpServer=MCP-сервер\napiKey=Ключ API\napiKeyDescription=API-ключ для аутентификации API-запросов демона XPipe. Подробнее о том, как проходить аутентификацию, читай в общей документации по API.\ndisableApiAuthentication=Отключить аутентификацию API\ndisableApiAuthenticationDescription=Отключает все необходимые методы аутентификации, так что любой неаутентифицированный запрос будет обработан.\\n\\nАутентификацию следует отключать только в целях разработки.\napi=API\nstoreIntroImportContent=Уже используешь XPipe на другой системе? Синхронизируй существующие соединения на нескольких системах через удаленный git-репозиторий. Ты также можешь синхронизировать позже в любой момент, если он еще не настроен.\nstoreIntroImportButton=Синхронизируй соединения ...\nstoreIntroImportHeader=Импортировать соединения\nshowNonRunningChildren=Показать неработающих детей\nhttpApi=HTTP API\nisOnlySupportedLimit=поддерживается только профессиональной лицензией при наличии более $COUNT$ соединений\nareOnlySupportedLimit=поддерживаются только профессиональной лицензией при наличии более чем $COUNT$ соединений\nenabled=Включено\nenableGitStoragePtbDisabled=Git-синхронизация отключена для публичных тестовых сборок, чтобы предотвратить их использование с обычными релизными git-репозиториями и не допустить использования PTB-сборки в качестве ежедневного драйвера.\ncopyId=Копирование идентификатора API\nrequireDoubleClickForConnections=Требуется двойной щелчок для подключения\nrequireDoubleClickForConnectionsDescription=Если эта опция включена, тебе придется дважды щелкнуть по соединениям, чтобы запустить их. Это полезно, если ты привык все запускать двойным щелчком.\nclearTransferDescription=Четкий выбор\nselectTab=Выберите вкладку\ncloseTab=Закройте вкладку\ncloseOtherTabs=Закрыть другие вкладки\ncloseAllTabs=Закрыть все вкладки\ncloseLeftTabs=Закрыть вкладки слева\ncloseRightTabs=Закрывать вкладки справа\naddSerial=Последовательное ...\n#custom\nconnect=Соединение\nworkspaces=Рабочие пространства\nmanageWorkspaces=Управляй рабочими пространствами\naddWorkspace=Добавь рабочее пространство ...\nworkspaceAdd=Добавьте новое рабочее пространство\nworkspaceAddDescription=Рабочие пространства - это отдельные конфигурации для запуска XPipe. В каждом рабочем пространстве есть каталог данных, где все данные хранятся локально. Сюда входят данные о соединениях, настройки и многое другое.\\n\\nЕсли ты используешь функцию синхронизации, ты также можешь выбрать синхронизацию каждого рабочего пространства с отдельным git-репозиторием.\nworkspaceName=Имя рабочей области\nworkspaceNameDescription=Отображаемое имя рабочей области\nworkspacePath=Путь к рабочему пространству\nworkspacePathDescription=Расположение каталога данных рабочей области\nworkspaceCreationAlertTitle=Создание рабочего пространства\ndeveloperForceSshTty=Принудительный SSH TTY\ndeveloperForceSshTtyDescription=Заставь все SSH-соединения выделять pty, чтобы проверить поддержку отсутствующего stderr и pty.\ndeveloperDisableSshTunnelGateways=Отключите туннелирование шлюза SSH\ndeveloperDisableSshTunnelGatewaysDescription=Не используй туннельные сессии для шлюзов и вместо этого подключайся напрямую к системе.\nttyWarning=Соединение принудительно выделило pty/tty и не предоставляет отдельный поток stderr.\\n\\nЭто может привести к нескольким проблемам.\\n\\nЕсли можешь, попробуй сделать так, чтобы команда подключения не выделяла pty.\nxshellSetup=Настройка Xshell\n#custom\ntermiusSetup=Настройка Termius\ntryPtbDescription=Опробуй новые функции на ранней стадии в сборках разработчиков XPipe\n#custom\nconfirmVaultUnencryptTitle=Подтверждение не зашифрованного хранилища\nconfirmVaultUnencryptContent=Ты действительно хочешь отключить расширенное шифрование хранилища? Это уберет дополнительное шифрование хранимых данных и перезапишет существующие данные.\nenableHttpApi=Включить HTTP API\nenableHttpApiDescription=Включает API, позволяя внешним программам вызывать демона XPipe для выполнения действий с твоими управляемыми соединениями.\nchooseCustomIcon=Выберите пользовательскую иконку\ngitVault=Гит-хранилище\nfileBrowser=Браузер файлов\n#custom\nconfirmAllDeletions=Подтверждение для удаления\nconfirmAllDeletionsDescription=Показывать ли диалог подтверждения для всех операций удаления. По умолчанию подтверждение требуется только для каталогов.\nyesterday=Вчера\ngreen=Зеленый\nyellow=Желтый\nblue=Синий\nred=Красный\ncyan=Cyan\npurple=Фиолетовый\nasktextAlertTitle=Prompt\nfileWriteSudoTitle=Запись файла Sudo\nfileWriteSudoContent=Файл, который ты пытаешься записать, не дает прав на запись твоему пользователю. Хочешь ли ты записать этот файл от имени root с помощью sudo? Это автоматически повысит права до root либо с помощью существующих учетных данных, либо через приглашение.\ndontAllowTerminalRestart=Не разрешайте перезагрузку терминала\ndontAllowTerminalRestartDescription=По умолчанию терминальные сессии могут быть перезапущены после их завершения изнутри терминала. Чтобы разрешить это, XPipe будет принимать такие внешние запросы от терминала, чтобы снова запустить сессию\\n\\nXPipe не имеет никакого контроля над терминалом и тем, откуда поступает этот вызов, поэтому вредоносные локальные приложения могут использовать эту функциональность и для запуска соединений через XPipe. Отключение этой функциональности предотвращает подобный сценарий.\n#custom\nopenDocumentation=Открыть документацию\nopenDocumentationDescription=Посетите страницу документации XPipe по этому вопросу\nrenameAll=Переименовать все\nlogging=Ведение журнала\nenableTerminalLogging=Включить ведение журнала терминала\nenableTerminalLoggingDescription=Включает ведение журнала на стороне клиента для всех терминальных сессий. Все входы и выходы терминальной сессии записываются в файл журнала сессии. Обрати внимание, что любая конфиденциальная информация вроде запросов пароля не записывается.\nterminalLoggingDirectory=Журналы терминальных сессий\nterminalLoggingDirectoryDescription=Все журналы хранятся в директории данных XPipe в твоей локальной системе.\nopenSessionLogs=Открытые журналы сеансов\nsessionLogging=Ведение журнала терминала\nsessionActive=Для этого соединения запущена фоновая сессия.\\n\\nЧтобы остановить эту сессию вручную, щелкни по индикатору состояния.\n#custom\nskipValidation=Пропустить проверку\nscriptsIntroHeader=О скриптах\nscriptsIntroContent=Ты можешь запускать скрипты в shell init, в браузере файлов и по требованию. Ты можешь сам создавать скрипты в XPipe или импортировать существующие из своей локальной системы или из удаленного git-репозитория.\nscriptsIntroBottomHeader=Использование скриптов\nscriptsIntroBottomContent=Для начала есть множество примеров скриптов. Ты можешь нажать на кнопку редактирования отдельных скриптов, чтобы посмотреть, как они реализованы. Сначала скрипты нужно включить, чтобы они запускались и отображались в меню, для этого в каждом скрипте есть тумблер.\nscriptsIntroBottomButton=Приступай к работе\nscriptSourcesIntroHeader=Источники скриптов\nscriptSourcesIntroContent=Ты можешь добавить пользовательские источники скриптов, чтобы иметь мгновенный доступ к целой коллекции shell-скриптов. В качестве источников поддерживаются как локальные источники, так и удаленные git-репозитории. Все обнаруженные скрипты из источника станут доступны автоматически.\nscriptSourcesIntroButton=Добавь источник ...\ncheckForSecurityUpdates=Проверьте наличие обновлений безопасности\ncheckForSecurityUpdatesDescription=XPipe может проверять потенциальные обновления безопасности отдельно от обычных обновлений функций. Когда эта функция включена, по крайней мере важные обновления безопасности будут рекомендованы к установке, даже если обычная проверка обновлений отключена.\\n\\nОтключение этой настройки приведет к тому, что внешний запрос версии не будет выполняться, и ты не будешь получать уведомления о каких-либо обновлениях безопасности.\nclickToDock=Нажмите, чтобы пристыковать терминал\nterminalStarting=Ожидание запуска терминала ...\n#custom\npinTab=Закрепить вкладку\nunpinTab=Открепить вкладку\n#custom\npinned=Закреплена\nenableConnectionHubTerminalDocking=Включить стыковку терминала с концентратором\nenableConnectionHubTerminalDockingDescription=Ты можешь пристыковать окна терминалов к окну приложения XPipe в хабе подключения, чтобы имитировать некий интегрированный терминал. При этом окна терминала управляются XPipe, чтобы всегда помещаться в док.\nenableFileBrowserTerminalDocking=Включение терминальной стыковки браузера файлов\nenableFileBrowserTerminalDockingDescription=Ты можешь пристыковать окна терминалов к окну приложения XPipe в браузере файлов, чтобы имитировать некий интегрированный терминал. Затем окна терминала управляются XPipe, чтобы всегда помещаться в док.\ndownloadsDirectory=Пользовательский каталог загрузок\ndownloadsDirectoryDescription=Пользовательская директория, в которую будут помещаться скачанные файлы при нажатии на кнопку перемещения в загрузки. По умолчанию XPipe будет использовать твой пользовательский каталог загрузок.\npinLocalMachineOnStartup=Закрепить вкладку локальной машины при запуске\npinLocalMachineOnStartupDescription=Автоматически открывай вкладку локальной машины и закрепляй ее. Это полезно, если ты часто используешь разделенный файловый браузер с открытыми локальной машиной и удаленной файловой системой.\nterminalErrorDescription=Эта ошибка является терминальной, и XPipe не сможет продолжить работу без ее устранения.\ngroupName=Название группы\nchmodPermissions=Новые разрешения\neditFilesWithDoubleClick=Редактирование файлов с помощью двойного клика\neditFilesWithDoubleClickDescription=Когда эта функция включена, двойной щелчок по файлам будет сразу открывать их в твоем текстовом редакторе, а не показывать контекстное меню.\ncensorMode=Режим цензуры\ncensorModeDescription=Размывает любую информацию, например, имена хостов, имена пользователей, названия соединений и прочее.\\n\\nЭто полезно, если ты собираешься сделать скриншот или скриншотер XPipe и не хочешь слить какую-либо информацию.\naddIdentity=Идентификация ...\nidentities=Идентификаторы\naddMacro=Действие ...\nidentitiesIntroHeader=Об идентификации\nidentitiesIntroContent=Если ты часто используешь комбинации имен пользователей, паролей и ключей, то, возможно, имеет смысл создать многоразовые идентификаторы. Это позволит тебе быстро ссылаться на них при добавлении новых соединений.\nidentitiesIntroBottomHeader=Совместное использование идентификационных данных\nidentitiesIntroBottomContent=Ты можешь добавлять идентификаторы локально, а также синхронизировать их в git-репозитории, если эта функция включена. Это позволяет выборочно делиться идентификаторами в нескольких системах и с другими членами команды.\nidentitiesIntroBottomButton=Синхронизация настроек\nidentitiesIntroButton=Создать идентификатор\nuserName=Имя пользователя\nuserAuth=Аутентификация по паролю на основе пользователя\ngroupAuth=Секретная аутентификация на основе группы\nteam=Команда\nteamSettings=Настройки команды\nteamVaults=Командные сейфы\nvaultTypeNameDefault=Хранилище по умолчанию\nvaultTypeNameLegacy=Наследственное личное хранилище\nvaultTypeNamePersonal=Личное хранилище\nvaultTypeNameTeam=Командное хранилище\nteamVaultsDescription=Командные хранилища позволяют нескольким пользователям и группам иметь безопасный доступ к общему хранилищу. Ты можешь настроить соединения и идентификаторы так, чтобы они были общими для всех пользователей или были доступны только отдельным пользователям и группам, зашифровав их собственным ключом. Другие пользователи хранилища не смогут получить доступ к персональным и групповым соединениям и идентификаторам, если у них нет доступа к ключу.\nvaultTypeContentDefault=В данный момент ты используешь хранилище по умолчанию, в котором не задан пользователь и пользовательская парольная фраза. Секреты шифруются с помощью локального ключа хранилища. Ты можешь перейти на персональное хранилище, создав учетную запись пользователя хранилища. Это позволит тебе шифровать секреты хранилища своей личной парольной фразой, которую ты должен вводить при каждом входе в систему, чтобы разблокировать хранилище.\nvaultTypeContentLegacy=Сейчас ты используешь унаследованное личное хранилище для своего пользователя. Секреты шифруются с помощью твоей личной парольной фразы. Эта устаревшая совместимость имеет ограниченные возможности и не может быть обновлена до командного хранилища на месте.\nvaultTypeContentPersonal=Сейчас ты используешь персональное хранилище для своего пользователя. Секреты шифруются твоей личной парольной фразой. Ты можешь перейти на командное хранилище, добавив дополнительных пользователей хранилища или добавив конфигурацию доступа на основе группы.\nvaultTypeContentTeam=Сейчас ты используешь командное хранилище, которое позволяет нескольким пользователям иметь безопасный доступ к общему хранилищу. Ты можешь настроить соединения и идентификаторы так, чтобы они были общими для всех пользователей или были доступны только твоему личному пользователю или группе, зашифровав их своим личным или групповым ключом. Другие пользователи хранилища не смогут получить доступ к твоим личным и групповым соединениям и идентификаторам, если у них нет доступа к ключу.\ngroupManagement=Управление группой\ngroupManagementEmpty=Управление группой\ngroupManagementDescription=Управляй существующими группами хранилищ или создавай новые. Каждая группа хранилищ имеет свой индивидуальный секретный ключ, который используется для шифрования соединений и личных данных, которые должны быть доступны только этой группе и не доступны другим.\ngroupManagementEmptyDescription=Управляй существующими группами хранилищ или создавай новые. Каждая группа хранилищ имеет свой индивидуальный секретный ключ, который используется для шифрования соединений и личных данных, которые должны быть доступны только этой группе и не доступны другим.\\n\\nГрупповые аккаунты для команды поддерживаются в профессиональном плане.\nuserManagement=Управление пользователями\nuserManagementEmpty=Управление пользователями\nuserManagementDescription=Управляй существующими пользователями хранилища или создавай новых. Каждый пользователь хранилища имеет свой индивидуальный пароль, который используется для шифрования соединений и личных данных, которые должны быть доступны только ему, но не другим.\nuserManagementEmptyDescription=Управляй существующими пользователями хранилища или создавай новых. Каждый пользователь хранилища имеет свой индивидуальный пароль, который используется для шифрования соединений и идентификации, которые должны быть доступны только ему, но не другим. Создай пользователя для себя, чтобы иметь возможность шифровать соединения и идентификационные данные своим личным ключом.\\n\\nВ версии для сообщества поддерживается одна учетная запись пользователя. В профессиональном плане поддерживается несколько учетных записей пользователей для команды.\nuserIntroHeader=Управление пользователями\nuserIntroContent=Создай для себя первую учетную запись пользователя, чтобы начать работу. Это позволит тебе заблокировать это рабочее пространство паролем.\naddReusableIdentity=Добавьте многоразовую идентификацию\nusers=Пользователи\nsyncVault=Синхронизация хранилища\nsyncVaultDescription=Чтобы синхронизировать свое хранилище с несколькими системами или с несколькими членами команды, включи синхронизацию git для этого хранилища.\nenableGitSync=Включить синхронизацию git\nbrowseVault=Хранилище данных\nbrowseVaultDescription=Ты можешь сам заглянуть в каталог хранилища в своем родном файловом менеджере. Учти, что внешние правки не рекомендуются и могут вызвать различные проблемы.\nbrowseVaultButton=Просматривай хранилище\nvaultUsers=Пользователи хранилища\ncreateHeapDump=Создайте дамп кучи\ncreateHeapDumpDescription=Сбрасывать содержимое памяти в файл, чтобы устранить неполадки с использованием памяти\ninitializingApp=Загрузочные соединения\ncheckingLicense=Проверка лицензии\n#custom\nloadingGit=Синхронизация с git ...\nloadingGpg=Запуск демона GnuPG для git\nloadingSettings=Настройки загрузки\nloadingConnections=Загрузочные соединения\nunlockingVault=Отпирающееся хранилище\nloadingUserInterface=Загружаемый пользовательский интерфейс\nptbNotice=Уведомление для публичной тестовой сборки\nuserDeletionTitle=Удаление пользователя\nuserDeletionContent=Хочешь удалить этого пользователя хранилища? Это позволит заново зашифровать все твои личные данные и секреты соединения с помощью ключа хранилища, который доступен всем пользователям. Это займет некоторое время, и XPipe перезапустится, чтобы применить изменения пользователя.\ngroupDeletionTitle=Удаление группы\ngroupDeletionContent=Хочешь удалить эту группу хранилища? Это приведет к повторному шифрованию всех идентификационных данных и секретов соединений, доступных только для группы, с помощью ключа хранилища, который доступен всем пользователям. Это займет некоторое время, и XPipe перезапустится, чтобы применить изменения в группе.\nkillTransfer=Убить передачу\ndestination=Назначение\nconfiguration=Конфигурация\nnewFile=Новый файл\nnewLink=Новая ссылка\nlinkName=Название ссылки\nscanConnections=Найдите доступные соединения ...\n#custom\nobserve=Начать наблюдение\n#custom\nstopObserve=Прекратить наблюдать\ncreateShortcut=Создать ярлык на рабочем столе\nbrowseFiles=Просмотр файлов\nclone=Клон\ntargetPath=Целевой путь\nnewDirectory=Новый каталог\ncopyShareLink=Копировать ссылку\nselectStore=Выберите магазин\nsaveSource=Сохранить на потом\nexecute=Выполните\ndeleteChildren=Удалите всех детей\nscriptGroupDescriptionDescription=Дайте этой группе дополнительное описание\nabstractHostDescriptionDescription=Дайте этому хосту дополнительное описание\nselectSource=Выберите источник\ncommandLineRead=Обновление\ncommandLineWrite=Напиши\nadditionalOptions=Дополнительные опции\ninput=Вход\nmachine=Машина\nopen=Открыть\nedit=Редактировать\nscriptContents=Содержание скрипта\nscriptContentsDescription=Команды скрипта для выполнения\nsnippets=Зависимости скриптов\nsnippetsDescription=Другие скрипты, которые нужно запустить первыми\nsnippetsDependenciesDescription=Все возможные скрипты, которые следует запустить, если это необходимо\nisDefault=Запускается на init во всех совместимых оболочках\nbringToShells=Принеси всем совместимым оболочкам\nisDefaultGroup=Запустить все групповые скрипты в shell init\nexecutionType=Тип исполнения\nexecutionTypeDescription=В каких контекстах использовать этот скрипт\nminimumShellDialect=Тип оболочки\nminimumShellDialectDescription=Тип оболочки, в которой нужно запустить этот скрипт\ndumbOnly=Тупой\nterminalOnly=Терминал\nboth=Оба\n#custom\nshouldElevate=Повышать привилегии\nshouldElevateDescription=Нужно ли запускать этот скрипт с повышенными правами\nscript.displayName=Скрипт оболочки\nscript.displayDescription=Создание многократно используемого сценария оболочки\nscriptGroup.displayName=Группа сценариев\nscriptGroup.displayDescription=Группируй скрипты вместе и организуй их внутри\nscriptGroup=Группа\nscriptGroupDescription=Группа, которой нужно назначить этот скрипт\nscriptGroupGroupDescription=Необязательная родительская группа, которой нужно назначить эту группу сценариев\nopenInNewTab=Открыть в новой вкладке\nexecuteInBackground=в фоновом режиме\nexecuteInTerminal=в $TERM$\n#custom\nback=Назад\nbrowseInWindowsExplorer=Обзор в проводнике Windows\nbrowseInDefaultFileManager=Обзор в файловом менеджере по умолчанию\nbrowseInFinder=Обзор в программе поиска\n#custom\ncopy=Копировать\npaste=Вставить\n#custom\ncopyLocation=Копировать путь\nabsolutePaths=Абсолютные пути\nabsoluteLinkPaths=Абсолютные пути ссылок\nabsolutePathsQuoted=Абсолютные пути с кавычками\nfileNames=Имена файлов\nlinkFileNames=Имена файлов ссылок\nfileNamesQuoted=Имена файлов (в кавычках)\ndeleteFile=Удалить $FILE$\neditWithEditor=Редактируй с $EDITOR$\nfollowLink=Перейдите по ссылке\n#custom\ngoForward=Вперед\nshowDetails=Показать подробности\nshowDetailsDescription=Показать трассировку стека ошибки\nopenFileWith=Открой с помощью ...\nopenWithDefaultApplication=Открыть с помощью приложения по умолчанию\nrename=Переименовать\nrun=Запустите\nopenInTerminal=Открыть в терминале\nfile=Файл\ndirectory=Каталог\nsymbolicLink=Символическая ссылка\ndesktopEnvironment.displayName=Окружение рабочего стола\ndesktopEnvironment.displayDescription=Создайте многоразовую конфигурацию среды удаленного рабочего стола\ndesktopHost=Хост рабочего стола\ndesktopHostDescription=Подключение к рабочему столу, которое будет использоваться в качестве базового\ndesktopShellDialect=Диалект оболочки\ndesktopShellDialectDescription=Диалект оболочки, который используется для запуска скриптов и приложений\ndesktopSnippets=Фрагменты сценария\ndesktopSnippetsDescription=Список многократно используемых фрагментов скриптов, которые нужно запустить первыми\ndesktopInitScript=Начальный скрипт\ndesktopInitScriptDescription=Начальные команды, специфичные для этой среды\ndesktopTerminal=Терминальное приложение\ndesktopTerminalDescription=Терминал, который нужно использовать на рабочем столе для запуска скриптов\ndesktopApplication.displayName=Приложение для рабочего стола\ndesktopApplication.displayDescription=Запустить приложение на удаленном рабочем столе\ndesktopBase=Рабочий стол\ndesktopBaseDescription=Рабочий стол, на котором будет запущено это приложение\ndesktopEnvironmentBase=Окружение рабочего стола\ndesktopEnvironmentBaseDescription=Среда рабочего стола, на котором будет запущено это приложение\ndesktopApplicationPath=Путь к приложению\ndesktopApplicationPathDescription=Путь к исполняемому файлу, который нужно запустить\ndesktopApplicationArguments=Аргументы\ndesktopApplicationArgumentsDescription=Необязательные аргументы, которые нужно передать приложению\ndesktopCommand.displayName=Команда рабочего стола\ndesktopCommand.displayDescription=Выполнить команду в среде удаленного рабочего стола\ndesktopCommandScript=Команды\ndesktopCommandScriptDescription=Команды для запуска в среде\nservice.displayName=Сервис\nservice.displayDescription=Перенаправить удаленный сервис на локальную машину\nserviceLocalPort=Явный локальный порт\nserviceLocalPortDescription=Локальный порт для переадресации, в противном случае используется случайный порт\nserviceRemotePort=Удаленный порт\nserviceRemotePortDescription=Порт, на котором работает служба\nserviceHost=Сервисный хост\nserviceHostDescription=Хост, на котором запущена служба\n#custom\nopenWebsite=Открыть сайт\n#custom\ncustomServiceGroup.displayName=Группа сервисов\ncustomServiceGroup.displayDescription=Сгруппируйте несколько сервисов в одну категорию\ninitScript=Init script - скрипт, запускаемый при инициализации оболочки\nshellScript=Скрипт сеанса оболочки - сделать скрипт доступным для выполнения во время сеанса оболочки\nrunnableScript=Запускаемый скрипт - позволяет запускать скрипт прямо из концентратора соединений\nfileScript=Файловый скрипт - позволяет вызывать скрипт для выбранных файлов в файловом браузере\nrunScript=Запуск скрипта\ncopyUrl=Копировать URL\n#custom\nfixedServiceGroup.displayName=Группа сервисов\nfixedServiceGroup.displayDescription=Список доступных сервисов в системе\nmappedService.displayName=Сервис\nmappedService.displayDescription=Взаимодействовать с сервисом, открываемым контейнером\ncustomService.displayName=Сервис\ncustomService.displayDescription=Автоматическое открытие или туннелирование порта удаленного сервиса на твоей локальной машине\nfixedService.displayName=Сервис\nfixedService.displayDescription=Использовать предопределенный сервис\nnoServices=Нет доступных сервисов\nhasServices=$COUNT$ доступные сервисы\nhasService=$COUNT$ доступный сервис\nnoConnections=Нет доступных соединений\nhasConnections=$COUNT$ доступные соединения\nhasConnection=$COUNT$ доступное соединение\nopenHttp=Открытый HTTP-сервис\nopenHttps=Открытая служба HTTPS\nnoScriptsAvailable=Отсутствие включенных и совместимых скриптов\nscriptsDisabled=Скрипты отключены\nchangeIcon=Значок изменения\ninit=Init\nshell=Оболочка\nhub=Хаб\n#custom\nscript=Скрипт\ngenericScript=Generic\ngradleTasks=Задачи Gradle\nrunTask=Выполнить задание\narchiveName=Название архива\ncompress=Сжать\ncompressContents=Сжать содержимое\n#custom\nuntarHere=Разархивировать здесь\n#custom\nuntarDirectory=Разархивировать к $DIR$\nunzipDirectory=Разархивировать в $DIR$\n#custom\nunzipHere=Разархивировать здесь\nrequiresRestart=Требует перезапуска для применения.\ndownload=Скачать\nservicePath=Сервисный путь\nservicePathDescription=Необязательный подпуть при открытии URL в браузере\nactive=Активный\ninactive=Неактивный\nstarting=Начало\nremotePort=Удаленный порт\nremotePortNumber=Удаленный порт $PORT$\nuserIdentity=Личная идентификация\n#custom\nglobalIdentity=Глобальная идентификация\nidentityChoice=Идентификация пользователя\nidentityChoiceDescription=Выберите предопределенный идентификатор или укажите данные для входа в систему только для этого соединения\ndefineNewIdentityOrSelect=Введите новый или выберите существующий\n#custom\nlocalIdentity.displayName=Локальная идентификация\nlocalIdentity.displayDescription=Создайте многоразовую идентификацию для этого локального рабочего стола\n#custom\nsyncedIdentity.displayName=Синхронизированная идентификация\nsyncedIdentity.displayDescription=Создай многоразовую идентификацию, которая синхронизируется между системами\n#custom\nlocalIdentity=Локальная идентификация\nkeyNotSynced=Ключевой файл еще не синхронизирован с git-репозиторием. Чтобы добавить его, воспользуйся кнопкой add to git для ключевого файла.\nusernameDescription=Имя пользователя, под которым нужно войти в систему\nidentity.displayName=Идентификация\nidentity.displayDescription=Создайте многоразовую идентификацию для соединений\n#custom\nlocal=Локальные\n#custom\nshared=Глобальные\nuserDescription=Имя пользователя или предопределенный идентификатор для входа в систему\nidentityAccessLevel=Уровень доступа\nidentityPerUser=Доступ к персональной идентификации\nidentityPerUserDescription=Ограничь доступ к этому идентификатору и связанным с ним соединениям только для своего пользователя хранилища\nidentityPerUserDisabled=Доступ к персональным данным (отключен)\nidentityPerUserDisabledDescription=Ограничь доступ к этому идентификатору и связанным с ним соединениям только для своего пользователя хранилища (требуется настройка команды)\nidentityPerGroup=Доступ к идентификационным данным только для группы\nidentityPerGroupDescription=Ограничь доступ к этому идентификатору и связанным с ним соединениям только для этой группы хранилища\nlibrary=Библиотека\nlocation=Расположение\nkeyAuthentication=Аутентификация на основе ключей\nkeyAuthenticationDescription=Метод аутентификации, который нужно использовать, если требуется аутентификация на основе ключей\nlocationDescription=Путь к файлу, в котором находится твой соответствующий закрытый ключ\n#custom\nkeyFile=Файл ключа\n#custom\nkeyPassword=Парольная фраза\nkey=Ключ\nyubikeyPiv=Yubikey PIV\npageant=Pageant\ngpgAgent=Агент GPG\ncustomPkcs11Library=Пользовательская библиотека PKCS#11\nsshAgent=Агент OpenSSH\nnone=Нет\nindex=Индекс ...\notherExternal=Другой внешний агент\nsync=Синхронизация\nvaultSync=Синхронизация хранилища\ncustomUsername=Имя пользователя\ncustomUsernameDescription=Дополнительный альтернативный пользователь, под которым можно войти в систему\ncustomUsernamePassword=Пароль\ncustomUsernamePasswordDescription=Пароль пользователя, который нужно использовать, когда требуется аутентификация sudo\nshowInternalPods=Показать внутренние капсулы\nshowAllNamespaces=Показать все пространства имен\nshowInternalContainers=Показать внутренние контейнеры\nrefresh=Обновить\nvmwareGui=Запуск графического интерфейса\nmonitorVm=Монитор ВМ\naddCluster=Добавить кластер ...\nshowNonRunningInstances=Показать неработающие экземпляры\nvmwareGuiDescription=Запускать ли виртуальную машину в фоновом режиме или в окне.\nvmwareEncryptionPassword=Пароль шифрования\nvmwareEncryptionPasswordDescription=Необязательный пароль, используемый для шифрования виртуальной машины.\nvmPasswordDescription=Необходимый пароль для гостевого пользователя.\nvmPassword=Пароль пользователя\nvmUser=Гость-пользователь\nrunTempContainer=Запуск временного контейнера\nvmUserDescription=Имя пользователя вашего основного гостевого пользователя\ndockerTempRunAlertTitle=Запуск временного контейнера\ndockerTempRunAlertHeader=Это запустит процесс оболочки во временном контейнере, который будет автоматически удален после его остановки.\nimageName=Название изображения\nimageNameDescription=Идентификатор образа контейнера для использования\ncontainerName=Название контейнера\ncontainerNameDescription=Необязательное пользовательское имя контейнера\nvm=Виртуальная машина\nvmDescription=Связанный с ним файл конфигурации.\nvmwareScan=Гипервизоры VMware для настольных компьютеров\nvmwareMachine.displayName=Виртуальная машина VMware\nvmwareMachine.displayDescription=Подключение к виртуальной машине через SSH\nvmwareInstallation.displayName=Установка гипервизора VMware для настольных компьютеров\nvmwareInstallation.displayDescription=Взаимодействуй с установленными виртуальными машинами через CLI\n#custom\nstart=Начать\n#custom\nstop=Остановить\npause=Пауза\nrdpTunnelHost=Целевой хост\nrdpTunnelHostDescription=SSH-соединение для туннелирования RDP-соединения\nrdpTunnelUsername=Имя пользователя\nrdpTunnelUsernameDescription=Пользовательский пользователь, под которым нужно входить в систему, если оставить пустым, то будет использоваться пользователь SSH\nrdpFileLocation=Расположение файла\nrdpFileLocationDescription=Путь к файлу .rdp\nrdpPasswordAuthentication=Проверка подлинности пароля\nrdpFiles=Файлы RDP\nrdpPasswordAuthenticationDescription=Пароль, который нужно заполнить или скопировать в буфер обмена, в зависимости от поддержки клиента\nrdpFile.displayName=RDP-файл\nrdpFile.displayDescription=Подключение к системе через существующий файл .rdp\nrequiredSshServerAlertTitle=Настройка SSH-сервера\nrequiredSshServerAlertHeader=Невозможно найти установленный SSH-сервер в виртуальной машине.\nrequiredSshServerAlertContent=Чтобы подключиться к ВМ, XPipe ищет работающий SSH-сервер, но для ВМ не было обнаружено ни одного доступного SSH-сервера.\ncomputerName=Имя компьютера\npssComputerNameDescription=Имя компьютера, к которому нужно подключиться\ncredentialUser=Учетная запись пользователя\ncredentialUserDescription=Пользователь, от имени которого нужно войти в систему.\ncredentialPassword=Пароль учетной записи\ncredentialPasswordDescription=Пароль пользователя.\nsshConfig=Файлы конфигурации SSH\nautostart=Автоматическое подключение при запуске XPipe\nacceptHostKey=Примите ключ хоста\nmodifyHostKeyPermissions=Изменение разрешений ключа хоста\nattachContainer=Прикрепить\ncontainerLogs=Показать журналы\nopenSftpClient=Открыть во внешнем SFTP-клиенте\n#custom\nopenTermius=Открыть в Termius\nshowInternalInstances=Показать внутренние экземпляры\neditPod=Редактировать стручок\n#custom\nacceptHostKeyDescription=Доверься новому ключу хоста и продолжить\nmodifyHostKeyPermissionsDescription=Попытка снять разрешения с оригинального файла, чтобы OpenSSH был доволен\npsSession.displayName=Удаленный сеанс PowerShell\npsSession.displayDescription=Подключение через New-PSSession и Enter-PSSession\nsshLocalTunnel.displayName=Локальный SSH-туннель\nsshLocalTunnel.displayDescription=Создайте SSH-туннель к удаленному хосту\nsshRemoteTunnel.displayName=Удаленный SSH-туннель\nsshRemoteTunnel.displayDescription=Создайте обратный SSH-туннель с удаленного хоста\nsshDynamicTunnel.displayName=Динамический SSH-туннель\nsshDynamicTunnel.displayDescription=Установите SOCKS-прокси через SSH-соединение\nshellEnvironmentGroup.displayName=Среды оболочки\nshellEnvironmentGroup.displayDescription=Среды оболочки\nshellEnvironment.displayName=Среда оболочки\nshellEnvironment.displayDescription=Создайте настраиваемую среду запуска оболочки\nshellEnvironment.informationFormat=$TYPE$ среда\nshellEnvironment.elevatedInformationFormat=$ELEVATION$ $TYPE$ среда\nenvironmentConnectionDescription=Базовое соединение для создания среды для\nenvironmentScriptDescription=Необязательный пользовательский скрипт init для запуска в оболочке\nenvironmentSnippets=Скрипты оболочки\ncommandSnippetsDescription=Дополнительные предопределенные сценарии оболочки, которые нужно запустить первыми\nenvironmentSnippetsDescription=Дополнительные предопределенные сценарии оболочки, запускаемые при инициализации\nshellTypeDescription=Явный тип оболочки для запуска\noriginPort=Порт происхождения\noriginAddress=Адрес происхождения\nremoteAddress=Удаленный адрес\nremoteSourceAddress=Адрес удаленного источника\nremoteSourcePort=Порт удаленного источника\noriginDestinationPort=Порт назначения\noriginDestinationAddress=Адрес места назначения\norigin=Origin\nremoteHost=Удаленный хост\naddress=Адрес\nproxmox.displayName=Proxmox\nproxmox.displayDescription=Подключение к системам в виртуальной среде Proxmox\nproxmoxVm.displayName=Proxmox VM\nproxmoxVm.displayDescription=Подключение к виртуальной машине в Proxmox VE через SSH\nproxmoxContainer.displayName=Контейнер Proxmox\nproxmoxContainer.displayDescription=Подключение к контейнеру в Proxmox VE\nsshDynamicTunnel.hostDescription=Система, которую нужно использовать в качестве SOCKS-прокси\nsshDynamicTunnel.bindingDescription=К каким адресам привязать туннель\nsshRemoteTunnel.hostDescription=Система, из которой нужно запустить удаленный туннель к источнику\nsshRemoteTunnel.bindingDescription=К каким адресам привязать туннель\nsshLocalTunnel.hostDescription=Система, к которой нужно открыть туннель\nsshLocalTunnel.bindingDescription=К каким адресам привязать туннель\nsshLocalTunnel.localAddressDescription=Локальный адрес для привязки\nsshLocalTunnel.remoteAddressDescription=Удаленный адрес для привязки\ncmd.displayName=Команда\ncmd.displayDescription=Выполнение произвольной команды в системе\nk8sPod.displayName=Kubernetes Pod\nk8sPod.displayDescription=Подключись к капсуле и ее контейнерам с помощью kubectl\nk8sContainer.displayName=Контейнер Kubernetes\nk8sContainer.displayDescription=Открыть оболочку для контейнера\nk8sCluster.displayName=Кластер Kubernetes\nk8sCluster.displayDescription=Подключись к кластеру и его капсулам с помощью kubectl\nsshTunnelGroup.displayName=Туннели SSH\nsshTunnelGroup.displayCategory=Все типы SSH-туннелей\nlocal.displayName=Локальная машина\nlocal.displayDescription=Оболочка локальной машины\ncygwin=Cygwin\nmsys2=MSYS2\ngitWindows=Git для Windows\ngitForWindows.displayName=Git для Windows\ngitForWindows.displayDescription=Получить доступ к локальной среде Git For Windows\nmsys2.displayName=MSYS2\nmsys2.displayDescription=Оболочки доступа к вашей среде MSYS2\ncygwin.displayName=Cygwin\ncygwin.displayDescription=Доступ к оболочкам вашей среды Cygwin\nnamespace=Пространство имен\ngitVaultIdentityStrategy=Идентификация Git SSH\ngitVaultIdentityStrategyDescription=Если ты решил использовать SSH git URL в качестве удаленного и твой удаленный репозиторий требует SSH-идентификации, то установи эту опцию.\\n\\nВ случае если ты предоставил HTTP-урл, можешь проигнорировать эту опцию.\ndockerContainers=Контейнеры Docker\ndockerCmd.displayName=клиент docker CLI\ndockerCmd.displayDescription=Доступ к контейнерам Docker через клиент docker CLI\nwslCmd.displayName=Установка WSL\nwslCmd.displayDescription=Доступ к экземплярам WSL с помощью клиента wsl CLI\nk8sCmd.displayName=клиент kubectl\nk8sCmd.displayDescription=Доступ к кластерам Kubernetes через kubectl\nk8sClusters=Кластеры Kubernetes\nshells=Доступные оболочки\n#custom\ninspectContainer=Осмотреть\n#custom\ninspectContext=Осмотреть контекст\nk8sClusterNameDescription=Название контекста, в котором находится кластер.\n#custom\npod=pod\n#custom\npodName=Имя pod`а\nk8sClusterContext=Контекст\nk8sClusterContextDescription=Название контекста, в котором находится кластер\nk8sClusterNamespace=Пространство имен\nk8sClusterNamespaceDescription=Пользовательское пространство имен или пространство по умолчанию, если оно пустое\nk8sConfigLocation=Конфигурационный файл\nk8sConfigLocationDescription=Пользовательский файл kubeconfig или файл по умолчанию, если он остался пустым\n#custom\ninspectPod=Осмотреть pod\nshowAllContainers=Показать неработающие контейнеры\n#custom\nshowAllPods=Показать неработающие pod`ы\n#custom\nk8sPodHostDescription=Хост, на котором находится pod\nk8sContainerDescription=Название контейнера Kubernetes\n#custom\nk8sPodDescription=Имя pod`а Kubernetes\npodDescription=Подвеска, на которой расположен контейнер\nk8sClusterHostDescription=Хост, через который должен осуществляться доступ к кластеру. Должен быть установлен и настроен kubectl, чтобы иметь возможность получить доступ к кластеру.\nconnection=Подключение\nshellCommand.displayName=Пользовательская команда оболочки\nshellCommand.displayDescription=Открыть стандартную оболочку через пользовательскую команду\nssh.displayName=SSH-соединение\nssh.displayDescription=Подключись к удаленной системе с помощью клиента командной строки SSH\nsshConfig.displayName=Файл конфигурации SSH\nsshConfig.displayDescription=Подключение к хостам, определенным в конфигурационном файле SSH\nsshConfigHost.displayName=SSH файл конфигурации хоста\nsshConfigHost.displayDescription=Подключение к хосту, заданному в конфигурационном файле SSH\nsshConfigHost.password=Пароль\nsshConfigHost.passwordDescription=Укажи необязательный пароль для входа пользователя в систему.\nsshConfigHost.identityPassphrase=Ключевая фраза\nsshConfigHost.identityPassphraseDescription=Укажи необязательную ключевую фразу для своего ключа.\nshellCommand.hostDescription=Хост, на котором нужно выполнить команду\nshellCommand.commandDescription=Команда, которая открывает оболочку\ncommandType=Тип команды\ncommandTypeDescription=Как выполнить команду\ncommandDescription=Пользовательские команды для выполнения на хосте\ncommandHostDescription=Хост, на котором нужно запустить команду\ncommandDataFlowDescription=Как эта команда обрабатывает ввод и вывод\ncommandElevationDescription=Запустите эту команду с повышенными правами\ncommandShellTypeDescription=Оболочка, которую нужно использовать для этой команды\nlimitedSystem=Это ограниченная или встроенная система\nlimitedSystemDescription=Не пытайся определить тип оболочки, это необходимо для ограниченных встраиваемых систем или IOT-устройств\n#custom\nsshForwardX11=Переадресация X11\nsshForwardX11Description=Включает переадресацию X11 для соединения\ncustomAgent=Пользовательский агент\nidentityAgent=Агент идентификации\nssh.proxyDescription=Необязательный прокси-хост, который будет использоваться при установлении SSH-соединения. Должен быть установлен ssh-клиент.\nusage=Использование\nwslHostDescription=Хост, на котором находится экземпляр WSL. Должен быть установлен wsl.\nwslDistributionDescription=Имя экземпляра WSL\nwslUsernameDescription=Явное имя пользователя, под которым нужно войти в систему. Если оно не указано, будет использоваться имя пользователя по умолчанию.\nwslPasswordDescription=Пароль пользователя, который можно использовать для команд sudo.\ndockerHostDescription=Хост, на котором расположен контейнер docker. Должен быть установлен docker.\ndockerContainerDescription=Имя докер-контейнера\nlocalMachine=Локальная машина\nrootScan=Среда оболочки Sudo\nloginEnvironmentScan=Пользовательская среда входа в систему\nk8sScan=Кластер Kubernetes\noptions=Опции\ndockerRunningScan=Запуск контейнеров docker\ndockerAllScan=Все контейнеры docker\nwslScan=Экземпляры WSL\nsshScan=Конфигурационные соединения SSH\nrunAsUser=Запуск от имени пользователя\nrunAsUserDescription=Запустите эту среду оболочки от имени другого пользователя\ndefault=По умолчанию\nadministrator=Администратор\n#custom\nwslHost=WSL хост\ntimeout=Таймаут\ninstallLocation=Место установки\ninstallLocationDescription=Место, где установлена твоя среда $NAME$\nwsl.displayName=Подсистема Windows для Linux\nwsl.displayDescription=Подключитесь к экземпляру WSL, работающему под Windows\ndocker.displayName=Докер-контейнер\ndocker.displayDescription=Подключение к контейнеру докера\nport=Порт\nuser=Пользователь\npassword=Пароль\nmethod=Метод\nuri=URL\nproxy=Прокси\ndistribution=Распространение\nusername=Имя пользователя\nshellType=Тип оболочки\nbrowseFile=Просмотр файла\nopenShell=Открыть оболочку в терминале\nopenCommand=Выполнить команду в терминале\neditFile=Редактировать файл\ndescription=Описание\nfurtherCustomization=Дальнейшая настройка\nfurtherCustomizationDescription=Для получения дополнительных параметров конфигурации используй файлы конфигурации ssh\n#custom\nbrowse=Просмотреть\nconfigHost=Хост\nconfigHostDescription=Хост, на котором расположен конфиг\nconfigLocation=Расположение конфигурации\nconfigLocationDescription=Путь к файлу конфигурации\ngateway=Шлюз\ngatewayDescription=Дополнительный шлюз, который нужно использовать при подключении\nconnectionInformation=Информация о подключении\nconnectionInformationDescription=К какой системе подключиться\npasswordAuthentication=Проверка подлинности пароля\npasswordAuthenticationDescription=Дополнительный пароль, который нужно использовать для аутентификации\nsshConfigString.displayName=SSH-соединение на основе конфигурации\nsshConfigString.displayDescription=Создай полностью настроенное SSH-соединение в формате SSH config\nsshConfigStringContent=Конфигурация\nsshConfigStringContentDescription=Опции SSH для соединения в формате OpenSSH config\nvnc.displayName=VNC-соединение через SSH\nvnc.displayDescription=Открыть сеанс VNC через туннельное соединение\nbinding=Переплет\nvncPortDescription=Порт, который прослушивает VNC-сервер\nrdpPortDescription=Порт, который прослушивает RDP-сервер\nvncUsername=Имя пользователя\nvncUsernameDescription=Дополнительное имя пользователя VNC\nvncPassword=Пароль\nvncPasswordDescription=Пароль VNC\nx11WslInstance=Экземпляр X11 Forward WSL\nx11WslInstanceDescription=Локальный дистрибутив Windows Subsystem for Linux для использования в качестве X11-сервера при использовании X11-переадресации в SSH-соединении. Этот дистрибутив должен быть WSL2.\nopenAsRoot=Открыть как root\nopenInWSL=Открыть в WSL\nlaunch=Запустите\nsshTrustKeyContent=Ключ хоста неизвестен, и ты включил ручную проверку ключа хоста. $CONTENT$\nsshTrustKeyTitle=Ключ неизвестного хоста\nrdpTunnel.displayName=RDP-соединение через SSH\nrdpTunnel.displayDescription=Подключение через RDP по туннельному соединению\nrdpEnableDesktopIntegration=Включить интеграцию с рабочим столом\nrdpEnableDesktopIntegrationDescription=Запускать удаленные приложения, предполагая, что список разрешений RDP разрешает это\nrdpSetupAdminTitle=Требуется настройка RDP\nrdpSetupAllowTitle=Удаленное приложение RDP\nrdpSetupAllowContent=Запуск удаленных приложений напрямую в настоящее время запрещен в этой системе. Ты хочешь разрешить его? Это позволит тебе запускать удаленные приложения прямо из XPipe, отключив список разрешений для удаленных приложений RDP.\nrdpServerEnableTitle=RDP-сервер\nrdpServerEnableContent=Сервер RDP отключен на целевой системе. Хочешь ли ты включить его в реестре, чтобы разрешить удаленные RDP-соединения?\nrdp=RDP\nrdpScan=RDP-туннель через SSH\nwslX11SetupTitle=Настройка WSL X11\nwslX11SetupContent=XPipe может использовать твой локальный дистрибутив WSL для работы в качестве сервера отображения X11. Хочешь настроить X11 на $DIST$? Это приведет к установке основных пакетов X11 на дистрибутив WSL и может занять некоторое время. Ты также можешь изменить используемый дистрибутив в меню настроек.\ncommand=Команда\ncommandGroup=Группа команд\nvncSystem=Целевая система VNC\nvncSystemDescription=Фактическая система, с которой нужно взаимодействовать. Обычно это то же самое, что и туннельный хост\nvncHost=Целевой VNC-хост\nvncHostDescription=Система, на которой работает VNC-сервер\nvncDirectHost=Хост\nvncDirectHostDescription=Запись хоста или ручной адрес сервера, на котором запущен VNC-сервер\nrdpDirectHost=Хост\nrdpDirectHostDescription=Запись хоста или ручной адрес сервера, на котором запущен RDP-сервер\ngitVaultTitle=Git-хранилище\ngitVaultForcePushContent=Хочешь ли ты принудительно выполнить push в удаленный репозиторий? Это полностью заменит все содержимое удаленного репозитория на локальный, включая историю.\ngitVaultOverwriteLocalContent=Хочешь отменить изменения в локальном хранилище? Это позволит применить все удаленные изменения к твоему локальному репозиторию.\nrdpSimple.displayName=Прямое RDP-соединение\nrdpSimple.displayDescription=Подключение к хосту через RDP\nrdpUsername=Имя пользователя\nrdpUsernameDescription=Пользователь, под которым нужно войти в систему. Может включать префикс домена\naddressDescription=К чему подключиться\nrdpAdditionalOptions=Дополнительные опции RDP\nrdpAdditionalOptionsDescription=Необработанные опции RDP, которые нужно включить, в том же формате, что и в файлах .rdp\nproxmoxVncConfirmTitle=Доступ к VNC\nproxmoxVncConfirmContent=Хочешь включить VNC-доступ для виртуальной машины? Это позволит включить прямой доступ VNC-клиента в конфигурационном файле ВМ и перезапустить виртуальную машину.\ndockerContext.displayName=Контекст Docker\ndockerContext.displayDescription=Взаимодействуй с контейнерами, расположенными в определенном контексте\nvmActions=Действия виртуальной машины\ndockerContextActions=Контекстные действия\nk8sPodActions=Действия в капсуле\nopenVnc=Включить доступ к VNC\naddVnc=Добавить VNC-соединение\ncommandGroup.displayName=Группа команд\ncommandGroup.displayDescription=Группа доступных команд для системы\nserial.displayName=Последовательное соединение\nserial.displayDescription=Открыть последовательное соединение в терминале\nserialPort=Последовательный порт\nserialPortDescription=Последовательный порт/устройство, к которому нужно подключиться\nbaudRate=Скорость передачи данных\ndataBits=Биты данных\nstopBits=Стоп-биты\nparity=Четность\nflowControlWindow=Контроль потока\nserialImplementation=Последовательная реализация\nserialImplementationDescription=Инструмент, который нужно использовать для подключения к последовательному порту\nserialHost=Хост\nserialHostDescription=Система для доступа к последовательному порту на\nserialPortConfiguration=Конфигурация последовательного порта\nserialPortConfigurationDescription=Параметры конфигурации подключенного последовательного устройства\nserialInformation=Серийная информация\nopenXShell=Открыть в XShell\ntsh.displayName=Телепорт\ntsh.displayDescription=Подключайтесь к узлам телепортации через tsh\ntshNode.displayName=Узел телепортации\ntshNode.displayDescription=Подключение к узлу телепортации в кластере\nteleportCluster=Кластер\nteleportClusterDescription=Кластер, в котором находится узел\nteleportProxy=Прокси\nteleportProxyDescription=Прокси-сервер, используемый для подключения к узлу\nteleportHost=Хост\nteleportHostDescription=Имя хоста узла\nteleportUser=Пользователь\nteleportUserDescription=Пользователь, от имени которого нужно войти в систему\nlogin=Логин\nhyperVInstall.displayName=Hyper-V\nhyperVInstall.displayDescription=Подключение к виртуальным машинам, управляемым Hyper-V\nhyperVVm.displayName=Hyper-V VM\nhyperVVm.displayDescription=Подключение к виртуальной машине Hyper-V через SSH или PSSession\ntrustHost=Доверительный хост\ntrustHostDescription=Добавьте ComputerName в список доверенных хостов\ncopyIp=Копировать IP\nvncDirect.displayName=Прямое VNC-соединение\nvncDirect.displayDescription=Подключись к системе через VNC напрямую\neditConfiguration=Редактирование конфигурации\nviewInDashboard=Вид в приборной панели\nsetDefault=Установить по умолчанию\nremoveDefault=Убрать значение по умолчанию\nconnectAsOtherUser=Подключиться как другой пользователь\nprovideUsername=Предоставьте альтернативное имя пользователя для входа в систему\nvmIdentity=Идентификация гостя\nvmIdentityDescription=Метод аутентификации SSH, который нужно использовать для подключения при необходимости\nvmPort=Порт\nvmPortDescription=Порт, к которому нужно подключиться через SSH\nforwardAgent=Форвард-агент\nforwardAgentDescription=Сделать идентификаторы SSH-агентов доступными на удаленной системе\nvirshUri=URI\nvirshUriDescription=URI гипервизора, также поддерживаются псевдонимы\nvirshDomain.displayName=домен libvirt\nvirshDomain.displayDescription=Подключение к домену libvirt\nvirshHypervisor.displayName=гипервизор libvirt\nvirshHypervisor.displayDescription=Подключение к драйверу гипервизора, поддерживаемого libvirt\nvirshInstall.displayName=клиент командной строки libvirt\nvirshInstall.displayDescription=Подключись ко всем доступным гипервизорам libvirt через virsh\naddHypervisor=Добавьте гипервизор\ninteractiveTerminal=Интерактивный терминал\neditDomain=Редактировать домен\nlibvirt=домены libvirt\ncustomIp=Пользовательский IP\ncustomIpDescription=Отмените определение IP-адреса локальной виртуальной машины по умолчанию, если вы используете расширенные сетевые возможности\nautomaticallyDetect=Автоматически обнаружить\nuserAddDialogTitle=Создание пользователя\ngroupAddDialogTitle=Создание группы\n#custom\npassphrase=Парольная фраза\nrepeatPassphrase=Повторная парольная фраза\ngroupSecret=Групповой секрет\nrepeatGroupSecret=Повторять групповой секрет\nvaultGroup=Группа хранилищ\n#custom\nloginAlertTitle=Необходима авторизация\nloginAlertHeader=Разблокируй хранилище, чтобы получить доступ к своим личным связям\nvaultUser=Пользователь хранилища\n#custom\nme=Я\naddGroup=Добавь группу ...\naddGroupDescription=Создай новую группу для этого хранилища\naddUser=Добавьте пользователя ...\naddUserDescription=Создай нового пользователя для этого хранилища\nskip=Пропустить\nuserChangePasswordAlertTitle=Смена пароля\ngroupChangeSecretAlertTitle=Секретное изменение\ndocs=Документация\nlxd.displayName=LXD-контейнер\nlxd.displayDescription=Подключись к контейнеру LXD через lxc\nlxdCmd.displayName=Клиент LXD CLI\nlxdCmd.displayDescription=Доступ к контейнерам LXD с помощью клиента lxc CLI\npodman.displayName=Контейнер Podman\npodman.displayDescription=Подключение к контейнеру Podman\n#custom\nincusInstall.displayName=Менеджер incus машин\nincusInstall.displayDescription=Доступ к контейнерам incus через клиент incus CLI\n#custom\nincusContainer.displayName=Контейнер для incus\n#custom\nincusContainer.displayDescription=Подключение к контейнеру incus\npodmanCmd.displayName=Podman CLI\npodmanCmd.displayDescription=Доступ к контейнерам Podman через CLI-клиент\nlxdHostDescription=Хост, на котором расположен контейнер LXD. Должен быть установлен lxc.\nlxdContainerDescription=Имя контейнера LXD\npodmanContainers=Контейнеры Podman\nlxdContainers=Контейнеры LXD\n#custom\nincusContainers=Контейнеры Incus\ncontainer=Контейнер\nhost=Хост\ncontainerActions=Действия с контейнером\nserialConsole=Последовательная консоль\neditRunConfiguration=Редактирование конфигурации запуска\ncommunityDescription=Инструмент для соединения, идеально подходящий для твоих личных целей.\nupgradeDescription=Профессиональное управление соединениями для всей твоей серверной инфраструктуры.\ndiscoverPlans=Узнайте о возможностях обновления\nextendProfessional=Обновление до последних профессиональных функций\ncommunityItem1=Неограниченное количество подключений к некоммерческим системам и инструментам\ncommunityItem2=Бесшовная интеграция с установленными у тебя терминалами и редакторами\ncommunityItem3=Полнофункциональный браузер удаленных файлов\ncommunityItem4=Мощная система скриптов для всех оболочек\ncommunityItem5=Интеграция Git для синхронизации и обмена информацией о соединениях\nupgradeItem1=Включает в себя все возможности community edition\nupgradeItem2=План Homelab поддерживает неограниченное количество гипервизоров и расширенные возможности SSH\nupgradeItem3=План Professional дополнительно поддерживает корпоративные операционные системы и инструменты\nupgradeItem4=План Enterprise обеспечивает полную гибкость для твоего индивидуального использования\nupgrade=Обновление\nupgradeTitle=Доступные планы\nstatus=Статус\ntype=Тип\nlicenseAlertTitle=Требуется лицензия\nuseCommunity=Продолжение сообщества\npreviewDescription=Опробуй новые возможности в течение пары недель после релиза.\ntryPreview=Активировать предварительный просмотр\npreviewItem1=Полный доступ к новым профессиональным функциям в течение 2 недель после релиза\npreviewItem2=Опробуй новые возможности без каких-либо обязательств\nlicensedTo=Лицензия на\nemail=Адрес электронной почты\napply=Применить\nclear=Очистить\n#custom\nactivate=Активировать\nvalidUntil=Действует до\nlicenseActivated=Лицензия активирована\n#custom\nrestart=Перезапустить\nlockVault=Хранилище с замком\nrestartApp=Перезапустить XPipe\nfree=Бесплатно\nupgradeInfo=Информацию об обновлении до лицензии ты найдешь ниже.\nupgradeInfoPreview=Ниже ты можешь найти информацию об обновлении до лицензии или попробовать предварительный просмотр.\nenterLicenseKey=Введите лицензионный ключ для обновления\nisOnlySupported=поддерживается только при наличии лицензии $TYPE$\nareOnlySupported=поддерживаются только при наличии лицензии $TYPE$\nlegacyLicense=Эта лицензия включает только новые функции Professional, выпущенные в течение одного года после покупки.\npreviewExpiredLicense=Недавно эта функция была доступна бесплатно в предварительном просмотре, но сейчас этот срок истек.\nopenApiDocs=Документация по API\nopenApiDocsDescription=Документация по HTTP API доступна онлайн, включая спецификацию OpenAPI .yaml. Ты можешь открыть ее в своем браузере или в предпочитаемом HTTP-клиенте.\nopenApiDocsButton=Открытые документы\npythonApi=Python API\npersonalConnection=Это соединение и все его дочерние элементы доступны только твоему пользователю, так как зависят от персональной идентификации.\ndeveloperPrintInitFiles=Выполнение файла Print init\ndeveloperPrintInitFilesDescription=Выведите все скрипты shell init, которые запускаются при запуске терминала.\ndeveloperShowSensitiveCommands=Запись в журнал чувствительных команд\ndeveloperShowSensitiveCommandsDescription=Включайте чувствительные команды в вывод журнала для отладки.\ncheckingForUpdates=Проверка наличия обновлений\ncheckingForUpdatesDescription=Получение информации о последнем релизе\ndownloadingUpdate=Извлечение релиза (версия $VERSION$)\ndownloadingUpdateDescription=Загрузка релиз-пакета\nupdateNag=Ты давно не обновлял XPipe. Возможно, ты упускаешь новые возможности и исправления из новых выпусков.\nupdateNagTitle=Напоминание об обновлении\nupdateNagButton=Смотри релизы\nrefreshServices=Обновление сервисов\nserviceProtocolType=Тип протокола обслуживания\nserviceProtocolTypeDescription=Управление тем, как открыть сервис\nserviceCommand=Команда, которую нужно выполнить, как только служба станет активной\nserviceCommandDescription=Заполнитель $PORT будет заменен на реальный туннелируемый локальный порт\n#custom\nvalue=Значение\nshowAdvancedOptions=Показать дополнительные опции\nsshAdditionalConfigOptions=Дополнительные параметры конфигурации\nremoteFileManager=Удаленный файловый менеджер\nclearUserData=Удаление пользовательских данных\nclearUserDataDescription=Удалить все данные о конфигурации пользователя, включая соединения\nclearUserDataTitle=Удаление пользовательских данных\nclearUserDataContent=Это приведет к удалению всех локальных пользовательских данных для xpipe и перезапуску. Если тебе дороги твои соединения, не забудь сначала синхронизировать их с git-репозиторием.\nundefined=Неопределенный\n#custom\ncopyAddress=Копировать адрес\nnetbirdDeviceScan=Соединения Netbird\nnetbirdId=Открытый ключ пира\nnetbirdIdDescription=Внутренний идентификатор открытого ключа Netbird для пира\ntailscaleDeviceScan=Соединения Tailscale\ntailscaleInstall.displayName=Установка Tailscale\ntailscaleInstall.displayDescription=Подключение к устройствам в твоей хвостовой сети через SSH\ntailscaleDevice.displayName=Устройство Tailscale\ntailscaleDevice.displayDescription=Подключись к устройству в твоей хвостовой сети через SSH\ntailscaleId=Идентификатор устройства\ntailscaleIdDescription=Внутренний идентификатор устройства tailscale\ntailscaleHostName=Имя хоста\ntailscaleHostNameDescription=Имя хоста устройства в хвостовой сети\ntailscaleUsername=Имя пользователя\ntailscaleUsernameDescription=Пользователь, от имени которого нужно войти в систему\ntailscalePassword=Пароль\ntailscalePasswordDescription=Необязательный пароль пользователя, который можно использовать для sudo\nscriptName=Имя скрипта\nscriptNameDescription=Дайте этому скрипту пользовательское имя\nscriptGroupName=Имя группы сценариев\nscriptGroupNameDescription=Дайте этой группе сценариев собственное имя\nidentityName=Имя личности\nidentityNameDescription=Дайте этому идентификатору пользовательское имя\ntailscaleTailnet.displayName=Tailnet\ntailscaleTailnet.displayDescription=Подключись к определенной сети хвостатых с помощью своего аккаунта\nputtyConnections=Соединения PuTTY\nkittyConnections=Соединения KiTTY\nicons=Иконки\ncustomIcons=Пользовательские иконки\niconSources=Источники иконок\niconSourcesDescription=Здесь ты можешь добавить свои собственные источники иконок. XPipe подхватит все .svg-файлы в добавленном месте и добавит их в доступный набор иконок.\\n\\nВ качестве места расположения иконок поддерживаются как локальные директории, так и удаленные git-репозитории.\nrefreshSources=Иконки обновления\nrefreshSourcesDescription=Обновите все иконки из доступных источников\naddDirectoryIconSource=Добавьте источник каталога ...\naddDirectoryIconSourceDescription=Добавить иконки из локальной директории\naddGitIconSource=Добавьте git source ...\naddGitIconSourceDescription=Добавление иконок, расположенных в удаленном git-репозитории\nrepositoryUrl=URL-адрес Git-репозитория\niconDirectory=Каталог иконок\naddUnsupportedKexMethod=Добавьте неподдерживаемый метод обмена ключами\naddUnsupportedKexMethodDescription=Разрешите использовать метод обмена ключами $VAL$ для этого соединения\naddUnsupportedHostKeyType=Добавьте неподдерживаемый тип ключа хоста\naddUnsupportedHostKeyTypeDescription=Разрешите использовать ключ типа $VAL$ для этого соединения\naddUnsupportedMacType=Добавьте неподдерживаемый тип MAC\naddUnsupportedMacTypeDescription=Разрешите использовать MAC-тип $VAL$ для этого соединения\nrunSilent=беззвучно в фоновом режиме\nrunInFileBrowser=в файловом браузере\nrunInConnectionHub=в соединительном хабе\ncommandOutput=Вывод команд\niconSourceDeletionTitle=Удалить источник иконок\niconSourceDeletionContent=Хочешь удалить этот источник иконок и все связанные с ним иконки?\nrefreshIcons=Иконки обновления\nrefreshIconsDescription=Получение, рендеринг и кэширование всех доступных 1000+ иконок из внешних источников в .png-файлы. Это может занять некоторое время...\nvaultUserLegacy=Пользователь хранилища (режим ограниченной совместимости с устаревшими версиями)\nupgradeInstructions=Инструкции по обновлению\nexternalActionTitle=Запрос на внешнее действие\nexternalActionContent=Было запрошено внешнее действие. Хочешь ли ты разрешить запуск действий извне XPipe?\nnoScriptStateAvailable=Обновите, чтобы определить совместимость скриптов ...\ndocumentationDescription=Ознакомьтесь с документацией\ncustomEditorCommandInTerminal=Запуск пользовательской команды в терминале\ncustomEditorCommandInTerminalDescription=Если твой редактор работает через терминал, ты можешь включить эту опцию, чтобы автоматически открыть терминал и вместо этого выполнить команду в терминальной сессии.\\n\\nЭту опцию можно использовать для таких редакторов, как vi, vim, nvim и других.\ndisableHttpsTlsCheck=Отключите проверку сертификата HTTPS-запроса\ndisableHttpsTlsCheckDescription=Если твоя организация расшифровывает твой HTTPS-трафик в брандмауэрах с помощью SSL-перехвата, то любые проверки обновлений или лицензий будут неудачными из-за несоответствия сертификатов. Исправить это можно, включив данную опцию и отключив проверку сертификатов TLS.\nconnectionsSelected=$NUMBER$ выбранные соединения\naddConnections=Добавить соединения\nbrowseDirectory=Просмотр каталога\nopenTerminal=Открытый терминал\ndocumentation=Документация\nreport=Ошибка в отчете\nkeePassXcNotAssociated=Ссылка на KeePassXC\nkeePassXcNotAssociatedDescription=XPipe не связан с твоей локальной базой данных KeePassXC. Щелкни ниже, чтобы выполнить одноразовый шаг по ассоциированию XPipe с базой данных KeePassXC, чтобы XPipe мог запрашивать пароли.\nkeePassXcAssociateMore=Подключите больше баз данных\nkeePassXcAssociateMoreDescription=Ты можешь быть подключен к нескольким базам данных KeePassXC одновременно\nkeePassXcAssociated=Ссылки на KeePassXC\nkeePassXcAssociatedDescription=XPipe подключен к следующим локальным базам данных KeePassXC:\nkeePassXcNotAssociatedButton=База данных ссылок\nidentifier=Идентификатор\npasswordManagerCommand=Пользовательская команда\npasswordManagerCommandDescription=Пользовательская команда, которую нужно выполнить для получения паролей. Строка-заполнитель $KEY при вызове будет заменена на заключенный в кавычки ключ пароля. Это должно вызвать CLI твоего менеджера паролей для печати пароля в stdout, например, mypassmgr get $KEY.\nchooseTemplate=Выберите шаблон\nkeePassXcPlaceholder=URL-адрес входа в KeePassXC\nterminalEnvironment=Терминальная среда\nterminalEnvironmentDescription=Если ты хочешь использовать для настройки терминала возможности локальной WSL-среды на базе Linux, ты можешь использовать их в качестве терминальной среды.\\n\\nТогда любые пользовательские команды инициализации терминала и настройки терминального мультиплексора будут выполняться в этом WSL-дистрибутиве.\nterminalInitScript=Скрипт инициализации терминала\nterminalInitScriptDescription=Команды, которые нужно запустить в терминальном окружении перед запуском соединения. С их помощью ты можешь настроить терминальное окружение при запуске.\nterminalMultiplexer=Терминальный мультиплексор\nterminalMultiplexerDescription=Терминальный мультиплексор для использования в качестве альтернативы вкладкам в терминале. Это позволит заменить некоторые характеристики работы с терминалом, например, работу с вкладками, на функциональность мультиплексора.\\n\\nТребуется, чтобы в системе был установлен соответствующий исполняемый файл мультиплексора.\nterminalMultiplexerWindowsDescription=Терминальный мультиплексор для использования в качестве альтернативы вкладкам в терминале. Это позволит заменить некоторые характеристики работы с терминалом, например, работу с вкладками, на функциональность мультиплексора.\\n\\nТребуется использование терминальной среды WSL под Windows и установка исполняемого файла мультиплексора в систему WSL.\nterminalAlwaysPauseOnExit=Всегда ставьте паузу при выходе\nterminalAlwaysPauseOnExitDescription=Если эта функция включена, то при выходе из терминальной сессии тебе всегда будет предложено либо перезапустить, либо закрыть сессию. Если отключить эту функцию, XPipe будет делать это только в случае неудачных соединений, которые завершаются с ошибкой.\nquerying=Запрос ...\nretrievedPassword=Получено: $PASSWORD$\nrefreshOpenpubkey=Обновите идентификатор openpubkey\nrefreshOpenpubkeyDescription=Запустите opkssh refresh, чтобы идентификатор openpubkey снова стал действительным\nall=Все\nterminalPrompt=Подсказка терминала\nterminalPromptDescription=Инструмент подсказки терминала, который будет использоваться в твоих удаленных терминалах. Включение подсказки терминала автоматически установит и настроит инструмент подсказки на целевой системе при открытии терминальной сессии.\\n\\nПри этом не изменяются существующие конфигурации подсказок или файлы профилей в системе. Это увеличит время загрузки терминала в первое время, пока подсказка будет настраиваться на удаленной системе. Твоему терминалу могут понадобиться дополнительные шрифты для корректного отображения подсказки.\nterminalPromptConfiguration=Настройка подсказки терминала\nterminalPromptConfig=Конфигурационный файл\nterminalPromptConfigDescription=Файл пользовательского конфига, который нужно применить к подсказке. Этот конфиг будет автоматически установлен на целевой системе при инициализации терминала и использован в качестве конфига подсказки по умолчанию.\\n\\nЕсли ты хочешь использовать существующий файл конфига по умолчанию на каждой системе, можешь оставить это поле пустым.\npasswordManagerKey=Ключ менеджера паролей\npasswordManagerKeyDescription=Идентификатор секретов в менеджере паролей\npasswordManagerAgent=Агент менеджера паролей\ndockerComposeProject.displayName=Проект Docker compose\n#custom\ndockerComposeProject.displayDescription=Сгруппировать контейнеры одного Docker compose вместе\nsshVerboseOutput=Включить подробный вывод SSH\nsshVerboseOutputDescription=При подключении по SSH будет выведено много отладочной информации. Полезно для устранения проблем с SSH-соединениями.\ndontUseGateway=Не используй шлюз\ndontUseGatewayDescription=Не используй хост гипервизора в качестве шлюза и подключайся напрямую к IP\ncategoryColor=Цвет категории\ncategoryColorDescription=Цвет по умолчанию, который будет использоваться для соединений в этой категории\ncategorySync=Синхронизация с git-репозиторием\ncategorySyncDescription=Автоматически синхронизируй все соединения с git-репозиторием. Все локальные изменения в соединениях будут выгружаться в удаленный.\ncategorySyncSpecial=Синхронизация с git-репозиторием\\n(Не настраивается для специальной категории \"$NAME$\")\ncategoryDontAllowScripts=Отключить все модификации\ncategoryDontAllowScriptsDescription=Отключи выполнение любых команд и других операций на системах из этой категории, чтобы предотвратить любые модификации. Это отключит все скриптовые функции, команды среды оболочки, подсказки и многое другое.\ncategoryConfirmAllModifications=Подтверди все модификации\ncategoryConfirmAllModificationsDescription=Любые изменения в соединении или файловой системе сначала подтверди. Это может предотвратить случайные операции с важными системами.\ncategoryDefaultIdentity=Идентификация по умолчанию\ncategoryDefaultIdentityDescription=Если ты часто используешь определенный идентификатор на многих системах из этой категории, то установка идентификатора по умолчанию позволит тебе заранее выбирать его при создании новых подключений.\ncategoryConfigTitle=$NAME$ конфигурация\n#custom\nconfigure=Настроить\naddConnection=Добавить соединение\nnoCompatibleConnection=Не найдено совместимое соединение\nnoCompatibleIdentity=Совместимая личность не найдена\nnewCategory=Новая категория\ndockerComposeRestricted=Проект compose ограничен $NAME$ и не может быть изменен извне. Пожалуйста, используй $NAME$ для управления этим проектом compose.\nrestricted=Ограниченный\ndisableSshPinCaching=Отключите кэширование SSH PIN-кода\ndisableSshPinCachingDescription=XPipe будет автоматически кэшировать все PIN-коды, которые были введены для ключа при использовании какой-либо формы аутентификации на основе аппаратного обеспечения.\\n\\nОтключение этой функции приведет к тому, что при каждой попытке подключения придется вводить PIN-код заново.\ngitSyncPull=Pull для синхронизации удаленных изменений в git\nenpassVaultFile=Файл хранилища\nenpassVaultFileDescription=Локальный файл хранилища Enpass.\nflat=Плоский\nrecursive=Рекурсивный\nrdpAllowListBlocked=Похоже, что выбранное RemoteApp не включено в список разрешенных RDP для сервера.\npsonoServerUrl=URL-адрес сервера\npsonoServerUrlDescription=URL-адрес внутреннего сервера psono\npsonoApiKey=Ключ API\npsonoApiKeyDescription=Ключ API, который нужно использовать, в формате uuid\npsonoApiSecretKey=Секретный ключ API\npsonoApiSecretKeyDescription=Секретный ключ API в виде 64-байтовой шестнадцатеричной строки\npassboltServerUrl=URL-адрес сервера\npassboltServerUrlDescription=URL-адрес внутреннего сервера passbolt\npassboltPassphrase=Пассфраза\npassboltPassphraseDescription=Парольная фраза для закрытого ключа хранилища\npassboltPrivateKey=Закрытый ключ\npassboltPrivateKeyDescription=Файл закрытого gpg-ключа для хранилища\nfocusWindowOnNotifications=Фокус окна на уведомлениях\nfocusWindowOnNotificationsDescription=Выведи XPipe на передний план при появлении уведомления или сообщения об ошибке, например, при неожиданном разрыве соединения или туннеля.\ngitUsername=Пользовательское имя пользователя git\ngitUsernameDescription=Пользовательский пользователь для аутентификации в удаленном репозитории git. По умолчанию XPipe будет использовать текущие настроенные учетные данные git CLI.\\n\\nЭта настройка отменяет любые учетные данные по умолчанию, которые уже настроены для твоего локального клиента git CLI.\ngitPassword=Пользовательский git-пароль / персональный токен доступа\ngitPasswordDescription=Пароль или персональный маркер доступа, который нужно использовать для аутентификации. Нужен ли тебе пароль или персональный токен доступа, зависит от удаленного провайдера git. Этот параметр отменяет все учетные данные по умолчанию, которые уже настроены для твоего локального клиента git CLI.\nsetReadOnly=Установить только для чтения\n#custom\nunsetReadOnly=Установить для чтения и записи\nreadOnlyStoreError=Конфигурация этой записи заморожена. Выбери другое имя, чтобы сохранить свои изменения в новой копии.\ncategoryFreeze=Заморозьте конфигурацию соединения\ncategoryFreezeDescription=Помечает конфигурации соединений как доступные только для чтения. Это значит, что ни одна существующая конфигурация соединения в этой категории не может быть изменена. Однако новые соединения можно добавлять.\nupdateFail=Установка обновлений не удалась\nupdateFailAction=Установить обновление вручную\nupdateFailActionDescription=Посмотри последние выпуски на GitHub\nonePasswordPlaceholder=Название предмета или URL-адрес op://\n#custom\ncomputeDirectorySizes=Вычислить размер каталога\n#custom\ncomputeSize=Вычислить размер\ncustomSpiceCommand=Пользовательская команда\ncustomSpiceCommandDescription=Пользовательская команда, которую нужно выполнить для запуска SPICE-сессий. Строка-заполнитель $FILE при вызове будет заменена на заключенный в кавычки путь к файлу .vv.\nvncClient=VNC-клиент\nvncClientDescription=VNC-клиент, который будет запускаться при открытии VNC-соединений в XPipe.\\n\\nУ тебя есть возможность либо использовать встроенный VNC-клиент в XPipe, либо запустить внешний локально установленный VNC-клиент, если тебе нужна более широкая настройка.\nintegratedXPipeVncClient=Встроенный VNC-клиент XPipe\ncustomVncCommand=Пользовательская команда\ncustomVncCommandDescription=Пользовательская команда, которую нужно выполнить для запуска VNC-сессий. Строка-заполнитель $ADDRESS при вызове будет заменена адресом, взятым в кавычки.\nvncConnections=VNC-соединения\npasswordManagerIdentity=Идентификация менеджера паролей\npasswordManagerIdentity.displayName=Идентификация менеджера паролей\npasswordManagerIdentity.displayDescription=Получение имени пользователя и пароля личности из менеджера паролей\npasswordCopied=Пароль подключения, скопированный в буфер обмена\nerrorOccurred=Произошла ошибка\nactionMacro.displayName=Макрос действия\nactionMacro.displayDescription=Запуск в действии с помощью настраиваемых триггеров\nmacroAdd=Добавить макрос\nmacroName=Имя макроса\nmacroNameDescription=Дайте этому макросу пользовательское имя\nactionId=Идентификатор действия\nactionIdDescription=Действие, которое нужно выполнить с помощью этого макроса\nmacroRefs=Ассоциированные соединения\nmacroRefsDescription=Соединения, с помощью которых можно запустить действие\nconnectionCopy=Копировать\nactionPickerTitle=Действие Pick\nactionPickerDescription=Щелкни на чем-нибудь, чтобы выполнить действие. Вместо того чтобы выполнять действие, ты можешь создавать и редактировать ярлыки к нему в режиме выбора ярлыка действия.\ncancelActionPicker=Выбор действия отмены\nactionShortcut=Ярлык действия\nactionShortcuts=Ярлыки действий\nactionStore=Магазин действий\nactionStoreDescription=Запись в магазине, на которой нужно выполнить действие\n#custom\nactionStores=Хранилища действий\nactionStoresDescription=Записи магазина, на которых нужно выполнить действие\nactionDesktopShortcut=Ярлык на рабочем столе\nactionDesktopShortcutDescription=Создайте ярлык для этого действия на рабочем столе\nactionUrlShortcut=Ярлык URL\nactionUrlShortcutDescription=Скопируй URL, который при открытии может вызывать такие действия\nactionUrlShortcutDisabled=Ярлык URL (недоступно)\nactionUrlShortcutDisabledDescription=Тип установки $TYPE$ не поддерживает открытие URL-адресов\nactionApiCall=API-запрос\nactionApiCallDescription=Вызовите это действие из HTTP API\nactionMacro=Макрос действия\nactionMacroDescription=Создай макрос с расширенной функциональностью для этого действия\ncreateMacro=Создать макрос\nactionConfiguration=Параметры\nactionConfigurationDescription=Параметры, которые нужно передать выполняемому действию\nconfirmAction=Подтвердить действие\nactionConnections=Действующие соединения\nactionConnectionsDescription=Соединения, на которых будет выполняться действие\nactionConnection=Подключение к действию\nactionConnectionDescription=Соединение, на котором будет выполняться действие\nappleContainerInstall.displayName=Контейнеры Apple\nappleContainerInstall.displayDescription=Получи доступ к экземплярам контейнеров apple через контейнерный CLI\nappleContainer.displayName=Контейнер Apple\nappleContainer.displayDescription=Получи доступ к экземплярам контейнеров apple через контейнерный CLI\n#custom\nappleContainerHostDescription=Хост, на котором расположен контейнер Apple\n#custom\nappleContainerDescription=Название контейнера Apple\nappleContainers=Контейнеры Apple\nchangeOrderIndexTitle=Изменить порядок\norderIndex=Индекс\norderIndexDescription=Явный индекс для упорядочивания этой записи относительно других. Самые низкие индексы показываются сверху, самые высокие - снизу\nmoveToFirst=Переместить на первое место\nmoveToLast=Переместись на последнюю строчку\ncategory=Категория\nincludeRoot=Включи корень\nexcludeRoot=Исключить корень\nfreezeConfiguration=Конфигурация замораживания\nunfreezeConfiguration=Разморозить конфигурацию\nwaylandScalingTitle=Масштабирование Wayland\nactionApiUrl=$URL$ (Копировать json-тело)\ncopyBody=Тело запроса на копирование\ngitRepoTerminalOpen=Открой репозиторий в терминале\ngitRepoTerminalOpenDescription=Посмотри на репозиторий самостоятельно с помощью командной строки\ngitRepoOverwriteLocal=Перезаписать локальный репозиторий\ngitRepoOverwriteLocalDescription=Замените все локальные изменения изменениями с удаленного компьютера\ngitRepoForcePush=Перезаписать удаленный репозиторий\ngitRepoForcePushDescription=Используй git push --force, чтобы применить свои локальные изменения к удалённым\ngitRepoDontWarn=Больше не предупреждай\ngitRepoDontWarnDescription=Если это ожидаемо, заставьте XPipe игнорировать эту ошибку в будущем\ngitRepoTryAgain=Попробуй ещё раз\ngitRepoTryAgainDescription=Повторная попытка выполнить ту же операцию\ngitRepoEnablePlain=Используйте синхронизацию обычных каталогов\ngitRepoEnablePlainDescription=Не инициализируй git-репозиторий для синхронизации изменений с директорией\ngitRepoCreateBare=Использовать git sync\ngitRepoCreateBareDescription=Инициализируй новый пустой git-репозиторий в директории sync\ngitRepoDisable=Отключи на время git vault\ngitRepoDisableDescription=Не вносите никаких изменений во время этой сессии\ngitRepoPullRefresh=Вытащить изменения и обновить\ngitRepoPullRefreshDescription=Объединить удаленные изменения и перезагрузить данные\nbreakOutCategory=Вырваться из категории\nmergeCategory=Категория слияния\nopenWinScp=Открыть в WinSCP\n#custom\nuninstallApplication=Деинсталировать\nuninstallApplicationDescription=Запускает скрипт установки .pkg для полного удаления XPipe\nk8sEditPodTitle=Применять изменения\nk8sEditPodContent=Хочешь ли ты применить изменения, сделанные командой kubectl apply? Скорее всего, для применения изменений потребуется перезагрузка.\n#custom\nvirshEditDomainTitle=Применить изменения\nvirshEditDomainContent=Хочешь ли ты применить изменения к домену? Скорее всего, для применения изменений потребуется перезагрузка.\npkcs11Library=Библиотека PKCS#11\npkcs11LibraryDescription=Путь к файлу динамически подключаемой библиотеки\nsshAgentSocket=Пользовательский сокет SSH-агента\nsshAgentSocketDescription=Пользовательский сокет, который будет использоваться для связи с агентом SSH. Этот пользовательский агент можно использовать для соединения, выбрав для него опцию пользовательского агента.\npublicKey=Идентификатор открытого ключа\npublicKeyDescription=Необязательный открытый ключ, чтобы заставить агента предлагать только подходящий закрытый ключ\nactions=Действия\nhcloudServer.displayName=Облачный сервер Hetzner\nhcloudServer.displayDescription=Получить доступ к серверу, расположенному в облаке Hetzner, через SSH\nhcloudInstall.displayName=Hetzner Cloud CLI\nhcloudInstall.displayDescription=Доступ к серверам, размещенным в облаке Hetzner, через hcloud\nhcloudContext.displayName=контекст hcloud\nhcloudContext.displayDescription=Серверы доступа к контексту hcloud\nmetrics=Метрика\nopenInVsCode=Открыть в VsCode\naddCloud=Облако ...\nhcloudToken=токен hcloud\nhcloudTokenDescription=Облачный токен Hetzner, который нужно использовать. Для получения дополнительной информации смотри документацию\nhcloudLogin=Вход в облако Hetzner\nclearHcloudToken=Очистить токен hcloud\nclearHcloudTokenDescription=Удали существующий токен, чтобы ты мог войти в систему снова\nselectIdentity=Выберите личность\nenableMcpServer=Включить MCP-сервер\nenableMcpServerDescription=Включает MCP-сервер XPipe, позволяя внешним MCP-клиентам отправлять запросы на MCP-сервер. Подробности настройки смотри ниже.\\n\\nОбрати внимание, что HTTP API не обязательно должен быть включен для функциональности MCP.\nenableMcpMutationTools=Включите инструменты для мутации MCP\nenableMcpMutationToolsDescription=По умолчанию на MCP-сервере включены только инструменты, предназначенные только для чтения. Это сделано для того, чтобы исключить возможность случайных операций, потенциально способных модифицировать систему.\\n\\nЕсли ты планируешь вносить изменения в системы через MCP-клиенты, то перед включением этой опции обязательно проверь, настроен ли твой MCP-клиент на подтверждение любых потенциально разрушительных действий. Требуется переподключение всех MCP-клиентов для применения.\nmcpClientConfigurationDetails=Настройка клиента MCP\nmcpClientConfigurationDetailsDescription=Используй эти конфигурационные данные для подключения к MCP-серверу XPipe с выбранного тобой MCP-клиента.\nswitchHostAddress=Изменить адрес хоста\naddAnotherHostName=Добавьте еще одно имя хоста\naddNetwork=Сетевое сканирование ...\nnetworkScan=Сканирование сети\nnetworkScanStore=Целевой хост\nnetworkScanStoreDescription=Хост, для которого нужно просканировать локальную сеть\nuseAsGateway=Использование хоста в качестве шлюза\nuseAsGatewayDescription=Использовать ли целевой хост в качестве шлюза для созданных соединений\nnetworkScanPorts=Порты для сканирования\nnetworkScanPortsDescription=Список портов, разделенных запятыми, который нужно включить в сканирование\nnetworkScanType=Тип соединения\nnetworkScanTypeDescription=Тип серверов, которые нужно искать\nemptyDirectory=Эта директория выглядит пустой\nhcloudConfigFile=конфигурационный файл hcloud\nhcloudConfigFileDescription=Расположение файла конфигурации hcloud CLI .toml\npreferMonochromeIcons=Предпочитай монохромные иконки\npreferMonochromeIconsDescription=Когда эта функция включена, монохромные варианты иконок будут выбираться вместо цветных версий иконок по умолчанию, при условии, что для иконки из источника доступен отдельный вариант светлого или темного режима.\\n\\nДля применения требуется обновить иконки.\nalwaysShowSshMotd=Всегда показывай MOTD\nalwaysShowSshMotdDescription=Показывать или нет сообщение дня, настроенное на удаленной системе, при входе в новую терминальную сессию. Учти, что изменение этого параметра может изменить поведение инициализации SSH-соединений.\nmanageSubscription=Управляй подпиской\nnoListeningServer=Нет прослушивающего сервера\nnetworkScanResults=Результаты сканирования\nnetworkScanResultsDescription=Список найденных систем в сети\nlocalShellDialect=Локальная оболочка\nlocalShellDialectDescription=Оболочка, которая используется для локальных операций. Если обычная локальная оболочка по умолчанию отключена или в какой-то степени сломана, с помощью этой опции можно вернуться к другой альтернативе.\\n\\nНекоторые конфигурации, например пользовательские записи PATH, могут не применяться с резервной оболочкой, если они еще не настроены в соответствующих файлах профиля оболочки.\nagentSocketNotFound=Не найдено ни одного активного сокета агента\nagentSocket=Расположение сокета\nagentSocketDescription=Путь к файлу сокета агента\nagentSocketNotConfigured=Пользовательский сокет еще не настроен\ndownloadInProgress=$NAME$ загрузка в процессе\nenableTerminalStartupBell=Включить звонок при запуске терминала\nenableTerminalStartupBellDescription=Воспроизведи команду звукового сигнала/звонка в новом терминальном сеансе. Если твой эмулятор терминала поддерживает колокольчики, это можно использовать для облегчения идентификации вновь запущенных экземпляров терминала.\ninvalidSshGatewayChain=Неверная конфигурация смешанной цепочки шлюзов с прыгающими и непрыгающими шлюзами.\nsyncFileExists=Синхронизированный файл $FILE$ уже существует\nreplaceFile=Заменить файл\nreplaceFileDescription=Замените существующий файл на этот\nrenameFile=Переименовать файл\nrenameFileDescription=Дайте этому файлу другое имя для синхронизации\nnewFileName=Новое имя файла\nparentHostDoesNotSupportTunneling=Родительский хост $NAME$ не поддерживает туннелирование\nconnectionNotesTemplate=Шаблон для заметок\nconnectionNotesTemplateDescription=Шаблон разметки, который нужно использовать при добавлении новой записи в заметках к соединению.\nconnectionNotesButton=Редактировать заметки\nrdpSmartSizing=Включите умный размер\nrdpSmartSizingDescription=Когда эта функция включена, mstsc уменьшает размер рабочего стола, если окно слишком мало для отображения его в полном разрешении. Соотношение сторон рабочего стола при уменьшении сохраняется.\ndisableStartOnInit=Отключить автоматический запуск\nenableStartOnInit=Включить автоматический запуск\nfileReadSudoTitle=Чтение файлов Sudo\nfileReadSudoContent=Файл, который ты пытаешься прочитать, не дает тебе разрешения на чтение от текущего пользователя. Хочешь ли ты прочитать этот файл от имени пользователя root с помощью sudo? Это автоматически повысит права до root либо с помощью существующих учетных данных, либо через приглашение.\nnetbirdInstall.displayName=Установка Netbird\nnetbirdInstall.displayDescription=Подключайся к аналогам в своей сети Netbird\nnetbirdProfile.displayName=Профиль Netbird\nnetbirdProfile.displayDescription=Список сверстников в определенном профиле\nnetbirdPeer.displayName=Netbird peer\nnetbirdPeer.displayDescription=Подключитесь к компьютеру через SSH\nnetbirdPublicKey=Открытый ключ\nnetbirdPublicKeyDescription=Внутренний открытый ключ пира\nnetbirdHostName=Имя хоста\nnetbirdHostNameDescription=Имя хоста однорангового компьютера в сети\nvncRefSystem=Ассоциированная система\nvncRefSystemDescription=Запись соединения, с которым нужно связать это VNC-соединение. Оставь пустым, если его нет\nabstractHost.displayName=Ведущий\nabstractHost.displayDescription=Создайте запись для хоста, который не поддерживает shell-соединения\nabstractHostAddress=Адрес хоста\nabstractHostAddressDescription=Адрес хоста\nabstractHostGateway=Шлюз\nabstractHostGatewayDescription=Дополнительная система шлюзов, через которую можно связаться с этим хостом\nabstractHostConvert=Преобразование в абстрактную запись хоста\nhostNoConnections=Нет доступных соединений\nhostHasConnections=$COUNT$ доступные соединения\nhostHasConnection=$COUNT$ доступное соединение\nlargeFileWarningTitle=Редактирование большого файла\nlargeFileWarningContent=Файл, который ты хочешь отредактировать, довольно большой - $SIZE$. Ты действительно хочешь открыть этот файл в текстовом редакторе?\nrdpAskpassUser=Имя пользователя RDP для хоста $HOST$\nrdpAskpassPassword=Пароль для пользователя $USER$\ninPlaceKey=Ключ\ninPlaceKeyText=Содержание закрытого ключа\ninPlaceKeyTextDescription=Содержимое закрытого ключа\nnetbirdSelfhosted=Самостоятельный экземпляр netbird\nnetbirdSelfhostedDescription=Предоставить пользовательский URL вместо того, чтобы использовать версию, размещенную в облаке\nnetbirdManagementUrl=URL-адрес управления Netbird\nnetbirdManagementUrlDescription=URL-адрес управления вашим самописным экземпляром\nnetbirdSetupKey=Клавиша настройки\nnetbirdSetupKeyDescription=Если ты используешь ключи настройки, то можешь использовать один из них для входа в систему\nnetbirdLogin=Вход в Netbird\naddProfile=Добавить профиль\nnetbirdProfileNameAsktext=Имя нового профиля netbird\nopenSftp=Открыть в SFTP-сессии\ncapslockWarning=У тебя включен capslock\ninherit=Наследуй\nsshConfigStringSelected=Целевой хост\nsshConfigStringSelectedDescription=Для нескольких хостов в качестве цели используется первый. Переставить хосты, чтобы изменить цель\ntunnelToLocalhost=Туннель к localhost\ntunnelToLocalhostDescription=Автоматическое туннелирование удаленного порта на localhost\ntags=Теги\ntag=Тег\naddNewTag=Создать новый тег\ncreateTag=Создай тег ...\ninPlacePublicKey=Открытый ключ\ninPlacePublicKeyDescription=Связанный открытый ключ для указанного закрытого ключа\nsshKeygenTitle=Сгенерировать новый SSH-ключ\nsshKeygenAlgorithm=Алгоритм\nsshKeygenAlgorithmDescription=Алгоритм асимметричного кейгена, который будет использоваться для ключа\nrsaBits=Биты\nrsaBitsDescription=Количество битов в сгенерированном ключе\nsshKeygenComment=Комментировать\nsshKeygenCommentDescription=Необязательный комментарий для этого ключа\nsshKeygenPassphrase=Пассфраза\nsshKeygenPassphraseDescription=Необязательная парольная фраза для этого ключа\ned25519SkResident=Сделать резидентный ключ\ned25519SkResidentDescription=Храните закрытый ключ на аппаратном ключе безопасности\ned25519SkResidentKeyName=Ярлык резидентного ключа\ned25519SkResidentKeyNameDescription=Дайте ключу метку. Необходим при хранении нескольких ключей на ключе безопасности\ned25519SkPinRequired=Требовать PIN-код\ned25519SkPinRequiredDescription=Требует ввода PIN-кода при использовании\ned25519SkUserPresenceRequired=Требование присутствия пользователя\ned25519SkUserPresenceRequiredDescription=Требует прикосновения или чего-то подобного при использовании. Некоторые ключи безопасности требуют, чтобы это было включено\ncopyPublicKey=Копирование открытого ключа\ngeneratePublicKey=Сгенерировать открытый ключ\npublicKeyGenerateNotice=Может быть сгенерирован на основе закрытого ключа\nidentityApplyTargetHost=Цель\nidentityApplyTargetHostDescription=Система для применения идентификации к\nidentityApplyAuthorizedHost=Авторизованный SSH-ключ\nidentityApplyAuthorizedHostDescription=Ключ SSH добавляется в файл авторизованных hosts\nidentityApplyAuthorizedHostButton=Добавить ключ в файл\napplyIdentityToHost=Примените идентификацию к хосту ...\nidentityApplyMissingPublicKeyTitle=Отсутствующий открытый ключ\nidentityApplyMissingPublicKeyContent=SSH-ключ идентификатора не имеет связанного с ним открытого ключа. Проверь конфигурацию, чтобы узнать подробности.\nvalid=Действительный\nnotValid=Недействительно\nwarning=Предупреждение\nidentityApplyTitle=Применить идентификацию\nidentityApplyConfigPasswordEnabled=Включена авторизация по паролю\nidentityApplyConfigPasswordEnabledDescription=В конфигурации sshd по-прежнему включена проверка подлинности пароля\nidentityApplyConfigPasswordDisabled=Авторизация пароля отключена\nidentityApplyConfigPasswordDisabledDescription=В конфигурации sshd по-прежнему отключена проверка подлинности пароля\nidentityApplyConfigKeyEnabled=Включен аутентификатор ключей\nidentityApplyConfigKeyEnabledDescription=Аутентификация на основе ключей по-прежнему включена в конфигурации sshd\nidentityApplyConfigKeyDisabled=Авторизация ключей отключена\nidentityApplyConfigKeyDisabledDescription=Аутентификация на основе ключей по-прежнему отключена в конфигурации sshd\nidentityApplyConfigRootDisabledWarning=Корневой вход отключен\nidentityApplyConfigRootDisabledWarningDescription=Вход корневого пользователя не включен в конфигурацию sshd\nidentityApplyConfigAdminWarning=Настройка ключей администратора\nidentityApplyConfigAdminWarningDescription=Возможно, для пользователей-администраторов ключ нужно будет добавить в administrators_authorized_keys\nidentityApplyEditConfig=Редактировать конфигурацию\nidentityApplyEditConfigDescription=Открой конфиг sshd в редакторе, чтобы исправить все проблемы\nidentityApplyEditAuthorizedKeys=Редактирование авторизованных ключей\nidentityApplyEditAuthorizedKeysDescription=Открой файл authorized_keys в редакторе, чтобы отредактировать или удалить другие ключи\nidentityApplyEditConfigButton=Открыть sshd_config\nidentityApplyEditAuthorizedKeysButton=Открыть авторизованные_ключи\nidentityApplySetStoreIdentity=Набор идентификаторов подключения\nidentityApplySetStoreIdentityDescription=Идентификатор настроен на использование соединения\nidentityApplySetStoreIdentityButton=Применить идентификацию\ngenerateKey=Сгенерировать ключ\ngroupSecretStrategy=Управление доступом на основе групп\ngroupSecretStrategyDescription=Как получить групповой секрет, используемый для шифрования и дешифрования для группы. Выбранный тобой метод извлечения будет запущен, когда пользователь войдет в хранилище при запуске.\\n\\nЭтот параметр настраивается для каждой группы отдельно. Чтобы изменить эту настройку для другой группы, отличной от активной в данный момент, тебе придется войти в хранилище как член этой группы.\nfileSecret=Секрет на основе файла\ncommandSecret=Команда\nhttpRequestSecret=HTTP-ответ\nfileSecretChoice=Расположение файла\nfileSecretChoiceDescription=Путь к файлу, содержащему групповой секрет шифрования. Поскольку этот файл может быть запрошен на всех платформах, ты можешь использовать ~ в пути, чтобы сослаться на домашнюю директорию. Файл должен быть доступен на всех системах, с которых ты разблокируешь хранилище, иначе вход будет неудачным.\ncommandSecretField=Скрипт извлечения\ncommandSecretFieldDescription=Команда, которая вернет секретный ключ шифрования для текущей группы. Команда выполняется в оболочке локальной системы по умолчанию, а ключ должен быть выведен в stdout.\nhttpRequestSecretField=URI запроса\nhttpRequestSecretFieldDescription=URI, на который нужно отправить HTTP-запрос. Секрет группы берется из тела HTTP-ответа.\nvaultAuthentication=Аутентификация в хранилище\nvaultAuthenticationDescription=Как аутентифицировать/разблокировать данные хранилища. Существует несколько различных способов шифрования и разблокировки данных хранилища, в зависимости от того, с кем ты хочешь поделиться данными хранилища.\ngroupAuthFailed=Секретная аутентификация не удалась\nuserAuthFailed=Не удалось выполнить проверку подлинности пароля\nsavingChanges=Сохранение изменений\nawsDeviceScan=AWS\naws=AWS\nawsCliInstallTitle=Требуется AWS CLI\nawsCliInstallContent=Интеграция с AWS требует установки AWS CLI в твоей локальной системе\nawsProfileCreateTitle=Новый профиль AWS\nawsProfileAccessKey=Ключ доступа\nawsProfileName=Имя профиля\nawsProfileNameDescription=Отображаемое имя нового профиля\nawsProfileRegion=Регион\nawsProfileRegionDescription=Регион AWS, связанный с профилем\nawsProfileAccessKeyId=Идентификатор ключа доступа\nawsProfileAccessKeyIdDescription=Идентификатор ключа доступа пользователя IAM\nawsProfileSecretAccessKey=Секретный ключ доступа\nawsProfileSecretAccessKeyDescription=Связанный с ним секретный ключ доступа\nawsInstall.displayName=Установка AWS CLI\nawsInstall.displayDescription=Подключайтесь к своим системам AWS с помощью AWS CLI\nawsProfile.displayName=Профиль AWS CLI\nawsProfile.displayDescription=Доступ к AWS через определенный профиль\nawsInstanceId=Идентификатор экземпляра\nawsInstanceIdDescription=Внутренний идентификатор этого экземпляра\nawsInstanceUseSsm=Подключение через SSM\nawsInstanceUseSsmDescription=Используй инструмент SSM, чтобы подключиться к экземпляру через SSH\nawsEc2Instance.displayName=Экземпляр AWS EC2\nawsEc2Instance.displayDescription=Подключение к экземпляру EC2 через SSH\nawsS3Group.displayName=Ведра S3\nawsS3Group.displayDescription=Доступ к ведрам S3 профиля AWS\nawsS3Bucket.displayName=Ведро S3\nawsS3Bucket.displayDescription=Доступ к ведру S3 профиля AWS\nawsEc2Group.displayName=Экземпляры EC2\nawsEc2Group.displayDescription=Доступ к экземплярам EC2 профиля AWS\nawsEc2InstanceSsmTerminal=Открыть терминал SSM\ngenericS3Bucket.displayName=Общий ковш S3\ngenericS3Bucket.displayDescription=Получить доступ к общему ведру S3 через AWS CLI\naddFileSystem=Файловая система ...\ngenericS3BucketHost=Хост\ngenericS3BucketHostDescription=Запись хоста или ручной адрес сервера S3\ngenericS3BucketPortDescription=Порт, который прослушивает сервер S3\ngenericS3BucketAccessKeyId=Идентификатор ключа доступа\ngenericS3BucketAccessKeyIdDescription=Идентификатор ключа доступа пользователя IAM\ngenericS3BucketSecretAccessKey=Секретный ключ доступа\ngenericS3BucketSecretAccessKeyDescription=Связанный с ним секретный ключ доступа\ngenericS3BucketHttps=Включить HTTPS\ngenericS3BucketHttpsDescription=Используй HTTPS для подключения к серверу. Некоторые провайдеры могут требовать HTTPS\ntunnelled=Туннелированный\nawsInstallSync=Синхронизация конфигурации\nawsInstallSyncDescription=Синхронизируй файлы конфигурации AWS CLI с git-хранилищем\nawsInstallLocation=Расположение пользовательских данных\nawsInstallLocationDescription=Путь, откуда берутся файлы конфигурации AWS CLI\ninstanceActions=Действия экземпляра\nopenSplit=Открыть в разделенном терминале\nterminalSplitStrategy=Направление раздельного просмотра\nterminalSplitStrategyDescription=Управляет тем, как разделяются вкладки терминала при использовании функции раздельного просмотра в пакетном режиме для открытия нескольких терминальных сессий рядом друг с другом.\nterminalSplitStrategyDisabledDescription=Управляет тем, как разделяются вкладки терминала при использовании функции раздельного просмотра в пакетном режиме для открытия нескольких терминальных сессий рядом друг с другом.\\n\\nТвоя текущая конфигурация терминала не поддерживает функцию раздельного просмотра.\nhorizontal=Горизонтальный\nvertical=Вертикальный\nbalanced=Сбалансированный\nclose=Закрыть\nhelpButton=$TOPIC$ ссылка на документацию\nquickAccess=Быстрый доступ\ntoggleEnabled=Состояние переключения\ncurrentPath=Текущий путь\ndirectoryContents=Содержание каталога\ndirectoryOptions=Параметры каталога\nchooseConnectionType=Выберите тип соединения\nbatchMode=Пакетный режим\ntoggleButton=Кнопка переключения\ntailscaleUseSsh=Используй SSH-авторизацию Tailscale\ntailscaleUseSshDescription=Входи в систему через сам SSH-сервер tailscale без каких-либо SSH-авторизаций\nportDescription=Порт, на котором работает SSH-сервер\nloginAs=Войдите в систему как\nsshGatewayType=Тип шлюза\nsshGatewayTypeDescription=Нужно ли подключаться к цели через туннель или с помощью опции ProxyJump\ngatewayTunnel=Шлюзовой туннель\nproxyJump=Прокси-переход\ncommandTypeAsyncBackground=Запускать detached в фоновом режиме\ncommandTypeSyncBackground=Запустить в фоновом режиме и дождаться завершения\ncommandTypeTerminalBackground=Открыть в терминале\nasyncBackgroundCommand=Фоновая команда\nsyncBackgroundCommand=Блокирующая фоновая команда\nterminalBackgroundCommand=Команда терминала\ntestingConnection=Тестирование соединения ...\nopenManagementConsole=Открытая консоль управления\nopenLxcTerminal=Откройте терминал LXC\nopenContainerConsole=Открыть последовательную консоль\nkeeper2fa=метод 2FA\nkeeper2faDescription=Основной метод двухфакторной аутентификации, который настроен для твоего аккаунта. Включи этот параметр, если твой аккаунт Keeper требует двухфакторной аутентификации для доступа к паролям.\nkeeperTotpDuration=Продолжительность действия пользовательского кода 2FA\nkeeperTotpDurationDescription=Переопредели стандартную продолжительность действия кода 2FA. Применяется только в том случае, если политика твоей организации позволяет изменять продолжительность.\\n\\nВозможные значения: $VALUES$\nkeeperOtherAuth=Прочее (RSA SecurID, Duo Security, Keeper DNA и так далее)\nextractReusableIdentities=Извлечение многократно используемых идентификационных данных\nidentitiesAdded=Добавлены идентификаторы\nsyncMode=Режим синхронизации\nsyncModeDescription=Управляет тем, как изменения должны синхронизироваться.\\n\\nМгновенный режим будет подталкивать и вытаскивать изменения как можно быстрее, режим запуска и выхода синхронизирует все изменения, сделанные во время сессии, сразу, а ручной режим синхронизирует только тогда, когда ты его инициируешь.\ntoggleTerminalDock=Переключаемая терминальная док-станция\nscriptDirectory=Расположение каталога\nscriptDirectoryDescription=Локальная директория, содержащая файлы сценариев оболочки\nscriptSourceUrl=URL-адрес репозитория\nscriptSourceUrlDescription=URL-адрес удаленного git-репозитория, содержащего файлы сценариев оболочки\nscriptCollectionSourceType=Тип источника\nscriptCollectionSourceTypeDescription=Тип источника, из которого должны загружаться скрипты оболочки\nscriptCollectionSourceEntry=Запись в источнике\nscriptCollectionSourceEntryDescription=Источник, из которого должны загружаться скрипты оболочки\ngitRepository=Git-репозиторий\nscriptCollectionSource.displayName=Источник сценария\nscriptCollectionSource.displayDescription=Автоматический импорт сценариев оболочки из существующего источника\ndirectorySource=Источник каталогов\ngitRepositorySource=Источник Git-репозитория\nrefreshSource=Обновить источник\nscriptTextSourceUrl=URL-адрес скрипта\nscriptTextSourceUrlDescription=URL-адрес, с которого нужно получить файл скрипта\nscriptSourceType=Источник сценария\nscriptSourceTypeDescription=Откуда взять скрипт\nscriptSourceTypeInPlace=Скрипт на месте\nscriptSourceTypeUrl=Внешний URL-адрес\nscriptSourceTypeSource=Существующий источник\nimportScripts=Импортные скрипты\nscriptsContained=$NUMBER$ скрипты\nscriptSourceCollectionImportTitle=Импорт скриптов из источника ($SELECTED$/$COUNT$)\nnoScriptsFound=Скрипты не найдены\ntunnel=Туннель\nnotInitialized=Не инициализирован\nselectCategory=Выберите категорию ...\nscriptSourceName=Имя скрипта\nscriptSourceNameDescription=Имя файла скрипта в исходнике\nworkspaceRestartTitle=Рабочее пространство готово\nworkspaceRestartContent=Ярлык новой рабочей области был создан по адресу $PATH$. Ты можешь перейти по этому ярлыку или перезапустить XPipe прямо сейчас, чтобы новая рабочая область открылась автоматически.\nbrowseShortcut=Просмотр файла\nsyncModeInstant=Синхронизируй мгновенно\nsyncModeSession=Синхронизация при запуске и выходе\nsyncModeManual=Синхронизировать вручную\npushChanges=Изменения при нажатии\npullChanges=Тянуть изменения\nsourcedFrom=Взято из $SOURCE$\ninPlaceScript=Скрипт на месте\ngeneric=Generic\nsyncToPlainDirectory=Синхронизация с обычным каталогом\nsyncToPlainDirectoryDescription=При синхронизации с локальной директорией ты можешь рассматривать эту директорию либо как еще один git-репозиторий, либо просто как обычную директорию. Если включена настройка \"Обычный каталог\", то каталог не инициализируется как git-репозиторий.\nopenSpiceSession=Открыть сессию SPICE\nterminalBehaviour=Поведение терминала\nnoScanPossible=Не найдено ни одного поддерживаемого соединения\nnetworkSwitchPorts=Сетевые порты\nnswitchGroup.displayName=Сетевые порты\nnswitchGroup.displayDescription=Список доступных портов на сетевом устройстве\nnswitchPort.displayName=Сетевой порт\nnswitchPort.displayDescription=Управление отдельным портом на устройстве сетевого коммутатора\nenablePort=Включить порт\nshutdownPort=Отключить порт\nresetPort=Сброс порта\nuseSystemDefault=Использовать систему по умолчанию\nportStatus=Состояние порта\nclearCounters=Очистить счетчики\nshowStatus=Показать состояние\nshowAllPorts=Показать все порты\nactiveLicense=Лицензия\nactiveLicenseDescription=Активировать лицензионный ключ XPipe\nauthenticatorApp=Приложение-аутентификатор\nsecurityKey=Ключ безопасности\nmcpAdditionalContext=Дополнительный контекст MCP\nmcpAdditionalContextDescription=Дополнительные инструкции, которые нужно передать MCP-клиенту. Используй это, чтобы управлять поведением агента и предоставлять дополнительный контекст для твоей индивидуальной настройки.\nmcpAdditionalContextSample=- Не перезапускай службы и демоны автоматически без предварительного подтверждения\\n- При настройке сетевого интерфейса всегда используй 192.168.1.1/24 в качестве шлюза\nprefsRestartTitle=Требуется перезагрузка\nprefsRestartContent=Некоторые параметры, которые ты изменил, требуют перезапуска приложения для применения. Хочешь ли ты перезапустить XPipe сейчас?\nbashShell=Оболочка Bash\n"
  },
  {
    "path": "lang/strings/translations_sv.properties",
    "content": "delete=Ta bort\nproperties=Egenskaper\nusedDate=Används $DATE$\nopenDir=Öppen katalog\nsortLastUsed=Sortera efter senaste användningsdatum\nsortAlphabetical=Sortera alfabetiskt efter namn\nsortIndexed=Sortera efter orderindex\nrestartDescription=En omstart kan ofta vara en snabb lösning\nreportIssue=Rapportera ett problem\nreportIssueDescription=Öppna reportern för integrerad fråga\nusefulActions=Användbara åtgärder\nstored=Sparade\ntroubleshootingOptions=Verktyg för felsökning\ntroubleshoot=Felsökning\nremote=Fjärrfil\naddShellStore=Lägg till Shell ...\naddShellTitle=Lägg till Shell-anslutning\nsavedConnections=Sparade anslutningar\nsave=Spara\nclean=Rengör\nmoveTo=Flytta till ...\naddDatabase=Databas ...\nbrowseInternalStorage=Bläddra i intern lagring\naddTunnel=Tunnel ...\naddService=Tjänst ...\naddScript=Skript ...\naddHost=Fjärrstyrd värd ...\naddShell=Shell-miljö ...\naddCommand=Kommando ...\naddAutomatically=Lägg till automatiskt ...\naddOther=Lägg till andra ...\nconnectionAdd=Lägg till anslutning\nscriptAdd=Lägg till skript\nscriptGroupAdd=Lägg till skriptgrupp\nidentityAdd=Lägg till identitet\nnew=Nya\nselectType=Välj typ\nselectTypeDescription=Välj anslutningstyp\nselectShellType=Typ av skal\nselectShellTypeDescription=Välj typ av Shell-anslutning\nname=Namn på\nstoreIntroHeader=Anslutningsnav\nstoreIntroContent=Här kan du hantera alla dina lokala och fjärrstyrda shell-anslutningar på ett och samma ställe. Till att börja med kan du snabbt upptäcka tillgängliga anslutningar automatiskt och välja vilka du vill lägga till.\nstoreIntroButton=Sök efter anslutningar ...\ndragAndDropFilesHere=Eller bara dra och släpp en fil här\nconfirmDsCreationAbortTitle=Bekräfta avbrytande\nconfirmDsCreationAbortHeader=Vill du avbryta skapandet av datakällan?\nconfirmDsCreationAbortContent=Alla framsteg i skapandet av datakällor kommer att gå förlorade.\nconfirmInvalidStoreTitle=Hoppa över validering\nconfirmInvalidStoreContent=Vill du hoppa över valideringen av anslutningen? Du kan lägga till den här anslutningen även om den inte kunde valideras och åtgärda anslutningsproblemen senare.\nexpand=Expandera\naccessSubConnections=Få tillgång till underanslutningar\ncommon=Vanlig\ncolor=Färg\nalwaysConfirmElevation=Bekräfta alltid behörighetshöjning\nalwaysConfirmElevationDescription=Styr hur man hanterar fall där förhöjda behörigheter krävs för att köra ett kommando på ett system, t.ex. med sudo.\\n\\nSom standard cachelagras eventuella sudo-autentiseringsuppgifter under en session och tillhandahålls automatiskt när det behövs. Om det här alternativet är aktiverat kommer du att bli ombedd att bekräfta den utökade behörigheten varje gång.\nallow=Tillåt\nask=Fråga\ndeny=Avvisa\nshare=Lägg till i git-förvaret\nunshare=Ta bort från git-förvaret\nremove=Ta bort\ncreateNewCategory=Ny underkategori\nprompt=Uppmaning\ncustomCommand=Anpassat kommando\nother=Övrigt\nsetLock=Ställ in lås\nselectConnection=Välj anslutning\nselectEntry=Välj post\ncreateLock=Skapa lösenfras\nchangeLock=Ändra lösenfras\ntest=Test\nfinish=Avsluta\nerror=Ett fel inträffade\ndownloadStageDescription=Flyttar nedladdade filer till systemets nedladdningskatalog och öppnar den.\nok=Ok\nsearch=Sök i\nrepeatPassword=Upprepa lösenord\naskpassAlertTitle=Askpass\nunsupportedOperation=Operation som inte stöds: $MSG$\nfileConflictAlertTitle=Lösa konflikt\nfileConflictAlertContent=En konflikt har uppstått. Filen $FILE$ finns redan på målsystemet.\\n\\nHur skulle du vilja fortsätta?\nfileConflictAlertContentMultiple=En konflikt har uppstått. Filen $FILE$ finns redan.\\n\\nHur vill du gå vidare? Det kan finnas fler konflikter som du kan lösa automatiskt genom att välja ett alternativ som gäller för alla.\nmoveAlertTitle=Bekräfta flyttning\nmoveAlertHeader=Vill du flytta de ($COUNT$) valda elementen till $TARGET$?\ndeleteAlertTitle=Bekräfta borttagning\ndeleteAlertHeader=Vill du ta bort de ($COUNT$) valda elementen?\nselectedElements=Valda element:\nmustNotBeEmpty=$VALUE$ får inte vara tom\nvalueMustNotBeEmpty=Värdet får inte vara tomt\ntransferDescription=Dra filer hit för att ladda ner\ndragLocalFiles=Dra nedladdningar från här\nnull=$VALUE$ får inte vara null\nroots=Rötter\nscripts=Skript\nsearchFilter=Sök ...\nrecent=Senaste\nshortcut=Genväg\nbrowserWelcomeEmptyHeader=Filbläddrare\nbrowserWelcomeEmptyContent=Du kan välja till vänster vilka system som ska öppnas i filbläddraren. XPipe kommer att komma ihåg vilka system och kataloger du har öppnat tidigare och visa dem i en snabbmeny här i framtiden.\nbrowserWelcomeEmptyButton=Öppna lokal filbläddrare\nbrowserWelcomeSystems=Du var nyligen ansluten till följande system:\nbrowserWelcomeDocsHeader=Dokumentation\nbrowserWelcomeDocsContent=Om du föredrar en mer vägledd metod för att bekanta dig med XPipe kan du kolla in dokumentationswebbplatsen.\nbrowserWelcomeDocsButton=Öppen dokumentation\nhostFeatureUnsupported=$FEATURE$ är inte installerad på värden\nmissingStore=$NAME$ existerar inte\nconnectionName=Namn på anslutning\nconnectionNameDescription=Ge den här anslutningen ett eget namn\nopenFileTitle=Öppna fil\nunknown=Okänd\nscanAlertTitle=Lägg till anslutningar\nscanAlertChoiceHeader=Mål\nscanAlertChoiceHeaderDescription=Välj var du vill söka efter anslutningar. Den här funktionen söker först efter alla tillgängliga anslutningar.\nscanAlertHeader=Typer av anslutningar\nscanAlertHeaderDescription=Välj de typer av anslutningar som du vill lägga till automatiskt i systemet.\nnoInformationAvailable=Ingen information tillgänglig\nyes=Ja, det\nno=Nej till\nerrorOccured=Ett fel uppstod\nterminalErrorOccured=Ett terminalfel uppstod\nerrorTypeOccured=Ett undantag av typen $TYPE$ kastades\npermissionsAlertTitle=Behörigheter krävs\npermissionsAlertHeader=Ytterligare behörigheter krävs för att utföra denna åtgärd.\npermissionsAlertContent=Följ popup-fönstret för att ge XPipe de nödvändiga behörigheterna i inställningsmenyn.\nerrorDetails=Detaljer om fel\nupdateReadyAlertTitle=Uppdatering klar\nupdateReadyAlertHeader=En uppdatering till version $VERSION$ är klar att installeras\nupdateReadyAlertContent=Detta kommer att installera den nya versionen och starta om XPipe när installationen är klar.\nerrorNoDetail=Ingen information om felet finns tillgänglig\nerrorNoExceptionMessage=Ett fel av typen $TYPE$ uppstod\nupdateAvailableTitle=Uppdatering tillgänglig\nupdateAvailableContent=En XPipe-uppdatering till version $VERSION$ är tillgänglig för installation. Även om XPipe inte kunde startas kan du försöka installera uppdateringen för att eventuellt åtgärda problemet.\nclipboardActionDetectedTitle=Urklippsåtgärd upptäckt\nclipboardActionDetectedContent=XPipe upptäckte innehåll i ditt klippbord som kan öppnas. Vill du öppna det nu? Vill du importera ditt urklippsinnehåll?\ninstall=Installera ...\nignore=Ignorera\npossibleActions=Tillgängliga åtgärder\nreportError=Rapportera fel\nreportOnGithub=Skapa en problemrapport på GitHub\nreportOnGithubDescription=Öppna en ny fråga i GitHub-förvaret\nreportErrorDescription=Skicka en felrapport med valfri användaråterkoppling och diagnostikinformation\nignoreError=Ignorera fel\nignoreErrorDescription=Ignorera detta fel och fortsätt som om ingenting hänt\nprovideEmail=Hur kan vi kontakta dig (valfritt, endast om du vill få ett svar). Din rapport är anonym som standard, så du kan ange kontaktinformation som en e-postadress här.\nadditionalErrorInfo=Tillhandahåll ytterligare information (valfritt)\nadditionalErrorAttachments=Välj bilagor (valfritt)\ndataHandlingPolicies=Sekretesspolicy\nsendReport=Skicka rapport\nerrorHandler=Felhanterare\nevents=Händelser\nvalidate=Validera\nstackTrace=Stackspårning\npreviousStep=< Tidigare\nnextStep=Nästa > Nästa\nfinishStep=Avsluta\nselect=Välj\nbrowseInternal=Bläddra internt\ncheckOutUpdate=Uppdatering av utcheckning\nquit=Avsluta\nnoTerminalSet=Ingen terminalapplikation har ställts in automatiskt. Du kan göra det manuellt i inställningsmenyn.\nconnections=Anslutningar\nconnectionHub=Anslutningshubb\nsettings=Inställningar\nexplorePlans=Licens\nhelp=Hjälp till\nabout=Om\ndeveloper=Utvecklare\nbrowseFileTitle=Bläddra i fil\nbrowser=Filbläddrare\nselectFileFromComputer=Välj en fil från den här datorn\nlinks=Länkar\nwebsite=Webbplats\ndiscordDescription=Gå med i Discord-servern\nredditDescription=Gå med i XPipe subreddit\nsecurity=Säkerhet\nsecurityPolicy=Säkerhetsinformation\nsecurityPolicyDescription=Läs den detaljerade säkerhetspolicyn\nprivacy=Sekretesspolicy\nprivacyDescription=Läs sekretesspolicyn för XPipe-applikationen\nslackDescription=Gå med i arbetsytan Slack\nsupport=Stöd för\ngithubDescription=Kolla in GitHub-förvaret\nopenSourceNotices=Meddelanden om öppen källkod\ncheckForUpdates=Sök efter uppdateringar\ncheckForUpdatesDescription=Ladda ner en uppdatering om det finns en\nlastChecked=Senast kontrollerad\nversion=Version\nbuild=Bygg version\nruntimeVersion=Version för körtid\nvirtualMachine=Virtuell maskin\nupdateReady=Installera uppdatering\nupdateReadyPortable=Uppdatering av utcheckning\nupdateReadyDescription=En uppdatering har laddats ner och är redo att installeras\nupdateReadyDescriptionPortable=En uppdatering finns tillgänglig för nedladdning\nupdateRestart=Starta om för att uppdatera\nnever=Aldrig\nupdateAvailableTooltip=Uppdatering tillgänglig\nptbAvailableTooltip=Offentlig testversion tillgänglig\nvisitGithubRepository=Besök GitHub-förvaret\nupdateAvailable=Uppdatering tillgänglig: $VERSION$\ndownloadUpdate=Ladda ner uppdatering\nlegalAccept=Jag accepterar licensavtalet för slutanvändare\nconfirm=Bekräfta\nprint=Skriv ut\nwhatsNew=Vad är nytt i version $VERSION$ ($DATE$)\nantivirusNoticeTitle=En anteckning om antivirusprogram\nupdateChangelogAlertTitle=Changelog\ngreetingsAlertTitle=Välkommen till XPipe\neula=Licensavtal för slutanvändare\nnews=Nyheter\nintroduction=Inledning\nprivacyPolicy=Sekretesspolicy\nagree=Håller med\ndisagree=Håller inte med\ndirectories=Kataloger\nlogFile=Loggfil\nlogFiles=Loggfiler\nlogFilesAttachment=Loggfiler\nissueReporter=Rapportör av problem\nopenCurrentLogFile=Loggfiler\nopenCurrentLogFileDescription=Öppna loggfilen för den aktuella sessionen\nopenLogsDirectory=Öppna loggar katalog\ninstallationFiles=Installationsfiler\nopenInstallationDirectory=Installationsfiler\nopenInstallationDirectoryDescription=Öppna XPipe installationskatalog\nlaunchDebugMode=Felsökningsläge\nlaunchDebugModeDescription=Starta om XPipe i felsökningsläge\nextensionInstallTitle=Ladda ner\nextensionInstallDescription=Den här åtgärden kräver ytterligare tredjepartsbibliotek som inte distribueras av XPipe. Du kan automatiskt installera dem här. Komponenterna laddas sedan ner från leverantörens webbplats:\nextensionInstallLicenseNote=Genom att utföra nedladdningen och den automatiska installationen godkänner du villkoren för tredjepartslicenserna:\nlicense=Licens\ninstallRequired=Installation krävs\nrestore=Återställer\nrestoreAllSessions=Återställ alla sessioner\nlimitedTouchscreenMode=Begränsat pekskärmsläge\nlimitedTouchscreenModeDescription=När du använder programmet på ett mer exotiskt pekskärmsgränssnitt, t.ex. en telefonskärm, kan det hända att vissa menyer inte fungerar korrekt. När det här alternativet är aktiverat använder menyimplementeringen mer begränsad funktionalitet för att arbeta med sparsamt skickade mus-/touchhändelser.\nappearance=Utseende\ndisplay=Visning\npersonalization=Personalisering\ndisplayOptions=Alternativ för visning\ntheme=Tema\nrdpConfiguration=Konfiguration av fjärrskrivbord\nrdpClient=RDP-klient\nrdpClientDescription=RDP-klientprogrammet som ska anropas när RDP-anslutningar startas.\\n\\nObservera att olika klienter har olika grader av förmågor och integrationer. Vissa klienter har inte stöd för att skicka lösenord automatiskt, så du måste fortfarande fylla i dem vid start.\nlocalShell=Lokalt skal\nthemeDescription=Ditt föredragna visningstema.\ndontAutomaticallyStartVmSshServer=Starta inte SSH-servern för virtuella datorer automatiskt när det behövs\ndontAutomaticallyStartVmSshServerDescription=Varje skalanslutning till en VM som körs i en hypervisor görs via SSH. XPipe kan automatiskt starta den installerade SSH-servern när det behövs. Om du inte vill ha detta av säkerhetsskäl kan du bara inaktivera detta beteende med det här alternativet.\nconfirmGitShareTitle=Git-synkronisering\nconfirmGitShareContent=Vill du lägga till den valda filen i ditt git vault-arkiv? Detta kommer att kopiera en krypterad version av filen till ditt git-valv och bekräfta dina ändringar. Du kommer då att ha tillgång till filen på alla synkroniserade skrivbord.\ngitShareFileTooltip=Lägg till fil i datakatalogen git vault så att den synkroniseras automatiskt.\\n\\nDen här åtgärden kan endast användas när git vault är aktiverat i inställningarna.\nperformanceMode=Prestanda-läge\nperformanceModeDescription=Inaktiverar alla visuella effekter som inte behövs för att förbättra programmets prestanda.\ndontAcceptNewHostKeys=Acceptera inte nya SSH-värdnycklar automatiskt\ndontAcceptNewHostKeysDescription=XPipe accepterar automatiskt värdnycklar som standard från system där din SSH-klient inte har någon känd värdnyckel redan sparad. Om någon känd värdnyckel har ändrats kommer den dock att vägra att ansluta om du inte accepterar den nya.\\n\\nOm du inaktiverar detta beteende kan du kontrollera alla värdnycklar, även om det inte finns någon konflikt initialt.\nuiScale=UI-skala\nuiScaleDescription=Ett anpassat skalningsvärde som kan ställas in oberoende av den systemomfattande visningsskalan. Värdena är i procent, så t.ex. ett värde på 150 resulterar i en UI-skala på 150%.\neditorProgram=Redaktörsprogram\neditorProgramDescription=Standardtextredigeraren som används vid redigering av alla typer av textdata.\nwindowOpacity=Fönstrets opacitet\nwindowOpacityDescription=Ändrar fönstrets opacitet för att hålla reda på vad som händer i bakgrunden.\nuseSystemFont=Använd systemteckensnitt\nopenDataDir=Vault datakatalog\nopenDataDirButton=Katalog med öppna data\nopenDataDirDescription=Om du vill synkronisera ytterligare filer, t.ex. SSH-nycklar, mellan system med ditt git-arkiv kan du lägga dem i katalogen för lagringsdata. Alla filer som refereras till där kommer att få sina filsökvägar automatiskt anpassade på alla synkroniserade system.\nupdates=Uppdateringar\nselectAll=Välj alla\nadvanced=Avancerad\nthirdParty=Meddelanden om öppen källkod\neulaDescription=Läs licensavtalet för slutanvändare för XPipe-applikationen\nthirdPartyDescription=Visa licenser för öppen källkod för tredjepartsbibliotek\nworkspaceLock=Master lösenfras\nenableGitStorage=Aktivera synkronisering\nsharing=Delning\ngitSync=Git-synkronisering\nenableGitStorageDescription=När det är aktiverat kommer XPipe att initiera ett git-arkiv för det lokala valvet och göra alla ändringar i det. Observera att detta kräver att git installeras och kan sakta ner laddnings- och sparoperationer.\\n\\nAlla kategorier som ska synkroniseras måste uttryckligen markeras som synkroniserade.\nstorageGitRemote=URL för fjärrsynkronisering\nstorageGitRemoteDescription=När den är inställd kommer XPipe automatiskt att dra alla ändringar när du laddar och skjuta alla ändringar till fjärrförvaret när du sparar.\\n\\nDetta gör att du kan dela ditt valv mellan flera XPipe-installationer. Den stöder HTTP- och SSH-URL: er, plus lokala kataloger.\nvault=Valv\nworkspaceLockDescription=Ställer in ett anpassat lösenord för att kryptera all känslig information som lagras i XPipe.\\n\\nDetta resulterar i ökad säkerhet eftersom det ger ett extra lager av kryptering för din lagrade känsliga information. Du kommer sedan att uppmanas att ange lösenordet när XPipe startar.\nuseSystemFontDescription=Kontrollerar om du ska använda systemets standardteckensnitt eller Inter-teckensnittet, som ingår i XPipe.\ntooltipDelay=Fördröjning av verktygstips\ntooltipDelayDescription=Antal millisekunder som ska vänta tills ett verktygstips visas.\nfontSize=Fontstorlek\nwindowOptions=Alternativ för fönster\nsaveWindowLocation=Spara plats för fönster\nsaveWindowLocationDescription=Styr om fönstrets koordinater ska sparas och återställas vid omstart.\nstartupShutdown=Uppstart / Avstängning\nshowChildrenConnectionsInParentCategory=Visa underordnade kategorier i överordnad kategori\nshowChildrenConnectionsInParentCategoryDescription=Huruvida alla anslutningar som finns i underkategorier ska inkluderas eller inte när en viss överkategori väljs.\\n\\nOm detta är avaktiverat beter sig kategorierna mer som klassiska mappar som bara visar sitt direkta innehåll utan att inkludera undermappar.\ncondenseConnectionDisplay=Kondensera anslutningsdisplayen\ncondenseConnectionDisplayDescription=Gör så att varje anslutning på högsta nivån tar mindre vertikalt utrymme för att möjliggöra en mer komprimerad anslutningslista.\nopenConnectionSearchWindowOnConnectionCreation=Öppna fönstret för sökning av anslutning vid skapande av anslutning\nopenConnectionSearchWindowOnConnectionCreationDescription=Huruvida fönstret för att söka efter tillgängliga underanslutningar ska öppnas automatiskt när en ny shell-anslutning läggs till.\nworkflow=Arbetsflöde\nsystem=Ett system\napplication=Applikation\nstorage=Förvaring\nrunOnStartup=Körs vid uppstart\ncloseBehaviour=Avsluta beteende\ncloseBehaviourDescription=Styr hur XPipe ska fortsätta när huvudfönstret stängs.\nlanguage=Språk\nlanguageDescription=Det visningsspråk som ska användas. Översättningarna förbättras genom bidrag från gemenskapen. Du kan hjälpa till med översättningsarbetet genom att skicka in översättningsfixar på GitHub.\nlightTheme=Ljust tema\ndarkTheme=Mörkt tema\nexit=Avsluta XPipe\ncontinueInBackground=Fortsätt i bakgrunden\nminimizeToTray=Minimera till facket\ncloseBehaviourAlertTitle=Ställ in stängningsbeteende\ncloseBehaviourAlertTitleHeader=Välj vad som ska hända när fönstret stängs. Alla aktiva anslutningar kommer att stängas när programmet stängs ner.\nstartupBehaviour=Uppstartsbeteende\nstartupBehaviourDescription=Styr standardbeteendet för skrivbordsprogrammet när XPipe startas.\nclearCachesAlertTitle=Rensa cachen\nclearCachesAlertContent=Vill du rensa alla XPipe-cacher? Detta kommer att radera all cache-data som lagras för att förbättra användarupplevelsen.\nstartGui=Starta GUI\nstartInTray=Börja i facket\nstartInBackground=Starta i bakgrunden\nclearCaches=Rensa cacheminnet ...\nclearCachesDescription=Ta bort all cache-data\ncancel=Avbryt\nnotAnAbsolutePath=Inte en absolut sökväg\nnotADirectory=Inte en katalog\nnotAnEmptyDirectory=Inte en tom katalog\nautomaticallyCheckForUpdates=Sök efter uppdateringar\nautomaticallyCheckForUpdatesDescription=När det är aktiverat hämtas information om nya versioner automatiskt medan XPipe körs efter ett tag. Du måste fortfarande uttryckligen bekräfta varje uppdateringsinstallation.\nsendAnonymousErrorReports=Skicka anonyma felrapporter\nsendUsageStatistics=Skicka anonym användningsstatistik\nstorageDirectory=Katalog för lagring\nstorageDirectoryDescription=Den plats där XPipe ska lagra all anslutningsinformation. När du ändrar detta kopieras inte data i den gamla katalogen till den nya.\nlogLevel=Loggnivå\nappBehaviour=Applikationens beteende\nlogLevelDescription=Den loggnivå som bör användas vid skrivning av loggfiler.\ndeveloperMode=Utvecklarläge\ndeveloperModeDescription=När den är aktiverad får du tillgång till en mängd ytterligare alternativ som är användbara för utveckling.\neditor=Redaktör\ncustom=Anpassad\npasswordManager=Lösenordshanterare\nexternalPasswordManager=Extern lösenordshanterare\npasswordManagerDescription=Den lokalt installerade lösenordshanteraren som ska integreras med.\\n\\nOm du har en lösenordshanterare installerad kan du konfigurera XPipe för att hämta lösenord från den så att XPipe inte behöver lagra lösenorden själv. När det är aktiverat kan alla lösenordsfält för en anslutning sedan konfigureras för att använda lösenordshanteraren.\npasswordManagerCommandTest=Testa lösenordshanterare\npasswordManagerCommandTestDescription=Här kan du testa om utmatningen ser korrekt ut om du har konfigurerat en lösenordshanterare.\npreferTerminalTabs=Föredrar att öppna nya flikar\npreferTerminalTabsDescription=Styr om XPipe ska försöka öppna nya flikar i den valda terminalen i stället för nya fönster. Inte alla terminaler stöder flikar.\ncustomRdpClientCommand=Anpassat kommando\ncustomRdpClientCommandDescription=Kommandot som ska köras för att starta den anpassade RDP-klienten.\\n\\nPlatshållarsträngen $FILE kommer att ersättas av det citerade absoluta .rdp-filnamnet när det anropas. Kom ihåg att citera sökvägen till den körbara filen om den innehåller mellanslag.\ncustomEditorCommand=Kommando för anpassad redigerare\ncustomEditorCommandDescription=Kommandot som ska utföras för att starta den anpassade redigeraren.\\n\\nPlatshållarsträngen $FILE kommer att ersättas av det citerade absoluta filnamnet när kommandot anropas. Kom ihåg att citera sökvägen till den körbara editorn om den innehåller mellanslag.\neditorReloadTimeout=Timeout för omladdning av redigerare\neditorReloadTimeoutDescription=Antal millisekunder som ska vänta innan en fil läses efter att den har uppdaterats. Detta undviker problem i fall där din editor är långsam när det gäller att skriva eller släppa fillås.\nencryptAllVaultData=Kryptera alla valvdata\nencryptAllVaultDataDescription=När detta är aktiverat kommer alla delar av valvanslutningsdata att krypteras med din krypteringsnyckel för användarvalvet, i motsats till endast hemligheter i dessa data. Detta lägger till ytterligare ett lager av säkerhet för andra parametrar som användarnamn, värdnamn etc. som inte krypteras som standard i valvet.\\n\\nDetta alternativ kommer att göra din git valvhistorik och diffs värdelös eftersom du inte kan se de ursprungliga ändringarna längre, bara binära ändringar.\nvaultSecurity=Säkerhet för valv\ndeveloperDisableUpdateVersionCheck=Inaktivera uppdatering av versionskontroll\ndeveloperDisableUpdateVersionCheckDescription=Styr om uppdateringskontrollen ska ignorera versionsnumret när den letar efter en uppdatering.\ndeveloperDisableGuiRestrictions=Inaktivera GUI-restriktioner\ndeveloperDisableGuiRestrictionsDescription=Kontrollerar om vissa inaktiverade åtgärder fortfarande kan utföras från användargränssnittet.\ndeveloperShowHiddenEntries=Visa dolda poster\ndeveloperShowHiddenEntriesDescription=När detta är aktiverat visas dolda och interna datakällor.\ndeveloperShowHiddenProviders=Visa dolda leverantörer\ndeveloperShowHiddenProvidersDescription=Styr om dolda och interna providers för anslutningar och datakällor ska visas i dialogrutan för skapande.\ndeveloperDisableConnectorInstallationVersionCheck=Inaktivera versionsgranskning av anslutningar\ndeveloperDisableConnectorInstallationVersionCheckDescription=Styr om uppdateringskontrollen ska ignorera versionsnumret när den kontrollerar versionen av en XPipe-kontakt som är installerad på en fjärrmaskin.\nshellCommandTest=Test av Shell-kommando\nshellCommandTestDescription=Kör ett kommando i den shell-session som används internt av XPipe.\nterminal=Terminal\nterminalType=Terminalemulator\nterminalConfiguration=Konfiguration av terminal\nterminalCustomization=Anpassning av terminaler\neditorConfiguration=Konfiguration av redigerare\ndefaultApplication=Standardapplikation\ninitialSetup=Inledande installation\nterminalTypeDescription=Standardterminalen som ska användas för att öppna shell-anslutningar.\\n\\nNivån på funktionsstödet varierar mellan olika terminaler, och varje terminal är markerad som antingen rekommenderad eller inte rekommenderad. Din användarupplevelse blir bäst när du använder en rekommenderad terminal.\nprogram=Program\ncustomTerminalCommand=Anpassat terminalkommando\ncustomTerminalCommandDescription=Det kommando som ska utföras för att öppna den anpassade terminalen med ett visst kommando.\\n\\nXPipe kommer att skapa ett tillfälligt startskript för att din terminal ska kunna köras. Platshållarsträngen $CMD i kommandot som du tillhandahåller kommer att ersättas av det faktiska startskriptet när det anropas. Kom ihåg att citera din terminals körbara sökväg om den innehåller mellanslag.\nclearTerminalOnInit=Rensa terminal vid init\nclearTerminalOnInitDescription=När XPipe är aktiverat körs kommandot clear efter att en ny terminalsession har startats för att ta bort onödiga utdata som skrevs ut när terminalsessionen startades.\ndontCachePasswords=Cacha inte efterfrågade lösenord\ndontCachePasswordsDescription=Styr om efterfrågade lösenord ska cachas internt av XPipe så att du inte behöver ange dem igen i den aktuella sessionen.\\n\\nOm det här beteendet är inaktiverat måste du ange alla efterfrågade uppgifter varje gång de krävs av systemet.\ndenyTempScriptCreation=Neka skapande av temporärt skript\ndenyTempScriptCreationDescription=För att förverkliga en del av sin funktionalitet skapar XPipe ibland tillfälliga skalskript på ett målsystem för att möjliggöra en enkel körning av enkla kommandon. Dessa innehåller inte någon känslig information och skapas bara för implementeringsändamål.\\n\\nOm detta beteende är inaktiverat kommer XPipe inte att skapa några temporära filer på ett fjärrsystem. Det här alternativet är användbart i högsäkerhetssammanhang där varje filsystemändring övervakas. Om detta är avaktiverat kommer vissa funktioner, t.ex. skalmiljöer och skript, inte att fungera som avsett.\ndisableCertutilUse=Inaktivera certutil-användning på Windows\nuseLocalFallbackShell=Använda lokalt reservskal\nuseLocalFallbackShellDescription=Byt till att använda ett annat lokalt skal för att hantera lokala operationer. Det kan vara PowerShell i Windows och Bourne Shell i andra system.\\n\\nDet här alternativet kan användas om det normala lokala standardskalet är inaktiverat eller trasigt i någon grad. Vissa funktioner kanske inte fungerar som förväntat när det här alternativet är aktiverat.\ndisableCertutilUseDescription=På grund av flera brister och buggar i cmd.exe skapas tillfälliga skalskript med certutil genom att använda den för att avkoda base64-ingång eftersom cmd.exe bryter på icke-ASCII-ingång. XPipe kan också använda PowerShell för det men det kommer att bli långsammare.\\n\\nDetta inaktiverar all användning av certutil på Windows-system för att förverkliga viss funktionalitet och faller tillbaka till PowerShell istället. Detta kan glädja vissa AV: er eftersom vissa av dem blockerar certutil-användning.\ndisableTerminalRemotePasswordPreparation=Inaktivera förberedelse av fjärrlösenord för terminal\ndisableTerminalRemotePasswordPreparationDescription=I situationer där en fjärranslutning som går via flera mellanliggande system ska upprättas i terminalen, kan det finnas ett krav på att förbereda eventuella lösenord på ett av de mellanliggande systemen för att möjliggöra automatisk ifyllning av eventuella uppmaningar.\\n\\nOm du inte vill att lösenorden någonsin ska överföras till något mellanliggande system kan du inaktivera det här beteendet. Eventuella lösenord för mellanliggande system kommer då att efterfrågas i själva terminalen när den öppnas.\nmore=Mer om\ntranslate=Översättningar\nallConnections=Alla anslutningar\nallScripts=Alla skript\nallIdentities=Alla identiteter\nsynced=Synkad\npredefined=Fördefinierad\nsamples=Prover\ngoodMorning=God morgon\ngoodAfternoon=God eftermiddag\ngoodEvening=God kväll\naddVisual=Visuell ...\naddDesktop=Skrivbord ...\nssh=SSH\nsshConfiguration=SSH-konfiguration\nsize=Storlek\nattributes=Attribut\nmodified=Ändrad\nowner=Ägare\nupdateReadyTitle=Uppdatering till $VERSION$ klar\ntemplates=Mallar\nretry=Försök igen\nretryAll=Försök igen alla\nreplace=Byt ut\nreplaceAll=Ersätt alla\nhibernateBehaviour=Beteende vid viloläge\nhibernateBehaviourDescription=Styr hur programmet ska bete sig när systemet försätts i viloläge/viloläge.\noverview=Översikt över\nhistory=Historik\nskipAll=Hoppa över allt\nnotes=Anteckningar\naddNotes=Lägg till anteckningar\norder=Ordna om\nkeepFirst=Håll först\nkeepLast=Behåll sista\npinToTop=Nål till toppen\nunpinFromTop=Plocka bort från toppen\norderAheadOf=Beställ i förväg av ...\nclearIndex=Återställ index\nhttpServer=HTTP-server\nmcpServer=MCP-server\napiKey=API-nyckel\napiKeyDescription=API-nyckeln för att autentisera XPipe daemon API-förfrågningar. För mer information om hur du autentiserar, se den allmänna API-dokumentationen.\ndisableApiAuthentication=Inaktivera API-autentisering\ndisableApiAuthenticationDescription=Inaktiverar alla nödvändiga autentiseringsmetoder så att alla oautentiserade förfrågningar kommer att hanteras.\\n\\nAutentisering bör endast inaktiveras för utvecklingsändamål.\napi=API\nstoreIntroImportContent=Använder du redan XPipe på ett annat system? Synkronisera dina befintliga anslutningar över flera system via ett fjärranslutet git-arkiv. Du kan också synkronisera senare när som helst om det inte är inställt ännu.\nstoreIntroImportButton=Synkronisera anslutningar ...\nstoreIntroImportHeader=Importera anslutningar\nshowNonRunningChildren=Visa barn som inte kör\nhttpApi=HTTP API\nisOnlySupportedLimit=stöds endast med en professionell licens när du har fler än $COUNT$ anslutningar\nareOnlySupportedLimit=stöds endast med en professionell licens när de har mer än $COUNT$ anslutningar\nenabled=Aktiverad\nenableGitStoragePtbDisabled=Git-synkronisering är inaktiverad för offentliga testbyggnader för att förhindra användning med vanliga git-arkiv och för att avskräcka från att använda en PTB-byggnad som din dagliga drivrutin.\ncopyId=Kopiera API-ID\nrequireDoubleClickForConnections=Kräver dubbelklick för anslutningar\nrequireDoubleClickForConnectionsDescription=Om funktionen är aktiverad måste du dubbelklicka på anslutningar för att starta dem. Detta är användbart om du är van vid att dubbelklicka på saker.\nclearTransferDescription=Rensa urvalet\nselectTab=Välj flik\ncloseTab=Stäng fliken\ncloseOtherTabs=Stäng andra flikar\ncloseAllTabs=Stäng alla flikar\ncloseLeftTabs=Stäng flikar till vänster\ncloseRightTabs=Stäng flikar till höger\naddSerial=Seriell ...\nconnect=Ansluta\nworkspaces=Arbetsytor\nmanageWorkspaces=Hantera arbetsytor\naddWorkspace=Lägg till arbetsyta ...\nworkspaceAdd=Lägga till en ny arbetsyta\nworkspaceAddDescription=Arbetsytor är distinkta konfigurationer för att köra XPipe. Varje arbetsyta har en datakatalog där all data lagras lokalt. Detta inkluderar anslutningsdata, inställningar och mer.\\n\\nOm du använder synkroniseringsfunktionen kan du också välja att synkronisera varje arbetsyta med ett annat git-repository.\nworkspaceName=Arbetsytans namn\nworkspaceNameDescription=Visningsnamnet för arbetsytan\nworkspacePath=Sökväg till arbetsytan\nworkspacePathDescription=Platsen för datakatalogen för arbetsytan\nworkspaceCreationAlertTitle=Skapande av arbetsyta\ndeveloperForceSshTty=Force SSH TTY\ndeveloperForceSshTtyDescription=Tilldela en pty till alla SSH-anslutningar för att testa stödet för en saknad stderr och en pty.\ndeveloperDisableSshTunnelGateways=Inaktivera SSH-gateway-tunnling\ndeveloperDisableSshTunnelGatewaysDescription=Använd inte tunnelsessioner för gateways utan anslut istället direkt till systemet.\nttyWarning=Anslutningen har tvingande allokerat en pty/tty och tillhandahåller inte en separat stderr-ström.\\n\\nDetta kan leda till en del problem.\\n\\nOm du kan, försök att få anslutningskommandot att inte allokera en pty.\nxshellSetup=Xshell-installation\ntermiusSetup=Termius installation\ntryPtbDescription=Testa nya funktioner tidigt i XPipe-utvecklarbyggnader\nconfirmVaultUnencryptTitle=Bekräfta avkryptering av valv\nconfirmVaultUnencryptContent=Vill du verkligen avaktivera avancerad valvkryptering? Detta tar bort den extra krypteringen för lagrade data och skriver över befintliga data.\nenableHttpApi=Aktivera HTTP API\nenableHttpApiDescription=Aktiverar API:et, vilket gör att externa program kan anropa XPipe-daemon för att utföra åtgärder med dina hanterade anslutningar.\nchooseCustomIcon=Välj anpassad ikon\ngitVault=Git-valv\nfileBrowser=Filbläddrare\nconfirmAllDeletions=Bekräfta alla borttagningar\nconfirmAllDeletionsDescription=Om en bekräftelsedialog ska visas för alla raderingsåtgärder. Som standard kräver endast kataloger en bekräftelse.\nyesterday=Igår\ngreen=Grönt\nyellow=Gul\nblue=Blå\nred=Röd\ncyan=Cyan\npurple=Lila\nasktextAlertTitle=Uppmaning\nfileWriteSudoTitle=Sudo filskrivning\nfileWriteSudoContent=Filen som du försöker skriva ger inte skrivbehörighet till din användare. Vill du skriva den här filen som root med sudo? Detta kommer automatiskt att höja till root med antingen de befintliga autentiseringsuppgifterna eller via en prompt.\ndontAllowTerminalRestart=Tillåt inte omstart av terminal\ndontAllowTerminalRestartDescription=Som standard kan terminalsessioner startas om efter att de har avslutats inifrån terminalen. För att möjliggöra detta kommer XPipe att acceptera dessa externa förfrågningar från terminalen för att starta sessionen igen\\n\\nXPipe har ingen kontroll över terminalen och var det här samtalet kommer ifrån, så skadliga lokala applikationer kan också använda den här funktionen för att starta anslutningar via XPipe. Om du inaktiverar den här funktionen förhindras detta scenario.\nopenDocumentation=Öppna dokumentation\nopenDocumentationDescription=Besök XPipes dokumentsida för den här utgåvan\nrenameAll=Byt namn på alla\nlogging=Loggning\nenableTerminalLogging=Aktivera terminalloggning\nenableTerminalLoggingDescription=Aktiverar loggning på klientsidan för alla terminalsessioner. Alla inmatningar och utmatningar från terminalsessionen skrivs in i en sessionsloggfil. Observera att all känslig information, t.ex. lösenordsuppmaningar, inte registreras.\nterminalLoggingDirectory=Loggar för terminalsessioner\nterminalLoggingDirectoryDescription=Alla loggar lagras i XPipe-datakatalogen på ditt lokala system.\nopenSessionLogs=Öppna sessionsloggar\nsessionLogging=Loggning av terminal\nsessionActive=En bakgrundssession körs för den här anslutningen.\\n\\nOm du vill stoppa sessionen manuellt klickar du på statusindikatorn.\nskipValidation=Hoppa över validering\nscriptsIntroHeader=Om skript\nscriptsIntroContent=Du kan köra skript på shell init, i filbläddraren och på begäran. Du kan skapa skript själv inom XPipe eller importera befintliga från ditt lokala system eller från ett fjärranslutet git-repository.\nscriptsIntroBottomHeader=Använda skript\nscriptsIntroBottomContent=Det finns en mängd olika exempelskript att börja med. Du kan klicka på redigeringsknappen för de enskilda skripten för att se hur de implementeras. Skript måste först aktiveras för att kunna köras och visas i menyer, det finns en växlingsknapp för detta i varje skript.\nscriptsIntroBottomButton=Kom igång\nscriptSourcesIntroHeader=Skriptkällor\nscriptSourcesIntroContent=Du kan lägga till egna skriptkällor för att få omedelbar tillgång till en hel samling av shell-skript. Både lokala källor och fjärranslutna git-repositorier stöds som källor. Alla upptäckta skript från källan kommer att bli tillgängliga automatiskt.\nscriptSourcesIntroButton=Lägg till källa ...\ncheckForSecurityUpdates=Sök efter säkerhetsuppdateringar\ncheckForSecurityUpdatesDescription=XPipe kan söka efter potentiella säkerhetsuppdateringar separat från normala funktionsuppdateringar. När detta är aktiverat kommer åtminstone viktiga säkerhetsuppdateringar att rekommenderas för installation även om den normala uppdateringskontrollen är inaktiverad.\\n\\nOm du avaktiverar den här inställningen utförs ingen extern versionsbegäran och du kommer inte att meddelas om några säkerhetsuppdateringar.\nclickToDock=Klicka för att docka terminal\nterminalStarting=Väntar på start av terminal ...\npinTab=Pin-flik\nunpinTab=Ta bort fliken\npinned=Pinnad\nenableConnectionHubTerminalDocking=Möjliggör anslutning hubb terminal dockning\nenableConnectionHubTerminalDockingDescription=Du kan docka terminalfönster till XPipe-applikationsfönstret i anslutningshubben för att simulera en något integrerad terminal. Terminalfönstren hanteras sedan av XPipe för att alltid passa in i dockan.\nenableFileBrowserTerminalDocking=Aktivera dockning av terminal för filbläddrare\nenableFileBrowserTerminalDockingDescription=Du kan docka terminalfönster till XPipe-applikationsfönstret i filbläddraren för att simulera en något integrerad terminal. Terminalfönstren hanteras sedan av XPipe för att alltid passa in i dockan.\ndownloadsDirectory=Anpassad katalog för nedladdningar\ndownloadsDirectoryDescription=Den anpassade katalogen för att sätta nedladdade filer i när du klickar på knappen Flytta till nedladdningar. Som standard kommer XPipe att använda din användares nedladdningskatalog.\npinLocalMachineOnStartup=Fäst fliken för lokal maskin vid start\npinLocalMachineOnStartupDescription=Öppna automatiskt en flik på den lokala maskinen och fäst den. Detta är användbart om du ofta använder en delad filbläddrare med den lokala maskinen och fjärrfilsystemet öppna.\nterminalErrorDescription=Det här felet är terminalt och XPipe kan inte fortsätta utan att åtgärda det.\ngroupName=Gruppens namn\nchmodPermissions=Nya behörigheter\neditFilesWithDoubleClick=Redigera filer med dubbelklick\neditFilesWithDoubleClickDescription=När detta är aktiverat öppnas filer direkt i textredigeraren om du dubbelklickar på dem, istället för att visa snabbmenyn.\ncensorMode=Censorläge\ncensorModeDescription=Suddar ut all information som värdnamn, användarnamn, anslutningsnamn med mera.\\n\\nDetta är användbart om du tänker skärmdumpa eller skärmdela XPipe och inte vill läcka någon information.\naddIdentity=Identitet ...\nidentities=Identiteter\naddMacro=Åtgärd ...\nidentitiesIntroHeader=Om identiteter\nidentitiesIntroContent=Om du återanvänder vanliga kombinationer av användarnamn, lösenord och nycklar kan det vara klokt att skapa återanvändbara identiteter. På så sätt kan du snabbt referera till dem när du lägger till nya anslutningar.\nidentitiesIntroBottomHeader=Delning av identiteter\nidentitiesIntroBottomContent=Du kan lägga till identiteter lokalt eller också synkronisera dem i git-arkivet när detta är aktiverat. Detta gör det möjligt att selektivt dela identiteter över flera system och med andra teammedlemmar.\nidentitiesIntroBottomButton=Synkronisering av inställningar\nidentitiesIntroButton=Skapa identitet\nuserName=Användarnamn\nuserAuth=Användarbaserad lösenordsautentisering\ngroupAuth=Gruppbaserad hemlig autentisering\nteam=Team\nteamSettings=Inställningar för team\nteamVaults=Teamets valv\nvaultTypeNameDefault=Standardvalv\nvaultTypeNameLegacy=Arvtagarens personliga valv\nvaultTypeNamePersonal=Personligt valv\nvaultTypeNameTeam=Lagets valv\nteamVaultsDescription=Med gruppvalv kan flera användare och grupper få säker åtkomst till ett delat valv. Du kan konfigurera anslutningar och identiteter så att de antingen delas för alla användare eller bara är tillgängliga för enskilda användare och grupper genom att kryptera dem med en egen nyckel. Andra valvanvändare kan inte komma åt personliga eller gruppbaserade anslutningar och identiteter om de inte har tillgång till nyckeln.\nvaultTypeContentDefault=Du använder för närvarande ett standardvalv utan användare och med en anpassad lösenfras. Hemligheter krypteras med den lokala valvnyckeln. Du kan uppgradera till ett personligt valv genom att skapa ett användarkonto för valvet. Då kan du kryptera valvets hemligheter med din egen personliga lösenfras som du måste ange vid varje inloggning för att låsa upp valvet.\nvaultTypeContentLegacy=Du använder för närvarande ett äldre personligt valv för din användare. Hemligheter krypteras med din personliga lösenfras. Den här äldre kompatibiliteten har begränsade funktioner och kan inte uppgraderas till ett teamvalv på plats.\nvaultTypeContentPersonal=Du använder för närvarande ett personligt valv för din användare. Hemligheter krypteras med din personliga lösenfras. Du kan uppgradera till ett teamvalv genom att lägga till ytterligare valvanvändare eller lägga till en gruppbaserad åtkomstkonfiguration.\nvaultTypeContentTeam=Du använder för närvarande ett teamvalv, som ger flera användare säker åtkomst till ett delat valv. Du kan konfigurera anslutningar och identiteter så att de antingen delas för alla användare eller bara är tillgängliga för din personliga användare eller grupp genom att kryptera dem med din personliga nyckel eller gruppnyckel. Andra valvanvändare kan inte komma åt dina personliga eller gruppbaserade anslutningar och identiteter om de inte har tillgång till nyckeln.\ngroupManagement=Grupphantering\ngroupManagementEmpty=Grupphantering\ngroupManagementDescription=Hantera befintliga valvgrupper eller skapa nya. Varje valvgrupp har sin egen individuella hemliga nyckel som används för att kryptera anslutningar och identiteter som bara ska vara tillgängliga för gruppen och inte för andra.\ngroupManagementEmptyDescription=Hantera befintliga valvgrupper eller skapa nya. Varje valvgrupp har sin egen individuella hemliga nyckel som används för att kryptera anslutningar och identiteter som bara ska vara tillgängliga för gruppen och inte för andra.\\n\\nGruppbaserade konton för ett team stöds i den professionella planen.\nuserManagement=Hantering av användare\nuserManagementEmpty=Hantering av användare\nuserManagementDescription=Hantera befintliga valvanvändare eller skapa nya. Varje valvanvändare har sitt eget individuella lösenord som används för att kryptera anslutningar och identiteter som endast ska vara tillgängliga för användaren och inte för andra.\nuserManagementEmptyDescription=Hantera befintliga valvanvändare eller skapa nya. Varje valvanvändare har sitt eget individuella lösenord som används för att kryptera anslutningar och identiteter som bara ska vara tillgängliga för användaren och inte för andra. Skapa en användare för dig själv för att kunna kryptera anslutningar och identiteter med din personliga nyckel.\\n\\nEtt enda användarkonto stöds i community-utgåvan. Flera användarkonton för ett team stöds i Professional-planen.\nuserIntroHeader=Hantering av användare\nuserIntroContent=Skapa det första användarkontot för dig själv för att komma igång. Detta gör att du kan låsa denna arbetsyta med ett lösenord.\naddReusableIdentity=Lägg till återanvändbar identitet\nusers=Användare\nsyncVault=Synkronisering av valv\nsyncVaultDescription=Om du vill synkronisera ditt valv med flera system eller med flera teammedlemmar aktiverar du git-synkronisering för det här valvet.\nenableGitSync=Aktivera git-synkronisering\nbrowseVault=Valvdata\nbrowseVaultDescription=Du kan själv ta en titt på valvkatalogen i din ursprungliga filhanterare. Observera att externa redigeringar inte rekommenderas och kan orsaka en mängd olika problem.\nbrowseVaultButton=Bläddra bland valv\nvaultUsers=Vault-användare\ncreateHeapDump=Skapa heap dump\ncreateHeapDumpDescription=Dumpa minnesinnehåll till fil för att felsöka minnesanvändning\ninitializingApp=Laddar anslutningar\ncheckingLicense=Kontroll av licens\nloadingGit=Synkronisering med git repo\nloadingGpg=Startar GnuPG-daemon för git\nloadingSettings=Inställningar för laddning\nloadingConnections=Laddar anslutningar\nunlockingVault=Lås upp valv\nloadingUserInterface=Laddar användargränssnitt\nptbNotice=Meddelande för den offentliga testversionen\nuserDeletionTitle=Radering av användare\nuserDeletionContent=Vill du ta bort den här valvanvändaren? Detta kommer att kryptera om alla dina personliga identiteter och anslutningshemligheter med hjälp av valvnyckeln som är tillgänglig för alla användare. Detta kommer att ta ett tag och XPipe kommer att starta om för att tillämpa användarändringarna.\ngroupDeletionTitle=Radering av grupp\ngroupDeletionContent=Vill du ta bort den här valvgruppen? Detta kommer att kryptera om alla gruppspecifika identiteter och anslutningshemligheter med hjälp av valvnyckeln som är tillgänglig för alla användare. Detta kommer att ta ett tag och XPipe kommer att starta om för att tillämpa gruppändringarna.\nkillTransfer=Döda överföring\ndestination=Destination\nconfiguration=Konfiguration\nnewFile=Ny fil\nnewLink=Ny länk\nlinkName=Länkens namn\nscanConnections=Hitta tillgängliga anslutningar ...\nobserve=Börja observera\nstopObserve=Sluta observera\ncreateShortcut=Skapa genväg på skrivbordet\nbrowseFiles=Bläddra bland filer\nclone=Klon\ntargetPath=Mål sökväg\nnewDirectory=Ny katalog\ncopyShareLink=Kopiera länk\nselectStore=Välj butik\nsaveSource=Spara för senare\nexecute=Utför\ndeleteChildren=Ta bort alla barn\nscriptGroupDescriptionDescription=Ge denna grupp en valfri beskrivning\nabstractHostDescriptionDescription=Ge den här värden en valfri beskrivning\nselectSource=Välj källa\ncommandLineRead=Uppdatering\ncommandLineWrite=Skriva\nadditionalOptions=Ytterligare alternativ\ninput=Inmatning\nmachine=Maskin\nopen=Öppna\nedit=Redigera\nscriptContents=Skriptets innehåll\nscriptContentsDescription=Skriptkommandona som ska köras\nsnippets=Skriptberoenden\nsnippetsDescription=Andra skript som ska köras först\nsnippetsDependenciesDescription=Alla möjliga skript som ska köras om det är tillämpligt\nisDefault=Körs på init i alla kompatibla skal\nbringToShells=Ta med till alla kompatibla skal\nisDefaultGroup=Kör alla gruppskript på shell init\nexecutionType=Typ av exekvering\nexecutionTypeDescription=I vilka sammanhang kan man använda detta skript\nminimumShellDialect=Typ av skal\nminimumShellDialectDescription=Shell-typen för att köra detta skript i\ndumbOnly=Dum\nterminalOnly=Terminal\nboth=Både\nshouldElevate=Bör höja\nshouldElevateDescription=Om detta skript ska köras med förhöjda behörigheter\nscript.displayName=Shell-skript\nscript.displayDescription=Skapa ett återanvändbart shell-skript\nscriptGroup.displayName=Skriptgrupp\nscriptGroup.displayDescription=Gruppera skript tillsammans och organisera dem inom\nscriptGroup=Gruppera\nscriptGroupDescription=Den grupp som ska tilldelas detta manus\nscriptGroupGroupDescription=Den valfria föräldragruppen att tilldela denna skriptgrupp till\nopenInNewTab=Öppna i ny flik\nexecuteInBackground=i bakgrunden\nexecuteInTerminal=i $TERM$\nback=Gå tillbaka\nbrowseInWindowsExplorer=Bläddra i Windows Explorer\nbrowseInDefaultFileManager=Bläddra i standardfilhanteraren\nbrowseInFinder=Bläddra i sökare\ncopy=En kopia\npaste=Klistra in\ncopyLocation=Plats för kopiering\nabsolutePaths=Absoluta sökvägar\nabsoluteLinkPaths=Absoluta länksökvägar\nabsolutePathsQuoted=Absoluta citerade sökvägar\nfileNames=Filnamn\nlinkFileNames=Namn på länkfiler\nfileNamesQuoted=Filnamn (citerad)\ndeleteFile=Ta bort $FILE$\neditWithEditor=Redigera med $EDITOR$\nfollowLink=Följ länk\ngoForward=Gå framåt\nshowDetails=Visa detaljer\nshowDetailsDescription=Visa stackspårning av fel\nopenFileWith=Öppna med ...\nopenWithDefaultApplication=Öppna med standardapplikation\nrename=Byt namn på\nrun=Kör\nopenInTerminal=Öppna i terminal\nfile=En fil\ndirectory=Katalog\nsymbolicLink=Symbolisk länk\ndesktopEnvironment.displayName=Skrivbordsmiljö\ndesktopEnvironment.displayDescription=Skapa en återanvändbar konfiguration för fjärrskrivbordsmiljö\ndesktopHost=Skrivbordsvärd\ndesktopHostDescription=Skrivbordsanslutningen att använda som bas\ndesktopShellDialect=Shell-dialekt\ndesktopShellDialectDescription=Den shell-dialekt som används för att köra skript och applikationer\ndesktopSnippets=Utdrag ur skript\ndesktopSnippetsDescription=Lista över återanvändbara skriptsnuttar som ska köras först\ndesktopInitScript=Init-skript\ndesktopInitScriptDescription=Init-kommandon som är specifika för denna miljö\ndesktopTerminal=Terminalapplikation\ndesktopTerminalDescription=Den terminal som ska användas på skrivbordet för att starta skript i\ndesktopApplication.displayName=Applikation för skrivbord\ndesktopApplication.displayDescription=Kör ett program på ett fjärrskrivbord\ndesktopBase=Skrivbord\ndesktopBaseDescription=Skrivbordet att köra denna applikation på\ndesktopEnvironmentBase=Skrivbordsmiljö\ndesktopEnvironmentBaseDescription=Skrivbordsmiljön att köra den här applikationen på\ndesktopApplicationPath=Applikationens sökväg\ndesktopApplicationPathDescription=Sökvägen till den körbara filen som ska köras\ndesktopApplicationArguments=Argumenten\ndesktopApplicationArgumentsDescription=De valfria argumenten som ska skickas till applikationen\ndesktopCommand.displayName=Kommando på skrivbordet\ndesktopCommand.displayDescription=Kör ett kommando i en fjärrskrivbordsmiljö\ndesktopCommandScript=Kommandon\ndesktopCommandScriptDescription=Kommandon som ska köras i miljön\nservice.displayName=Tjänst\nservice.displayDescription=Vidarebefordra en fjärrtjänst till din lokala maskin\nserviceLocalPort=Explicit lokal port\nserviceLocalPortDescription=Den lokala port som ska vidarebefordras till, annars används en slumpmässig port\nserviceRemotePort=Fjärrport\nserviceRemotePortDescription=Den port som tjänsten körs på\nserviceHost=Värd för tjänsten\nserviceHostDescription=Den värd som tjänsten körs på\nopenWebsite=Öppna webbplats\ncustomServiceGroup.displayName=Tjänstegrupp\ncustomServiceGroup.displayDescription=Gruppera flera tjänster i en kategori\ninitScript=Init script - Körs vid init av skal\nshellScript=Skript för skalsession - Gör skript tillgängligt för körning under en skalsession\nrunnableScript=Runnable script - Tillåter att skript körs direkt från anslutningshubben\nfileScript=Filskript - Tillåt att skript anropas för valda filer i filbläddraren\nrunScript=Kör skript\ncopyUrl=Kopiera URL\nfixedServiceGroup.displayName=Tjänstegrupp\nfixedServiceGroup.displayDescription=Lista de tillgängliga tjänsterna på ett system\nmappedService.displayName=Tjänst\nmappedService.displayDescription=Interagera med en tjänst som exponeras av en container\ncustomService.displayName=Tjänst\ncustomService.displayDescription=Automatiskt öppna eller tunnla en fjärrtjänstport på din lokala maskin\nfixedService.displayName=Tjänst\nfixedService.displayDescription=Använd en fördefinierad tjänst\nnoServices=Inga tillgängliga tjänster\nhasServices=$COUNT$ tillgängliga tjänster\nhasService=$COUNT$ tillgänglig tjänst\nnoConnections=Inga tillgängliga anslutningar\nhasConnections=$COUNT$ tillgängliga anslutningar\nhasConnection=$COUNT$ tillgänglig anslutning\nopenHttp=Öppen HTTP-tjänst\nopenHttps=Öppen HTTPS-tjänst\nnoScriptsAvailable=Inga aktiverade och kompatibla skript tillgängliga\nscriptsDisabled=Skript inaktiverade\nchangeIcon=Ändra ikon\ninit=Init\nshell=Skal\nhub=Navet\nscript=skript\ngenericScript=Generisk text\ngradleTasks=Uppgifter för Gradle\nrunTask=Kör uppgift\narchiveName=Arkivets namn\ncompress=Komprimera\ncompressContents=Komprimera innehåll\nuntarHere=Untar här\nuntarDirectory=Untar till $DIR$\nunzipDirectory=Packa upp till $DIR$\nunzipHere=Packa upp här\nrequiresRestart=Kräver en omstart för att appliceras.\ndownload=Ladda ner\nservicePath=Tjänstens sökväg\nservicePathDescription=Den valfria undervägen när du öppnar URL:en i en webbläsare\nactive=Aktiv\ninactive=Inaktiv\nstarting=Startar\nremotePort=Fjärrport\nremotePortNumber=Fjärrport $PORT$\nuserIdentity=Personlig identitet\nglobalIdentity=Global identitet\nidentityChoice=Användarens identitet\nidentityChoiceDescription=Välj en fördefinierad identitet eller ange inloggningsuppgifter bara för den här anslutningen\ndefineNewIdentityOrSelect=Skriv in nytt eller välj befintligt\nlocalIdentity.displayName=Lokal identitet\nlocalIdentity.displayDescription=Skapa en återanvändbar identitet för det här lokala skrivbordet\nsyncedIdentity.displayName=Synkroniserad identitet\nsyncedIdentity.displayDescription=Skapa en återanvändbar identitet som synkroniseras mellan olika system\nlocalIdentity=Lokal identitet\nkeyNotSynced=Nyckelfilen är inte synkroniserad med git repository ännu. Använd knappen Lägg till i git för nyckelfilen för att lägga till den.\nusernameDescription=Användarnamn att logga in som\nidentity.displayName=Identitet\nidentity.displayDescription=Skapa en återanvändbar identitet för anslutningar\nlocal=Lokalt\nshared=Global\nuserDescription=Användarnamnet eller den fördefinierade identiteten att logga in som\nidentityAccessLevel=Åtkomstnivå\nidentityPerUser=Tillgång till personlig identitet\nidentityPerUserDescription=Begränsa åtkomsten till denna identitet och dess associerade anslutningar till endast din valv-användare\nidentityPerUserDisabled=Tillgång till personlig identitet (inaktiverad)\nidentityPerUserDisabledDescription=Begränsa åtkomsten till den här identiteten och dess associerade anslutningar till endast din valvanvändare (kräver att teamet konfigureras)\nidentityPerGroup=Identitetsåtkomst endast för grupp\nidentityPerGroupDescription=Begränsa åtkomsten till den här identiteten och dess associerade anslutningar till endast den här valvgruppen\nlibrary=Biblioteket\nlocation=Plats\nkeyAuthentication=Nyckelbaserad autentisering\nkeyAuthenticationDescription=Den autentiseringsmetod som ska användas om nyckelbaserad autentisering krävs\nlocationDescription=Filvägen till din motsvarande privata nyckel\nkeyFile=Lokal nyckelfil\nkeyPassword=Lösenfras\nkey=Nyckel\nyubikeyPiv=Yubikey PIV\npageant=Tävling\ngpgAgent=GPG-agent\ncustomPkcs11Library=Anpassat PKCS#11-bibliotek\nsshAgent=OpenSSH-agent\nnone=Ingen\nindex=Index ...\notherExternal=Annan extern agent\nsync=Synka\nvaultSync=Synkronisering av valv\ncustomUsername=Användarnamn\ncustomUsernameDescription=Den valfria alternativa användaren att logga in som\ncustomUsernamePassword=Lösenord\ncustomUsernamePasswordDescription=Användarens lösenord som ska användas när sudo-autentisering krävs\nshowInternalPods=Visa interna pods\nshowAllNamespaces=Visa alla namnrymder\nshowInternalContainers=Visa interna behållare\nrefresh=Uppdatera\nvmwareGui=Starta GUI\nmonitorVm=Övervaka VM\naddCluster=Lägg till kluster ...\nshowNonRunningInstances=Visa instanser som inte körs\nvmwareGuiDescription=Huruvida en virtuell maskin ska startas i bakgrunden eller i ett fönster.\nvmwareEncryptionPassword=Krypteringslösenord\nvmwareEncryptionPasswordDescription=Det valfria lösenord som används för att kryptera den virtuella datorn.\nvmPasswordDescription=Det lösenord som krävs för gästanvändaren.\nvmPassword=Användarens lösenord\nvmUser=Gästanvändare\nrunTempContainer=Kör tillfällig container\nvmUserDescription=Användarnamnet för din primära gästanvändare\ndockerTempRunAlertTitle=Kör tillfällig container\ndockerTempRunAlertHeader=Detta kommer att köra en shell-process i en tillfällig container som automatiskt tas bort när den stoppas.\nimageName=Bildnamn\nimageNameDescription=Containerbildsidentifieraren att använda\ncontainerName=Namn på behållare\ncontainerNameDescription=Det valfria anpassade containernamnet\nvm=Virtuell maskin\nvmDescription=Den tillhörande konfigurationsfilen.\nvmwareScan=VMware skrivbordshypervisorer\nvmwareMachine.displayName=VMware virtuell maskin\nvmwareMachine.displayDescription=Ansluta till en virtuell maskin via SSH\nvmwareInstallation.displayName=Installation av VMware desktop hypervisor\nvmwareInstallation.displayDescription=Interagera med de installerade virtuella datorerna via dess CLI\nstart=Starta\nstop=Stoppa\npause=Pausa\nrdpTunnelHost=Målvärd\nrdpTunnelHostDescription=SSH-anslutningen för att tunnla RDP-anslutningen till\nrdpTunnelUsername=Användarnamn\nrdpTunnelUsernameDescription=Den anpassade användaren att logga in som, använder SSH-användaren om den lämnas tom\nrdpFileLocation=Plats för fil\nrdpFileLocationDescription=Filvägen för filen .rdp\nrdpPasswordAuthentication=Autentisering av lösenord\nrdpFiles=RDP-filer\nrdpPasswordAuthenticationDescription=Lösenordet som ska fyllas i eller kopieras till urklipp, beroende på klientstöd\nrdpFile.displayName=RDP-fil\nrdpFile.displayDescription=Anslut till ett system via en befintlig .rdp-fil\nrequiredSshServerAlertTitle=Konfigurera SSH-server\nrequiredSshServerAlertHeader=Det går inte att hitta en installerad SSH-server i den virtuella datorn.\nrequiredSshServerAlertContent=För att ansluta till VM letar XPipe efter en SSH-server som körs men ingen tillgänglig SSH-server upptäcktes för VM.\ncomputerName=Datorns namn\npssComputerNameDescription=Datornamnet att ansluta till\ncredentialUser=Legitimationsanvändare\ncredentialUserDescription=Användaren att logga in som.\ncredentialPassword=Lösenord för referenser\ncredentialPasswordDescription=Användarens lösenord.\nsshConfig=SSH-konfigurationsfiler\nautostart=Anslut automatiskt vid start av XPipe\nacceptHostKey=Acceptera värdnyckel\nmodifyHostKeyPermissions=Ändra behörigheter för värdnyckel\nattachContainer=Bifoga\ncontainerLogs=Visa loggar\nopenSftpClient=Öppna i extern SFTP-klient\nopenTermius=Öppna i Termius\nshowInternalInstances=Visa interna instanser\neditPod=Redigera pod\nacceptHostKeyDescription=Lita på den nya värdnyckeln och fortsätt\nmodifyHostKeyPermissionsDescription=Försök att ta bort behörigheterna för originalfilen så att OpenSSH blir nöjd\npsSession.displayName=PowerShell fjärrsession\npsSession.displayDescription=Anslut via New-PSSession och Enter-PSSession\nsshLocalTunnel.displayName=Lokal SSH-tunnel\nsshLocalTunnel.displayDescription=Upprätta en SSH-tunnel till en fjärrvärd\nsshRemoteTunnel.displayName=SSH-tunnel på distans\nsshRemoteTunnel.displayDescription=Upprätta en omvänd SSH-tunnel från en fjärrvärd\nsshDynamicTunnel.displayName=Dynamisk SSH-tunnel\nsshDynamicTunnel.displayDescription=Upprätta en SOCKS-proxy via en SSH-anslutning\nshellEnvironmentGroup.displayName=Shell-miljöer\nshellEnvironmentGroup.displayDescription=Shell-miljöer\nshellEnvironment.displayName=Shell-miljö\nshellEnvironment.displayDescription=Skapa en anpassad startmiljö för skalet\nshellEnvironment.informationFormat=$TYPE$ miljö\nshellEnvironment.elevatedInformationFormat=$ELEVATION$ $TYPE$ miljö\nenvironmentConnectionDescription=Basanslutningen för att skapa en miljö för\nenvironmentScriptDescription=Det valfria anpassade init-skriptet som ska köras i skalet\nenvironmentSnippets=Shell-skript\ncommandSnippetsDescription=De valfria fördefinierade skalskript som ska köras först\nenvironmentSnippetsDescription=Valfria fördefinierade skalskript som ska köras vid initiering\nshellTypeDescription=Den explicita shell-typen som ska startas\noriginPort=Ursprung port\noriginAddress=Ursprunglig adress\nremoteAddress=Fjärradress\nremoteSourceAddress=Fjärrkällans adress\nremoteSourcePort=Port för fjärrkälla\noriginDestinationPort=Ursprung destination port\noriginDestinationAddress=Ursprung destination adress\norigin=Ursprung\nremoteHost=Fjärrvärd\naddress=Adress\nproxmox.displayName=Proxmox\nproxmox.displayDescription=Ansluta till system i en virtuell Proxmox-miljö\nproxmoxVm.displayName=Proxmox VM\nproxmoxVm.displayDescription=Ansluta till en virtuell maskin i en Proxmox VE via SSH\nproxmoxContainer.displayName=Proxmox behållare\nproxmoxContainer.displayDescription=Anslut till en container i en Proxmox VE\nsshDynamicTunnel.hostDescription=Det system som ska användas som SOCKS-proxy\nsshDynamicTunnel.bindingDescription=Vilka adresser ska tunneln bindas till\nsshRemoteTunnel.hostDescription=Det system från vilket fjärrtunneln till ursprunget ska startas\nsshRemoteTunnel.bindingDescription=Vilka adresser ska tunneln bindas till\nsshLocalTunnel.hostDescription=Systemet för att öppna tunneln till\nsshLocalTunnel.bindingDescription=Vilka adresser ska tunneln bindas till\nsshLocalTunnel.localAddressDescription=Den lokala adressen att binda\nsshLocalTunnel.remoteAddressDescription=Fjärradressen som ska bindas\ncmd.displayName=Kommando\ncmd.displayDescription=Exekvera ett godtyckligt kommando på ett system\nk8sPod.displayName=Kubernetes Pod\nk8sPod.displayDescription=Anslut till en pod och dess containrar via kubectl\nk8sContainer.displayName=Kubernetes-behållare\nk8sContainer.displayDescription=Öppna ett skal till en container\nk8sCluster.displayName=Kubernetes-kluster\nk8sCluster.displayDescription=Ansluta till ett kluster och dess pods via kubectl\nsshTunnelGroup.displayName=SSH-tunnlar\nsshTunnelGroup.displayCategory=Alla typer av SSH-tunnlar\nlocal.displayName=Lokal maskin\nlocal.displayDescription=Skalet på den lokala maskinen\ncygwin=Cygwin\nmsys2=MSYS2\ngitWindows=Git för Windows\ngitForWindows.displayName=Git för Windows\ngitForWindows.displayDescription=Få åtkomst till din lokala Git For Windows-miljö\nmsys2.displayName=MSYS2\nmsys2.displayDescription=Åtkomstskal för din MSYS2-miljö\ncygwin.displayName=Cygwin\ncygwin.displayDescription=Åtkomst till skal i din Cygwin-miljö\nnamespace=Namnrymden\ngitVaultIdentityStrategy=Git SSH-identitet\ngitVaultIdentityStrategyDescription=Om du väljer att använda en SSH git-URL som fjärranslutning och fjärrarkivet kräver en SSH-identitet ska du ange det här alternativet.\\n\\nOm du har angett en HTTP-URL kan du ignorera det här alternativet.\ndockerContainers=Docker-containrar\ndockerCmd.displayName=cLI-klient för docker\ndockerCmd.displayDescription=Åtkomst till Docker-containrar via CLI-klienten docker\nwslCmd.displayName=WSL-installation\nwslCmd.displayDescription=Åtkomst till WSL-instanser via wsl CLI-klienten\nk8sCmd.displayName=kubectl-klient\nk8sCmd.displayDescription=Få tillgång till Kubernetes-kluster via kubectl\nk8sClusters=Kubernetes-kluster\nshells=Tillgängliga skal\ninspectContainer=Inspektera\ninspectContext=Inspektera\nk8sClusterNameDescription=Namnet på det sammanhang som klustret befinner sig i.\npod=Pod\npodName=Pod-namn\nk8sClusterContext=Sammanhang\nk8sClusterContextDescription=Namnet på det sammanhang som klustret befinner sig i\nk8sClusterNamespace=Namnrymden\nk8sClusterNamespaceDescription=Den anpassade namnrymden eller standardnamnrymden om den är tom\nk8sConfigLocation=Konfigureringsfil\nk8sConfigLocationDescription=Den anpassade kubeconfig-filen eller standardfilen om den lämnas tom\ninspectPod=Inspektera\nshowAllContainers=Visa containrar som inte körs\nshowAllPods=Visa pods som inte körs\nk8sPodHostDescription=Den värd som podden finns på\nk8sContainerDescription=Namnet på Kubernetes-containern\nk8sPodDescription=Namnet på Kubernetes-podden\npodDescription=Den pod på vilken containern är placerad\nk8sClusterHostDescription=Den värd genom vilken klustret ska nås. Måste ha kubectl installerat och konfigurerat för att kunna komma åt klustret.\nconnection=Anslutning\nshellCommand.displayName=Anpassat shell-kommando\nshellCommand.displayDescription=Öppna ett standardskal genom ett anpassat kommando\nssh.displayName=SSH-anslutning\nssh.displayDescription=Ansluta till ett fjärrsystem via kommandoradsklienten SSH\nsshConfig.displayName=SSH-konfigurationsfil\nsshConfig.displayDescription=Ansluta till värdar som definieras i en SSH-konfigurationsfil\nsshConfigHost.displayName=SSH konfigurationsfil värd\nsshConfigHost.displayDescription=Ansluta till en värd som definieras i en SSH-konfigurationsfil\nsshConfigHost.password=Lösenord\nsshConfigHost.passwordDescription=Ange det valfria lösenordet för användarinloggningen.\nsshConfigHost.identityPassphrase=Nyckel lösenordsfras\nsshConfigHost.identityPassphraseDescription=Ange den valfria lösenfrasen för din nyckel.\nshellCommand.hostDescription=Värden att utföra kommandot på\nshellCommand.commandDescription=Kommandot som öppnar ett skal\ncommandType=Typ av kommando\ncommandTypeDescription=Hur man utför kommandot\ncommandDescription=De anpassade kommandon som ska köras på värden\ncommandHostDescription=Värddatorn att köra kommandot på\ncommandDataFlowDescription=Hur detta kommando hanterar in- och utdata\ncommandElevationDescription=Kör detta kommando med förhöjda behörigheter\ncommandShellTypeDescription=Det shell som ska användas för detta kommando\nlimitedSystem=Detta är ett begränsat eller inbäddat system\nlimitedSystemDescription=Försök inte identifiera skaltyp, nödvändigt för begränsade inbäddade system eller IOT-enheter\nsshForwardX11=Framåt X11\nsshForwardX11Description=Aktiverar X11-vidarebefordran för anslutningen\ncustomAgent=Anpassad agent\nidentityAgent=Identitetsagent\nssh.proxyDescription=Den valfria proxyvärden som ska användas när SSH-anslutningen upprättas. Måste ha en ssh-klient installerad.\nusage=Användning\nwslHostDescription=Den värd som WSL-instansen finns på. Måste ha wsl installerat.\nwslDistributionDescription=Namnet på WSL-instansen\nwslUsernameDescription=Det uttryckliga användarnamnet att logga in med. Om inget anges kommer standardanvändarnamnet att användas.\nwslPasswordDescription=Användarens lösenord som kan användas för sudo-kommandon.\ndockerHostDescription=Värden som docker-containern finns på. Måste ha docker installerat.\ndockerContainerDescription=Namnet på docker-containern\nlocalMachine=Lokal maskin\nrootScan=Sudo skalmiljö\nloginEnvironmentScan=Anpassad inloggningsmiljö\nk8sScan=Kubernetes-kluster\noptions=Alternativ\ndockerRunningScan=Körning av docker-containrar\ndockerAllScan=Alla docker-containrar\nwslScan=WSL-instanser\nsshScan=SSH-konfigurationsanslutningar\nrunAsUser=Kör som användare\nrunAsUserDescription=Starta den här shell-miljön som en annan användare\ndefault=Standard\nadministrator=Administratör\nwslHost=WSL-värd\ntimeout=Timeout\ninstallLocation=Installera plats\ninstallLocationDescription=Den plats där din $NAME$ -miljö är installerad\nwsl.displayName=Windows Subsystem för Linux\nwsl.displayDescription=Anslut till en WSL-instans som körs på Windows\ndocker.displayName=Docker-behållare\ndocker.displayDescription=Anslut till en docker-container\nport=Port\nuser=Användare\npassword=Lösenord\nmethod=Metod\nuri=URL\nproxy=Proxy\ndistribution=Distribution\nusername=Användarnamn\nshellType=Typ av skal\nbrowseFile=Bläddra i fil\nopenShell=Öppna skal i terminal\nopenCommand=Utför kommando i terminal\neditFile=Redigera fil\ndescription=Beskrivning\nfurtherCustomization=Ytterligare anpassning\nfurtherCustomizationDescription=För fler konfigurationsalternativ, använd ssh-konfigurationsfilerna\nbrowse=Bläddra i\nconfigHost=Värd\nconfigHostDescription=Den värd som konfigurationen finns på\nconfigLocation=Konfigurerad plats\nconfigLocationDescription=Filsökvägen till konfigurationsfilen\ngateway=Gateway\ngatewayDescription=Den valfria gateway som ska användas vid anslutning\nconnectionInformation=Information om anslutning\nconnectionInformationDescription=Vilket system ska man ansluta till\npasswordAuthentication=Autentisering av lösenord\npasswordAuthenticationDescription=Det valfria lösenordet som ska användas för att autentisera\nsshConfigString.displayName=Konfigureringsbaserad SSH-anslutning\nsshConfigString.displayDescription=Skapa en helt anpassad SSH-anslutning i SSH config-format\nsshConfigStringContent=Konfiguration\nsshConfigStringContentDescription=SSH-alternativ för anslutningen i OpenSSH:s konfigurationsformat\nvnc.displayName=VNC-anslutning över SSH\nvnc.displayDescription=Öppna en VNC-session över en tunnelanslutning\nbinding=Bindning\nvncPortDescription=Den port som VNC-servern lyssnar på\nrdpPortDescription=Den port som RDP-servern lyssnar på\nvncUsername=Användarnamn\nvncUsernameDescription=Det valfria VNC-användarnamnet\nvncPassword=Lösenord\nvncPasswordDescription=VNC-lösenordet\nx11WslInstance=X11 Framåtriktad WSL-instans\nx11WslInstanceDescription=Den lokala Windows Subsystem for Linux-distributionen som ska användas som X11-server när X11-vidarebefordran används i en SSH-anslutning. Denna distribution måste vara en WSL2-distribution.\nopenAsRoot=Öppna som root\nopenInWSL=Öppna i WSL\nlaunch=Starta\nsshTrustKeyContent=Värdnyckeln är inte känd och du har aktiverat manuell verifiering av värdnyckeln. $CONTENT$\nsshTrustKeyTitle=Okänd värdnyckel\nrdpTunnel.displayName=RDP-anslutning över SSH\nrdpTunnel.displayDescription=Anslut via RDP över en tunnelanslutning\nrdpEnableDesktopIntegration=Möjliggör integrering av skrivbord\nrdpEnableDesktopIntegrationDescription=Kör fjärrprogram förutsatt att RDP:s tillåtelselista tillåter det\nrdpSetupAdminTitle=RDP-installation krävs\nrdpSetupAllowTitle=RDP fjärrtillämpning\nrdpSetupAllowContent=Att starta fjärrapplikationer direkt är för närvarande inte tillåtet i det här systemet. Vill du aktivera det? Detta gör att du kan köra dina fjärrprogram direkt från XPipe genom att inaktivera listan med tillåtna program för RDP-fjärrprogram.\nrdpServerEnableTitle=RDP-server\nrdpServerEnableContent=RDP-servern är inaktiverad på målsystemet. Vill du aktivera den i registret för att tillåta RDP-anslutningar på distans?\nrdp=RDP\nrdpScan=RDP-tunnel över SSH\nwslX11SetupTitle=WSL X11-installation\nwslX11SetupContent=XPipe kan använda din lokala WSL-distribution för att fungera som en X11-displayserver. Vill du konfigurera X11 på $DIST$? Detta kommer att installera de grundläggande X11-paketen på WSL-distributionen och kan ta ett tag. Du kan också ändra vilken distribution som används i inställningsmenyn.\ncommand=Kommando\ncommandGroup=Kommandogrupp\nvncSystem=VNC-målsystem\nvncSystemDescription=Det faktiska systemet att interagera med. Detta är vanligtvis detsamma som tunnelvärden\nvncHost=Mål VNC-värd\nvncHostDescription=Det system som VNC-servern körs på\nvncDirectHost=Värd\nvncDirectHostDescription=Värdposten eller den manuella adressen till den server som VNC-servern körs på\nrdpDirectHost=Värd\nrdpDirectHostDescription=Värdposten eller den manuella adressen till den server som RDP-servern körs på\ngitVaultTitle=Git-valv\ngitVaultForcePushContent=Vill du tvinga fram en push till fjärrförvaret? Detta kommer helt att ersätta allt innehåll i fjärrförvaret med ditt lokala, inklusive historiken.\ngitVaultOverwriteLocalContent=Vill du åsidosätta dina lokala valvändringar? Detta kommer att tillämpa alla fjärranslutna ändringar i ditt lokala arkiv.\nrdpSimple.displayName=Direkt RDP-anslutning\nrdpSimple.displayDescription=Ansluta till en värd via RDP\nrdpUsername=Användarnamn\nrdpUsernameDescription=Användaren att logga in som. Kan innehålla ett domänprefix\naddressDescription=Var ska man ansluta till\nrdpAdditionalOptions=Ytterligare RDP-alternativ\nrdpAdditionalOptionsDescription=Raw RDP-alternativ att inkludera, formaterade på samma sätt som i .rdp-filer\nproxmoxVncConfirmTitle=VNC-åtkomst\nproxmoxVncConfirmContent=Vill du aktivera VNC-åtkomst för den virtuella datorn? Detta kommer att aktivera direkt VNC-klientåtkomst i VM-konfigurationsfilen och starta om den virtuella maskinen.\ndockerContext.displayName=Docker-sammanhang\ndockerContext.displayDescription=Interagera med containrar som befinner sig i ett specifikt sammanhang\nvmActions=VM-åtgärder\ndockerContextActions=Åtgärder i sammanhanget\nk8sPodActions=Pod-åtgärder\nopenVnc=Aktivera VNC-åtkomst\naddVnc=Lägg till VNC-anslutning\ncommandGroup.displayName=Kommandogrupp\ncommandGroup.displayDescription=Gruppera tillgängliga kommandon för ett system\nserial.displayName=Seriell anslutning\nserial.displayDescription=Öppna en seriell anslutning i en terminal\nserialPort=Seriell port\nserialPortDescription=Den seriella port / enhet som ska anslutas till\nbaudRate=Baud-frekvens\ndataBits=Data bits\nstopBits=Stoppbitar\nparity=Paritet\nflowControlWindow=Flödeskontroll\nserialImplementation=Seriell implementering\nserialImplementationDescription=Verktyget som ska användas för att ansluta till serieporten\nserialHost=Värd\nserialHostDescription=Systemet för att komma åt den seriella porten på\nserialPortConfiguration=Konfiguration av seriell port\nserialPortConfigurationDescription=Konfigurationsparametrar för den anslutna seriella enheten\nserialInformation=Seriell information\nopenXShell=Öppna i XShell\ntsh.displayName=Teleportera\ntsh.displayDescription=Anslut till dina teleportnoder via tsh\ntshNode.displayName=Teleport-nod\ntshNode.displayDescription=Ansluta till en teleportnod i ett kluster\nteleportCluster=Kluster\nteleportClusterDescription=Klustret som noden befinner sig i\nteleportProxy=Proxy\nteleportProxyDescription=Den proxyserver som används för att ansluta till noden\nteleportHost=Värd\nteleportHostDescription=Värdnamnet på noden\nteleportUser=Användare\nteleportUserDescription=Användaren att logga in som\nlogin=Logga in\nhyperVInstall.displayName=Hyper-V\nhyperVInstall.displayDescription=Ansluta till virtuella datorer som hanteras av Hyper-V\nhyperVVm.displayName=Hyper-V VM\nhyperVVm.displayDescription=Ansluta till en Hyper-V VM via SSH eller PSSession\ntrustHost=Förtroende värd\ntrustHostDescription=Lägga till datornamn i listan över betrodda värdar\ncopyIp=Kopiera IP\nvncDirect.displayName=Direkt VNC-anslutning\nvncDirect.displayDescription=Anslut direkt till ett system via VNC\neditConfiguration=Redigera konfiguration\nviewInDashboard=Visa i instrumentpanelen\nsetDefault=Ställ in standard\nremoveDefault=Ta bort standard\nconnectAsOtherUser=Anslut som annan användare\nprovideUsername=Ange alternativt användarnamn att logga in med\nvmIdentity=Gästens identitet\nvmIdentityDescription=SSH-identitetsautentiseringsmetoden som ska användas för att ansluta vid behov\nvmPort=Port\nvmPortDescription=Port att ansluta till via SSH\nforwardAgent=Framåtriktad agent\nforwardAgentDescription=Göra SSH-agentens identiteter tillgängliga på fjärrsystemet\nvirshUri=URI\nvirshUriDescription=Hypervisorns URI, alias stöds också\nvirshDomain.displayName=libvirt-domän\nvirshDomain.displayDescription=Ansluta till en libvirt-domän\nvirshHypervisor.displayName=libvirt hypervisor\nvirshHypervisor.displayDescription=Anslut till en hypervisor-drivrutin som stöds av libvirt\nvirshInstall.displayName=libvirt kommandoradsklient\nvirshInstall.displayDescription=Anslut till alla tillgängliga libvirt-hypervisorer via virsh\naddHypervisor=Lägg till hypervisor\ninteractiveTerminal=Interaktiv terminal\neditDomain=Redigera domän\nlibvirt=libvirt-domäner\ncustomIp=Anpassad IP\ncustomIpDescription=Åsidosätta den lokala VM-IP-avkänningen om du använder avancerade nätverk\nautomaticallyDetect=Automatiskt upptäcka\nuserAddDialogTitle=Skapande av användare\ngroupAddDialogTitle=Skapande av grupp\npassphrase=Lösenfras\nrepeatPassphrase=Upprepa lösenfras\ngroupSecret=Grupphemlighet\nrepeatGroupSecret=Upprepa gruppens hemlighet\nvaultGroup=Valvgrupp\nloginAlertTitle=Inloggning krävs\nloginAlertHeader=Lås upp valvet för att få tillgång till dina personliga anslutningar\nvaultUser=Användare av valv\nme=Jag\naddGroup=Lägg till grupp ...\naddGroupDescription=Skapa en ny grupp för detta valv\naddUser=Lägg till användare ...\naddUserDescription=Skapa en ny användare för detta valv\nskip=Hoppa över\nuserChangePasswordAlertTitle=Ändra lösenord\ngroupChangeSecretAlertTitle=Hemlig förändring\ndocs=Dokumentation\nlxd.displayName=LXD-behållare\nlxd.displayDescription=Anslut till en LXD-container via lxc\nlxdCmd.displayName=LXD CLI-klient\nlxdCmd.displayDescription=Åtkomst till LXD-containrar via lxc CLI-klienten\npodman.displayName=Podman-behållare\npodman.displayDescription=Anslut till en Podman-container\nincusInstall.displayName=Incus maskinhanterare\nincusInstall.displayDescription=Åtkomst till incus-containrar via incus CLI-klienten\nincusContainer.displayName=Incus-behållare\nincusContainer.displayDescription=Anslut till en inkusbehållare\npodmanCmd.displayName=Podman CLI\npodmanCmd.displayDescription=Åtkomst till Podman-containrar via CLI-klienten\nlxdHostDescription=Värden som LXD-containern är placerad på. Måste ha lxc installerat.\nlxdContainerDescription=Namnet på LXD-containern\npodmanContainers=Podman-containrar\nlxdContainers=LXD-behållare\nincusContainers=Incus-behållare\ncontainer=En behållare\nhost=Värd\ncontainerActions=Åtgärder för behållare\nserialConsole=Seriell konsol\neditRunConfiguration=Redigera körkonfiguration\ncommunityDescription=Ett kraftfullt anslutningsverktyg som är perfekt för dina personliga användningsområden.\nupgradeDescription=Professionell anslutningshantering för hela din serverinfrastruktur.\ndiscoverPlans=Upptäck uppgraderingsalternativ\nextendProfessional=Uppgradering till senaste professionella funktioner\ncommunityItem1=Obegränsade anslutningar till icke-kommersiella system och verktyg\ncommunityItem2=Sömlös integration med dina installerade terminaler och editorer\ncommunityItem3=Fullt utrustad webbläsare för fjärrfiler\ncommunityItem4=Kraftfullt skriptsystem för alla skal\ncommunityItem5=Git-integration för synkronisering och delning av anslutningsinformation\nupgradeItem1=Innehåller alla funktioner i community-utgåvan\nupgradeItem2=Homelab-planen stöder obegränsade hypervisorer och avancerade SSH-funktioner\nupgradeItem3=Professional-planen stöder dessutom operativsystem och verktyg för företag\nupgradeItem4=Enterprise-planen kommer med full flexibilitet för ditt individuella användningsfall\nupgrade=Uppgradering\nupgradeTitle=Tillgängliga planer\nstatus=Status\ntype=Typ av text\nlicenseAlertTitle=Licens krävs\nuseCommunity=Fortsätt med community\npreviewDescription=Testa nya funktioner under ett par veckor efter lansering.\ntryPreview=Aktivera förhandsgranskning\npreviewItem1=Full tillgång till nyligen lanserade professionella funktioner i 2 veckor efter lansering\npreviewItem2=Prova nya funktioner utan några förpliktelser\nlicensedTo=Licensierad till\nemail=E-postadress\napply=Tillämpa\nclear=Rensa\nactivate=Aktivera\nvalidUntil=Giltig till\nlicenseActivated=Licens aktiverad\nrestart=Starta om\nlockVault=Lås valv\nrestartApp=Starta om XPipe\nfree=Fri\nupgradeInfo=Du hittar information om uppgradering till en licens nedan.\nupgradeInfoPreview=Du kan hitta information om uppgradering till en licens nedan eller prova förhandsgranskningen.\nenterLicenseKey=Ange licensnyckel för att uppgradera\nisOnlySupported=stöds endast med minst en $TYPE$ -licens\nareOnlySupported=stöds endast med minst en $TYPE$ -licens\nlegacyLicense=Denna licens omfattar endast nya Professional-funktioner som släpps inom ett år efter köpet.\npreviewExpiredLicense=Denna funktion var nyligen tillgänglig gratis i en förhandsgranskning, men denna period har nu löpt ut.\nopenApiDocs=API-dokumentation\nopenApiDocsDescription=HTTP API-dokumentationen finns tillgänglig online, inklusive en OpenAPI .yaml-specifikation. Du kan öppna den i din webbläsare eller i den HTTP-klient du föredrar.\nopenApiDocsButton=Öppna dokument\npythonApi=Python API\npersonalConnection=Denna anslutning och alla dess barn är endast tillgängliga för din användare eftersom de är beroende av en personlig identitet.\ndeveloperPrintInitFiles=Exekvering av filen Print init\ndeveloperPrintInitFilesDescription=Skriv ut alla shell init-skript som körs när en terminal startas.\ndeveloperShowSensitiveCommands=Logga känsliga kommandon\ndeveloperShowSensitiveCommandsDescription=Inkludera känsliga kommandon i loggutdata för felsökning.\ncheckingForUpdates=Kontrollerar för uppdateringar\ncheckingForUpdatesDescription=Hämtar information om senaste utgåvan\ndownloadingUpdate=Hämtar release (Version $VERSION$)\ndownloadingUpdateDescription=Nedladdning av releasepaket\nupdateNag=Du har inte uppdaterat XPipe på ett tag. Du kanske missar nya funktioner och korrigeringar av nyare utgåvor.\nupdateNagTitle=Påminnelse om uppdatering\nupdateNagButton=Se releaser\nrefreshServices=Uppdatera tjänster\nserviceProtocolType=Typ av serviceprotokoll\nserviceProtocolTypeDescription=Styr hur tjänsten ska öppnas\nserviceCommand=Kommandot som ska köras när tjänsten är aktiv\nserviceCommandDescription=Platshållaren $PORT kommer att ersättas med den faktiska tunnlade lokala porten\nvalue=Värde\nshowAdvancedOptions=Visa avancerade alternativ\nsshAdditionalConfigOptions=Ytterligare konfigurationsalternativ\nremoteFileManager=Fjärrstyrd filhanterare\nclearUserData=Ta bort användardata\nclearUserDataDescription=Ta bort alla användarkonfigurationsdata, inklusive anslutningar\nclearUserDataTitle=Radering av användardata\nclearUserDataContent=Detta kommer att radera alla lokala användardata för xpipe och starta om. Om du bryr dig om dina anslutningar, se till att synkronisera dem först med ett git-arkiv.\nundefined=Odefinierad\ncopyAddress=Kopiera adress\nnetbirdDeviceScan=Netbird-anslutningar\nnetbirdId=Peer offentlig nyckel\nnetbirdIdDescription=Den interna offentliga Netbird-nyckelns id för peer\ntailscaleDeviceScan=Tailscale-anslutningar\ntailscaleInstall.displayName=Tailscale installation\ntailscaleInstall.displayDescription=Anslut till enheter i ditt tailnet via SSH\ntailscaleDevice.displayName=Tailscale-enhet\ntailscaleDevice.displayDescription=Anslut till en enhet i ditt tailnet via SSH\ntailscaleId=Enhetens ID\ntailscaleIdDescription=Det interna ID:t för Tailscale-enheten\ntailscaleHostName=Värdnamn\ntailscaleHostNameDescription=Värdnamnet på enheten i tailnet\ntailscaleUsername=Användarnamn\ntailscaleUsernameDescription=Användaren att logga in som\ntailscalePassword=Lösenord\ntailscalePasswordDescription=Det valfria användarlösenordet som kan användas för sudo\nscriptName=Namn på skript\nscriptNameDescription=Ge detta skript ett eget namn\nscriptGroupName=Namn på skriptgrupp\nscriptGroupNameDescription=Ge denna skriptgrupp ett eget namn\nidentityName=Identitetens namn\nidentityNameDescription=Ge denna identitet ett eget namn\ntailscaleTailnet.displayName=Tailnet\ntailscaleTailnet.displayDescription=Anslut till ett specifikt tailnet med ditt konto\nputtyConnections=PuTTY-anslutningar\nkittyConnections=KiTTY-anslutningar\nicons=Ikoner\ncustomIcons=Anpassade ikoner\niconSources=Ikonens källor\niconSourcesDescription=Du kan lägga till dina egna källor för ikoner här. XPipe kommer att hämta alla .svg-filer på den tillagda platsen och lägga till dem i den tillgängliga uppsättningen ikoner.\\n\\nBåde lokala kataloger och fjärranslutna git-arkiv stöds som ikonplatser.\nrefreshSources=Uppdatera ikoner\nrefreshSourcesDescription=Uppdatera alla ikoner från tillgängliga källor\naddDirectoryIconSource=Lägg till katalogkälla ...\naddDirectoryIconSourceDescription=Lägga till ikoner från en lokal katalog\naddGitIconSource=Lägg till git-källa ...\naddGitIconSourceDescription=Lägg till ikoner som finns i ett fjärranslutet git-arkiv\nrepositoryUrl=URL till Git-förvaret\niconDirectory=Katalog med ikoner\naddUnsupportedKexMethod=Lägg till icke-stödd nyckelutbytesmetod\naddUnsupportedKexMethodDescription=Tillåt att nyckelutbytesmetoden $VAL$ används för den här anslutningen\naddUnsupportedHostKeyType=Lägga till icke-stödd värdnyckeltyp\naddUnsupportedHostKeyTypeDescription=Tillåt att värdnyckeltypen $VAL$ används för den här anslutningen\naddUnsupportedMacType=Lägg till MAC-typ som inte stöds\naddUnsupportedMacTypeDescription=Tillåt att MAC-typen $VAL$ används för den här anslutningen\nrunSilent=tyst i bakgrunden\nrunInFileBrowser=i filbläddrare\nrunInConnectionHub=i anslutningsnav\ncommandOutput=Kommandoutgång\niconSourceDeletionTitle=Ta bort ikon källa\niconSourceDeletionContent=Vill du ta bort den här ikonkällan och alla ikoner som är associerade med den?\nrefreshIcons=Uppdatera ikoner\nrefreshIconsDescription=Hämtar, renderar och cachar alla tillgängliga 1000+ ikoner från externa källor till .png-filer. Detta kan ta ett tag ...\nvaultUserLegacy=Vault-användare (begränsat äldre kompatibilitetsläge)\nupgradeInstructions=Instruktioner för uppgradering\nexternalActionTitle=Begäran om extern åtgärd\nexternalActionContent=En extern åtgärd begärdes. Vill du tillåta att åtgärder startas från utanför XPipe?\nnoScriptStateAvailable=Uppdatera för att fastställa skriptkompatibilitet ...\ndocumentationDescription=Kolla in dokumentationen\ncustomEditorCommandInTerminal=Kör ett anpassat kommando i en terminal\ncustomEditorCommandInTerminalDescription=Om din editor är terminalbaserad kan du aktivera det här alternativet för att automatiskt öppna en terminal och köra kommandot i terminalsessionen istället.\\n\\nDu kan använda det här alternativet för editorer som vi, vim, nvim och andra.\ndisableHttpsTlsCheck=Avaktivera verifiering av certifikat för HTTPS-begäran\ndisableHttpsTlsCheckDescription=Om din organisation dekrypterar HTTPS-trafiken i brandväggar med SSL-avlyssning kommer alla uppdateringskontroller eller licenskontroller att misslyckas på grund av att certifikaten inte matchar varandra. Du kan åtgärda detta genom att aktivera det här alternativet och inaktivera TLS-certifikatvalidering.\nconnectionsSelected=$NUMBER$ anslutningar valda\naddConnections=Lägg till anslutningar\nbrowseDirectory=Bläddra i katalogen\nopenTerminal=Öppen terminal\ndocumentation=Dokumentation\nreport=Rapportera fel\nkeePassXcNotAssociated=KeePassXC-länk\nkeePassXcNotAssociatedDescription=XPipe är inte kopplad till din lokala KeePassXC-databas. Klicka nedan för att utföra engångssteget för att associera XPipe med KeePassXC-databasen så att XPipe kan fråga lösenord.\nkeePassXcAssociateMore=Anslut fler databaser\nkeePassXcAssociateMoreDescription=Du kan vara ansluten till flera KeePassXC-databaser samtidigt\nkeePassXcAssociated=KeePassXC länkar\nkeePassXcAssociatedDescription=XPipe är ansluten till följande lokala KeePassXC-databaser:\nkeePassXcNotAssociatedButton=Länk databas\nidentifier=Identifierare\npasswordManagerCommand=Anpassat kommando\npasswordManagerCommandDescription=Det anpassade kommandot som ska köras för att hämta lösenord. Platshållarsträngen $KEY kommer att ersättas av den citerade lösenordsnyckeln när kommandot anropas. Detta bör anropa din lösenordshanterare CLI för att skriva ut lösenordet till stdout, t.ex. mypassmgr get $KEY.\nchooseTemplate=Välj mall\nkeePassXcPlaceholder=URL för KeePassXC-inträde\nterminalEnvironment=Terminalmiljö\nterminalEnvironmentDescription=Om du vill använda funktioner i en lokal Linux-baserad WSL-miljö för att anpassa din terminal kan du använda dem som terminalmiljö.\\n\\nAlla anpassade init-kommandon för terminalen och konfigurationen av terminalmultiplexern kommer då att köras i den här WSL-distributionen.\nterminalInitScript=Init-skript för terminal\nterminalInitScriptDescription=Kommandon som ska köras i terminalmiljön innan anslutningen startas. Du kan använda detta för att konfigurera terminalmiljön vid uppstart.\nterminalMultiplexer=Terminal multiplexer\nterminalMultiplexerDescription=Terminalmultiplexer att använda som ett alternativ till flikar i en terminal. Detta kommer att ersätta vissa terminalhanteringsegenskaper, t.ex. tabbhantering, med multiplexerfunktionaliteten.\\n\\nKräver att den körbara filen för respektive multiplexer installeras på systemet.\nterminalMultiplexerWindowsDescription=Terminalmultiplexer att använda som ett alternativ till flikar i en terminal. Detta kommer att ersätta vissa terminalhanteringsegenskaper, t.ex. tabbhantering, med multiplexerfunktionaliteten.\\n\\nKräver användning av en WSL-terminalmiljö på Windows och att den körbara filen för multiplexern installeras på WSL-systemet.\nterminalAlwaysPauseOnExit=Pausa alltid vid avslutning\nterminalAlwaysPauseOnExitDescription=När det är aktiverat kommer avslutning av en terminalsession alltid att uppmana dig att antingen starta om eller stänga sessionen. Om den är inaktiverad kommer XPipe bara att göra det för misslyckade anslutningar som avslutas med ett fel.\nquerying=Förfrågan ...\nretrievedPassword=Erhållen: $PASSWORD$\nrefreshOpenpubkey=Uppdatera openpubkey-identitet\nrefreshOpenpubkeyDescription=Kör opkssh refresh för att göra openpubkey-identiteten giltig igen\nall=Alla\nterminalPrompt=Terminalens prompt\nterminalPromptDescription=Terminalpromptverktyget som ska användas i dina fjärrterminaler. Om du aktiverar en terminalprompt kommer promptverktyget automatiskt att konfigureras på målsystemet när en terminalsession öppnas.\\n\\nDetta ändrar inte några befintliga promptkonfigurationer eller profilfiler på ett system. Detta ökar terminalens laddningstid första gången medan prompten konfigureras på fjärrsystemet. Din terminal kan behöva ytterligare teckensnitt för att visa prompten korrekt.\nterminalPromptConfiguration=Konfiguration av terminalprompt\nterminalPromptConfig=Konfigureringsfil\nterminalPromptConfigDescription=Den anpassade konfigurationsfilen som ska tillämpas på prompten. Denna konfiguration kommer att installeras automatiskt på målsystemet när terminalen initieras och användas som standardkonfiguration för prompten.\\n\\nOm du vill använda den befintliga standardkonfigurationsfilen på varje system kan du lämna det här fältet tomt.\npasswordManagerKey=Nyckel för lösenordshanterare\npasswordManagerKeyDescription=Lösenordshanterarens identifierare av hemligheten\npasswordManagerAgent=Agent för lösenordshanterare\ndockerComposeProject.displayName=Docker compose-projekt\ndockerComposeProject.displayDescription=Gruppera containrar i ett kompositprojekt tillsammans\nsshVerboseOutput=Aktivera utförlig SSH-utdata\nsshVerboseOutputDescription=Detta skriver ut en hel del felsökningsinformation när du ansluter via SSH. Användbart för felsökning av problem med SSH-anslutningar.\ndontUseGateway=Använd inte gateway\ndontUseGatewayDescription=Använd inte hypervisor-värden som gateway utan anslut direkt till IP\ncategoryColor=Kategori färg\ncategoryColorDescription=Standardfärgen som ska användas för anslutningar inom denna kategori\ncategorySync=Synkronisera med git-förvaret\ncategorySyncDescription=Synkronisera alla anslutningar automatiskt med git-förvaret. Alla lokala ändringar av anslutningar kommer att skjutas till fjärrkontrollen.\ncategorySyncSpecial=Synkronisera med git-förvaret\\n(Ej konfigurerbar för specialkategori \"$NAME$\")\ncategoryDontAllowScripts=Inaktivera alla modifieringar\ncategoryDontAllowScriptsDescription=Inaktivera all kommandokörning och andra operationer på system inom denna kategori för att förhindra alla ändringar. Detta inaktiverar alla skriptfunktioner, kommandon i skalmiljön, uppmaningar med mera.\ncategoryConfirmAllModifications=Bekräfta alla ändringar\ncategoryConfirmAllModificationsDescription=Bekräfta först alla typer av ändringar av en anslutning eller ett filsystem. Detta kan förhindra oavsiktliga operationer på viktiga system.\ncategoryDefaultIdentity=Standardidentitet\ncategoryDefaultIdentityDescription=Om du ofta använder en viss identitet på många av systemen i den här kategorin kan du ange en standardidentitet så att du kan välja den i förväg när du skapar nya anslutningar.\ncategoryConfigTitle=$NAME$ konfiguration\nconfigure=Konfigurera\naddConnection=Lägg till anslutning\nnoCompatibleConnection=Ingen kompatibel anslutning hittades\nnoCompatibleIdentity=Ingen kompatibel identitet hittades\nnewCategory=Ny kategori\ndockerComposeRestricted=Compose-projektet är begränsat av $NAME$ och kan inte ändras externt. Använd $NAME$ för att hantera detta compose-projekt.\nrestricted=Begränsad\ndisableSshPinCaching=Inaktivera cachelagring av SSH PIN-kod\ndisableSshPinCachingDescription=XPipe kommer automatiskt att cacha alla PIN-koder som angavs för en nyckel när du använder någon form av hårdvarubaserad autentisering.\\n\\nOm du inaktiverar detta måste du ange PIN-koden på nytt vid varje anslutningsförsök.\ngitSyncPull=Pull för att synkronisera fjärranslutna git-ändringar\nenpassVaultFile=Vault-fil\nenpassVaultFileDescription=Den lokala Enpass-valvfilen.\nflat=Platt\nrecursive=Rekursiv\nrdpAllowListBlocked=Den valda RemoteApp verkar inte finnas med i listan över tillåtna RDP-datorer för servern.\npsonoServerUrl=URL till server\npsonoServerUrlDescription=URL för backend-servern för psono\npsonoApiKey=API-nyckel\npsonoApiKeyDescription=Den API-nyckel som ska användas, formaterad som en uuid\npsonoApiSecretKey=API hemlig nyckel\npsonoApiSecretKeyDescription=API:s hemliga nyckel som 64 byte hex-sträng\npassboltServerUrl=URL till server\npassboltServerUrlDescription=URL för passbolt backend-servern\npassboltPassphrase=Lösenfras\npassboltPassphraseDescription=Passfras för valvets privata nyckel\npassboltPrivateKey=Privat nyckel\npassboltPrivateKeyDescription=Den privata gpg-nyckelfilen för valvet\nfocusWindowOnNotifications=Fokusera fönstret på meddelanden\nfocusWindowOnNotificationsDescription=Ta XPipe till förgrunden när ett meddelande eller felmeddelande visas, t.ex. när en anslutning eller tunnel oväntat avslutas.\ngitUsername=Anpassat git-användarnamn\ngitUsernameDescription=Den anpassade användaren för att autentisera till git-fjärrförvaret. Som standard kommer XPipe att använda de aktuella konfigurerade autentiseringsuppgifterna för git CLI.\\n\\nDen här inställningen åsidosätter alla standardautentiseringsuppgifter som redan har konfigurerats för din lokala git CLI-klient.\ngitPassword=Anpassat git-lösenord / personlig access token\ngitPasswordDescription=Lösenordet eller den personliga åtkomsttoken som ska användas för att autentisera. Om du behöver ett lösenord eller en personlig åtkomsttoken beror på git-fjärrleverantören. Den här inställningen åsidosätter eventuella standardautentiseringsuppgifter som redan har konfigurerats för din lokala git CLI-klient.\nsetReadOnly=Ställ in skrivskyddad\nunsetReadOnly=Oinställd skrivskyddad\nreadOnlyStoreError=Den här postens konfiguration är fryst. Välj ett annat namn för att spara dina ändringar i en ny kopia.\ncategoryFreeze=Frys anslutningskonfigurationer\ncategoryFreezeDescription=Markerar anslutningskonfigurationer som skrivskyddade. Det innebär att ingen befintlig konfiguration av anslutningsposter i den här kategorin kan ändras. Nya anslutningar kan dock läggas till.\nupdateFail=Uppdateringsinstallationen lyckades inte\nupdateFailAction=Installera uppdatering manuellt\nupdateFailActionDescription=Kolla in de senaste versionerna på GitHub\nonePasswordPlaceholder=Artikelns namn eller op:// URL\ncomputeDirectorySizes=Beräkna katalogstorlekar\ncomputeSize=Beräkna storlek\ncustomSpiceCommand=Anpassat kommando\ncustomSpiceCommandDescription=Det anpassade kommandot som ska köras för att starta SPICE-sessioner. Platshållarsträngen $FILE kommer att ersättas av den citerade filsökvägen till .vv-filen när kommandot anropas.\nvncClient=VNC-klient\nvncClientDescription=Den VNC-klient som ska startas när VNC-anslutningar öppnas i XPipe.\\n\\nDu har möjlighet att antingen använda den integrerade VNC-klienten i XPipe eller alternativt starta en extern lokalt installerad VNC-klient om du vill ha mer anpassning.\nintegratedXPipeVncClient=Integrerad XPipe VNC-klient\ncustomVncCommand=Anpassat kommando\ncustomVncCommandDescription=Det anpassade kommandot som ska köras för att starta VNC-sessioner. Platshållarsträngen $ADDRESS kommer att ersättas av den citerade adressen när kommandot anropas.\nvncConnections=VNC-anslutningar\npasswordManagerIdentity=Identitet för lösenordshanterare\npasswordManagerIdentity.displayName=Identitet för lösenordshanterare\npasswordManagerIdentity.displayDescription=Hämta användarnamn och lösenord för en identitet från din lösenordshanterare\npasswordCopied=Lösenord för anslutning kopieras till urklipp\nerrorOccurred=Fel inträffade\nactionMacro.displayName=Makro för åtgärd\nactionMacro.displayDescription=Kör i aktion med hjälp av anpassade triggers\nmacroAdd=Lägg till makro\nmacroName=Namn på makro\nmacroNameDescription=Ge detta makro ett eget namn\nactionId=Åtgärds-ID\nactionIdDescription=Den åtgärd som ska köras med detta makro\nmacroRefs=Associerade anslutningar\nmacroRefsDescription=De anslutningar som behövs för att köra åtgärden\nconnectionCopy=En kopia\nactionPickerTitle=Välj åtgärd\nactionPickerDescription=Klicka på något för att utföra en åtgärd. I stället för att utföra åtgärden kan du skapa och redigera genvägar till åtgärden i läget för val av genväg till åtgärden.\ncancelActionPicker=Avbryt åtgärd plocka\nactionShortcut=Snabbkommando för åtgärder\nactionShortcuts=Kortkommandon för åtgärder\nactionStore=Åtgärd butik\nactionStoreDescription=Butiksposten för att köra åtgärden på\nactionStores=Åtgärder butiker\nactionStoresDescription=Butiksposterna för att köra åtgärden på\nactionDesktopShortcut=Genväg till skrivbordet\nactionDesktopShortcutDescription=Skapa en genväg för den här åtgärden på skrivbordet\nactionUrlShortcut=URL-genväg\nactionUrlShortcutDescription=Kopiera en URL som kan utlösa dessa åtgärder när den öppnas\nactionUrlShortcutDisabled=URL-genväg (Ej tillgänglig)\nactionUrlShortcutDisabledDescription=Installationstypen $TYPE$ stöder inte öppning av webbadresser\nactionApiCall=API-begäran\nactionApiCallDescription=Anropa denna åtgärd från HTTP API\nactionMacro=Makro för åtgärd\nactionMacroDescription=Skapa ett makro med avancerad funktionalitet för denna åtgärd\ncreateMacro=Skapa makro\nactionConfiguration=Parametrar\nactionConfigurationDescription=Parametrarna som ska skickas till den utförda åtgärden\nconfirmAction=Bekräfta åtgärd\nactionConnections=Anslutningar för åtgärder\nactionConnectionsDescription=Anslutningarna för att köra åtgärden på\nactionConnection=Action-anslutning\nactionConnectionDescription=Anslutningen för att köra åtgärden på\nappleContainerInstall.displayName=Apple-containrar\nappleContainerInstall.displayDescription=Åtkomst till apple-containerinstanser via container CLI\nappleContainer.displayName=Apple container\nappleContainer.displayDescription=Åtkomst till apple-containerinstanser via container CLI\nappleContainerHostDescription=Värden på vilken apple-containern är placerad på\nappleContainerDescription=Namnet på Apple-containern\nappleContainers=Apple-containrar\nchangeOrderIndexTitle=Ändra ordning\norderIndex=Index\norderIndexDescription=Explicit index för att ordna denna post i förhållande till andra. Lägsta index visas högst upp, högsta längst ner\nmoveToFirst=Flytta till första\nmoveToLast=Flytta till sista plats\ncategory=En kategori\nincludeRoot=Inkludera rot\nexcludeRoot=Utesluta rot\nfreezeConfiguration=Frys konfiguration\nunfreezeConfiguration=Frigör konfiguration\nwaylandScalingTitle=Wayland-skalning\nactionApiUrl=$URL$ (Kopiera json-kropp)\ncopyBody=Kopiera begäran om kropp\ngitRepoTerminalOpen=Öppna förvaret i terminalen\ngitRepoTerminalOpenDescription=Ta en titt på arkivet själv med kommandoraden\ngitRepoOverwriteLocal=Skriva över lokalt arkiv\ngitRepoOverwriteLocalDescription=Ersätt alla lokala ändringar med ändringar från fjärrdatorn\ngitRepoForcePush=Skriva över fjärrförvar\ngitRepoForcePushDescription=Använd git push --force för att tillämpa dina lokala ändringar på fjärrdatorn\ngitRepoDontWarn=Varna inte längre\ngitRepoDontWarnDescription=Om detta är väntat, se till att XPipe ignorerar detta fel i framtiden\ngitRepoTryAgain=Försök igen\ngitRepoTryAgainDescription=Försök att utföra samma operation igen\ngitRepoEnablePlain=Använd vanlig katalogsynkronisering\ngitRepoEnablePlainDescription=Initialisera inte ett git-arkiv för att synkronisera ändringar till katalogen\ngitRepoCreateBare=Använd git sync\ngitRepoCreateBareDescription=Initiera ett nytt git-repository i sync-katalogen\ngitRepoDisable=Inaktivera git valv för nu\ngitRepoDisableDescription=Gör inga ändringar under den här sessionen\ngitRepoPullRefresh=Dra ändringar och uppdatera\ngitRepoPullRefreshDescription=Sammanfoga fjärranslutna ändringar och ladda om data\nbreakOutCategory=Bryt ut kategori\nmergeCategory=Sammanfoga kategori\nopenWinScp=Öppna i WinSCP\nuninstallApplication=Avinstallera\nuninstallApplicationDescription=Kör .pkg ett installationsskript för att helt avinstallera XPipe\nk8sEditPodTitle=Tillämpa ändringar\nk8sEditPodContent=Vill du tillämpa de ändringar som gjorts via kommandot kubectl apply? Det krävs sannolikt en omstart för att ändringarna ska gälla.\nvirshEditDomainTitle=Tillämpa ändringar\nvirshEditDomainContent=Vill du tillämpa ändringarna på domänen? Det krävs sannolikt en omstart för att ändringarna ska gälla.\npkcs11Library=PKCS#11-bibliotek\npkcs11LibraryDescription=Sökvägen till den dynamiskt länkade biblioteksfilen\nsshAgentSocket=Anpassad SSH-agent socket\nsshAgentSocketDescription=Det anpassade uttaget som ska användas för att kommunicera med SSH-agenten. Den här anpassade agenten kan användas för en anslutning genom att du väljer alternativet anpassad agent för den.\npublicKey=Identifierare för publik nyckel\npublicKeyDescription=Den valfria publika nyckeln för att tvinga agenten att endast erbjuda den matchande privata nyckeln\nactions=Åtgärder\nhcloudServer.displayName=Hetzner molnserver\nhcloudServer.displayDescription=Få åtkomst till en server i Hetzner-molnet via SSH\nhcloudInstall.displayName=Hetzner Cloud CLI\nhcloudInstall.displayDescription=Åtkomst till servrar som finns på Hetzner-molnet via hcloud\nhcloudContext.displayName=hcloud sammanhang\nhcloudContext.displayDescription=Åtkomstservrar i en hcloud-kontext\nmetrics=Metrik\nopenInVsCode=Öppna i VsCode\naddCloud=Moln ...\nhcloudToken=hcloud token\nhcloudTokenDescription=Den Hetzner-molntoken som ska användas. För mer information, se dokumentationen\nhcloudLogin=Hetzner moln inloggning\nclearHcloudToken=Rensa hcloud-token\nclearHcloudTokenDescription=Ta bort befintlig token så att du kan logga in igen\nselectIdentity=Välj identitet\nenableMcpServer=Aktivera MCP-server\nenableMcpServerDescription=Aktiverar XPipe MCP-servern, vilket gör det möjligt för externa MCP-klienter att skicka förfrågningar till MCP-servern. Se nedan för konfigurationsdetaljer.\\n\\nObservera att HTTP API inte behöver vara aktiverat för MCP-funktionaliteten.\nenableMcpMutationTools=Aktivera MCP-mutationsverktyg\nenableMcpMutationToolsDescription=Som standard är endast skrivskyddade verktyg aktiverade i MCP-servern. Detta för att säkerställa att inga oavsiktliga åtgärder kan vidtas som potentiellt kan modifiera ett system.\\n\\nOm du planerar att göra ändringar i system via MCP-klienter bör du kontrollera att MCP-klienten är konfigurerad för att bekräfta potentiellt destruktiva åtgärder innan du aktiverar det här alternativet. Kräver en återanslutning av alla MCP-klienter för att gälla.\nmcpClientConfigurationDetails=Konfiguration av MCP-klient\nmcpClientConfigurationDetailsDescription=Använd dessa konfigurationsdata för att ansluta till XPipe MCP-servern från den MCP-klient du väljer.\nswitchHostAddress=Ändra värdadress\naddAnotherHostName=Lägga till ett annat värdnamn\naddNetwork=Nätverksskanning ...\nnetworkScan=Nätverksskanning\nnetworkScanStore=Målvärd\nnetworkScanStoreDescription=Den värd som ska skanna det lokala nätverket\nuseAsGateway=Använd host som gateway\nuseAsGatewayDescription=Om målvärden ska användas som gateway för de skapade anslutningarna\nnetworkScanPorts=Portar att skanna\nnetworkScanPortsDescription=Den kommaseparerade listan med portar som ska ingå i sökningen\nnetworkScanType=Typ av anslutning\nnetworkScanTypeDescription=Vilken typ av servrar du ska leta efter\nemptyDirectory=Den här katalogen ser ut att vara tom\nhcloudConfigFile=hcloud konfigurationsfil\nhcloudConfigFileDescription=Platsen för konfigurationsfilen hcloud CLI .toml\npreferMonochromeIcons=Föredrar monokroma ikoner\npreferMonochromeIconsDescription=När detta är aktiverat kommer monokroma ikonvariabler att väljas framför standardfärgade versioner av en ikon, förutsatt att en separat ikonvariant för ljust eller mörkt läge finns tillgänglig för en ikon från en källa.\\n\\nKräver en uppdatering av ikonerna för att tillämpas.\nalwaysShowSshMotd=Visa alltid MOTD\nalwaysShowSshMotdDescription=Huruvida dagens meddelande som konfigurerats på ett fjärrsystem ska visas vid inloggning i en ny terminalsession eller inte. Observera att om du ändrar detta kan initialiseringsbeteendet för SSH-anslutningar ändras.\nmanageSubscription=Hantera prenumeration\nnoListeningServer=Ingen lyssnande server\nnetworkScanResults=Resultat av skanning\nnetworkScanResultsDescription=Listan över hittade system i nätverket\nlocalShellDialect=Lokalt skal\nlocalShellDialectDescription=Det skal som används för lokala operationer. Om det normala lokala standardskalet är inaktiverat eller trasigt i någon grad kan det här alternativet användas för att falla tillbaka på ett annat alternativ.\\n\\nVissa konfigurationer, t.ex. anpassade PATH-poster, kanske inte gäller för reservskalet om de inte har konfigurerats i respektive skalprofilfil ännu.\nagentSocketNotFound=Inget aktivt agentuttag hittades\nagentSocket=Plats för uttag\nagentSocketDescription=Sökvägen till agentens socketfil\nagentSocketNotConfigured=Inget anpassat uttag har konfigurerats ännu\ndownloadInProgress=$NAME$ nedladdning pågår\nenableTerminalStartupBell=Aktivera terminalens startklocka\nenableTerminalStartupBellDescription=Spela upp ett pip-/klockkommando i en ny terminalsession. Om din terminalemulator stöder klockor kan detta användas för att göra det lättare att identifiera nystartade terminalinstanser.\ninvalidSshGatewayChain=Ogiltig konfiguration av kedja med blandade gateways med jump-gateways och non-jump-gateways.\nsyncFileExists=Synkroniserad fil $FILE$ finns redan\nreplaceFile=Ersätt fil\nreplaceFileDescription=Ersatte den befintliga filen med den här\nrenameFile=Byt namn på fil\nrenameFileDescription=Ge den här filen ett annat namn för att synkronisera\nnewFileName=Nytt filnamn\nparentHostDoesNotSupportTunneling=Överordnad värd $NAME$ stöder inte tunnling\nconnectionNotesTemplate=Mall för anteckningar\nconnectionNotesTemplateDescription=Den markdown-mall som ska användas när du lägger till en ny anteckningspost till en anslutning.\nconnectionNotesButton=Redigera anteckningar\nrdpSmartSizing=Aktivera smart dimensionering\nrdpSmartSizingDescription=När detta är aktiverat kommer mstsc att skala ner skrivbordsstorleken om fönstret är för litet för att visas i sin fulla upplösning. Skrivbordets bildförhållande bevaras när det skalas ned.\ndisableStartOnInit=Inaktivera automatisk start\nenableStartOnInit=Aktivera automatisk start\nfileReadSudoTitle=Sudo fil läses\nfileReadSudoContent=Filen du försöker läsa ger dig inte läsbehörighet som nuvarande användare. Vill du läsa den här filen som root-användare med sudo? Detta kommer automatiskt att höja till root med antingen de befintliga autentiseringsuppgifterna eller via en prompt.\nnetbirdInstall.displayName=Installation av Netbird\nnetbirdInstall.displayDescription=Anslut till kollegor i ditt Netbird-nätverk\nnetbirdProfile.displayName=Netbird-profil\nnetbirdProfile.displayDescription=Lista kollegor i en specifik profil\nnetbirdPeer.displayName=Netbird peer\nnetbirdPeer.displayDescription=Ansluta till en peer via SSH\nnetbirdPublicKey=Offentlig nyckel\nnetbirdPublicKeyDescription=Den interna publika nyckeln för motparten\nnetbirdHostName=Värdnamn\nnetbirdHostNameDescription=Värdnamnet för peer i nätverket\nvncRefSystem=Associerat system\nvncRefSystemDescription=Den anslutningspost som denna VNC-anslutning ska associeras med. Lämna tomt om det inte finns någon\nabstractHost.displayName=Abstrakt värd\nabstractHost.displayDescription=Skapa en post för en värd som inte stöder shell-anslutningar\nabstractHostAddress=Värdadress\nabstractHostAddressDescription=Adressen till värden\nabstractHostGateway=Gateway\nabstractHostGatewayDescription=Det valfria gatewaysystemet genom vilket man kan nå denna host\nabstractHostConvert=Konvertera till abstrakt värdpost\nhostNoConnections=Inga tillgängliga anslutningar\nhostHasConnections=$COUNT$ tillgängliga anslutningar\nhostHasConnection=$COUNT$ tillgänglig anslutning\nlargeFileWarningTitle=Redigera stor fil\nlargeFileWarningContent=Filen som du vill redigera är ganska stor med $SIZE$. Vill du verkligen öppna den här filen i din textredigerare?\nrdpAskpassUser=RDP-användarnamn för värd $HOST$\nrdpAskpassPassword=Lösenord för användare $USER$\ninPlaceKey=Nyckel\ninPlaceKeyText=Innehåll i privat nyckel\ninPlaceKeyTextDescription=Den privata nyckelns innehåll\nnetbirdSelfhosted=Självhanterad Netbird-instans\nnetbirdSelfhostedDescription=Tillhandahålla en anpassad URL istället för att använda den molnbaserade versionen\nnetbirdManagementUrl=URL för Netbird-hantering\nnetbirdManagementUrlDescription=Hanteringsadressen för din självhostade instans\nnetbirdSetupKey=Inställningsnyckel\nnetbirdSetupKeyDescription=Om du använder installationsnycklar kan du använda en för inloggning\nnetbirdLogin=Netbird inloggning\naddProfile=Lägg till profil\nnetbirdProfileNameAsktext=Namn på ny netbird-profil\nopenSftp=Öppna i en SFTP-session\ncapslockWarning=Du har aktiverat capslock\ninherit=Ärva\nsshConfigStringSelected=Målvärd\nsshConfigStringSelectedDescription=För flera värdar används den första som mål. Ordna om dina värdar för att ändra målet\ntunnelToLocalhost=Tunnel till localhost\ntunnelToLocalhostDescription=Tunnla automatiskt fjärrporten till localhost\ntags=Etiketter\ntag=Tagg\naddNewTag=Skapa ny tagg\ncreateTag=Skapa tagg ...\ninPlacePublicKey=Offentlig nyckel\ninPlacePublicKeyDescription=Den associerade publika nyckeln för den angivna privata nyckeln\nsshKeygenTitle=Generera ny SSH-nyckel\nsshKeygenAlgorithm=Algoritm\nsshKeygenAlgorithmDescription=Den asymmetriska keygen-algoritm som ska användas för nyckeln\nrsaBits=Bitar\nrsaBitsDescription=Antal bitar i den genererade nyckeln\nsshKeygenComment=Kommentar\nsshKeygenCommentDescription=Den valfria kommentaren för denna nyckel\nsshKeygenPassphrase=Lösenfras\nsshKeygenPassphraseDescription=Den valfria lösenfrasen för den här nyckeln\ned25519SkResident=Gör invånarnyckel\ned25519SkResidentDescription=Lagra privat nyckel på maskinvarans säkerhetsnyckel\ned25519SkResidentKeyName=Etikett för resident nyckel\ned25519SkResidentKeyNameDescription=Ge nyckeln en etikett. Behövs när flera nycklar lagras på säkerhetsnyckeln\ned25519SkPinRequired=Kräver PIN-kod\ned25519SkPinRequiredDescription=Kräver PIN-inmatning vid användning\ned25519SkUserPresenceRequired=Kräver användarnärvaro\ned25519SkUserPresenceRequiredDescription=Kräver touch eller liknande vid användning. Vissa säkerhetsnycklar kräver att detta aktiveras\ncopyPublicKey=Kopiera publik nyckel\ngeneratePublicKey=Generera offentlig nyckel\npublicKeyGenerateNotice=Kan genereras från privat nyckel\nidentityApplyTargetHost=Mål\nidentityApplyTargetHostDescription=Systemet för att tillämpa identiteten på\nidentityApplyAuthorizedHost=SSH-nyckel auktoriserad\nidentityApplyAuthorizedHostDescription=SSH-nyckeln läggs till i den auktoriserade hosts-filen\nidentityApplyAuthorizedHostButton=Lägg till nyckel i fil\napplyIdentityToHost=Tillämpa identitet på värd ...\nidentityApplyMissingPublicKeyTitle=Saknad publik nyckel\nidentityApplyMissingPublicKeyContent=Identitetens SSH-nyckel har inte en offentlig nyckel associerad med sig. Kolla in konfigurationen för mer information.\nvalid=Giltig\nnotValid=Inte giltig\nwarning=Varning för\nidentityApplyTitle=Tillämpa identitet\nidentityApplyConfigPasswordEnabled=Lösenord auth aktiverat\nidentityApplyConfigPasswordEnabledDescription=Lösenordsautentisering är fortfarande aktiverad i sshd-konfigurationen\nidentityApplyConfigPasswordDisabled=Lösenord auth inaktiverad\nidentityApplyConfigPasswordDisabledDescription=Lösenordsautentisering är fortfarande inaktiverad i sshd-konfigurationen\nidentityApplyConfigKeyEnabled=Nyckelautentisering aktiverad\nidentityApplyConfigKeyEnabledDescription=Nyckelbaserad autentisering är fortfarande aktiverad i sshd-konfigurationen\nidentityApplyConfigKeyDisabled=Nyckelautentisering inaktiverad\nidentityApplyConfigKeyDisabledDescription=Nyckelbaserad autentisering är fortfarande inaktiverad i sshd-konfigurationen\nidentityApplyConfigRootDisabledWarning=Rotinloggning inaktiverad\nidentityApplyConfigRootDisabledWarningDescription=Inloggning av rotanvändare är inte aktiverad i sshd-konfigurationen\nidentityApplyConfigAdminWarning=Administratörsnycklar konfigurerade\nidentityApplyConfigAdminWarningDescription=Nyckeln kan behöva läggas till i administrators_authorized_keys istället för admin-användare\nidentityApplyEditConfig=Redigera konfiguration\nidentityApplyEditConfigDescription=Öppna sshd-konfigurationen i redigeraren för att åtgärda eventuella problem\nidentityApplyEditAuthorizedKeys=Redigera auktoriserade nycklar\nidentityApplyEditAuthorizedKeysDescription=Öppna filen authorized_keys i redigeraren för att redigera eller ta bort andra nycklar\nidentityApplyEditConfigButton=Öppna sshd_config\nidentityApplyEditAuthorizedKeysButton=Öppna auktoriserade_nycklar\nidentityApplySetStoreIdentity=Identitetsuppsättning för anslutning\nidentityApplySetStoreIdentityDescription=Identiteten är konfigurerad för att användas av anslutningen\nidentityApplySetStoreIdentityButton=Tillämpa identitet\ngenerateKey=Generera nyckel\ngroupSecretStrategy=Gruppbaserad åtkomstkontroll\ngroupSecretStrategyDescription=Hur man hämtar den grupphemlighet som används för kryptering och dekryptering för gruppen. Den hämtningsmetod du väljer kommer att köras när en användare loggar in i valvet vid uppstart.\\n\\nDen här inställningen konfigureras per grupp. Om du vill ändra den här inställningen för en annan grupp än den som är aktiv för tillfället måste du logga in i valvet som medlem i den gruppen.\nfileSecret=Filbaserad hemlighet\ncommandSecret=Kommando\nhttpRequestSecret=HTTP-svar\nfileSecretChoice=Plats för fil\nfileSecretChoiceDescription=Sökvägen till den fil som innehåller gruppens krypteringshemlighet. Eftersom den här filen kan hämtas på alla plattformar kan du använda ~ i sökvägen för att hänvisa till hemkatalogen. Filen måste finnas tillgänglig på alla system som du låser upp valvet från, annars misslyckas inloggningen.\ncommandSecretField=Skript för hämtning\ncommandSecretFieldDescription=Kommandot som returnerar den hemliga krypteringsnyckeln för den aktuella gruppen. Kommandot körs i det lokala systemets standardskal och nyckeln ska skrivas ut till stdout.\nhttpRequestSecretField=Begäran om URI\nhttpRequestSecretFieldDescription=Den URI som en HTTP-begäran ska skickas till. Grupphemligheten hämtas från HTTP-svarskroppen.\nvaultAuthentication=Autentisering av valv\nvaultAuthenticationDescription=Hur man autentiserar/låser upp valvdata. Det finns flera olika sätt att kryptera och låsa upp valvdata, beroende på vem du vill dela valvdata med.\ngroupAuthFailed=Hemlig autentisering misslyckades\nuserAuthFailed=Autentisering av lösenord misslyckades\nsavingChanges=Spara ändringar\nawsDeviceScan=AWS\naws=AWS\nawsCliInstallTitle=AWS CLI krävs\nawsCliInstallContent=AWS-integrationen kräver att AWS CLI installeras på ditt lokala system\nawsProfileCreateTitle=Ny AWS-profil\nawsProfileAccessKey=Åtkomstnyckel\nawsProfileName=Namn på profil\nawsProfileNameDescription=Visningsnamnet för den nya profilen\nawsProfileRegion=Region\nawsProfileRegionDescription=AWS-regionen som är associerad med profilen\nawsProfileAccessKeyId=ID för åtkomstnyckel\nawsProfileAccessKeyIdDescription=ID för åtkomstnyckel för IAM-användare\nawsProfileSecretAccessKey=Hemlig åtkomstnyckel\nawsProfileSecretAccessKeyDescription=Den associerade hemliga åtkomstnyckeln\nawsInstall.displayName=Installation av AWS CLI\nawsInstall.displayDescription=Anslut till dina AWS-system via AWS CLI\nawsProfile.displayName=AWS CLI-profil\nawsProfile.displayDescription=Åtkomst till AWS genom en specifik profil\nawsInstanceId=ID för instans\nawsInstanceIdDescription=Det interna ID:t för denna instans\nawsInstanceUseSsm=Anslut via SSM\nawsInstanceUseSsmDescription=Använd SSM-verktyget för att ansluta till instansen via SSH\nawsEc2Instance.displayName=AWS EC2-instans\nawsEc2Instance.displayDescription=Anslut till en EC2-instans via SSH\nawsS3Group.displayName=S3 hinkar\nawsS3Group.displayDescription=Åtkomst till S3-buckets för en AWS-profil\nawsS3Bucket.displayName=S3-skopa\nawsS3Bucket.displayDescription=Få åtkomst till en S3-bucket för en AWS-profil\nawsEc2Group.displayName=EC2-instanser\nawsEc2Group.displayDescription=Åtkomst till EC2-instanser för en AWS-profil\nawsEc2InstanceSsmTerminal=Öppna SSM terminal\ngenericS3Bucket.displayName=Generisk S3-skopa\ngenericS3Bucket.displayDescription=Få åtkomst till en generisk S3-bucket via AWS CLI\naddFileSystem=Filsystem ...\ngenericS3BucketHost=Värd\ngenericS3BucketHostDescription=S3-serverns värdpost eller manuella adress\ngenericS3BucketPortDescription=Den port som S3-servern lyssnar på\ngenericS3BucketAccessKeyId=ID för åtkomstnyckel\ngenericS3BucketAccessKeyIdDescription=ID för åtkomstnyckel för IAM-användare\ngenericS3BucketSecretAccessKey=Hemlig åtkomstnyckel\ngenericS3BucketSecretAccessKeyDescription=Den associerade hemliga åtkomstnyckeln\ngenericS3BucketHttps=Aktivera HTTPS\ngenericS3BucketHttpsDescription=Använd HTTPS för att ansluta till servern. Vissa leverantörer kan kräva HTTPS\ntunnelled=Tunnel\nawsInstallSync=Synkronisering av konfiguration\nawsInstallSyncDescription=Synkronisera konfigurationsfilerna för AWS CLI till git-valvet\nawsInstallLocation=Plats för användardata\nawsInstallLocationDescription=Sökvägen från vilken konfigurationsfilerna för AWS CLI hämtas\ninstanceActions=Åtgärder för instansen\nopenSplit=Öppna i delad terminal\nterminalSplitStrategy=Riktning för delad vy\nterminalSplitStrategyDescription=Styr hur terminalflikar delas upp när funktionen för delad vy används i batchläge för att öppna flera terminalsessioner bredvid varandra.\nterminalSplitStrategyDisabledDescription=Styr hur terminalflikar delas upp när funktionen för delad vy används i batchläge för att öppna flera terminalsessioner bredvid varandra.\\n\\nDin nuvarande terminalkonfiguration stöder inte delade vyer.\nhorizontal=Horisontell\nvertical=Vertikal\nbalanced=Balanserad\nclose=Avsluta\nhelpButton=$TOPIC$ länk till dokumentation\nquickAccess=Snabb åtkomst\ntoggleEnabled=Växla tillstånd\ncurrentPath=Aktuell sökväg\ndirectoryContents=Innehåll i katalog\ndirectoryOptions=Alternativ för katalog\nchooseConnectionType=Välj anslutningstyp\nbatchMode=Batch-läge\ntoggleButton=Växla knapp\ntailscaleUseSsh=Använd tailscale SSH auth\ntailscaleUseSshDescription=Logga in via själva tailscale SSH-servern utan någon SSH-autentisering\nportDescription=Den port som SSH-servern körs på\nloginAs=Logga in som\nsshGatewayType=Typ av gateway\nsshGatewayTypeDescription=Om du vill ansluta till målet via en tunnel eller med alternativet ProxyJump\ngatewayTunnel=Gateway-tunnel\nproxyJump=Proxy-hopp\ncommandTypeAsyncBackground=Kör fristående i bakgrunden\ncommandTypeSyncBackground=Körs i bakgrunden och väntar på avslut\ncommandTypeTerminalBackground=Öppna i terminal\nasyncBackgroundCommand=Bakgrundskommando\nsyncBackgroundCommand=Blockering av bakgrundskommando\nterminalBackgroundCommand=Kommando för terminal\ntestingConnection=Testning av anslutning ...\nopenManagementConsole=Öppen hanteringskonsol\nopenLxcTerminal=Öppna LXC-terminalen\nopenContainerConsole=Öppna seriell konsol\nkeeper2fa=2FA-metod\nkeeper2faDescription=Den primära tvåfaktorsautentiseringsmetoden som är konfigurerad för ditt konto. Aktivera detta om ditt Keeper-konto kräver tvåfaktorsautentisering för att komma åt lösenord.\nkeeperTotpDuration=Anpassad 2FA-kods varaktighet\nkeeperTotpDurationDescription=Åsidosätt standardtiden för hur länge en 2FA-kod är giltig. Gäller endast om organisationens policy tillåter ändring av giltighetstiden.\\n\\nMöjliga värden är: $VALUES$\nkeeperOtherAuth=Annat (RSA SecurID, Duo Security, Keeper DNA, etc.)\nextractReusableIdentities=Extrahera återanvändbara identiteter\nidentitiesAdded=Identiteter tillagda\nsyncMode=Synkroniseringsläge\nsyncModeDescription=Styr hur ändringar ska synkroniseras.\\n\\nOmedelbart läge skickar och hämtar ändringar så snart som möjligt, start- och avslutningsläge synkroniserar alla ändringar som görs under en session på en gång och manuellt läge synkroniserar endast när du initierar det.\ntoggleTerminalDock=Växla terminalens dockningsstation\nscriptDirectory=Plats i katalog\nscriptDirectoryDescription=Den lokala katalogen som innehåller shell-skriptfiler\nscriptSourceUrl=Förvarets URL\nscriptSourceUrlDescription=URL:en till ett fjärranslutet git-repository som innehåller filer med skalskript\nscriptCollectionSourceType=Källa typ\nscriptCollectionSourceTypeDescription=Den typ av källa från vilken shell-skript ska laddas\nscriptCollectionSourceEntry=Källans post\nscriptCollectionSourceEntryDescription=Källan från vilken shell-skript ska laddas\ngitRepository=Git-förvar\nscriptCollectionSource.displayName=Skriptkälla\nscriptCollectionSource.displayDescription=Importera automatiskt shell-skript från en befintlig källa\ndirectorySource=Källa till katalog\ngitRepositorySource=Git-arkivets källa\nrefreshSource=Uppdatera källa\nscriptTextSourceUrl=URL för skript\nscriptTextSourceUrlDescription=URL:en för att hämta skriptfilen från\nscriptSourceType=Skriptkälla\nscriptSourceTypeDescription=Varifrån ska manuset hämtas\nscriptSourceTypeInPlace=Skript på plats\nscriptSourceTypeUrl=Extern webbadress\nscriptSourceTypeSource=Befintlig källa\nimportScripts=Importera skript\nscriptsContained=$NUMBER$ skript\nscriptSourceCollectionImportTitle=Importera skript från källan ($SELECTED$/$COUNT$)\nnoScriptsFound=Inga skript hittades\ntunnel=Tunnel\nnotInitialized=Inte initialiserad\nselectCategory=Välj kategori ...\nscriptSourceName=Namn på skript\nscriptSourceNameDescription=Filnamnet på skriptet i källan\nworkspaceRestartTitle=Arbetsytan är klar\nworkspaceRestartContent=En genväg till den nya arbetsytan har skapats på $PATH$. Du kan navigera till genvägen eller starta om XPipe nu för att öppna den nya arbetsytan automatiskt.\nbrowseShortcut=Bläddra i fil\nsyncModeInstant=Synkronisera direkt\nsyncModeSession=Synkronisera vid start och avslut\nsyncModeManual=Synkronisera manuellt\npushChanges=Tryck på ändringar\npullChanges=Dra ändringar\nsourcedFrom=Hämtad från $SOURCE$\ninPlaceScript=Skript på plats\ngeneric=Generisk text\nsyncToPlainDirectory=Synkronisera till vanlig katalog\nsyncToPlainDirectoryDescription=När du synkroniserar till en lokal katalog kan du antingen behandla den här katalogen som ett annat git-arkiv eller bara som en vanlig katalog. Om inställningen för en vanlig katalog är aktiverad initieras inte katalogen som ett git-arkiv.\nopenSpiceSession=Öppna SPICE-session\nterminalBehaviour=Terminalens beteende\nnoScanPossible=Inga anslutningar som stöds hittades\nnetworkSwitchPorts=Nätverksportar\nnswitchGroup.displayName=Nätverksportar\nnswitchGroup.displayDescription=Lista tillgängliga portar på en nätverksenhet\nnswitchPort.displayName=Nätverksport\nnswitchPort.displayDescription=Styr en enskild port på en nätverksväxel\nenablePort=Aktivera port\nshutdownPort=Stäng ner port\nresetPort=Återställ port\nuseSystemDefault=Använda systemets standard\nportStatus=Status för port\nclearCounters=Rensa räknare\nshowStatus=Visa status\nshowAllPorts=Visa alla portar\nactiveLicense=Licens\nactiveLicenseDescription=Aktivera en XPipe-licensnyckel\nauthenticatorApp=Autentiseringsapplikation\nsecurityKey=Säkerhetsnyckel\nmcpAdditionalContext=Ytterligare MCP-kontext\nmcpAdditionalContextDescription=Ytterligare instruktioner att vidarebefordra till MCP-klienten. Använd detta för att styra agentens beteende och ge ytterligare sammanhang för din individuella installation.\nmcpAdditionalContextSample=- Starta inte om några tjänster och daemons automatiskt utan att först bekräfta\\n- När du konfigurerar ett nätverksgränssnitt ska du alltid använda 192.168.1.1/24 som gateway\nprefsRestartTitle=Omstart krävs\nprefsRestartContent=Vissa alternativ som du ändrade kräver en omstart av programmet för att gälla. Vill du starta om XPipe nu?\nbashShell=Bash-skal\n"
  },
  {
    "path": "lang/strings/translations_tr.properties",
    "content": "delete=Silme\nproperties=Özellikler\nusedDate=Kullanılmış $DATE$\nopenDir=Açık Dizin\nsortLastUsed=Son kullanım tarihine göre sırala\nsortAlphabetical=İsme göre alfabetik sıralama\nsortIndexed=Sıra indeksine göre sırala\nrestartDescription=Yeniden başlatma genellikle hızlı bir çözüm olabilir\nreportIssue=Bir sorun bildirin\nreportIssueDescription=Entegre sorun raportörünü açın\nusefulActions=Yararlı eylemler\nstored=Kurtarıldı\ntroubleshootingOptions=Sorun giderme araçları\ntroubleshoot=Sorun Giderme\nremote=Uzak Dosya\naddShellStore=Kabuk Ekle ...\naddShellTitle=Kabuk Bağlantısı Ekleme\nsavedConnections=Kaydedilen Bağlantılar\nsave=Kaydet\nclean=Temiz\nmoveTo=Taşınmak ...\naddDatabase=Veritabanı ...\nbrowseInternalStorage=Dahili depolama alanına göz atın\naddTunnel=Tünel ...\naddService=Hizmet ...\naddScript=Senaryo ...\naddHost=Uzak Ana Bilgisayar ...\naddShell=Shell Çevre ...\naddCommand=Komut ...\naddAutomatically=Otomatik olarak ekle ...\naddOther=Diğerlerini Ekle ...\nconnectionAdd=Bağlantı ekle\nscriptAdd=Komut dosyası ekle\nscriptGroupAdd=Kod grubu ekleme\nidentityAdd=Kimlik ekleyin\nnew=Yeni\nselectType=Tip Seçiniz\nselectTypeDescription=Bağlantı türünü seçin\nselectShellType=Kabuk Tipi\nselectShellTypeDescription=Kabuk Bağlantı Türünü Seçin\nname=İsim\nstoreIntroHeader=Bağlantı Merkezi\nstoreIntroContent=Burada tüm yerel ve uzak kabuk bağlantılarınızı tek bir yerden yönetebilirsiniz. Başlangıç olarak, mevcut bağlantıları otomatik olarak hızlı bir şekilde algılayabilir ve hangilerinin ekleneceğini seçebilirsiniz.\nstoreIntroButton=Bağlantıları arayın ...\ndragAndDropFilesHere=Ya da bir dosyayı buraya sürükleyip bırakın\nconfirmDsCreationAbortTitle=İptal işlemini onayla\nconfirmDsCreationAbortHeader=Veri kaynağı oluşturma işlemini iptal etmek istiyor musunuz?\nconfirmDsCreationAbortContent=Tüm veri kaynağı oluşturma ilerlemeleri kaybolacaktır.\nconfirmInvalidStoreTitle=Doğrulamayı atla\nconfirmInvalidStoreContent=Bağlantı doğrulamasını atlamak mı istiyorsunuz? Doğrulanamamış olsa bile bu bağlantıyı ekleyebilir ve bağlantı sorunlarını daha sonra düzeltebilirsiniz.\nexpand=Genişlet\naccessSubConnections=Alt bağlantılara erişim\ncommon=Ortak\ncolor=Renk\nalwaysConfirmElevation=Her zaman izin yükseltmesini onaylayın\nalwaysConfirmElevationDescription=Sistemde bir komut çalıştırmak için sudo gibi yükseltilmiş izinlerin gerektiği durumların nasıl ele alınacağını kontrol eder.\\n\\nVarsayılan olarak, tüm sudo kimlik bilgileri bir oturum sırasında önbelleğe alınır ve gerektiğinde otomatik olarak sağlanır. Bu seçenek etkinleştirilirse, her seferinde yükseltme erişimini onaylamanızı isteyecektir.\nallow=İzin ver\nask=Sor\ndeny=Reddet\nshare=Git deposuna ekle\nunshare=Git deposundan kaldır\nremove=Kaldırmak\ncreateNewCategory=Yeni alt kategori\nprompt=İstem\ncustomCommand=Özel komut\nother=Diğer\nsetLock=Kilidi ayarla\nselectConnection=Bağlantı seçin\nselectEntry=Girişi seçin\ncreateLock=Parola oluştur\nchangeLock=Parolayı değiştir\ntest=Test\nfinish=Bitirmek\nerror=Bir hata oluştu\ndownloadStageDescription=İndirilen dosyaları sisteminizin indirilenler dizinine taşır ve açar.\nok=Tamam\nsearch=Arama\nrepeatPassword=Şifreyi tekrarla\naskpassAlertTitle=Askpass\nunsupportedOperation=Desteklenmeyen işlem: $MSG$\nfileConflictAlertTitle=Çatışma çözme\nfileConflictAlertContent=Bir çakışma ile karşılaşıldı. $FILE$ dosyası hedef sistemde zaten var.\\n\\nNasıl devam etmek istersiniz?\nfileConflictAlertContentMultiple=Bir çakışma ile karşılaşıldı. $FILE$ dosyası zaten var.\\n\\nNasıl devam etmek istersiniz? Herkes için geçerli olan bir seçeneği seçerek otomatik olarak çözebileceğiniz daha fazla çakışma olabilir.\nmoveAlertTitle=Hareketi onayla\nmoveAlertHeader=($COUNT$) seçili öğeleri $TARGET$ adresine taşımak istiyor musunuz?\ndeleteAlertTitle=Silme işlemini onayla\ndeleteAlertHeader=($COUNT$) seçili öğeleri silmek istiyor musunuz?\nselectedElements=Seçilen unsurlar:\nmustNotBeEmpty=$VALUE$ boş olmamalıdır\nvalueMustNotBeEmpty=Değer boş olmamalıdır\ntransferDescription=İndirmek için dosyaları buraya sürükleyin\ndragLocalFiles=İndirmeleri buradan sürükleyin\nnull=$VALUE$ null olmamalıdır\nroots=Kökler\nscripts=Senaryolar\nsearchFilter=Arama ...\nrecent=Yakın zamanda\nshortcut=Kısayol\nbrowserWelcomeEmptyHeader=Dosya tarayıcısı\nbrowserWelcomeEmptyContent=Dosya tarayıcısında hangi sistemlerin açılacağını sol taraftan seçebilirsiniz. XPipe daha önce hangi sistemlere ve dizinlere eriştiğinizi hatırlayacak ve gelecekte bunları burada bir hızlı erişim menüsünde gösterecektir.\nbrowserWelcomeEmptyButton=Yerel dosya tarayıcısını açma\nbrowserWelcomeSystems=Yakın zamanda aşağıdaki sistemlere bağlandınız:\nbrowserWelcomeDocsHeader=Dokümantasyon\nbrowserWelcomeDocsContent=XPipe'ı tanımak için daha rehberli bir yaklaşım tercih ediyorsanız, dokümantasyon web sitesine göz atın.\nbrowserWelcomeDocsButton=Açık dokümantasyon\nhostFeatureUnsupported=$FEATURE$ ana bilgisayarda yüklü değil\nmissingStore=$NAME$ mevcut değil\nconnectionName=Bağlantı adı\nconnectionNameDescription=Bu bağlantıya özel bir ad verin\nopenFileTitle=Dosya aç\nunknown=Bilinmiyor\nscanAlertTitle=Bağlantı ekleme\nscanAlertChoiceHeader=Hedef\nscanAlertChoiceHeaderDescription=Bağlantıların nerede aranacağını seçin. Bu, önce mevcut tüm bağlantıları arayacaktır.\nscanAlertHeader=Bağlantı türleri\nscanAlertHeaderDescription=Sistem için otomatik olarak eklemek istediğiniz bağlantı türlerini seçin.\nnoInformationAvailable=Bilgi mevcut değil\nyes=Evet\nno=Hayır\nerrorOccured=Bir hata oluştu\nterminalErrorOccured=Bir terminal hatası oluştu\nerrorTypeOccured=$TYPE$ türünde bir istisna fırlatıldı\npermissionsAlertTitle=Gerekli izinler\npermissionsAlertHeader=Bu işlemi gerçekleştirmek için ek izinler gereklidir.\npermissionsAlertContent=XPipe'a ayarlar menüsünde gerekli izinleri vermek için lütfen açılır pencereyi izleyin.\nerrorDetails=Hata ayrıntıları\nupdateReadyAlertTitle=Güncelleme Hazır\nupdateReadyAlertHeader=$VERSION$ sürümüne bir güncelleme yüklenmeye hazırdır\nupdateReadyAlertContent=Bu işlem yeni sürümü yükleyecek ve yükleme tamamlandığında XPipe'ı yeniden başlatacaktır.\nerrorNoDetail=Hata ayrıntıları mevcut değil\nerrorNoExceptionMessage=$TYPE$ türünde bir hata atıldı\nupdateAvailableTitle=Güncelleme Mevcut\nupdateAvailableContent=$VERSION$ sürümüne yönelik bir XPipe güncellemesi yüklenebilir. XPipe başlatılamamış olsa da, sorunu çözmek için güncellemeyi yüklemeyi deneyebilirsiniz.\nclipboardActionDetectedTitle=Pano Eylemi algılandı\nclipboardActionDetectedContent=XPipe panonuzda açılabilecek bir içerik algıladı. Şimdi açmak istiyor musunuz? Pano içeriğinizi içe aktarmak istiyor musunuz?\ninstall=Yükle ...\nignore=Görmezden gel\npossibleActions=Mevcut eylemler\nreportError=Hata bildir\nreportOnGithub=GitHub'da bir sorun raporu oluşturun\nreportOnGithubDescription=GitHub deposunda yeni bir sorun açın\nreportErrorDescription=İsteğe bağlı kullanıcı geri bildirimi ve tanılama bilgileri içeren bir hata raporu gönderin\nignoreError=Hatayı yoksay\nignoreErrorDescription=Bu hatayı görmezden gelin ve hiçbir şey olmamış gibi devam edin\nprovideEmail=Sizinle nasıl iletişime geçebiliriz (isteğe bağlı, yalnızca yanıt almak istiyorsanız). Raporunuz varsayılan olarak anonimdir, bu nedenle burada bir e-posta adresi gibi iletişim bilgileri sağlayabilirsiniz.\nadditionalErrorInfo=Ek bilgi sağlayın (isteğe bağlı)\nadditionalErrorAttachments=Ekleri seçin (isteğe bağlı)\ndataHandlingPolicies=Gizlilik Politikası\nsendReport=Rapor gönder\nerrorHandler=Hata işleyici\nevents=Etkinlikler\nvalidate=Doğrulama\nstackTrace=Yığın izi\npreviousStep=< Önceki\nnextStep=Sonraki >\nfinishStep=Bitirmek\nselect=Seçiniz\nbrowseInternal=Dahili Gözat\ncheckOutUpdate=Güncellemeye göz atın\nquit=Bırak\nnoTerminalSet=Hiçbir terminal uygulaması otomatik olarak ayarlanmamıştır. Bunu ayarlar menüsünden manuel olarak yapabilirsiniz.\nconnections=Bağlantılar\nconnectionHub=Bağlantı merkezi\nsettings=Ayarlar\nexplorePlans=Lisans\nhelp=Yardım\nabout=Hakkında\ndeveloper=Geliştirici\nbrowseFileTitle=Dosyaya göz at\nbrowser=Dosya tarayıcısı\nselectFileFromComputer=Bu bilgisayardan bir dosya seçin\nlinks=Bağlantılar\nwebsite=Web sitesi\ndiscordDescription=Discord sunucusuna katılın\nredditDescription=XPipe alt dizinine katılın\nsecurity=Güvenlik\nsecurityPolicy=Güvenlik bilgileri\nsecurityPolicyDescription=Ayrıntılı güvenlik politikasını okuyun\nprivacy=Gizlilik Politikası\nprivacyDescription=XPipe uygulaması için gizlilik politikasını okuyun\nslackDescription=Slack çalışma alanına katılın\nsupport=Destek\ngithubDescription=GitHub deposuna göz atın\nopenSourceNotices=Açık Kaynak Bildirimleri\ncheckForUpdates=Güncellemeleri kontrol edin\ncheckForUpdatesDescription=Varsa bir güncelleme indirin\nlastChecked=Son kontrol\nversion=Versiyon\nbuild=Sürüm oluştur\nruntimeVersion=Çalışma zamanı sürümü\nvirtualMachine=Sanal makine\nupdateReady=Güncellemeyi yükleyin\nupdateReadyPortable=Güncellemeye göz atın\nupdateReadyDescription=Bir güncelleme indirildi ve yüklenmeye hazır\nupdateReadyDescriptionPortable=Bir güncelleme indirilebilir\nupdateRestart=Güncellemek için yeniden başlatın\nnever=Asla\nupdateAvailableTooltip=Güncelleme mevcut\nptbAvailableTooltip=Herkese Açık Test Yapısı mevcut\nvisitGithubRepository=GitHub deposunu ziyaret edin\nupdateAvailable=Güncelleme mevcut: $VERSION$\ndownloadUpdate=Güncellemeyi indirin\nlegalAccept=Son Kullanıcı Lisans Sözleşmesini kabul ediyorum\nconfirm=Onaylayın\nprint=Yazdır\nwhatsNew=$VERSION$ sürümündeki yenilikler ($DATE$)\nantivirusNoticeTitle=Antivirüs programları hakkında bir not\nupdateChangelogAlertTitle=Değişiklik Günlüğü\ngreetingsAlertTitle=XPipe'a Hoş Geldiniz\neula=Son Kullanıcı Lisans Sözleşmesi\nnews=Haberler\nintroduction=Giriş\nprivacyPolicy=Gizlilik Politikası\nagree=Katılıyorum\ndisagree=Katılmıyorum\ndirectories=Dizinler\nlogFile=Günlük Dosyası\nlogFiles=Günlük Dosyaları\nlogFilesAttachment=Günlük Dosyaları\nissueReporter=Sayı Muhabiri\nopenCurrentLogFile=Günlük dosyaları\nopenCurrentLogFileDescription=Geçerli oturumun günlük dosyasını açın\nopenLogsDirectory=Günlükler dizinini açın\ninstallationFiles=Kurulum Dosyaları\nopenInstallationDirectory=Kurulum dosyaları\nopenInstallationDirectoryDescription=XPipe kurulum dizinini açın\nlaunchDebugMode=Hata ayıklama modu\nlaunchDebugModeDescription=XPipe'ı hata ayıklama modunda yeniden başlatma\nextensionInstallTitle=İndir\nextensionInstallDescription=Bu eylem XPipe tarafından dağıtılmayan ek üçüncü taraf kütüphaneleri gerektirir. Bunları buradan otomatik olarak yükleyebilirsiniz. Bileşenler daha sonra satıcının web sitesinden indirilir:\nextensionInstallLicenseNote=İndirme ve otomatik yükleme işlemini gerçekleştirerek üçüncü taraf lisanslarının koşullarını kabul etmiş olursunuz:\nlicense=Lisans\ninstallRequired=Kurulum Gerekli\nrestore=Geri Yükleme\nrestoreAllSessions=Tüm oturumları geri yükle\nlimitedTouchscreenMode=Sınırlı dokunmatik ekran modu\nlimitedTouchscreenModeDescription=Bu uygulamayı telefon ekranı gibi daha egzotik bir dokunmatik ekran arayüzünde kullanırken, bazı menüler düzgün çalışmayabilir. Bu seçenek etkinleştirildiğinde, menü uygulaması seyrek gönderilen fare/dokunma olaylarıyla çalışmak için daha sınırlı işlevsellik kullanır.\nappearance=Görünüş\ndisplay=Ekran\npersonalization=Kişiselleştirme\ndisplayOptions=Görüntüleme seçenekleri\ntheme=Tema\nrdpConfiguration=Uzak masaüstü yapılandırması\nrdpClient=RDP istemcisi\nrdpClientDescription=RDP bağlantıları başlatılırken çağrılacak RDP istemci programı.\\n\\nÇeşitli istemcilerin farklı derecelerde yeteneklere ve entegrasyonlara sahip olduğunu unutmayın. Bazı istemciler parolaların otomatik olarak aktarılmasını desteklemez, bu nedenle bunları başlatırken doldurmanız gerekir.\nlocalShell=Yerel kabuk\nthemeDescription=Tercih ettiğiniz ekran teması.\ndontAutomaticallyStartVmSshServer=Gerektiğinde VM'ler için SSH sunucusunu otomatik olarak başlatma\ndontAutomaticallyStartVmSshServerDescription=Bir hipervizörde çalışan bir sanal makineye herhangi bir kabuk bağlantısı SSH aracılığıyla yapılır. XPipe gerektiğinde kurulu SSH sunucusunu otomatik olarak başlatabilir. Güvenlik nedeniyle bunu istemiyorsanız, bu seçenekle bu davranışı devre dışı bırakabilirsiniz.\nconfirmGitShareTitle=Git senkronizasyonu\nconfirmGitShareContent=Seçili dosyayı git vault deponuza eklemek istiyor musunuz? Bu, dosyanın şifrelenmiş bir sürümünü git vault'unuza kopyalayacak ve değişikliklerinizi işleyecektir. Daha sonra senkronize edilmiş tüm masaüstlerinde dosyaya erişebileceksiniz.\ngitShareFileTooltip=Dosyayı git vault veri dizinine ekleyin, böylece otomatik olarak senkronize edilir.\\n\\nBu eylem yalnızca ayarlarda git vault etkinleştirildiğinde kullanılabilir.\nperformanceMode=Performans modu\nperformanceModeDescription=Uygulama performansını artırmak için gerekli olmayan tüm görsel efektleri devre dışı bırakır.\ndontAcceptNewHostKeys=Yeni SSH ana bilgisayar anahtarlarını otomatik olarak kabul etme\ndontAcceptNewHostKeysDescription=XPipe, SSH istemcinizin bilinen bir ana bilgisayar anahtarı kaydetmediği sistemlerden ana bilgisayar anahtarlarını varsayılan olarak otomatik olarak kabul edecektir. Ancak bilinen herhangi bir ana bilgisayar anahtarı değişmişse, yenisini kabul etmediğiniz sürece bağlanmayı reddedecektir.\\n\\nBu davranışın devre dışı bırakılması, başlangıçta herhangi bir çakışma olmasa bile tüm ana bilgisayar anahtarlarını kontrol etmenizi sağlar.\nuiScale=UI Ölçeği\nuiScaleDescription=Sistem genelindeki ekran ölçeğinizden bağımsız olarak ayarlanabilen özel bir ölçeklendirme değeri. Değerler yüzde cinsindendir, bu nedenle örneğin 150 değeri %150'lik bir UI ölçeği ile sonuçlanacaktır.\neditorProgram=Editör Programı\neditorProgramDescription=Her türlü metin verisini düzenlerken kullanılacak varsayılan metin düzenleyicisi.\nwindowOpacity=Pencere opaklığı\nwindowOpacityDescription=Arka planda neler olduğunu takip etmek için pencere opaklığını değiştirir.\nuseSystemFont=Sistem yazı tipini kullan\nopenDataDir=Kasa veri dizini\nopenDataDirButton=Açık veri dizini\nopenDataDirDescription=SSH anahtarları gibi ek dosyaları git deponuzla sistemler arasında senkronize etmek istiyorsanız, bunları depolama veri dizinine koyabilirsiniz. Burada referans verilen tüm dosyaların dosya yolları, senkronize edilen herhangi bir sistemde otomatik olarak uyarlanacaktır.\nupdates=Güncellemeler\nselectAll=Tümünü seçin\nadvanced=Gelişmiş\nthirdParty=Açık kaynak bildirimleri\neulaDescription=XPipe uygulaması için Son Kullanıcı Lisans Sözleşmesini okuyun\nthirdPartyDescription=Üçüncü taraf kütüphanelerin açık kaynak lisanslarını görüntüleyin\nworkspaceLock=Ana parola\nenableGitStorage=Senkronizasyonu etkinleştir\nsharing=Paylaşım\ngitSync=Git senkronizasyonu\nenableGitStorageDescription=Etkinleştirildiğinde, XPipe yerel kasa için bir git deposu başlatır ve değişiklikleri bu depoya işler. Bunun için git'in yüklü olması gerektiğini ve yükleme ve kaydetme işlemlerini yavaşlatabileceğini unutmayın.\\n\\nSenkronize edilmesi gereken tüm kategoriler açıkça senkronize edildi olarak işaretlenmelidir.\nstorageGitRemote=Uzak senkronizasyon URL'si\nstorageGitRemoteDescription=Ayarlandığında, XPipe yükleme sırasında tüm değişiklikleri otomatik olarak çekecek ve kaydetme sırasında tüm değişiklikleri uzak depoya itecektir.\\n\\nBu, kasanızı birden fazla XPipe kurulumu arasında paylaşmanıza olanak tanır. HTTP ve SSH URL'lerinin yanı sıra yerel dizinleri de destekler.\nvault=Kasa\nworkspaceLockDescription=XPipe'da saklanan hassas bilgileri şifrelemek için özel bir parola belirler.\\n\\nBu, depolanan hassas bilgileriniz için ek bir şifreleme katmanı sağladığından daha fazla güvenlikle sonuçlanır. Daha sonra XPipe başlatıldığında şifreyi girmeniz istenecektir.\nuseSystemFontDescription=Varsayılan sistem yazı tipinizin mi yoksa XPipe ile birlikte gelen Inter yazı tipinin mi kullanılacağını kontrol eder.\ntooltipDelay=Araç ipucu gecikmesi\ntooltipDelayDescription=Bir araç ipucu görüntülenene kadar beklenecek milisaniye miktarı.\nfontSize=Yazı tipi boyutu\nwindowOptions=Pencere Seçenekleri\nsaveWindowLocation=Pencere konumunu kaydet\nsaveWindowLocationDescription=Pencere koordinatlarının kaydedilip kaydedilmeyeceğini ve yeniden başlatmalarda geri yüklenip yüklenmeyeceğini kontrol eder.\nstartupShutdown=Başlatma / Kapatma\nshowChildrenConnectionsInParentCategory=Üst kategoride alt kategorileri göster\nshowChildrenConnectionsInParentCategoryDescription=Belirli bir üst kategori seçildiğinde alt kategorilerde bulunan tüm bağlantıların dahil edilip edilmeyeceği.\\n\\nBu devre dışı bırakılırsa kategoriler, alt klasörleri dahil etmeden yalnızca doğrudan içeriklerini gösteren klasik klasörler gibi davranır.\ncondenseConnectionDisplay=Yoğunlaştırılmış bağlantı ekranı\ncondenseConnectionDisplayDescription=Daha yoğun bir bağlantı listesi elde etmek için her üst düzey bağlantının daha az dikey alan kaplamasını sağlayın.\nopenConnectionSearchWindowOnConnectionCreation=Bağlantı oluşturma sırasında bağlantı arama penceresini açma\nopenConnectionSearchWindowOnConnectionCreationDescription=Yeni bir kabuk bağlantısı eklendiğinde mevcut alt bağlantıları aramak için pencerenin otomatik olarak açılıp açılmayacağı.\nworkflow=İş akışı\nsystem=Sistem\napplication=Uygulama\nstorage=Depolama\nrunOnStartup=Başlangıçta çalıştır\ncloseBehaviour=Çıkış davranışı\ncloseBehaviourDescription=XPipe'ın ana penceresini kapattıktan sonra nasıl devam edeceğini kontrol eder.\nlanguage=Dil\nlanguageDescription=Kullanılacak görüntüleme dili. Çeviriler topluluk katkılarıyla geliştirilmektedir. GitHub'da çeviri düzeltmeleri göndererek çeviri çabalarına yardımcı olabilirsiniz.\nlightTheme=Işık Teması\ndarkTheme=Koyu Tema\nexit=XPipe'tan çıkın\ncontinueInBackground=Arka planda devam et\nminimizeToTray=Tepsiye küçült\ncloseBehaviourAlertTitle=Kapanış davranışını ayarlama\ncloseBehaviourAlertTitleHeader=Pencere kapatılırken ne olması gerektiğini seçin. Uygulama kapatıldığında tüm etkin bağlantılar kapatılacaktır.\nstartupBehaviour=Başlangıç davranışı\nstartupBehaviourDescription=XPipe başlatıldığında masaüstü uygulamasının varsayılan davranışını kontrol eder.\nclearCachesAlertTitle=Önbelleği Temizle\nclearCachesAlertContent=Tüm XPipe önbelleklerini temizlemek ister misiniz? Bu, kullanıcı deneyimini iyileştirmek için depolanan tüm önbellek verilerini silecektir.\nstartGui=GUI'yi Başlat\nstartInTray=Tepside başlat\nstartInBackground=Arka planda başlat\nclearCaches=Önbellekleri temizle ...\nclearCachesDescription=Tüm önbellek verilerini sil\ncancel=İptal\nnotAnAbsolutePath=Mutlak bir yol değil\nnotADirectory=Dizin değil\nnotAnEmptyDirectory=Boş bir dizin değil\nautomaticallyCheckForUpdates=Güncellemeleri kontrol edin\nautomaticallyCheckForUpdatesDescription=Etkinleştirildiğinde, XPipe çalışırken yeni sürüm bilgileri bir süre sonra otomatik olarak getirilir. Yine de herhangi bir güncelleme yüklemesini açıkça onaylamanız gerekir.\nsendAnonymousErrorReports=Anonim hata raporları gönderin\nsendUsageStatistics=Anonim kullanım istatistikleri gönderin\nstorageDirectory=Depolama dizini\nstorageDirectoryDescription=XPipe'ın tüm bağlantı bilgilerini saklaması gereken konum. Bunu değiştirirken, eski dizindeki veriler yenisine kopyalanmaz.\nlogLevel=Günlük seviyesi\nappBehaviour=Uygulama davranışı\nlogLevelDescription=Günlük dosyaları yazılırken kullanılması gereken günlük düzeyi.\ndeveloperMode=Geliştirici modu\ndeveloperModeDescription=Etkinleştirildiğinde, geliştirme için yararlı olan çeşitli ek seçeneklere erişiminiz olacaktır.\neditor=Editör\ncustom=Özel\npasswordManager=Parola yöneticisi\nexternalPasswordManager=Harici şifre yöneticisi\npasswordManagerDescription=Entegre edilecek yerel olarak yüklenmiş parola yöneticisi.\\n\\nYüklü bir parola yöneticiniz varsa, XPipe'ı parolaları ondan alacak şekilde yapılandırabilirsiniz, böylece XPipe'ın parolaları kendisinin depolaması gerekmez. Etkinleştirildiğinde, bir bağlantı için herhangi bir parola alanı parola yöneticisini kullanacak şekilde yapılandırılabilir.\npasswordManagerCommandTest=Parola yöneticisini test edin\npasswordManagerCommandTestDescription=Bir parola yöneticisi kurduysanız çıktının doğru görünüp görünmediğini burada test edebilirsiniz.\npreferTerminalTabs=Yeni sekmeler açmayı tercih edin\npreferTerminalTabsDescription=XPipe'ın yeni pencereler yerine seçtiğiniz terminalde yeni sekmeler açmayı deneyip denemeyeceğini kontrol eder. Her terminal sekmeleri desteklemez.\ncustomRdpClientCommand=Özel komut\ncustomRdpClientCommandDescription=Özel RDP istemcisini başlatmak için yürütülecek komut.\\n\\nYer tutucu dize $FILE, çağrıldığında tırnak içine alınmış mutlak .rdp dosya adıyla değiştirilecektir. Boşluk içeriyorsa çalıştırılabilir yolunuzu tırnak içine almayı unutmayın.\ncustomEditorCommand=Özel düzenleyici komutu\ncustomEditorCommandDescription=Özel düzenleyiciyi başlatmak için yürütülecek komut.\\n\\nYer tutucu dize $FILE, çağrıldığında alıntılanan mutlak dosya adıyla değiştirilecektir. Boşluk içeriyorsa düzenleyicinizin çalıştırılabilir yolunu tırnak içine almayı unutmayın.\neditorReloadTimeout=Editör yeniden yükleme zaman aşımı\neditorReloadTimeoutDescription=Güncellendikten sonra bir dosyayı okumadan önce beklenecek milisaniye miktarı. Bu, düzenleyicinizin dosya kilitlerini yazma veya serbest bırakma konusunda yavaş olduğu durumlarda sorunları önler.\nencryptAllVaultData=Tüm kasa verilerini şifreleyin\nencryptAllVaultDataDescription=Etkinleştirildiğinde, kasa bağlantı verilerinin her parçası, yalnızca bu verilerdeki gizli bilgilerin aksine, kullanıcı kasası şifreleme anahtarınızla şifrelenecektir. Bu, kasada varsayılan olarak şifrelenmeyen kullanıcı adları, ana bilgisayar adları vb. gibi diğer parametreler için başka bir güvenlik katmanı ekler.\\n\\nBu seçenek git vault geçmişinizi ve farklarınızı işe yaramaz hale getirecektir çünkü artık orijinal değişiklikleri göremezsiniz, sadece ikili değişiklikleri görebilirsiniz.\nvaultSecurity=Kasa güvenliği\ndeveloperDisableUpdateVersionCheck=Güncelleme Sürüm Denetimini Devre Dışı Bırak\ndeveloperDisableUpdateVersionCheckDescription=Güncelleme denetleyicisinin bir güncelleme ararken sürüm numarasını göz ardı edip etmeyeceğini denetler.\ndeveloperDisableGuiRestrictions=GUI kısıtlamalarını devre dışı bırakma\ndeveloperDisableGuiRestrictionsDescription=Devre dışı bırakılan bazı eylemlerin kullanıcı arayüzünden yürütülmeye devam edilip edilemeyeceğini kontrol eder.\ndeveloperShowHiddenEntries=Gizli girişleri göster\ndeveloperShowHiddenEntriesDescription=Etkinleştirildiğinde, gizli ve dahili veri kaynakları gösterilecektir.\ndeveloperShowHiddenProviders=Gizli sağlayıcıları göster\ndeveloperShowHiddenProvidersDescription=Gizli ve dahili bağlantı ve veri kaynağı sağlayıcılarının oluşturma diyalog penceresinde gösterilip gösterilmeyeceğini kontrol eder.\ndeveloperDisableConnectorInstallationVersionCheck=Konektör Sürüm Kontrolünü Devre Dışı Bırak\ndeveloperDisableConnectorInstallationVersionCheckDescription=Güncelleme denetleyicisinin uzak makinede yüklü bir XPipe bağlayıcısının sürümünü incelerken sürüm numarasını göz ardı edip etmeyeceğini kontrol eder.\nshellCommandTest=Kabuk Komut Testi\nshellCommandTestDescription=XPipe tarafından dahili olarak kullanılan kabuk oturumunda bir komut çalıştırın.\nterminal=Terminal\nterminalType=Terminal emülatörü\nterminalConfiguration=Terminal yapılandırması\nterminalCustomization=Terminal özelleştirme\neditorConfiguration=Editör yapılandırması\ndefaultApplication=Varsayılan uygulama\ninitialSetup=İlk kurulum\nterminalTypeDescription=Kabuk bağlantılarını açmak için kullanılacak varsayılan terminal.\\n\\nÖzellik desteği seviyesi terminale göre değişir ve her biri önerilen ya da önerilmeyen olarak işaretlenmiştir. Önerilen bir terminal kullandığınızda kullanıcı deneyiminiz en iyi olacaktır.\nprogram=Program\ncustomTerminalCommand=Özel terminal komutu\ncustomTerminalCommandDescription=Özel terminali belirli bir komutla açmak için çalıştırılacak komut.\\n\\nXPipe, terminalinizin çalıştırması için geçici bir başlatıcı kabuk betiği oluşturacaktır. Verdiğiniz komuttaki $CMD yer tutucu dizesi, çağrıldığında gerçek başlatıcı betiği ile değiştirilecektir. Terminal çalıştırılabilir yolunuz boşluk içeriyorsa alıntı yapmayı unutmayın.\nclearTerminalOnInit=Başlangıçta terminali temizle\nclearTerminalOnInitDescription=Etkinleştirildiğinde, XPipe terminal oturumu başlatılırken yazdırılan gereksiz çıktıları kaldırmak için yeni bir terminal oturumu başlatıldıktan sonra bir clear komutu çalıştırır.\ndontCachePasswords=İstenen parolaları önbelleğe almayın\ndontCachePasswordsDescription=Sorgulanan parolaların XPipe tarafından dahili olarak önbelleğe alınıp alınmayacağını kontrol eder, böylece geçerli oturumda bunları tekrar girmeniz gerekmez.\\n\\nBu davranış devre dışı bırakılırsa, sistem tarafından her istendiğinde istenen kimlik bilgilerini yeniden girmeniz gerekir.\ndenyTempScriptCreation=Geçici komut dosyası oluşturmayı reddetme\ndenyTempScriptCreationDescription=XPipe, bazı işlevlerini gerçekleştirmek için bazen basit komutların kolayca yürütülmesini sağlamak üzere hedef sistemde geçici kabuk komut dosyaları oluşturur. Bunlar herhangi bir hassas bilgi içermez ve sadece uygulama amacıyla oluşturulur.\\n\\nBu davranış devre dışı bırakılırsa, XPipe uzak bir sistemde herhangi bir geçici dosya oluşturmaz. Bu seçenek, her dosya sistemi değişikliğinin izlendiği yüksek güvenlikli bağlamlarda kullanışlıdır. Bu devre dışı bırakılırsa, kabuk ortamları ve komut dosyaları gibi bazı işlevler amaçlandığı gibi çalışmayacaktır.\ndisableCertutilUse=Windows'ta certutil kullanımını devre dışı bırakma\nuseLocalFallbackShell=Yerel yedek kabuk kullan\nuseLocalFallbackShellDescription=Yerel işlemleri gerçekleştirmek için başka bir yerel kabuk kullanmaya geçin. Bu, Windows'ta PowerShell ve diğer sistemlerde bourne shell olabilir.\\n\\nBu seçenek, normal yerel varsayılan kabuğun devre dışı bırakılması veya bir dereceye kadar bozulması durumunda kullanılabilir. Bu seçenek etkinleştirildiğinde bazı özellikler beklendiği gibi çalışmayabilir.\ndisableCertutilUseDescription=Cmd.exe'deki çeşitli eksiklikler ve hatalar nedeniyle, geçici kabuk betikleri certutil ile oluşturulur ve cmd.exe ASCII olmayan girdilerde bozulduğu için base64 girdisinin kodunu çözmek için kullanılır. XPipe bunun için PowerShell de kullanabilir ancak bu daha yavaş olacaktır.\\n\\nBu, bazı işlevleri gerçekleştirmek ve bunun yerine PowerShell'e geri dönmek için Windows sistemlerinde herhangi bir certutil kullanımını devre dışı bırakır. Bu, bazıları certutil kullanımını engellediği için bazı AV'leri memnun edebilir.\ndisableTerminalRemotePasswordPreparation=Terminal uzaktan parola hazırlamayı devre dışı bırakma\ndisableTerminalRemotePasswordPreparationDescription=Terminalde birden fazla ara sistemden geçen bir uzak kabuk bağlantısının kurulması gereken durumlarda, herhangi bir sorgunun otomatik olarak doldurulmasına izin vermek için ara sistemlerden birinde gerekli parolaların hazırlanması gerekebilir.\\n\\nParolaların herhangi bir ara sisteme aktarılmasını istemiyorsanız, bu davranışı devre dışı bırakabilirsiniz. Gerekli herhangi bir ara parola daha sonra açıldığında terminalin kendisinde sorgulanacaktır.\nmore=Daha fazla\ntranslate=Çeviriler\nallConnections=Tüm bağlantılar\nallScripts=Tüm senaryolar\nallIdentities=Tüm kimlikler\nsynced=Senkronize\npredefined=Önceden tanımlı\nsamples=Örnekler\ngoodMorning=Günaydın\ngoodAfternoon=İyi günler\ngoodEvening=İyi akşamlar\naddVisual=Görsel ...\naddDesktop=Masaüstü ...\nssh=SSH\nsshConfiguration=SSH Yapılandırması\nsize=Boyut\nattributes=Nitelikler\nmodified=Değiştirilmiş\nowner=Sahibi\nupdateReadyTitle=$VERSION$ için güncelleme hazır\ntemplates=Şablonlar\nretry=Yeniden Dene\nretryAll=Tümünü yeniden dene\nreplace=Değiştirin\nreplaceAll=Tümünü değiştirin\nhibernateBehaviour=Kış uykusu davranışı\nhibernateBehaviourDescription=Sisteminiz hazırda bekletme/uyku moduna geçirildiğinde uygulamanın nasıl davranacağını kontrol eder.\noverview=Genel Bakış\nhistory=Tarih\nskipAll=Tümünü atla\nnotes=Notlar\naddNotes=Notlar ekleyin\norder=Yeniden sırala\nkeepFirst=İlk sende kalsın\nkeepLast=Son kalsın\npinToTop=Üste sabitleyin\nunpinFromTop=Üstten pimi çıkarın\norderAheadOf=Önceden sipariş verin ...\nclearIndex=Endeksi sıfırla\nhttpServer=HTTP sunucusu\nmcpServer=MCP sunucusu\napiKey=API anahtarı\napiKeyDescription=XPipe daemon API isteklerinin kimliğini doğrulamak için API anahtarı. Kimlik doğrulamanın nasıl yapılacağı hakkında daha fazla bilgi için genel API belgelerine bakın.\ndisableApiAuthentication=API kimlik doğrulamasını devre dışı bırakma\ndisableApiAuthenticationDescription=Gerekli tüm kimlik doğrulama yöntemlerini devre dışı bırakır, böylece kimliği doğrulanmamış herhangi bir istek işlenir.\\n\\nKimlik doğrulama yalnızca geliştirme amacıyla devre dışı bırakılmalıdır.\napi=API\nstoreIntroImportContent=XPipe'ı zaten başka bir sistemde mi kullanıyorsunuz? Mevcut bağlantılarınızı uzak bir git deposu aracılığıyla birden fazla sistem arasında senkronize edin. Henüz kurulmamışsa daha sonra istediğiniz zaman senkronize edebilirsiniz.\nstoreIntroImportButton=Senkronizasyon bağlantıları ...\nstoreIntroImportHeader=Bağlantıları İçe Aktar\nshowNonRunningChildren=Çalışmayan çocukları göster\nhttpApi=HTTP API\nisOnlySupportedLimit=yalnızca $COUNT$ adresinden daha fazla bağlantıya sahip olunduğunda profesyonel lisans ile desteklenir\nareOnlySupportedLimit=yalnızca $COUNT$ adresinden daha fazla bağlantıya sahip olunduğunda profesyonel lisans ile desteklenir\nenabled=Etkin\nenableGitStoragePtbDisabled=Git senkronizasyonu, normal sürüm git depoları ile kullanımı önlemek ve günlük sürücünüz olarak bir PTB derlemesini kullanmaktan vazgeçirmek için genel test derlemeleri için devre dışı bırakılmıştır.\ncopyId=API Kimliğini Kopyala\nrequireDoubleClickForConnections=Bağlantılar için çift tıklama gerektir\nrequireDoubleClickForConnectionsDescription=Etkinleştirilirse, bağlantıları başlatmak için çift tıklamanız gerekir. Bu, bir şeyleri çift tıklamaya alışkınsanız kullanışlıdır.\nclearTransferDescription=Seçimi temizle\nselectTab=Sekme seçin\ncloseTab=Sekmeyi kapat\ncloseOtherTabs=Diğer sekmeleri kapatın\ncloseAllTabs=Tüm sekmeleri kapat\ncloseLeftTabs=Sekmeleri sola doğru kapatın\ncloseRightTabs=Sekmeleri sağa doğru kapatın\naddSerial=Seri ...\nconnect=Bağlan\nworkspaces=Çalışma Alanları\nmanageWorkspaces=Çalışma alanlarını yönetme\naddWorkspace=Çalışma alanı ekle ...\nworkspaceAdd=Yeni bir çalışma alanı ekleme\nworkspaceAddDescription=Çalışma alanları XPipe'ı çalıştırmak için farklı konfigürasyonlardır. Her çalışma alanı, tüm verilerin yerel olarak depolandığı bir veri dizinine sahiptir. Buna bağlantı verileri, ayarlar ve daha fazlası dahildir.\\n\\nSenkronizasyon özelliğini kullanırsanız, her çalışma alanını farklı bir git deposu ile senkronize etmeyi de seçebilirsiniz.\nworkspaceName=Çalışma alanı adı\nworkspaceNameDescription=Çalışma alanının görünen adı\nworkspacePath=Çalışma alanı yolu\nworkspacePathDescription=Çalışma alanı veri dizininin konumu\nworkspaceCreationAlertTitle=Çalışma alanı oluşturma\ndeveloperForceSshTty=SSH TTY'yi Zorla\ndeveloperForceSshTtyDescription=Eksik bir stderr ve bir pty desteğini test etmek için tüm SSH bağlantılarının bir pty ayırmasını sağlayın.\ndeveloperDisableSshTunnelGateways=SSH ağ geçidi tünellemesini devre dışı bırakma\ndeveloperDisableSshTunnelGatewaysDescription=Ağ geçitleri için tünel oturumlarını kullanmayın ve bunun yerine doğrudan sisteme bağlanın.\nttyWarning=Bağlantı zorla bir pty/tty ayırmış ve ayrı bir stderr akışı sağlamıyor.\\n\\nBu durum birkaç soruna yol açabilir.\\n\\nEğer yapabiliyorsanız, bağlantı komutunun bir pty tahsis etmemesini sağlayın.\nxshellSetup=Xshell kurulumu\ntermiusSetup=Termius kurulumu\ntryPtbDescription=XPipe geliştirici sürümlerinde yeni özellikleri erkenden deneyin\nconfirmVaultUnencryptTitle=Kasa şifrelemesinin kaldırılmasını onaylayın\nconfirmVaultUnencryptContent=Gelişmiş kasa şifrelemesini gerçekten devre dışı bırakmak istiyor musunuz? Bu, depolanan veriler için ek şifrelemeyi kaldıracak ve mevcut verilerin üzerine yazacaktır.\nenableHttpApi=HTTP API'yi Etkinleştir\nenableHttpApiDescription=API'yi etkinleştirerek harici programların yönetilen bağlantılarınızla eylemler gerçekleştirmek için XPipe arka plan programını çağırmasına izin verir.\nchooseCustomIcon=Özel simge seçin\ngitVault=Git kasası\nfileBrowser=Dosya tarayıcısı\nconfirmAllDeletions=Tüm silme işlemlerini onaylayın\nconfirmAllDeletionsDescription=Tüm silme işlemleri için bir onay iletişim kutusu gösterilip gösterilmeyeceği. Varsayılan olarak, yalnızca dizinler onay gerektirir.\nyesterday=Dün\ngreen=Yeşil\nyellow=Sarı\nblue=Mavi\nred=Kırmızı\ncyan=Cyan\npurple=Mor\nasktextAlertTitle=İstem\nfileWriteSudoTitle=Sudo dosya yazma\nfileWriteSudoContent=Yazmaya çalıştığınız dosya, kullanıcınıza yazma izinleri vermiyor. Bu dosyayı sudo ile root olarak mı yazmak istiyorsunuz? Bu, mevcut kimlik bilgileriyle veya bir komut istemi aracılığıyla otomatik olarak root'a yükselecektir.\ndontAllowTerminalRestart=Terminalin yeniden başlatılmasına izin verme\ndontAllowTerminalRestartDescription=Varsayılan olarak, terminal oturumları terminal içinden sonlandırıldıktan sonra yeniden başlatılabilir. Buna izin vermek için XPipe, oturumu tekrar başlatmak üzere terminalden gelen şu harici istekleri kabul edecektir\\n\\nXPipe terminal ve bu çağrının nereden geldiği üzerinde herhangi bir kontrole sahip değildir, bu nedenle kötü niyetli yerel uygulamalar XPipe üzerinden bağlantı başlatmak için bu işlevi de kullanabilir. Bu işlevselliğin devre dışı bırakılması bu senaryoyu önler.\nopenDocumentation=Açık dokümantasyon\nopenDocumentationDescription=Bu sorun için XPipe dokümanlar sayfasını ziyaret edin\nrenameAll=Tümünü yeniden adlandır\nlogging=Günlük kaydı\nenableTerminalLogging=Terminal günlüğünü etkinleştirme\nenableTerminalLoggingDescription=Tüm terminal oturumları için istemci tarafı günlüğünü etkinleştirir. Terminal oturumunun tüm girdileri ve çıktıları bir oturum günlüğü dosyasına yazılır. Parola istemleri gibi hassas bilgilerin kaydedilmediğini unutmayın.\nterminalLoggingDirectory=Terminal oturum günlükleri\nterminalLoggingDirectoryDescription=Tüm günlükler yerel sisteminizdeki XPipe veri dizininde saklanır.\nopenSessionLogs=Oturum günlüklerini açın\nsessionLogging=Terminal günlüğü\nsessionActive=Bu bağlantı için bir arka plan oturumu çalışıyor.\\n\\nBu oturumu manuel olarak durdurmak için durum göstergesine tıklayın.\nskipValidation=Doğrulamayı atla\nscriptsIntroHeader=Senaryolar hakkında\nscriptsIntroContent=Komut dosyalarını kabuk başlangıcında, dosya tarayıcısında ve isteğe bağlı olarak çalıştırabilirsiniz. Komut dosyalarını XPipe içinde kendiniz oluşturabilir veya mevcut olanları yerel sisteminizden veya uzaktaki bir git deposundan içe aktarabilirsiniz.\nscriptsIntroBottomHeader=Komut dosyalarını kullanma\nscriptsIntroBottomContent=Başlangıç için çeşitli örnek komut dosyaları vardır. Nasıl uygulandıklarını görmek için tek tek komut dosyalarının düzenleme düğmesine tıklayabilirsiniz. Komut dosyalarının çalıştırılması ve menülerde görünmesi için öncelikle etkinleştirilmesi gerekir, bunun için her komut dosyasında bir geçiş vardır.\nscriptsIntroBottomButton=Başlayın\nscriptSourcesIntroHeader=Senaryo kaynakları\nscriptSourcesIntroContent=Tüm kabuk komut dosyası koleksiyonuna anında erişmek için özel komut dosyası kaynakları ekleyebilirsiniz. Hem yerel kaynaklar hem de uzak git depoları kaynak olarak desteklenir. Kaynaktan algılanan tüm komut dosyaları otomatik olarak kullanılabilir hale gelecektir.\nscriptSourcesIntroButton=Kaynak ekle ...\ncheckForSecurityUpdates=Güvenlik güncellemelerini kontrol edin\ncheckForSecurityUpdatesDescription=XPipe olası güvenlik güncellemelerini normal özellik güncellemelerinden ayrı olarak kontrol edebilir. Bu etkinleştirildiğinde, normal güncelleme denetimi devre dışı bırakılsa bile en azından önemli güvenlik güncellemeleri yükleme için önerilecektir.\\n\\nBu ayarın devre dışı bırakılması, harici sürüm talebinin gerçekleştirilmemesine neden olur ve herhangi bir güvenlik güncellemesi hakkında bilgilendirilmezsiniz.\nclickToDock=Terminali yerleştirmek için tıklayın\nterminalStarting=Terminal başlangıcı bekleniyor ...\npinTab=Pim sekmesi\nunpinTab=Sabitleme sekmesini aç\npinned=Sabitlendi\nenableConnectionHubTerminalDocking=Bağlantı hub'ı terminal yerleştirmeyi etkinleştirme\nenableConnectionHubTerminalDockingDescription=Terminal pencerelerini bağlantı hub'ındaki XPipe uygulama penceresine kenetleyerek bir nevi entegre terminal simülasyonu yapabilirsiniz. Terminal pencereleri daha sonra XPipe tarafından her zaman dock'a sığacak şekilde yönetilir.\nenableFileBrowserTerminalDocking=Dosya tarayıcısı terminal yerleştirmeyi etkinleştirme\nenableFileBrowserTerminalDockingDescription=Terminal pencerelerini dosya tarayıcısındaki XPipe uygulama penceresine kenetleyerek bir nevi entegre terminal simülasyonu yapabilirsiniz. Terminal pencereleri daha sonra XPipe tarafından her zaman dock'a sığacak şekilde yönetilir.\ndownloadsDirectory=Özel indirme dizini\ndownloadsDirectoryDescription=İndirilen dosyaları indirilenlere taşı düğmesine tıklandığında yerleştirilecek özel dizin. Varsayılan olarak, XPipe kullanıcı indirme dizininizi kullanacaktır.\npinLocalMachineOnStartup=Başlangıçta yerel makine sekmesini sabitleme\npinLocalMachineOnStartupDescription=Otomatik olarak bir yerel makine sekmesi açın ve sabitleyin. Bu, yerel makine ve uzak dosya sistemi açıkken sık sık bölünmüş bir dosya tarayıcısı kullanıyorsanız kullanışlıdır.\nterminalErrorDescription=Bu hata ölümcüldür ve XPipe bunu düzeltmeden devam edemez.\ngroupName=Grup adı\nchmodPermissions=Yeni izinler\neditFilesWithDoubleClick=Dosyaları çift tıklama ile düzenleme\neditFilesWithDoubleClickDescription=Etkinleştirildiğinde, dosyalara çift tıklamak içerik menüsünü göstermek yerine onları doğrudan metin düzenleyicinizde açacaktır.\ncensorMode=Sansür modu\ncensorModeDescription=Ana bilgisayar adları, kullanıcı adları, bağlantı adları ve daha fazlası gibi tüm bilgileri bulanıklaştırır.\\n\\nBu, XPipe'ın ekran görüntüsünü almak veya ekran paylaşımını yapmak istiyorsanız ve herhangi bir bilgi sızdırmak istemiyorsanız kullanışlıdır.\naddIdentity=Kimlik ...\nidentities=Kimlikler\naddMacro=Eylem ...\nidentitiesIntroHeader=Kimlikler hakkında\nidentitiesIntroContent=Kullanıcı adları, parolalar ve anahtarların ortak kombinasyonlarını yeniden kullanıyorsanız, yeniden kullanılabilir kimlikler oluşturmak mantıklı olabilir. Bu, yeni bağlantılar eklerken bunlara hızlı bir şekilde başvurmanızı sağlar.\nidentitiesIntroBottomHeader=Kimliklerin paylaşılması\nidentitiesIntroBottomContent=Bu özellik etkinleştirildiğinde kimlikleri yerel olarak ekleyebilir veya git deposunda senkronize edebilirsiniz. Bu, kimliklerin birden fazla sistemde ve diğer ekip üyeleriyle seçici olarak paylaşılmasına olanak tanır.\nidentitiesIntroBottomButton=Senkronizasyonu ayarla\nidentitiesIntroButton=Kimlik oluşturun\nuserName=Kullanıcı Adı\nuserAuth=Kullanıcı tabanlı parola kimlik doğrulaması\ngroupAuth=Grup tabanlı gizli kimlik doğrulama\nteam=Takım\nteamSettings=Ekip ayarları\nteamVaults=Takım tonozları\nvaultTypeNameDefault=Varsayılan kasa\nvaultTypeNameLegacy=Miras kişisel kasa\nvaultTypeNamePersonal=Kişisel kasa\nvaultTypeNameTeam=Takım tonozu\nteamVaultsDescription=Ekip kasaları, birden fazla kullanıcı ve grubun paylaşılan bir kasaya güvenli erişim sağlamasına olanak tanır. Bağlantıları ve kimlikleri tüm kullanıcılar için paylaşılacak şekilde yapılandırabilir ya da kendi anahtarlarıyla şifreleyerek yalnızca bireysel kullanıcılar ve gruplar için kullanılabilir hale getirebilirsiniz. Diğer kasa kullanıcıları, anahtara erişimleri yoksa kişisel ve grup tabanlı bağlantılara ve kimliklere erişemezler.\nvaultTypeContentDefault=Şu anda kullanıcı ve özel parola ayarlanmamış varsayılan bir kasa kullanıyorsunuz. Sırlar yerel kasa anahtarı ile şifrelenir. Bir kasa kullanıcı hesabı oluşturarak kişisel kasaya yükseltebilirsiniz. Bu, kasa sırlarını, kasanın kilidini açmak için her oturum açmada girmeniz gereken kendi kişisel parolanızla şifrelemenize olanak tanır.\nvaultTypeContentLegacy=Şu anda kullanıcınız için eski bir kişisel kasa kullanıyorsunuz. Sırlar kişisel parolanızla şifrelenir. Bu eski uyumluluk sınırlı özelliklere sahiptir ve yerinde bir ekip kasasına yükseltilemez.\nvaultTypeContentPersonal=Şu anda kullanıcınız için kişisel bir kasa kullanıyorsunuz. Sırlar kişisel parolanızla şifrelenir. Ek kasa kullanıcıları ekleyerek veya grup tabanlı bir erişim yapılandırması ekleyerek bir ekip kasasına yükseltebilirsiniz.\nvaultTypeContentTeam=Şu anda birden fazla kullanıcının paylaşılan bir kasaya güvenli erişimine olanak tanıyan bir ekip kasası kullanıyorsunuz. Bağlantıları ve kimlikleri tüm kullanıcılar için paylaşılacak şekilde yapılandırabilir ya da kişisel veya grup anahtarınızla şifreleyerek yalnızca kişisel kullanıcınız veya grubunuz için kullanılabilir hale getirebilirsiniz. Diğer kasa kullanıcıları, anahtara erişimleri yoksa kişisel ve grup tabanlı bağlantılarınıza ve kimliklerinize erişemezler.\ngroupManagement=Grup yönetimi\ngroupManagementEmpty=Grup yönetimi\ngroupManagementDescription=Mevcut kasa gruplarını yönetin veya yenilerini oluşturun. Her kasa grubunun, yalnızca grup tarafından kullanılabilen ve başkaları tarafından kullanılamayan bağlantıları ve kimlikleri şifrelemek için kullanılan kendi gizli anahtarı vardır.\ngroupManagementEmptyDescription=Mevcut kasa gruplarını yönetin veya yenilerini oluşturun. Her kasa grubunun, yalnızca grup tarafından kullanılabilen ve başkaları tarafından kullanılamayan bağlantıları ve kimlikleri şifrelemek için kullanılan kendi gizli anahtarı vardır.\\n\\nBir ekip için grup bazlı hesaplar profesyonel planda desteklenmektedir.\nuserManagement=Kullanıcı yönetimi\nuserManagementEmpty=Kullanıcı yönetimi\nuserManagementDescription=Mevcut kasa kullanıcılarını yönetin veya yenilerini oluşturun. Her kasa kullanıcısının, yalnızca kullanıcının kullanabileceği ve başkalarının kullanamayacağı bağlantıları ve kimlikleri şifrelemek için kullanılan kendi bireysel şifresi vardır.\nuserManagementEmptyDescription=Mevcut kasa kullanıcılarını yönetin veya yenilerini oluşturun. Her kasa kullanıcısının, yalnızca kullanıcının kullanabileceği ve başkalarının kullanamayacağı bağlantıları ve kimlikleri şifrelemek için kullanılan kendi bireysel şifresi vardır. Bağlantıları ve kimlikleri kişisel anahtarınızla şifreleyebilmek için kendinize bir kullanıcı oluşturun.\\n\\nTopluluk sürümünde tek bir kullanıcı hesabı desteklenir. Profesyonel planda bir ekip için birden fazla kullanıcı hesabı desteklenir.\nuserIntroHeader=Kullanıcı yönetimi\nuserIntroContent=Başlamak için ilk kullanıcı hesabını kendiniz oluşturun. Bu, bu çalışma alanını bir parola ile kilitlemenizi sağlar.\naddReusableIdentity=Yeniden kullanılabilir kimlik ekleyin\nusers=Kullanıcılar\nsyncVault=Kasa senkronizasyonu\nsyncVaultDescription=Kasanızı birden fazla sistemle veya birden fazla ekip üyesiyle senkronize etmek için, bu kasa için git senkronizasyonunu etkinleştirin.\nenableGitSync=Git senkronizasyonunu etkinleştir\nbrowseVault=Kasa verileri\nbrowseVaultDescription=Yerel dosya yöneticinizde kasa dizinine kendiniz göz atabilirsiniz. Harici düzenlemelerin tavsiye edilmediğini ve çeşitli sorunlara neden olabileceğini unutmayın.\nbrowseVaultButton=Kasaya göz atın\nvaultUsers=Kasa kullanıcıları\ncreateHeapDump=Yığın dökümü oluştur\ncreateHeapDumpDescription=Bellek kullanımında sorun gidermek için bellek içeriğini dosyaya dökme\ninitializingApp=Yükleme bağlantıları\ncheckingLicense=Lisans kontrolü\nloadingGit=Git repo ile senkronize etme\nloadingGpg=Git için GnuPG arka plan programını başlatma\nloadingSettings=Yükleme ayarları\nloadingConnections=Yükleme bağlantıları\nunlockingVault=Kasa kilidini açma\nloadingUserInterface=Kullanıcı arayüzü yükleniyor\nptbNotice=Genel test derlemesi için bildirim\nuserDeletionTitle=Kullanıcı silme\nuserDeletionContent=Bu kasa kullanıcısını silmek istiyor musunuz? Bu, tüm kullanıcılar için mevcut olan kasa anahtarını kullanarak tüm kişisel kimliklerinizi ve bağlantı sırlarınızı yeniden şifreleyecektir. Bu işlem biraz zaman alacaktır ve kullanıcı değişikliklerini uygulamak için XPipe yeniden başlatılacaktır.\ngroupDeletionTitle=Grup silme\ngroupDeletionContent=Bu kasa grubunu silmek istiyor musunuz? Bu, tüm kullanıcılar tarafından kullanılabilen kasa anahtarını kullanarak yalnızca gruba özel tüm kimlikleri ve bağlantı gizli dizilerini yeniden şifreleyecektir. Bu işlem biraz zaman alacaktır ve XPipe grup değişikliklerini uygulamak için yeniden başlatılacaktır.\nkillTransfer=Öldürme transferi\ndestination=Hedef\nconfiguration=Konfigürasyon\nnewFile=Yeni dosya\nnewLink=Yeni bağlantı\nlinkName=Bağlantı adı\nscanConnections=Mevcut bağlantıları bulun ...\nobserve=Gözlemlemeye başlayın\nstopObserve=Gözlemlemeyi bırak\ncreateShortcut=Masaüstü kısayolu oluşturma\nbrowseFiles=Dosyalara Gözat\nclone=Klon\ntargetPath=Hedef yol\nnewDirectory=Yeni dizin\ncopyShareLink=Bağlantıyı kopyala\nselectStore=Mağaza Seçiniz\nsaveSource=Daha sonrası için kaydet\nexecute=Yürütmek\ndeleteChildren=Tüm çocukları kaldırın\nscriptGroupDescriptionDescription=Bu gruba isteğe bağlı bir açıklama verin\nabstractHostDescriptionDescription=Bu ana bilgisayara isteğe bağlı bir açıklama verin\nselectSource=Kaynak Seçiniz\ncommandLineRead=Güncelleme\ncommandLineWrite=Yazmak\nadditionalOptions=Ek Seçenekler\ninput=Giriş\nmachine=Makine\nopen=Açık\nedit=Düzenle\nscriptContents=Senaryo içeriği\nscriptContentsDescription=Çalıştırılacak komut dosyası komutları\nsnippets=Komut dosyası bağımlılıkları\nsnippetsDescription=Önce çalıştırılacak diğer betikler\nsnippetsDependenciesDescription=Varsa çalıştırılması gereken tüm olası komut dosyaları\nisDefault=Tüm uyumlu kabuklarda init üzerinde çalıştırın\nbringToShells=Tüm uyumlu kabukları getirin\nisDefaultGroup=Tüm grup komut dosyalarını kabuk başlangıcında çalıştırın\nexecutionType=Yürütme türü\nexecutionTypeDescription=Bu komut dosyası hangi bağlamlarda kullanılmalı\nminimumShellDialect=Kabuk tipi\nminimumShellDialectDescription=Bu betiğin çalıştırılacağı kabuk türü\ndumbOnly=Aptal\nterminalOnly=Terminal\nboth=Her ikisi de\nshouldElevate=Yükseltmeli\nshouldElevateDescription=Bu betiğin yükseltilmiş izinlerle çalıştırılıp çalıştırılmayacağı\nscript.displayName=Kabuk betiği\nscript.displayDescription=Yeniden kullanılabilir bir kabuk betiği oluşturma\nscriptGroup.displayName=Senaryo grubu\nscriptGroup.displayDescription=Senaryoları birlikte gruplandırın ve\nscriptGroup=Grup\nscriptGroupDescription=Bu komut dosyasının atanacağı grup\nscriptGroupGroupDescription=Bu kod grubunun atanacağı isteğe bağlı üst grup\nopenInNewTab=Yeni sekmede aç\nexecuteInBackground=arka planda\nexecuteInTerminal=içinde $TERM$\nback=Geri dön\nbrowseInWindowsExplorer=Windows gezgininde göz atın\nbrowseInDefaultFileManager=Varsayılan dosya yöneticisine göz atın\nbrowseInFinder=Bulucuya göz atın\ncopy=Anlaşıldı\npaste=Yapıştır\ncopyLocation=Kopyalama konumu\nabsolutePaths=Mutlak yollar\nabsoluteLinkPaths=Mutlak bağlantı yolları\nabsolutePathsQuoted=Mutlak alıntılanmış yollar\nfileNames=Dosya adları\nlinkFileNames=Dosya adlarını bağlama\nfileNamesQuoted=Dosya adları (Alıntı)\ndeleteFile=Silme $FILE$\neditWithEditor=İle düzenleyin $EDITOR$\nfollowLink=Bağlantıyı takip edin\ngoForward=İleri git\nshowDetails=Detayları göster\nshowDetailsDescription=Hatanın yığın izini göster\nopenFileWith=İle açın ...\nopenWithDefaultApplication=Varsayılan uygulama ile aç\nrename=Yeniden Adlandır\nrun=Koşmak\nopenInTerminal=Terminalde aç\nfile=Dosya\ndirectory=Rehber\nsymbolicLink=Sembolik bağlantı\ndesktopEnvironment.displayName=Masaüstü ortamı\ndesktopEnvironment.displayDescription=Yeniden kullanılabilir bir uzak masaüstü ortamı yapılandırması oluşturma\ndesktopHost=Masaüstü ana bilgisayar\ndesktopHostDescription=Temel olarak kullanılacak masaüstü bağlantısı\ndesktopShellDialect=Kabuk lehçesi\ndesktopShellDialectDescription=Komut dosyalarını ve uygulamaları çalıştırmak için kullanılacak kabuk lehçesi\ndesktopSnippets=Kod parçacıkları\ndesktopSnippetsDescription=Önce çalıştırılacak yeniden kullanılabilir kod parçacıklarının listesi\ndesktopInitScript=Başlangıç komut dosyası\ndesktopInitScriptDescription=Bu ortama özgü başlangıç komutları\ndesktopTerminal=Terminal uygulaması\ndesktopTerminalDescription=Komut dosyalarını başlatmak için masaüstünde kullanılacak terminal\ndesktopApplication.displayName=Masaüstü uygulaması\ndesktopApplication.displayDescription=Uzak masaüstünde bir uygulama çalıştırma\ndesktopBase=Masaüstü\ndesktopBaseDescription=Bu uygulamanın çalıştırılacağı masaüstü\ndesktopEnvironmentBase=Masaüstü ortamı\ndesktopEnvironmentBaseDescription=Bu uygulamanın çalıştırılacağı masaüstü ortamı\ndesktopApplicationPath=Başvuru yolu\ndesktopApplicationPathDescription=Çalıştırılacak yürütülebilir dosyanın yolu\ndesktopApplicationArguments=Argümanlar\ndesktopApplicationArgumentsDescription=Uygulamaya iletilecek isteğe bağlı argümanlar\ndesktopCommand.displayName=Masaüstü komutu\ndesktopCommand.displayDescription=Uzak masaüstü ortamında bir komut çalıştırma\ndesktopCommandScript=Komutlar\ndesktopCommandScriptDescription=Ortamda çalıştırılacak komutlar\nservice.displayName=Hizmet\nservice.displayDescription=Uzak bir hizmeti yerel makinenize iletme\nserviceLocalPort=Açık yerel bağlantı noktası\nserviceLocalPortDescription=Yönlendirilecek yerel bağlantı noktası, aksi takdirde rastgele bir bağlantı noktası kullanılır\nserviceRemotePort=Uzak bağlantı noktası\nserviceRemotePortDescription=Hizmetin üzerinde çalıştığı bağlantı noktası\nserviceHost=Hizmet sunucusu\nserviceHostDescription=Hizmetin üzerinde çalıştığı ana bilgisayar\nopenWebsite=Açık web sitesi\ncustomServiceGroup.displayName=Hizmet grubu\ncustomServiceGroup.displayDescription=Birden fazla hizmeti tek bir kategoride gruplayın\ninitScript=Başlangıç betiği - Kabuk başlangıcında çalıştır\nshellScript=Kabuk oturumu komut dosyası - Komut dosyasını kabuk oturumu sırasında çalıştırılabilir hale getirin\nrunnableScript=Çalıştırılabilir komut dosyası - Komut dosyasının doğrudan bağlantı hub'ından çalıştırılmasına izin ver\nfileScript=Dosya komut dosyası - Dosya tarayıcısında seçilen dosyalar için komut dosyasının çağrılmasına izin ver\nrunScript=Komut dosyasını çalıştır\ncopyUrl=URL'yi kopyala\nfixedServiceGroup.displayName=Hizmet grubu\nfixedServiceGroup.displayDescription=Bir sistemdeki mevcut hizmetleri listeleme\nmappedService.displayName=Hizmet\nmappedService.displayDescription=Bir konteyner tarafından sunulan bir hizmetle etkileşim\ncustomService.displayName=Hizmet\ncustomService.displayDescription=Yerel makinenizde bir uzak hizmet bağlantı noktasını otomatik olarak açma veya tünelleme\nfixedService.displayName=Hizmet\nfixedService.displayDescription=Önceden tanımlanmış bir hizmet kullanın\nnoServices=Mevcut hizmet yok\nhasServices=$COUNT$ mevcut hi̇zmetler\nhasService=$COUNT$ mevcut hizmet\nnoConnections=Mevcut bağlantı yok\nhasConnections=$COUNT$ mevcut bağlantılar\nhasConnection=$COUNT$ mevcut bağlantı\nopenHttp=Açık HTTP hizmeti\nopenHttps=HTTPS hizmetini açın\nnoScriptsAvailable=Etkin ve uyumlu komut dosyası yok\nscriptsDisabled=Komut dosyaları devre dışı\nchangeIcon=Simge değiştir\ninit=Başlangıç\nshell=Kabuk\nhub=Hub\nscript=senaryo\ngenericScript=Jenerik\ngradleTasks=Gradle görevleri\nrunTask=Görevi çalıştır\narchiveName=Arşiv adı\ncompress=Sıkıştır\ncompressContents=İçeriği sıkıştır\nuntarHere=Untar burada\nuntarDirectory=Untar'a $DIR$\nunzipDirectory=Açmak için $DIR$\nunzipHere=Buradan açın\nrequiresRestart=Uygulamak için yeniden başlatma gerekir.\ndownload=İndir\nservicePath=Servis yolu\nservicePathDescription=URL'yi bir tarayıcıda açarken isteğe bağlı alt yol\nactive=Aktif\ninactive=Aktif değil\nstarting=Başlangıç\nremotePort=Uzak bağlantı noktası\nremotePortNumber=Uzak bağlantı noktası $PORT$\nuserIdentity=Kişisel kimlik\nglobalIdentity=Küresel kimlik\nidentityChoice=Kullanıcı kimliği\nidentityChoiceDescription=Önceden tanımlanmış bir kimlik seçin veya yalnızca bu bağlantı için oturum açma ayrıntılarını belirtin\ndefineNewIdentityOrSelect=Yeni girin veya mevcut olanı seçin\nlocalIdentity.displayName=Yerel kimlik\nlocalIdentity.displayDescription=Bu yerel masaüstü için yeniden kullanılabilir bir kimlik oluşturun\nsyncedIdentity.displayName=Senkronize kimlik\nsyncedIdentity.displayDescription=Sistemler arasında senkronize edilen yeniden kullanılabilir bir kimlik oluşturun\nlocalIdentity=Yerel kimlik\nkeyNotSynced=Anahtar dosyası henüz git deposu ile senkronize edilmedi. Eklemek için anahtar dosyası için git'e ekle düğmesini kullanın.\nusernameDescription=Oturum açılacak kullanıcı adı\nidentity.displayName=Kimlik\nidentity.displayDescription=Bağlantılar için yeniden kullanılabilir bir kimlik oluşturun\nlocal=Yerel\nshared=Küresel\nuserDescription=Oturum açmak için kullanıcı adı veya önceden tanımlanmış kimlik\nidentityAccessLevel=Erişim seviyesi\nidentityPerUser=Kişisel kimlik erişimi\nidentityPerUserDescription=Bu kimliğe ve ilişkili bağlantılarına erişimi yalnızca kasa kullanıcınızla kısıtlayın\nidentityPerUserDisabled=Kişisel kimlik erişimi (devre dışı)\nidentityPerUserDisabledDescription=Bu kimliğe ve ilişkili bağlantılarına erişimi yalnızca kasa kullanıcınızla kısıtlayın (Ekibin yapılandırılmasını gerektirir)\nidentityPerGroup=Yalnızca gruba özel kimlik erişimi\nidentityPerGroupDescription=Bu kimliğe ve ilişkili bağlantılarına erişimi yalnızca bu kasa grubuyla kısıtlayın\nlibrary=Kütüphane\nlocation=Konum\nkeyAuthentication=Anahtar tabanlı kimlik doğrulama\nkeyAuthenticationDescription=Anahtar tabanlı kimlik doğrulama gerekiyorsa kullanılacak kimlik doğrulama yöntemi\nlocationDescription=İlgili özel anahtarınızın dosya yolu\nkeyFile=Yerel anahtar dosyası\nkeyPassword=Parola\nkey=Anahtar\nyubikeyPiv=Yubikey PIV\npageant=Pageant\ngpgAgent=GPG Temsilcisi\ncustomPkcs11Library=Özel PKCS#11 kütüphanesi\nsshAgent=OpenSSH aracısı\nnone=Hiçbiri\nindex=Dizin ...\notherExternal=Diğer harici ajan\nsync=Senkronizasyon\nvaultSync=Kasa senkronizasyonu\ncustomUsername=Kullanıcı Adı\ncustomUsernameDescription=Oturum açmak için isteğe bağlı alternatif kullanıcı\ncustomUsernamePassword=Şifre\ncustomUsernamePasswordDescription=Sudo kimlik doğrulaması gerektiğinde kullanılacak kullanıcı parolası\nshowInternalPods=Dahili bölmeleri göster\nshowAllNamespaces=Tüm ad alanlarını göster\nshowInternalContainers=Dahili konteynerleri göster\nrefresh=Yenile\nvmwareGui=GUI'yi Başlat\nmonitorVm=Sanal Makineyi İzleme\naddCluster=Küme ekleyin ...\nshowNonRunningInstances=Çalışmayan örnekleri göster\nvmwareGuiDescription=Bir sanal makinenin arka planda mı yoksa bir pencerede mi başlatılacağı.\nvmwareEncryptionPassword=Şifreleme parolası\nvmwareEncryptionPasswordDescription=VM'yi şifrelemek için kullanılan isteğe bağlı parola.\nvmPasswordDescription=Konuk kullanıcı için gerekli parola.\nvmPassword=Kullanıcı şifresi\nvmUser=Misafir kullanıcı\nrunTempContainer=Geçici konteyneri çalıştır\nvmUserDescription=Birincil konuk kullanıcınızın kullanıcı adı\ndockerTempRunAlertTitle=Geçici konteyneri çalıştır\ndockerTempRunAlertHeader=Bu, durdurulduğunda otomatik olarak kaldırılacak geçici bir kapsayıcıda bir kabuk işlemi çalıştıracaktır.\nimageName=Resim adı\nimageNameDescription=Kullanılacak konteyner imaj tanımlayıcısı\ncontainerName=Konteyner adı\ncontainerNameDescription=İsteğe bağlı özel konteyner adı\nvm=Sanal makine\nvmDescription=İlişkili yapılandırma dosyası.\nvmwareScan=VMware masaüstü hipervizörleri\nvmwareMachine.displayName=VMware Sanal Makinesi\nvmwareMachine.displayDescription=SSH aracılığıyla bir sanal makineye bağlanma\nvmwareInstallation.displayName=VMware masaüstü hipervizör kurulumu\nvmwareInstallation.displayDescription=CLI aracılığıyla kurulu VM'lerle etkileşim\nstart=Başlangıç\nstop=Dur\npause=Duraklat\nrdpTunnelHost=Hedef ev sahibi\nrdpTunnelHostDescription=RDP bağlantısını tünellemek için SSH bağlantısı\nrdpTunnelUsername=Kullanıcı Adı\nrdpTunnelUsernameDescription=Oturum açılacak özel kullanıcı, boş bırakılırsa SSH kullanıcısını kullanır\nrdpFileLocation=Dosya konumu\nrdpFileLocationDescription=.rdp dosyasının dosya yolu\nrdpPasswordAuthentication=Şifre doğrulama\nrdpFiles=RDP dosyaları\nrdpPasswordAuthenticationDescription=İstemci desteğine bağlı olarak doldurulacak veya panoya kopyalanacak parola\nrdpFile.displayName=RDP dosyası\nrdpFile.displayDescription=Mevcut bir .rdp dosyası üzerinden bir sisteme bağlanma\nrequiredSshServerAlertTitle=SSH sunucusunu kurun\nrequiredSshServerAlertHeader=VM'de kurulu bir SSH sunucusu bulunamıyor.\nrequiredSshServerAlertContent=Sanal makineye bağlanmak için XPipe çalışan bir SSH sunucusu arıyor ancak sanal makine için kullanılabilir bir SSH sunucusu algılanmadı.\ncomputerName=Bilgisayar Adı\npssComputerNameDescription=Bağlanılacak bilgisayar adı\ncredentialUser=Kimlik Bilgisi Kullanıcısı\ncredentialUserDescription=Oturum açılacak kullanıcı.\ncredentialPassword=Kimlik Bilgisi Şifresi\ncredentialPasswordDescription=Kullanıcının parolası.\nsshConfig=SSH yapılandırma dosyaları\nautostart=XPipe başlatıldığında otomatik olarak bağlan\nacceptHostKey=Ana bilgisayar anahtarını kabul et\nmodifyHostKeyPermissions=Ana bilgisayar anahtar izinlerini değiştirme\nattachContainer=Eklemek\ncontainerLogs=Günlükleri göster\nopenSftpClient=Harici SFTP istemcisinde açın\nopenTermius=Termius'da açık\nshowInternalInstances=Dahili örnekleri göster\neditPod=Düzenleme bölmesi\nacceptHostKeyDescription=Yeni ana bilgisayar anahtarına güvenin ve devam edin\nmodifyHostKeyPermissionsDescription=OpenSSH'nin mutlu olması için orijinal dosyanın izinlerini kaldırmaya çalışın\npsSession.displayName=PowerShell Uzak Oturumu\npsSession.displayDescription=New-PSSession ve Enter-PSSession aracılığıyla bağlanın\nsshLocalTunnel.displayName=Yerel SSH tüneli\nsshLocalTunnel.displayDescription=Uzak bir ana bilgisayara SSH tüneli oluşturma\nsshRemoteTunnel.displayName=Uzak SSH tüneli\nsshRemoteTunnel.displayDescription=Uzak bir ana bilgisayardan ters SSH tüneli oluşturma\nsshDynamicTunnel.displayName=Dinamik SSH tüneli\nsshDynamicTunnel.displayDescription=SSH bağlantısı aracılığıyla bir SOCKS proxy oluşturma\nshellEnvironmentGroup.displayName=Kabuk ortamları\nshellEnvironmentGroup.displayDescription=Kabuk ortamları\nshellEnvironment.displayName=Kabuk ortamı\nshellEnvironment.displayDescription=Özelleştirilmiş bir kabuk başlangıç ortamı oluşturma\nshellEnvironment.informationFormat=$TYPE$ çevre\nshellEnvironment.elevatedInformationFormat=$ELEVATION$ $TYPE$ çevre\nenvironmentConnectionDescription=Için bir ortam yaratmak için temel bağlantı\nenvironmentScriptDescription=Kabukta çalıştırılacak isteğe bağlı özel init betiği\nenvironmentSnippets=Kabuk komut dosyaları\ncommandSnippetsDescription=İlk olarak çalıştırılacak isteğe bağlı ön tanımlı kabuk komut dosyaları\nenvironmentSnippetsDescription=Başlatma sırasında çalıştırılacak isteğe bağlı önceden tanımlanmış kabuk komut dosyaları\nshellTypeDescription=Başlatılacak açık kabuk türü\noriginPort=Menşe bağlantı noktası\noriginAddress=Menşe adresi\nremoteAddress=Uzak adres\nremoteSourceAddress=Uzak kaynak adresi\nremoteSourcePort=Uzak kaynak bağlantı noktası\noriginDestinationPort=Kaynak hedef bağlantı noktası\noriginDestinationAddress=Kaynak hedef adresi\norigin=Köken\nremoteHost=Uzak ana bilgisayar\naddress=Adres\nproxmox.displayName=Proxmox\nproxmox.displayDescription=Proxmox Sanal Ortamındaki sistemlere bağlanma\nproxmoxVm.displayName=Proxmox VM\nproxmoxVm.displayDescription=SSH aracılığıyla Proxmox VE'deki bir sanal makineye bağlanma\nproxmoxContainer.displayName=Proxmox Konteyner\nproxmoxContainer.displayDescription=Proxmox VE'deki bir konteynere bağlanma\nsshDynamicTunnel.hostDescription=SOCKS proxy olarak kullanılacak sistem\nsshDynamicTunnel.bindingDescription=Tünelin hangi adreslere bağlanacağı\nsshRemoteTunnel.hostDescription=Orijine giden uzak tünelin başlatılacağı sistem\nsshRemoteTunnel.bindingDescription=Tünelin hangi adreslere bağlanacağı\nsshLocalTunnel.hostDescription=Tüneli açmak için sistem\nsshLocalTunnel.bindingDescription=Tünelin hangi adreslere bağlanacağı\nsshLocalTunnel.localAddressDescription=Bağlanacak yerel adres\nsshLocalTunnel.remoteAddressDescription=Bağlanacak uzak adres\ncmd.displayName=Komuta\ncmd.displayDescription=Sistem üzerinde rastgele bir komut çalıştırma\nk8sPod.displayName=Kubernetes Pod\nk8sPod.displayDescription=Kubectl aracılığıyla bir pod'a ve kapsayıcılarına bağlanma\nk8sContainer.displayName=Kubernetes Konteyner\nk8sContainer.displayDescription=Bir konteynere kabuk açma\nk8sCluster.displayName=Kubernetes Kümesi\nk8sCluster.displayDescription=Bir kümeye ve podlarına kubectl aracılığıyla bağlanma\nsshTunnelGroup.displayName=SSH Tünelleri\nsshTunnelGroup.displayCategory=Her tür SSH tüneli\nlocal.displayName=Yerel makine\nlocal.displayDescription=Yerel makinenin kabuğu\ncygwin=Cygwin\nmsys2=MSYS2\ngitWindows=Windows için Git\ngitForWindows.displayName=Windows için Git\ngitForWindows.displayDescription=Yerel Git For Windows ortamınıza erişin\nmsys2.displayName=MSYS2\nmsys2.displayDescription=MSYS2 ortamınızın kabuklarına erişim\ncygwin.displayName=Cygwin\ncygwin.displayDescription=Cygwin ortamınızın kabuklarına erişim\nnamespace=İsim Alanı\ngitVaultIdentityStrategy=Git SSH kimliği\ngitVaultIdentityStrategyDescription=Uzak olarak bir SSH git URL'si kullanmayı seçtiyseniz ve uzak deponuz bir SSH kimliği gerektiriyorsa, bu seçeneği ayarlayın.\\n\\nBir HTTP URL'si sağlamanız durumunda, bu seçeneği göz ardı edebilirsiniz.\ndockerContainers=Docker konteynerleri\ndockerCmd.displayName=docker CLI istemcisi\ndockerCmd.displayDescription=Docker konteynerlerine docker CLI istemcisi aracılığıyla erişin\nwslCmd.displayName=WSL kurulumu\nwslCmd.displayDescription=WSL örneklerine wsl CLI istemcisi aracılığıyla erişme\nk8sCmd.displayName=kubectl istemcisi\nk8sCmd.displayDescription=Kubectl aracılığıyla Kubernetes kümelerine erişim\nk8sClusters=Kubernetes kümeleri\nshells=Mevcut kabuklar\ninspectContainer=Kontrol edin\ninspectContext=Kontrol edin\nk8sClusterNameDescription=Kümenin içinde bulunduğu bağlamın adı.\npod=Pod\npodName=Bölme adı\nk8sClusterContext=Bağlam\nk8sClusterContextDescription=Kümenin içinde bulunduğu bağlamın adı\nk8sClusterNamespace=İsim Alanı\nk8sClusterNamespaceDescription=Özel ad alanı veya boşsa varsayılan ad alanı\nk8sConfigLocation=Yapılandırma dosyası\nk8sConfigLocationDescription=Özel kubeconfig dosyası veya boş bırakılırsa varsayılan dosya\ninspectPod=Kontrol edin\nshowAllContainers=Çalışmayan kapsayıcıları göster\nshowAllPods=Çalışmayan podları göster\nk8sPodHostDescription=Pod'un bulunduğu ana bilgisayar\nk8sContainerDescription=Kubernetes konteynerinin adı\nk8sPodDescription=Kubernetes podunun adı\npodDescription=Konteynerin üzerinde bulunduğu bölme\nk8sClusterHostDescription=Kümeye erişilmesi gereken ana bilgisayar. Kümeye erişebilmek için kubectl yüklü ve yapılandırılmış olmalıdır.\nconnection=Bağlantı\nshellCommand.displayName=Özel kabuk komutu\nshellCommand.displayDescription=Özel bir komut aracılığıyla standart bir kabuk açma\nssh.displayName=SSH bağlantısı\nssh.displayDescription=SSH komut satırı istemcisi aracılığıyla uzak bir sisteme bağlanma\nsshConfig.displayName=SSH yapılandırma dosyası\nsshConfig.displayDescription=SSH yapılandırma dosyasında tanımlanan ana bilgisayarlara bağlanma\nsshConfigHost.displayName=SSH yapılandırma dosyası ana bilgisayarı\nsshConfigHost.displayDescription=SSH yapılandırma dosyasında tanımlanan bir ana bilgisayara bağlanma\nsshConfigHost.password=Şifre\nsshConfigHost.passwordDescription=Kullanıcı girişi için isteğe bağlı parolayı girin.\nsshConfigHost.identityPassphrase=Anahtar parolası\nsshConfigHost.identityPassphraseDescription=Anahtarınız için isteğe bağlı parolayı girin.\nshellCommand.hostDescription=Komutun çalıştırılacağı ana bilgisayar\nshellCommand.commandDescription=Bir kabuk açacak komut\ncommandType=Komut türü\ncommandTypeDescription=Komut nasıl çalıştırılır\ncommandDescription=Ana bilgisayarda yürütülecek özel komutlar\ncommandHostDescription=Komutun çalıştırılacağı ana bilgisayar\ncommandDataFlowDescription=Bu komut girdi ve çıktıyı nasıl işler?\ncommandElevationDescription=Bu komutu yükseltilmiş izinlerle çalıştırın\ncommandShellTypeDescription=Bu komut için kullanılacak kabuk\nlimitedSystem=Bu sınırlı veya gömülü bir sistemdir\nlimitedSystemDescription=Sınırlı gömülü sistemler veya IOT cihazları için gerekli olan kabuk türünü belirlemeye çalışmayın\nsshForwardX11=İleri X11\nsshForwardX11Description=Bağlantı için X11 yönlendirmesini etkinleştirir\ncustomAgent=Özel Temsilci\nidentityAgent=Kimlik temsilcisi\nssh.proxyDescription=SSH bağlantısı kurulurken kullanılacak isteğe bağlı proxy ana bilgisayarı. Bir ssh istemcisinin kurulu olması gerekir.\nusage=Kullanım\nwslHostDescription=WSL örneğinin üzerinde bulunduğu ana bilgisayar. Wsl yüklü olmalıdır.\nwslDistributionDescription=WSL örneğinin adı\nwslUsernameDescription=Oturum açılacak açık kullanıcı adı. Belirtilmezse, varsayılan kullanıcı adı kullanılacaktır.\nwslPasswordDescription=Kullanıcının sudo komutları için kullanılabilecek parolası.\ndockerHostDescription=Docker konteynerinin üzerinde bulunduğu ana bilgisayar. Docker yüklü olmalıdır.\ndockerContainerDescription=Docker konteynerinin adı\nlocalMachine=Yerel Makine\nrootScan=Sudo kabuk ortamı\nloginEnvironmentScan=Özel oturum açma ortamı\nk8sScan=Kubernetes kümesi\noptions=Seçenekler\ndockerRunningScan=Docker konteynerlerini çalıştırma\ndockerAllScan=Tüm docker konteynerleri\nwslScan=WSL örnekleri\nsshScan=SSH yapılandırma bağlantıları\nrunAsUser=Kullanıcı olarak çalıştır\nrunAsUserDescription=Bu kabuk ortamını farklı bir kullanıcı olarak başlatın\ndefault=Varsayılan\nadministrator=Yönetici\nwslHost=WSL Ev Sahibi\ntimeout=Zaman Aşımı\ninstallLocation=Kurulum yeri\ninstallLocationDescription=$NAME$ ortamınızın kurulu olduğu konum\nwsl.displayName=Linux için Windows Alt Sistemi\nwsl.displayDescription=Windows üzerinde çalışan bir WSL örneğine bağlanma\ndocker.displayName=Docker Konteyner\ndocker.displayDescription=Bir docker konteynerine bağlanma\nport=Liman\nuser=Kullanıcı\npassword=Şifre\nmethod=Yöntem\nuri=URL\nproxy=Proxy\ndistribution=Dağıtım\nusername=Kullanıcı Adı\nshellType=Kabuk tipi\nbrowseFile=Dosyaya göz at\nopenShell=Terminalde kabuk açın\nopenCommand=Terminalde komut çalıştırma\neditFile=Dosya düzenleme\ndescription=Açıklama\nfurtherCustomization=Daha fazla özelleştirme\nfurtherCustomizationDescription=Daha fazla yapılandırma seçeneği için ssh yapılandırma dosyalarını kullanın\nbrowse=Gözat\nconfigHost=Ev sahibi\nconfigHostDescription=Yapılandırmanın üzerinde bulunduğu ana bilgisayar\nconfigLocation=Konfigürasyon konumu\nconfigLocationDescription=Yapılandırma dosyasının dosya yolu\ngateway=Ağ Geçidi\ngatewayDescription=Bağlanırken kullanılacak isteğe bağlı ağ geçidi\nconnectionInformation=Bağlantı bilgileri\nconnectionInformationDescription=Hangi sisteme bağlanılacağı\npasswordAuthentication=Şifre doğrulama\npasswordAuthenticationDescription=Kimlik doğrulamak için kullanılacak isteğe bağlı parola\nsshConfigString.displayName=Yapılandırma tabanlı SSH bağlantısı\nsshConfigString.displayDescription=SSH yapılandırma biçiminde tamamen özelleştirilmiş bir SSH bağlantısı oluşturun\nsshConfigStringContent=Konfigürasyon\nsshConfigStringContentDescription=OpenSSH yapılandırma biçiminde bağlantı için SSH seçenekleri\nvnc.displayName=SSH üzerinden VNC bağlantısı\nvnc.displayDescription=Tünelli bağlantı üzerinden bir VNC oturumu açma\nbinding=Bağlayıcı\nvncPortDescription=VNC sunucusunun dinlediği bağlantı noktası\nrdpPortDescription=RDP sunucusunun dinlediği bağlantı noktası\nvncUsername=Kullanıcı Adı\nvncUsernameDescription=İsteğe bağlı VNC kullanıcı adı\nvncPassword=Şifre\nvncPasswordDescription=VNC şifresi\nx11WslInstance=X11 İleri WSL örneği\nx11WslInstanceDescription=Bir SSH bağlantısında X11 iletimi kullanılırken X11 sunucusu olarak kullanılacak yerel Linux için Windows Alt Sistemi dağıtımı. Bu dağıtım bir WSL2 dağıtımı olmalıdır.\nopenAsRoot=Kök olarak aç\nopenInWSL=WSL'de Açık\nlaunch=Fırlatma\nsshTrustKeyContent=Ana bilgisayar anahtarı bilinmiyor ve manuel ana bilgisayar anahtarı doğrulamasını etkinleştirdiniz. $CONTENT$\nsshTrustKeyTitle=Bilinmeyen ana bilgisayar anahtarı\nrdpTunnel.displayName=SSH üzerinden RDP bağlantısı\nrdpTunnel.displayDescription=Tünelli bir bağlantı üzerinden RDP ile bağlanma\nrdpEnableDesktopIntegration=Masaüstü entegrasyonunu etkinleştirin\nrdpEnableDesktopIntegrationDescription=RDP izin listesinin buna izin verdiğini varsayarak uzak uygulamaları çalıştırın\nrdpSetupAdminTitle=RDP kurulumu gerekli\nrdpSetupAllowTitle=RDP uzak uygulama\nrdpSetupAllowContent=Uzak uygulamaların doğrudan başlatılmasına şu anda bu sistemde izin verilmemektedir. Etkinleştirmek istiyor musunuz? Bu, RDP uzak uygulamaları için izin listesini devre dışı bırakarak uzak uygulamalarınızı doğrudan XPipe'dan çalıştırmanıza olanak tanır.\nrdpServerEnableTitle=RDP sunucusu\nrdpServerEnableContent=RDP sunucusu hedef sistemde devre dışı bırakılmıştır. Uzak RDP bağlantılarına izin vermek için kayıt defterinde etkinleştirmek istiyor musunuz?\nrdp=RDP\nrdpScan=SSH üzerinden RDP tüneli\nwslX11SetupTitle=WSL X11 kurulumu\nwslX11SetupContent=XPipe, yerel WSL dağıtımınızı bir X11 görüntü sunucusu olarak kullanabilir. X11'i $DIST$ adresinde kurmak ister misiniz? Bu, WSL dağıtımına temel X11 paketlerini yükleyecektir ve biraz zaman alabilir. Ayarlar menüsünden hangi dağıtımın kullanılacağını da değiştirebilirsiniz.\ncommand=Komut\ncommandGroup=Komuta grubu\nvncSystem=VNC hedef sistemi\nvncSystemDescription=Etkileşim kurulacak asıl sistem. Bu genellikle tünel ana bilgisayarıyla aynıdır\nvncHost=Hedef VNC ana bilgisayarı\nvncHostDescription=VNC sunucusunun üzerinde çalıştığı sistem\nvncDirectHost=Ev sahibi\nvncDirectHostDescription=VNC sunucusunun üzerinde çalıştığı sunucunun ana bilgisayar girişi veya manuel adresi\nrdpDirectHost=Ev sahibi\nrdpDirectHostDescription=RDP sunucusunun üzerinde çalıştığı sunucunun ana bilgisayar girişi veya manuel adresi\ngitVaultTitle=Git kasası\ngitVaultForcePushContent=Uzak depoya itmeye zorlamak istiyor musunuz? Bu, geçmiş de dahil olmak üzere tüm uzak depo içeriğini yerel deponuzla tamamen değiştirecektir.\ngitVaultOverwriteLocalContent=Yerel kasa değişikliklerinizi geçersiz kılmak mı istiyorsunuz? Bu, tüm uzak değişiklikleri yerel deponuza uygulayacaktır.\nrdpSimple.displayName=Doğrudan RDP bağlantısı\nrdpSimple.displayDescription=RDP aracılığıyla bir ana bilgisayara bağlanma\nrdpUsername=Kullanıcı Adı\nrdpUsernameDescription=Oturum açılacak kullanıcı. Bir alan adı öneki içerebilir\naddressDescription=Nereye bağlanmalı\nrdpAdditionalOptions=Ek RDP seçenekleri\nrdpAdditionalOptionsDescription=Dahil edilecek ham RDP seçenekleri, .rdp dosyalarında olduğu gibi biçimlendirilir\nproxmoxVncConfirmTitle=VNC erişimi\nproxmoxVncConfirmContent=VM için VNC erişimini etkinleştirmek istiyor musunuz? Bu, VM yapılandırma dosyasında doğrudan VNC istemci erişimini etkinleştirecek ve sanal makineyi yeniden başlatacaktır.\ndockerContext.displayName=Docker bağlamı\ndockerContext.displayDescription=Belirli bir bağlamda bulunan konteynerlerle etkileşim\nvmActions=VM eylemleri\ndockerContextActions=Bağlam eylemleri\nk8sPodActions=Pod eylemleri\nopenVnc=VNC erişimini etkinleştirin\naddVnc=VNC bağlantısı ekleme\ncommandGroup.displayName=Komuta grubu\ncommandGroup.displayDescription=Bir sistem için mevcut komutları gruplama\nserial.displayName=Seri bağlantı\nserial.displayDescription=Terminalde bir seri bağlantı açın\nserialPort=Seri bağlantı noktası\nserialPortDescription=Bağlanılacak seri port / cihaz\nbaudRate=Baud hızı\ndataBits=Veri bitleri\nstopBits=Dur bitleri\nparity=Parite\nflowControlWindow=Akış kontrolü\nserialImplementation=Seri uygulama\nserialImplementationDescription=Seri porta bağlanmak için kullanılacak araç\nserialHost=Ev sahibi\nserialHostDescription=Seri porta erişmek için sistem\nserialPortConfiguration=Seri bağlantı noktası yapılandırması\nserialPortConfigurationDescription=Bağlı seri cihazın konfigürasyon parametreleri\nserialInformation=Seri bilgileri\nopenXShell=XShell'de Aç\ntsh.displayName=Işınlanma\ntsh.displayDescription=Teleport düğümlerinize tsh ile bağlanın\ntshNode.displayName=Işınlanma düğümü\ntshNode.displayDescription=Kümedeki bir ışınlanma düğümüne bağlanma\nteleportCluster=Küme\nteleportClusterDescription=Düğümün içinde bulunduğu küme\nteleportProxy=Proxy\nteleportProxyDescription=Düğüme bağlanmak için kullanılan proxy sunucusu\nteleportHost=Ev sahibi\nteleportHostDescription=Düğümün ana bilgisayar adı\nteleportUser=Kullanıcı\nteleportUserDescription=Oturum açılacak kullanıcı\nlogin=Giriş\nhyperVInstall.displayName=Hyper-V\nhyperVInstall.displayDescription=Hyper-V tarafından yönetilen VM'lere bağlanma\nhyperVVm.displayName=Hyper-V VM\nhyperVVm.displayDescription=SSH veya PSSession aracılığıyla bir Hyper-V sanal makinesine bağlanma\ntrustHost=Güven ev sahibi\ntrustHostDescription=ComputerName'i güvenilir ana bilgisayarlar listesine ekleme\ncopyIp=IP Kopyala\nvncDirect.displayName=Doğrudan VNC bağlantısı\nvncDirect.displayDescription=Doğrudan VNC aracılığıyla bir sisteme bağlanma\neditConfiguration=Yapılandırmayı düzenle\nviewInDashboard=Gösterge tablosunda görüntüle\nsetDefault=Varsayılanı ayarla\nremoveDefault=Varsayılanı kaldır\nconnectAsOtherUser=Diğer kullanıcı olarak bağlan\nprovideUsername=Oturum açmak için alternatif kullanıcı adı sağlayın\nvmIdentity=Misafir kimliği\nvmIdentityDescription=Gerekirse bağlanmak için kullanılacak SSH kimlik doğrulama yöntemi\nvmPort=Liman\nvmPortDescription=SSH aracılığıyla bağlanılacak bağlantı noktası\nforwardAgent=İleri temsilci\nforwardAgentDescription=SSH aracı kimliklerini uzak sistemde kullanılabilir hale getirin\nvirshUri=URI\nvirshUriDescription=Hipervizör URI'si, takma adlar da desteklenir\nvirshDomain.displayName=libvirt etki alanı\nvirshDomain.displayDescription=Bir libvirt etki alanına bağlanma\nvirshHypervisor.displayName=libvirt hipervizörü\nvirshHypervisor.displayDescription=Libvirt destekli bir hipervizör sürücüsüne bağlanma\nvirshInstall.displayName=libvirt komut satırı istemcisi\nvirshInstall.displayDescription=Virsh aracılığıyla mevcut tüm libvirt hipervizörlerine bağlanın\naddHypervisor=Hipervizör ekleme\ninteractiveTerminal=İnteraktif terminal\neditDomain=Etki alanını düzenle\nlibvirt=libvirt etki alanları\ncustomIp=Özel IP\ncustomIpDescription=Gelişmiş ağ kullanıyorsanız varsayılan yerel VM IP algılamasını geçersiz kılın\nautomaticallyDetect=Otomatik olarak algıla\nuserAddDialogTitle=Kullanıcı oluşturma\ngroupAddDialogTitle=Grup oluşturma\npassphrase=Parola\nrepeatPassphrase=Parolayı tekrarla\ngroupSecret=Grup sırrı\nrepeatGroupSecret=Grup sırrını tekrarla\nvaultGroup=Kasa grubu\nloginAlertTitle=Giriş gerekli\nloginAlertHeader=Kişisel bağlantılarınıza erişmek için kasanın kilidini açın\nvaultUser=Kasa kullanıcısı\nme=Ben\naddGroup=Grup ekle ...\naddGroupDescription=Bu kasa için yeni bir grup oluşturun\naddUser=Kullanıcı ekle ...\naddUserDescription=Bu kasa için yeni bir kullanıcı oluşturun\nskip=Atla\nuserChangePasswordAlertTitle=Şifre değişikliği\ngroupChangeSecretAlertTitle=Gizli değişim\ndocs=Dokümantasyon\nlxd.displayName=LXD Konteyner\nlxd.displayDescription=Lxc aracılığıyla bir LXD konteynerine bağlanma\nlxdCmd.displayName=LXD CLI istemcisi\nlxdCmd.displayDescription=LXD kapsayıcılarına lxc CLI istemcisi aracılığıyla erişin\npodman.displayName=Podman Konteyner\npodman.displayDescription=Bir Podman konteynerine bağlanma\nincusInstall.displayName=Incus makine yöneticisi\nincusInstall.displayDescription=Incus konteynerlerine incus CLI istemcisi üzerinden erişim\nincusContainer.displayName=Incus konteyner\nincusContainer.displayDescription=Bir incus konteynerine bağlanın\npodmanCmd.displayName=Podman CLI\npodmanCmd.displayDescription=CLI istemcisi aracılığıyla Podman kapsayıcılarına erişim\nlxdHostDescription=LXD konteynerinin üzerinde bulunduğu ana bilgisayar. Lxc yüklü olmalıdır.\nlxdContainerDescription=LXD konteynerinin adı\npodmanContainers=Podman konteynerleri\nlxdContainers=LXD konteynerleri\nincusContainers=Incus konteynerleri\ncontainer=Konteyner\nhost=Ev sahibi\ncontainerActions=Konteyner eylemleri\nserialConsole=Seri konsol\neditRunConfiguration=Çalıştırma yapılandırmasını düzenleme\ncommunityDescription=Kişisel kullanım durumlarınız için mükemmel bir bağlantı güç aracı.\nupgradeDescription=Tüm sunucu altyapınız için profesyonel bağlantı yönetimi.\ndiscoverPlans=Yükseltme seçeneklerini keşfedin\nextendProfessional=En son profesyonel özelliklere yükseltme\ncommunityItem1=Ticari olmayan sistemlere ve araçlara sınırsız bağlantı\ncommunityItem2=Kurulu terminalleriniz ve editörlerinizle sorunsuz entegrasyon\ncommunityItem3=Tam özellikli uzak dosya tarayıcısı\ncommunityItem4=Tüm kabuklar için güçlü komut dosyası sistemi\ncommunityItem5=Senkronizasyon ve bağlantı bilgilerinin paylaşımı için Git entegrasyonu\nupgradeItem1=Tüm topluluk sürümü özelliklerini içerir\nupgradeItem2=Homelab planı sınırsız hipervizör ve gelişmiş SSH özelliklerini destekler\nupgradeItem3=Profesyonel plan ayrıca kurumsal işletim sistemlerini ve araçlarını da destekler\nupgradeItem4=Kurumsal plan, bireysel kullanım durumunuz için tam esneklikle birlikte gelir\nupgrade=Yükseltme\nupgradeTitle=Mevcut planlar\nstatus=Durum\ntype=Tip\nlicenseAlertTitle=Lisans gerekli\nuseCommunity=Topluluk ile devam edin\npreviewDescription=Yayınlandıktan sonra birkaç hafta boyunca yeni özellikleri deneyin.\ntryPreview=Önizlemeyi etkinleştir\npreviewItem1=Piyasaya sürüldükten sonra 2 hafta boyunca yeni çıkan profesyonel özelliklere tam erişim\npreviewItem2=Herhangi bir taahhütte bulunmadan yeni özellikleri deneyin\nlicensedTo=Lisanslı\nemail=E-posta adresi\napply=Başvurmak\nclear=Temiz\nactivate=Etkinleştir\nvalidUntil=Şu tarihe kadar geçerlidir\nlicenseActivated=Lisans etkinleştirildi\nrestart=Yeniden Başlat\nlockVault=Kilit kasası\nrestartApp=XPipe'ı yeniden başlatın\nfree=Ücretsiz\nupgradeInfo=Bir lisansa yükseltme ile ilgili bilgileri aşağıda bulabilirsiniz.\nupgradeInfoPreview=Aşağıda lisansa yükseltme hakkında bilgi bulabilir veya önizlemeyi deneyebilirsiniz.\nenterLicenseKey=Yükseltmek için lisans anahtarını girin\nisOnlySupported=yalnızca en az $TYPE$ lisansı ile desteklenir\nareOnlySupported=yalnızca en az bir $TYPE$ lisansı ile desteklenir\nlegacyLicense=Bu lisans yalnızca satın alma işleminden sonraki bir yıl içinde yayınlanan yeni Profesyonel özellikleri içerir.\npreviewExpiredLicense=Bu özellik kısa süre önce önizleme aşamasında ücretsiz olarak kullanıma sunulmuştu, ancak bu süre artık sona erdi.\nopenApiDocs=API belgeleri\nopenApiDocsDescription=HTTP API belgeleri, OpenAPI .yaml spesifikasyonu da dahil olmak üzere çevrimiçi olarak mevcuttur. Web tarayıcınızda veya tercih ettiğiniz HTTP istemcisinde açabilirsiniz.\nopenApiDocsButton=Açık dokümanlar\npythonApi=Python API\npersonalConnection=Bu bağlantı ve tüm çocukları, kişisel bir kimliğe bağlı oldukları için yalnızca kullanıcınız tarafından kullanılabilir.\ndeveloperPrintInitFiles=Init dosyası yürütmesini yazdır\ndeveloperPrintInitFilesDescription=Bir terminal başlatıldığında çalıştırılan tüm kabuk başlangıç betiklerini yazdırır.\ndeveloperShowSensitiveCommands=Hassas komutları günlüğe kaydetme\ndeveloperShowSensitiveCommandsDescription=Hata ayıklama için hassas komutları günlük çıktısına dahil edin.\ncheckingForUpdates=Güncellemeleri kontrol etme\ncheckingForUpdatesDescription=En son sürüm bilgilerini getirme\ndownloadingUpdate=Serbest bırakma alınıyor (Sürüm $VERSION$)\ndownloadingUpdateDescription=Sürüm paketini indirme\nupdateNag=XPipe'ı bir süredir güncellemediniz. Yeni sürümlerin yeni özelliklerini ve düzeltmelerini kaçırıyor olabilirsiniz.\nupdateNagTitle=Güncelleme hatırlatması\nupdateNagButton=Yayınlara bakın\nrefreshServices=Yenileme hizmetleri\nserviceProtocolType=Hizmet protokolü türü\nserviceProtocolTypeDescription=Hizmetin nasıl açılacağını kontrol edin\nserviceCommand=Hizmet etkin olduğunda çalıştırılacak komut\nserviceCommandDescription=PORT yer tutucusu gerçek tünellenmiş yerel port ile değiştirilecektir\nvalue=Değer\nshowAdvancedOptions=Gelişmiş seçenekleri göster\nsshAdditionalConfigOptions=Ek yapılandırma seçenekleri\nremoteFileManager=Uzak dosya yöneticisi\nclearUserData=Kullanıcı verilerini silme\nclearUserDataDescription=Bağlantılar da dahil olmak üzere tüm kullanıcı yapılandırma verilerini silme\nclearUserDataTitle=Kullanıcı verilerinin silinmesi\nclearUserDataContent=Bu, xpipe için tüm yerel kullanıcı verilerini silecek ve yeniden başlatacaktır. Bağlantılarınızı önemsiyorsanız, önce bir git deposu ile senkronize ettiğinizden emin olun.\nundefined=Tanımsız\ncopyAddress=Adres kopyalayın\nnetbirdDeviceScan=Netbird bağlantıları\nnetbirdId=Eş açık anahtar\nnetbirdIdDescription=Eşin dahili netbird genel anahtar kimliği\ntailscaleDeviceScan=Kuyruk ölçeği bağlantıları\ntailscaleInstall.displayName=Kuyruk ölçeği kurulumu\ntailscaleInstall.displayDescription=Tailnet'inizdeki cihazlara SSH ile bağlanın\ntailscaleDevice.displayName=Kuyruk ölçeği cihazı\ntailscaleDevice.displayDescription=SSH aracılığıyla tailnet'inizdeki bir cihaza bağlanın\ntailscaleId=Cihaz Kimliği\ntailscaleIdDescription=Dahili kuyruk ölçeği cihaz kimliği\ntailscaleHostName=Ana bilgisayar adı\ntailscaleHostNameDescription=Kuyruk ağındaki cihazın ana bilgisayar adı\ntailscaleUsername=Kullanıcı Adı\ntailscaleUsernameDescription=Oturum açılacak kullanıcı\ntailscalePassword=Şifre\ntailscalePasswordDescription=Sudo için kullanılabilecek isteğe bağlı kullanıcı parolası\nscriptName=Komut dosyası adı\nscriptNameDescription=Bu komut dosyasına özel bir ad verin\nscriptGroupName=Komut dosyası grup adı\nscriptGroupNameDescription=Bu kod grubuna özel bir ad verin\nidentityName=Kimlik adı\nidentityNameDescription=Bu kimliğe özel bir ad verin\ntailscaleTailnet.displayName=Tailnet\ntailscaleTailnet.displayDescription=Hesabınızla belirli bir kuyruk ağına bağlanın\nputtyConnections=PuTTY bağlantıları\nkittyConnections=KiTTY bağlantıları\nicons=Simgeler\ncustomIcons=Özel simgeler\niconSources=Simge kaynakları\niconSourcesDescription=Simgeler için kendi kaynaklarınızı buraya ekleyebilirsiniz. XPipe, eklenen konumdaki tüm .svg dosyalarını alır ve bunları mevcut simgeler kümesine ekler.\\n\\nHem yerel dizinler hem de uzak git depoları veya simge konumları olarak desteklenir.\nrefreshSources=Yenileme simgeleri\nrefreshSourcesDescription=Mevcut kaynaklardan tüm simgeleri güncelleyin\naddDirectoryIconSource=Dizin kaynağı ekle ...\naddDirectoryIconSourceDescription=Yerel bir dizinden simgeler ekleme\naddGitIconSource=Git kaynağı ekle ...\naddGitIconSourceDescription=Uzak bir git deposunda bulunan simgeleri ekleme\nrepositoryUrl=Git Depo URL'si\niconDirectory=Simge dizini\naddUnsupportedKexMethod=Desteklenmeyen anahtar değişim yöntemi ekleme\naddUnsupportedKexMethodDescription=Bu bağlantı için $VAL$ anahtar değişim yönteminin kullanılmasına izin verin\naddUnsupportedHostKeyType=Desteklenmeyen ana bilgisayar anahtar türü ekleme\naddUnsupportedHostKeyTypeDescription=Bu bağlantı için $VAL$ ana bilgisayar anahtar türünün kullanılmasına izin verin\naddUnsupportedMacType=Desteklenmeyen MAC türü ekle\naddUnsupportedMacTypeDescription=Bu bağlantı için $VAL$ MAC türünün kullanılmasına izin verin\nrunSilent=sessizce arka planda\nrunInFileBrowser=dosya tarayıcısında\nrunInConnectionHub=bağlantı merkezinde\ncommandOutput=Komut çıktısı\niconSourceDeletionTitle=Simge kaynağını sil\niconSourceDeletionContent=Bu simge kaynağını ve onunla ilişkili tüm simgeleri silmek istiyor musunuz?\nrefreshIcons=Yenileme simgeleri\nrefreshIconsDescription=Mevcut tüm 1000'den fazla simgeyi harici kaynaklardan .png dosyalarına alma, işleme ve önbelleğe alma. Bu biraz zaman alabilir ...\nvaultUserLegacy=Vault kullanıcısı (Sınırlı eski uyumluluk modu)\nupgradeInstructions=Yükseltme talimatları\nexternalActionTitle=Harici eylem talebi\nexternalActionContent=Harici bir eylem talep edildi. XPipe dışından eylemlerin başlatılmasına izin vermek istiyor musunuz?\nnoScriptStateAvailable=Komut dosyası uyumluluğunu belirlemek için yenileyin ...\ndocumentationDescription=Belgelere göz atın\ncustomEditorCommandInTerminal=Terminalde özel komut çalıştırma\ncustomEditorCommandInTerminalDescription=Düzenleyiciniz terminal tabanlı ise, otomatik olarak bir terminal açmak ve bunun yerine komutu terminal oturumunda çalıştırmak için bu seçeneği etkinleştirebilirsiniz.\\n\\nBu seçeneği vi, vim, nvim ve diğerleri gibi editörler için kullanabilirsiniz.\ndisableHttpsTlsCheck=HTTPS istek sertifikası doğrulamasını devre dışı bırakma\ndisableHttpsTlsCheckDescription=Kuruluşunuz güvenlik duvarlarındaki HTTPS trafiğinizin şifresini SSL müdahalesi kullanarak çözüyorsa, sertifikaların eşleşmemesi nedeniyle tüm güncelleme kontrolleri veya lisans kontrolleri başarısız olacaktır. Bu seçeneği etkinleştirerek ve TLS sertifika doğrulamasını devre dışı bırakarak bunu düzeltebilirsiniz.\nconnectionsSelected=$NUMBER$ bağlantılar seçildi\naddConnections=Bağlantı ekleme\nbrowseDirectory=Dizine göz atın\nopenTerminal=Açık terminal\ndocumentation=Dokümantasyon\nreport=Hata bildir\nkeePassXcNotAssociated=KeePassXC bağlantısı\nkeePassXcNotAssociatedDescription=XPipe yerel KeePassXC veritabanınızla ilişkilendirilmemiştir. XPipe'ın parolaları sorgulayabilmesi için XPipe'ı KeePassXC veritabanı ile ilişkilendirmeye yönelik tek seferlik adımı gerçekleştirmek için aşağıya tıklayın.\nkeePassXcAssociateMore=Daha fazla veritabanı bağlayın\nkeePassXcAssociateMoreDescription=Aynı anda birden fazla KeePassXC veritabanına bağlanabilirsiniz\nkeePassXcAssociated=KeePassXC bağlantıları\nkeePassXcAssociatedDescription=XPipe aşağıdaki yerel KeePassXC veritabanlarına bağlıdır:\nkeePassXcNotAssociatedButton=Bağlantı veritabanı\nidentifier=Tanımlayıcı\npasswordManagerCommand=Özel komut\npasswordManagerCommandDescription=Parolaları almak için çalıştırılacak özel komut. Yer tutucu dize $KEY, çağrıldığında alıntılanan parola anahtarıyla değiştirilecektir. Bu, parolayı stdout'a yazdırmak için parola yöneticinizin CLI'sini çağırmalıdır, örneğin mypassmgr get $KEY.\nchooseTemplate=Şablon seçin\nkeePassXcPlaceholder=KeePassXC giriş URL'si\nterminalEnvironment=Terminal ortamı\nterminalEnvironmentDescription=Terminal özelleştirmeniz için yerel bir Linux tabanlı WSL ortamının özelliklerini kullanmak istemeniz durumunda, bunları terminal ortamı olarak kullanabilirsiniz.\\n\\nTüm özel terminal başlatma komutları ve terminal çoklayıcı yapılandırması bu WSL dağıtımında çalıştırılacaktır.\nterminalInitScript=Terminal başlangıç betiği\nterminalInitScriptDescription=Bağlantı başlatılmadan önce terminal ortamında çalıştırılacak komutlar. Başlangıçta terminal ortamını yapılandırmak için bunu kullanabilirsiniz.\nterminalMultiplexer=Terminal çoklayıcı\nterminalMultiplexerDescription=Bir terminaldeki sekmelere alternatif olarak kullanılacak terminal çoklayıcısı. Bu, sekme işleme gibi belirli terminal işleme özelliklerini çoklayıcı işlevselliği ile değiştirecektir.\\n\\nİlgili çoklayıcı yürütülebilir dosyasının sistemde yüklü olmasını gerektirir.\nterminalMultiplexerWindowsDescription=Bir terminaldeki sekmelere alternatif olarak kullanılacak terminal çoklayıcısı. Bu, sekme işleme gibi belirli terminal işleme özelliklerini çoklayıcı işlevselliği ile değiştirecektir.\\n\\nWindows üzerinde bir WSL terminal ortamının kullanılmasını ve WSL sistemine çoklayıcı çalıştırılabilir dosyasının yüklenmesini gerektirir.\nterminalAlwaysPauseOnExit=Çıkışta her zaman duraklat\nterminalAlwaysPauseOnExitDescription=Etkinleştirildiğinde, bir terminal oturumundan çıkarken her zaman oturumu yeniden başlatmanız ya da kapatmanız istenir. Devre dışı bırakılırsa, XPipe bunu yalnızca bir hata ile çıkan başarısız bağlantılar için yapar.\nquerying=Sorgulama ...\nretrievedPassword=Elde edildi: $PASSWORD$\nrefreshOpenpubkey=Openpubkey kimliğini yenileyin\nrefreshOpenpubkeyDescription=Openpubkey kimliğini tekrar geçerli kılmak için opkssh refresh'i çalıştırın\nall=Tümü\nterminalPrompt=Terminal istemi\nterminalPromptDescription=Uzak terminallerinizde kullanılacak terminal komut istemi aracı. Bir terminal isteminin etkinleştirilmesi, bir terminal oturumu açıldığında hedef sistemdeki istem aracını otomatik olarak kuracak ve yapılandıracaktır.\\n\\nBu, bir sistemdeki mevcut istem yapılandırmalarını veya profil dosyalarını değiştirmez. Bu, istem uzak sistemde kurulurken ilk kez terminal yükleme süresini artıracaktır. Terminaliniz istemi doğru şekilde görüntülemek için ek yazı tiplerine ihtiyaç duyabilir.\nterminalPromptConfiguration=Terminal istemi yapılandırması\nterminalPromptConfig=Yapılandırma dosyası\nterminalPromptConfigDescription=Komut istemine uygulanacak özel yapılandırma dosyası. Bu yapılandırma, terminal başlatıldığında hedef sistemde otomatik olarak kurulacak ve varsayılan istem yapılandırması olarak kullanılacaktır.\\n\\nHer sistemde mevcut varsayılan yapılandırma dosyasını kullanmak istiyorsanız, bu alanı boş bırakabilirsiniz.\npasswordManagerKey=Parola yöneticisi anahtarı\npasswordManagerKeyDescription=Sırrın parola yöneticisi tanımlayıcısı\npasswordManagerAgent=Parola yöneticisi aracı\ndockerComposeProject.displayName=Docker compose projesi\ndockerComposeProject.displayDescription=Bir compose projesinin kapsayıcılarını birlikte gruplama\nsshVerboseOutput=Ayrıntılı SSH çıktısını etkinleştirme\nsshVerboseOutputDescription=Bu, SSH üzerinden bağlanırken birçok hata ayıklama bilgisi yazdıracaktır. SSH bağlantıları ile ilgili sorunları gidermek için kullanışlıdır.\ndontUseGateway=Ağ geçidi kullanmayın\ndontUseGatewayDescription=Hipervizör ana bilgisayarını ağ geçidi olarak kullanmayın ve doğrudan IP'ye bağlanın\ncategoryColor=Kategori rengi\ncategoryColorDescription=Bu kategorideki bağlantılar için kullanılacak varsayılan renk\ncategorySync=Git deposu ile senkronize et\ncategorySyncDescription=Tüm bağlantıları git deposu ile otomatik olarak senkronize edin. Bağlantılardaki tüm yerel değişiklikler uzağa itilecektir.\ncategorySyncSpecial=Git deposu ile senkronize et\\n(Özel kategori \"$NAME$\" için yapılandırılamaz)\ncategoryDontAllowScripts=Tüm değişiklikleri devre dışı bırak\ncategoryDontAllowScriptsDescription=Herhangi bir değişikliği önlemek için bu kategorideki sistemlerde komut yürütmeyi ve diğer işlemleri devre dışı bırakın. Bu, tüm komut dosyası işlevlerini, kabuk ortamı komutlarını, istemleri ve daha fazlasını devre dışı bırakacaktır.\ncategoryConfirmAllModifications=Tüm değişiklikleri onaylayın\ncategoryConfirmAllModificationsDescription=Önce bir bağlantı veya dosya sistemi için her türlü değişikliği onaylayın. Bu, önemli sistemler üzerinde yanlışlıkla işlem yapılmasını önleyebilir.\ncategoryDefaultIdentity=Varsayılan kimlik\ncategoryDefaultIdentityDescription=Bu kategorideki sistemlerin çoğunda belirli bir kimliği sıklıkla kullanıyorsanız, varsayılan bir kimlik ayarlamak yeni bağlantılar oluştururken bu kimliği önceden seçmenize olanak tanır.\ncategoryConfigTitle=$NAME$ yapılandırma\nconfigure=Yapılandırma\naddConnection=Bağlantı ekle\nnoCompatibleConnection=Uyumlu bağlantı bulunamadı\nnoCompatibleIdentity=Uyumlu kimlik bulunamadı\nnewCategory=Yeni kategori\ndockerComposeRestricted=Compose projesi $NAME$ tarafından kısıtlanmıştır ve harici olarak değiştirilemez. Bu compose projesini yönetmek için lütfen $NAME$ adresini kullanın.\nrestricted=Kısıtlı\ndisableSshPinCaching=SSH PIN önbelleğe almayı devre dışı bırakma\ndisableSshPinCachingDescription=XPipe, bir tür donanım tabanlı kimlik doğrulama kullanırken bir anahtar için girilen PIN'leri otomatik olarak önbelleğe alacaktır.\\n\\nBunun devre dışı bırakılması, her bağlantı denemesinde PIN kodunun yeniden girilmesine neden olacaktır.\ngitSyncPull=Uzak git değişikliklerini senkronize etmek için çekin\nenpassVaultFile=Kasa dosyası\nenpassVaultFileDescription=Yerel Enpass kasa dosyası.\nflat=Düz\nrecursive=Yinelemeli\nrdpAllowListBlocked=Seçilen RemoteApp, sunucu için RDP izin verilenler listesine dahil edilmemiş gibi görünüyor.\npsonoServerUrl=Sunucu URL'si\npsonoServerUrlDescription=Psono arka uç sunucusunun URL'si\npsonoApiKey=API Anahtarı\npsonoApiKeyDescription=Kullanılacak API anahtarı, uuid olarak biçimlendirilmiş\npsonoApiSecretKey=API gizli anahtarı\npsonoApiSecretKeyDescription=API gizli anahtarı 64 baytlık hex dizesi olarak\npassboltServerUrl=Sunucu URL'si\npassboltServerUrlDescription=Passbolt arka uç sunucusunun URL'si\npassboltPassphrase=Parola\npassboltPassphraseDescription=Kasa özel anahtarı için parola\npassboltPrivateKey=Özel anahtar\npassboltPrivateKeyDescription=Kasa için özel gpg anahtar dosyası\nfocusWindowOnNotifications=Bildirimlere odaklanma penceresi\nfocusWindowOnNotificationsDescription=Bir bildirim veya hata mesajı gösterildiğinde, örneğin bir bağlantı veya tünel beklenmedik bir şekilde sonlandırıldığında XPipe'ı ön plana getirin.\ngitUsername=Özel git kullanıcı adı\ngitUsernameDescription=Git uzak deposuna kimlik doğrulaması yapmak için özel kullanıcı. Varsayılan olarak, XPipe git CLI'nin o anda yapılandırılmış kimlik bilgilerini kullanacaktır.\\n\\nBu ayar, yerel git CLI istemciniz için zaten yapılandırılmış olan varsayılan kimlik bilgilerini geçersiz kılacaktır.\ngitPassword=Özel git parolası / kişisel erişim belirteci\ngitPasswordDescription=Kimlik doğrulamak için kullanılacak parola veya kişisel erişim belirteci. Parola veya kişisel erişim belirtecine ihtiyacınız olup olmadığı git uzak sağlayıcısına bağlıdır. Bu ayar, yerel git CLI istemciniz için zaten yapılandırılmış olan varsayılan kimlik bilgilerini geçersiz kılacaktır.\nsetReadOnly=Salt okunur olarak ayarla\nunsetReadOnly=Ayarlanmamış salt okunur\nreadOnlyStoreError=Bu girişin yapılandırması dondurulmuştur. Değişikliklerinizi yeni bir kopyaya kaydetmek için farklı bir ad seçin.\ncategoryFreeze=Bağlantı konfigürasyonlarını dondurma\ncategoryFreezeDescription=Bağlantı konfigürasyonlarını salt okunur olarak işaretler. Bu, bu kategorideki mevcut hiçbir bağlantı girişi yapılandırmasının değiştirilemeyeceği anlamına gelir. Ancak yeni bağlantılar eklenebilir.\nupdateFail=Güncelleme yüklemesi başarılı olmadı\nupdateFailAction=Güncellemeyi manuel olarak yükleme\nupdateFailActionDescription=GitHub'daki en son sürümlere göz atın\nonePasswordPlaceholder=Öğe adı veya op:// URL\ncomputeDirectorySizes=Dizin boyutlarını hesaplama\ncomputeSize=Boyut hesaplama\ncustomSpiceCommand=Özel komut\ncustomSpiceCommandDescription=SPICE oturumlarını başlatmak için yürütülecek özel komut. Yer tutucu dize $FILE, çağrıldığında .vv dosyasına giden tırnak içindeki dosya yolu ile değiştirilecektir.\nvncClient=VNC istemcisi\nvncClientDescription=XPipe'da VNC bağlantıları açılırken başlatılacak VNC istemcisi.\\n\\nXPipe içindeki entegre VNC istemcisini kullanma veya daha fazla özelleştirme arıyorsanız alternatif olarak yerel olarak yüklenmiş harici bir VNC istemcisi başlatma seçeneğiniz vardır.\nintegratedXPipeVncClient=Entegre XPipe VNC istemcisi\ncustomVncCommand=Özel komut\ncustomVncCommandDescription=VNC oturumlarını başlatmak için yürütülecek özel komut. Yer tutucu dize $ADDRESS, çağrıldığında alıntılanan adresle değiştirilecektir.\nvncConnections=VNC bağlantıları\npasswordManagerIdentity=Parola yöneticisi kimliği\npasswordManagerIdentity.displayName=Parola yöneticisi kimliği\npasswordManagerIdentity.displayDescription=Parola yöneticinizden bir kimliğin kullanıcı adını ve parolasını alma\npasswordCopied=Bağlantı şifresi panoya kopyalandı\nerrorOccurred=Hata oluştu\nactionMacro.displayName=Eylem makrosu\nactionMacro.displayDescription=Özelleştirilmiş tetikleyicileri kullanarak harekete geçin\nmacroAdd=Makro ekle\nmacroName=Makro adı\nmacroNameDescription=Bu makroya özel bir ad verin\nactionId=Eylem Kimliği\nactionIdDescription=Bu makro ile çalıştırılacak eylem\nmacroRefs=İlişkili bağlantılar\nmacroRefsDescription=Eylemin çalıştırılacağı bağlantılar\nconnectionCopy=Anlaşıldı\nactionPickerTitle=Eylemi seçin\nactionPickerDescription=Bir eylem gerçekleştirmek için bir şeye tıklayın. Eylemi yürütmek yerine, eylem kısayolu seçme modunda eylem için kısayollar oluşturabilir ve düzenleyebilirsiniz.\ncancelActionPicker=Eylem seçimini iptal et\nactionShortcut=Eylem kısayolu\nactionShortcuts=Eylem kısayolları\nactionStore=Eylem mağazası\nactionStoreDescription=Eylemin üzerinde çalıştırılacağı mağaza girişi\nactionStores=Eylem mağazaları\nactionStoresDescription=Eylemin üzerinde çalıştırılacağı mağaza girdileri\nactionDesktopShortcut=Masaüstü kısayolu\nactionDesktopShortcutDescription=Masaüstünüzde bu eylem için bir kısayol oluşturun\nactionUrlShortcut=URL kısayolu\nactionUrlShortcutDescription=Açıldığında bu eylemleri tetikleyebilecek bir URL kopyalayın\nactionUrlShortcutDisabled=URL kısayolu (Kullanılamıyor)\nactionUrlShortcutDisabledDescription=$TYPE$ yükleme türü URL'lerin açılmasını desteklemez\nactionApiCall=API isteği\nactionApiCallDescription=Bu eylemi HTTP API'sinden çağırın\nactionMacro=Eylem makrosu\nactionMacroDescription=Bu eylem için gelişmiş işlevselliğe sahip bir makro oluşturun\ncreateMacro=Makro oluşturun\nactionConfiguration=Parametreler\nactionConfigurationDescription=Yürütülen eyleme aktarılacak parametreler\nconfirmAction=Eylemi onaylayın\nactionConnections=Eylem bağlantıları\nactionConnectionsDescription=Eylemin üzerinde çalıştırılacağı bağlantılar\nactionConnection=Eylem bağlantısı\nactionConnectionDescription=Eylemin çalıştırılacağı bağlantı\nappleContainerInstall.displayName=Elma kapları\nappleContainerInstall.displayDescription=Apple konteyner örneklerine konteyner CLI aracılığıyla erişim\nappleContainer.displayName=Elma kabı\nappleContainer.displayDescription=Apple konteyner örneklerine konteyner CLI aracılığıyla erişim\nappleContainerHostDescription=Apple konteynerinin üzerinde bulunduğu ana bilgisayar\nappleContainerDescription=Elma kabının adı\nappleContainers=Elma kapları\nchangeOrderIndexTitle=Değişiklik emri\norderIndex=Dizin\norderIndexDescription=Bu girdiyi diğerlerine göre sıralamak için açık indeks. En düşük indeksler üstte, en yüksekler altta gösterilir\nmoveToFirst=Birinci sıraya geç\nmoveToLast=Sona doğru ilerle\ncategory=Kategori\nincludeRoot=Kökü dahil et\nexcludeRoot=Kökü hariç tut\nfreezeConfiguration=Dondurma yapılandırması\nunfreezeConfiguration=Yapılandırmayı çöz\nwaylandScalingTitle=Wayland ölçekleme\nactionApiUrl=$URL$ (Json gövdesini kopyala)\ncopyBody=İstek gövdesini kopyala\ngitRepoTerminalOpen=Depoyu terminalde açın\ngitRepoTerminalOpenDescription=Komut satırını kullanarak depoya kendiniz bir göz atın\ngitRepoOverwriteLocal=Yerel deponun üzerine yaz\ngitRepoOverwriteLocalDescription=Tüm yerel değişiklikleri uzaktaki değişikliklerle değiştirin\ngitRepoForcePush=Uzak deponun üzerine yaz\ngitRepoForcePushDescription=Yerel değişikliklerinizi uzağa uygulamak için git push --force kullanın\ngitRepoDontWarn=Artık uyarma\ngitRepoDontWarnDescription=Bu bekleniyorsa, XPipe'ın gelecekte bu hatayı görmezden gelmesini sağlayın\ngitRepoTryAgain=Tekrar deneyin\ngitRepoTryAgainDescription=Aynı işlemi tekrar deneyin\ngitRepoEnablePlain=Düz dizin senkronizasyonu kullanın\ngitRepoEnablePlainDescription=Değişiklikleri dizinle senkronize etmek için bir git deposunu başlatma\ngitRepoCreateBare=Git sync kullanın\ngitRepoCreateBareDescription=Senkronizasyon dizininde yeni bir çıplak git deposu başlatma\ngitRepoDisable=Şimdilik git vault'u devre dışı bırakın\ngitRepoDisableDescription=Bu oturum sırasında herhangi bir değişiklik yapmayın\ngitRepoPullRefresh=Değişiklikleri çekin ve yenileyin\ngitRepoPullRefreshDescription=Uzaktan değişiklikleri birleştirme ve verileri yeniden yükleme\nbreakOutCategory=Kategori ayrımı\nmergeCategory=Kategori birleştirme\nopenWinScp=WinSCP'de açın\nuninstallApplication=Kaldırma\nuninstallApplicationDescription=XPipe'ı tamamen kaldırmak için .pkg kurulum betiğini çalıştırır\nk8sEditPodTitle=Değişiklikleri uygula\nk8sEditPodContent=Kubectl apply komutu ile yapılan değişiklikleri uygulamak istiyor musunuz? Değişikliklerin uygulanması için muhtemelen yeniden başlatma gerekir.\nvirshEditDomainTitle=Değişiklikleri uygula\nvirshEditDomainContent=Değişiklikleri etki alanına uygulamak istiyor musunuz? Değişikliklerin uygulanması için muhtemelen yeniden başlatma gerekir.\npkcs11Library=PKCS#11 kütüphanesi\npkcs11LibraryDescription=Dinamik olarak bağlı kütüphane dosyasının yolu\nsshAgentSocket=Özel SSH aracı soketi\nsshAgentSocketDescription=SSH aracısı ile iletişim kurmak için kullanılacak özel soket. Bu özel aracı, kendisi için özel aracı seçeneği seçilerek bir bağlantı için kullanılabilir.\npublicKey=Açık anahtar tanımlayıcısı\npublicKeyDescription=Aracıları yalnızca eşleşen özel anahtarı sunmaya zorlamak için isteğe bağlı açık anahtar\nactions=Eylemler\nhcloudServer.displayName=Hetzner bulut sunucusu\nhcloudServer.displayDescription=SSH aracılığıyla Hetzner bulutunda barındırılan bir sunucuya erişin\nhcloudInstall.displayName=Hetzner Bulut CLI\nhcloudInstall.displayDescription=Hetzner bulutunda barındırılan sunuculara hcloud üzerinden erişin\nhcloudContext.displayName=hcloud bağlamı\nhcloudContext.displayDescription=Bir hcloud bağlamının sunucularına erişim\nmetrics=Metrikler\nopenInVsCode=VsCode'da açın\naddCloud=Bulut ...\nhcloudToken=hcloud belirteci\nhcloudTokenDescription=Kullanılacak Hetzner bulut belirteci. Daha fazla bilgi için belgelere bakın\nhcloudLogin=Hetzner bulut girişi\nclearHcloudToken=Hcloud belirtecini temizle\nclearHcloudTokenDescription=Tekrar oturum açabilmeniz için mevcut belirteci silin\nselectIdentity=Kimlik seçin\nenableMcpServer=MCP sunucusunu etkinleştirin\nenableMcpServerDescription=XPipe MCP sunucusunu etkinleştirerek harici MCP istemcilerinin MCP sunucusuna istek göndermesini sağlar. Yapılandırma ayrıntıları için aşağıya bakın.\\n\\nMCP işlevselliği için HTTP API'sinin etkinleştirilmesi gerekmediğini unutmayın.\nenableMcpMutationTools=MCP mutasyon araçlarını etkinleştirin\nenableMcpMutationToolsDescription=Varsayılan olarak, MCP sunucusunda yalnızca salt okunur araçlar etkinleştirilir. Bu, bir sistemi potansiyel olarak değiştirecek kazara işlemlerin yapılmamasını sağlamak içindir.\\n\\nMCP istemcileri aracılığıyla sistemlerde değişiklik yapmayı planlıyorsanız, bu seçeneği etkinleştirmeden önce MCP istemcinizin potansiyel olarak yıkıcı eylemleri onaylamak üzere yapılandırıldığından emin olun. Uygulanması için tüm MCP istemcilerinin yeniden bağlanmasını gerektirir.\nmcpClientConfigurationDetails=MCP istemci yapılandırması\nmcpClientConfigurationDetailsDescription=Seçtiğiniz MCP istemcisinden XPipe MCP sunucusuna bağlanmak için bu yapılandırma verilerini kullanın.\nswitchHostAddress=Ana bilgisayar adresini değiştirme\naddAnotherHostName=Başka bir ana bilgisayar adı ekleyin\naddNetwork=Ağ taraması ...\nnetworkScan=Ağ taraması\nnetworkScanStore=Hedef ev sahibi\nnetworkScanStoreDescription=Yerel ağın taranacağı ana bilgisayar\nuseAsGateway=Ana bilgisayarı ağ geçidi olarak kullanma\nuseAsGatewayDescription=Hedef ana bilgisayarın oluşturulan bağlantılar için bir ağ geçidi olarak kullanılıp kullanılmayacağı\nnetworkScanPorts=Taranacak bağlantı noktaları\nnetworkScanPortsDescription=Taramaya dahil edilecek bağlantı noktalarının virgülle ayrılmış listesi\nnetworkScanType=Bağlantı türü\nnetworkScanTypeDescription=Aranacak sunucu türleri\nemptyDirectory=Bu dizin boş görünüyor\nhcloudConfigFile=hcloud yapılandırma dosyası\nhcloudConfigFileDescription=Hcloud CLI .toml yapılandırma dosyasının konumu\npreferMonochromeIcons=Tek renkli simgeleri tercih edin\npreferMonochromeIconsDescription=Etkinleştirildiğinde, bir kaynaktan gelen bir simge için ayrı bir açık veya koyu mod simge değişkeninin mevcut olduğu varsayılarak, tek renkli simge değişkenleri bir simgenin varsayılan renkli sürümlerine tercih edilecektir.\\n\\nUygulanacak simgelerin yenilenmesini gerektirir.\nalwaysShowSshMotd=Her zaman MOTD'yi göster\nalwaysShowSshMotdDescription=Yeni bir terminal oturumunda oturum açıldığında uzak bir sistemde yapılandırılan günün mesajının gösterilip gösterilmeyeceği. Bunu değiştirmenin SSH bağlantılarının başlatma davranışını değiştirebileceğini unutmayın.\nmanageSubscription=Aboneliği yönet\nnoListeningServer=Dinleme sunucusu yok\nnetworkScanResults=Tarama sonuçları\nnetworkScanResultsDescription=Ağda bulunan sistemlerin listesi\nlocalShellDialect=Yerel kabuk\nlocalShellDialectDescription=Yerel işlemler için kullanılan kabuk. Normal yerel varsayılan kabuğun devre dışı bırakılması veya bir dereceye kadar bozulması durumunda, bu seçenek başka bir alternatife geri dönmek için kullanılabilir.\\n\\nÖzel PATH girişleri gibi bazı yapılandırmalar, ilgili kabuk profil dosyalarında henüz yapılandırılmamışsa, yedek kabukta uygulanmayabilir.\nagentSocketNotFound=Etkin ajan soketi bulunamadı\nagentSocket=Soket konumu\nagentSocketDescription=Aracı soket dosyasının yolu\nagentSocketNotConfigured=Henüz özel bir soket yapılandırılmadı\ndownloadInProgress=$NAME$ indirme işlemi devam ediyor\nenableTerminalStartupBell=Terminal başlatma zilini etkinleştir\nenableTerminalStartupBellDescription=Yeni bir terminal oturumunda bir bip/zil komutu çalın. Terminal öykünücünüz zil seslerini destekliyorsa, bu yeni başlatılan terminal örneklerini tanımlamayı kolaylaştırmak için kullanılabilir.\ninvalidSshGatewayChain=Atlama ağ geçitleri ve atlama olmayan ağ geçitleri içeren geçersiz karma ağ geçidi zinciri yapılandırması.\nsyncFileExists=Eşitlenen dosya $FILE$ zaten var\nreplaceFile=Dosyayı değiştir\nreplaceFileDescription=Mevcut dosyayı bununla değiştirdim\nrenameFile=Dosyayı yeniden adlandır\nrenameFileDescription=Senkronize etmek için bu dosyaya farklı bir ad verin\nnewFileName=Yeni dosya adı\nparentHostDoesNotSupportTunneling=Ana bilgisayar $NAME$ tünellemeyi desteklemiyor\nconnectionNotesTemplate=Notlar şablonu\nconnectionNotesTemplateDescription=Bir bağlantıya yeni bir not girişi eklenirken kullanılması gereken markdown şablonu.\nconnectionNotesButton=Notları düzenle\nrdpSmartSizing=Akıllı boyutlandırmayı etkinleştirin\nrdpSmartSizingDescription=Etkinleştirildiğinde, pencere tam çözünürlüğünde görüntülenemeyecek kadar küçükse mstsc masaüstü boyutunu küçültür. Masaüstünün en boy oranı küçültüldüğünde korunur.\ndisableStartOnInit=Otomatik başlatmayı devre dışı bırak\nenableStartOnInit=Otomatik başlatmayı etkinleştir\nfileReadSudoTitle=Sudo dosya okuma\nfileReadSudoContent=Okumaya çalıştığınız dosya size mevcut kullanıcı okuma izinlerini vermiyor. Bu dosyayı sudo ile root kullanıcısı olarak okumak istiyor musunuz? Bu, mevcut kimlik bilgileriyle veya bir komut istemi aracılığıyla otomatik olarak root'a yükselecektir.\nnetbirdInstall.displayName=Netbird kurulumu\nnetbirdInstall.displayDescription=Netbird ağınızdaki eşlere bağlanın\nnetbirdProfile.displayName=Netbird profili\nnetbirdProfile.displayDescription=Belirli bir profildeki eşleri listeleme\nnetbirdPeer.displayName=Netbird eş\nnetbirdPeer.displayDescription=SSH aracılığıyla bir eşe bağlanma\nnetbirdPublicKey=Açık anahtar\nnetbirdPublicKeyDescription=Eşin dahili açık anahtarı\nnetbirdHostName=Ana bilgisayar adı\nnetbirdHostNameDescription=Ağdaki eşin ana bilgisayar adı\nvncRefSystem=İlişkili sistem\nvncRefSystemDescription=Bu VNC bağlantısının ilişkilendirileceği bağlantı girişi. Eğer yoksa boş bırakın\nabstractHost.displayName=Özet ev sahibi\nabstractHost.displayDescription=Kabuk bağlantılarını desteklemeyen bir ana bilgisayar için giriş oluşturma\nabstractHostAddress=Ana bilgisayar adresi\nabstractHostAddressDescription=Ana bilgisayarın adresi\nabstractHostGateway=Ağ Geçidi\nabstractHostGatewayDescription=Bu ana bilgisayara ulaşılacak isteğe bağlı ağ geçidi sistemi\nabstractHostConvert=Soyut ana bilgisayar girişine dönüştür\nhostNoConnections=Mevcut bağlantı yok\nhostHasConnections=$COUNT$ mevcut bağlantılar\nhostHasConnection=$COUNT$ mevcut bağlantı\nlargeFileWarningTitle=Büyük dosya düzenleme\nlargeFileWarningContent=Düzenlemek istediğiniz dosya $SIZE$ ile oldukça büyük. Bu dosyayı gerçekten metin düzenleyicinizde açmak istiyor musunuz?\nrdpAskpassUser=Ana bilgisayar için RDP kullanıcı adı $HOST$\nrdpAskpassPassword=Kullanıcı için şifre $USER$\ninPlaceKey=Anahtar\ninPlaceKeyText=Özel anahtar içeriği\ninPlaceKeyTextDescription=Özel anahtar içeriği\nnetbirdSelfhosted=Kendi kendine barındırılan netbird örneği\nnetbirdSelfhostedDescription=Bulutta barındırılan sürümü kullanmak yerine özel bir URL sağlayın\nnetbirdManagementUrl=Netbird yönetim URL'si\nnetbirdManagementUrlDescription=Kendi barındırdığınız örneğin yönetim URL'si\nnetbirdSetupKey=Kurulum tuşu\nnetbirdSetupKeyDescription=Kurulum anahtarları kullanıyorsanız, giriş için bir tane kullanabilirsiniz\nnetbirdLogin=Netbird giriş\naddProfile=Profil ekle\nnetbirdProfileNameAsktext=Yeni netbird profilinin adı\nopenSftp=SFTP oturumunda açın\ncapslockWarning=Capslock'u etkinleştirdiniz\ninherit=Miras\nsshConfigStringSelected=Hedef ev sahibi\nsshConfigStringSelectedDescription=Birden fazla ana bilgisayar için ilk ana bilgisayar hedef olarak kullanılır. Hedefi değiştirmek için ana bilgisayarlarınızı yeniden sıralayın\ntunnelToLocalhost=Localhost'a tünel\ntunnelToLocalhostDescription=Uzak bağlantı noktasını otomatik olarak localhost'a tünelleme\ntags=Etiketler\ntag=Etiket\naddNewTag=Yeni etiket oluştur\ncreateTag=Etiket oluştur ...\ninPlacePublicKey=Açık anahtar\ninPlacePublicKeyDescription=Belirtilen özel anahtar için ilişkili açık anahtar\nsshKeygenTitle=Yeni SSH anahtarı oluşturun\nsshKeygenAlgorithm=Algoritma\nsshKeygenAlgorithmDescription=Anahtar için kullanılacak asimetrik keygen algoritması\nrsaBits=Bitler\nrsaBitsDescription=Oluşturulan anahtardaki bit sayısı\nsshKeygenComment=Yorum\nsshKeygenCommentDescription=Bu anahtar için isteğe bağlı açıklama\nsshKeygenPassphrase=Parola\nsshKeygenPassphraseDescription=Bu anahtar için isteğe bağlı parola\ned25519SkResident=Yerleşik anahtarı yapın\ned25519SkResidentDescription=Özel anahtarı donanım güvenlik anahtarında saklayın\ned25519SkResidentKeyName=Yerleşik anahtar etiketi\ned25519SkResidentKeyNameDescription=Anahtara bir etiket verin. Güvenlik anahtarında birden fazla anahtar depolanırken gereklidir\ned25519SkPinRequired=PIN Gerektir\ned25519SkPinRequiredDescription=Kullanım sırasında PIN girişi gerektir\ned25519SkUserPresenceRequired=Kullanıcı varlığı gerektir\ned25519SkUserPresenceRequiredDescription=Kullanımda dokunma veya benzeri bir özellik gerektirir. Bazı güvenlik anahtarları bunun etkinleştirilmesini gerektirir\ncopyPublicKey=Açık anahtarı kopyala\ngeneratePublicKey=Açık anahtar oluşturun\npublicKeyGenerateNotice=Özel anahtardan oluşturulabilir\nidentityApplyTargetHost=Hedef\nidentityApplyTargetHostDescription=Kimliğin uygulanacağı sistem\nidentityApplyAuthorizedHost=SSH anahtarı yetkilendirildi\nidentityApplyAuthorizedHostDescription=SSH anahtarı yetkili hosts dosyasına eklenir\nidentityApplyAuthorizedHostButton=Anahtarı dosyaya ekle\napplyIdentityToHost=Ev sahibine kimlik uygulayın ...\nidentityApplyMissingPublicKeyTitle=Eksik açık anahtar\nidentityApplyMissingPublicKeyContent=Kimliğin SSH anahtarının kendisiyle ilişkili bir genel anahtarı yok. Ayrıntılar için yapılandırmaya göz atın.\nvalid=Geçerli\nnotValid=Geçerli değil\nwarning=Uyarı\nidentityApplyTitle=Kimlik uygulayın\nidentityApplyConfigPasswordEnabled=Parola doğrulama etkin\nidentityApplyConfigPasswordEnabledDescription=Parola kimlik doğrulaması sshd yapılandırmasında hala etkindir\nidentityApplyConfigPasswordDisabled=Parola doğrulama devre dışı\nidentityApplyConfigPasswordDisabledDescription=Parola kimlik doğrulaması sshd yapılandırmasında hala devre dışıdır\nidentityApplyConfigKeyEnabled=Anahtar kimlik doğrulama etkin\nidentityApplyConfigKeyEnabledDescription=Anahtar tabanlı kimlik doğrulama sshd yapılandırmasında hala etkindir\nidentityApplyConfigKeyDisabled=Anahtar doğrulama devre dışı\nidentityApplyConfigKeyDisabledDescription=Anahtar tabanlı kimlik doğrulama sshd yapılandırmasında hala devre dışıdır\nidentityApplyConfigRootDisabledWarning=Kök oturum açma devre dışı\nidentityApplyConfigRootDisabledWarningDescription=Kök kullanıcı girişi sshd yapılandırmasında etkin değil\nidentityApplyConfigAdminWarning=Yönetici anahtarları yapılandırıldı\nidentityApplyConfigAdminWarningDescription=Yönetici kullanıcılar için anahtarın administrators_authorized_keys'e eklenmesi gerekebilir\nidentityApplyEditConfig=Yapılandırmayı düzenle\nidentityApplyEditConfigDescription=Herhangi bir sorunu düzeltmek için sshd yapılandırmasını düzenleyicide açın\nidentityApplyEditAuthorizedKeys=Yetkili anahtarları düzenleme\nidentityApplyEditAuthorizedKeysDescription=Diğer anahtarları düzenlemek veya kaldırmak için yetkili_anahtarlar dosyasını düzenleyicide açın\nidentityApplyEditConfigButton=Sshd_config'i açın\nidentityApplyEditAuthorizedKeysButton=Authorized_keys'i açın\nidentityApplySetStoreIdentity=Bağlantı kimlik seti\nidentityApplySetStoreIdentityDescription=Kimlik, bağlantı tarafından kullanılmak üzere yapılandırılmıştır\nidentityApplySetStoreIdentityButton=Kimlik uygulayın\ngenerateKey=Anahtar oluştur\ngroupSecretStrategy=Grup tabanlı erişim kontrolü\ngroupSecretStrategyDescription=Grup için şifreleme ve şifre çözme için kullanılan grup sırrının nasıl alınacağı. Seçtiğiniz alma yöntemi, bir kullanıcı başlangıçta kasada oturum açtığında çalıştırılacaktır.\\n\\nBu ayar grup bazında yapılandırılır. Bu ayarı o anda etkin olan gruptan farklı bir grup için değiştirmek için, kasaya o grubun bir üyesi olarak giriş yapmanız gerekecektir.\nfileSecret=Dosya tabanlı gizli\ncommandSecret=Komut\nhttpRequestSecret=HTTP yanıtı\nfileSecretChoice=Dosya konumu\nfileSecretChoiceDescription=Grup şifreleme sırrını içeren dosyanın yolu. Bu dosya tüm platformlarda sorgulanabildiğinden, ev dizinine başvurmak için yolda ~ kullanabilirsiniz. Dosya, kasanın kilidini açtığınız tüm sistemlerde mevcut olmalıdır, aksi takdirde oturum açma başarısız olur.\ncommandSecretField=Geri alma komut dosyası\ncommandSecretFieldDescription=Geçerli grup için gizli şifreleme anahtarını döndüren komut. Komut yerel sistem varsayılan kabuğunda çalıştırılır ve anahtar stdout'a yazdırılmalıdır.\nhttpRequestSecretField=İstek URI\nhttpRequestSecretFieldDescription=HTTP isteği gönderilecek URI. Grup sırrı HTTP yanıt gövdesinden alınır.\nvaultAuthentication=Kasa kimlik doğrulaması\nvaultAuthenticationDescription=Kasa verilerinin kimliği nasıl doğrulanır / kilidi nasıl açılır. Kasa verilerini kiminle paylaşmak istediğinize bağlı olarak, kasa verilerini şifrelemenin ve kilidini açmanın birden fazla farklı yolu vardır.\ngroupAuthFailed=Gizli kimlik doğrulama başarısız oldu\nuserAuthFailed=Parola kimlik doğrulaması başarısız oldu\nsavingChanges=Değişiklikleri kaydetme\nawsDeviceScan=AWS\naws=AWS\nawsCliInstallTitle=AWS CLI gerekli\nawsCliInstallContent=AWS entegrasyonu, AWS CLI'nin yerel sisteminizde yüklü olmasını gerektirir\nawsProfileCreateTitle=Yeni AWS profili\nawsProfileAccessKey=Erişim anahtarı\nawsProfileName=Profil adı\nawsProfileNameDescription=Yeni profilin görünen adı\nawsProfileRegion=Bölge\nawsProfileRegionDescription=Profil ile ilişkili AWS bölgesi\nawsProfileAccessKeyId=Erişim anahtarı kimliği\nawsProfileAccessKeyIdDescription=IAM kullanıcı erişim anahtarı kimliği\nawsProfileSecretAccessKey=Gizli erişim anahtarı\nawsProfileSecretAccessKeyDescription=İlişkili gizli erişim anahtarı\nawsInstall.displayName=AWS CLI kurulumu\nawsInstall.displayDescription=AWS CLI aracılığıyla AWS sistemlerinize bağlanın\nawsProfile.displayName=AWS CLI profili\nawsProfile.displayDescription=AWS'ye belirli bir profil üzerinden erişim\nawsInstanceId=Örnek Kimliği\nawsInstanceIdDescription=Bu örneğin dahili kimliği\nawsInstanceUseSsm=SSM üzerinden bağlanın\nawsInstanceUseSsmDescription=SSH aracılığıyla örneğe bağlanmak için SSM aracını kullanın\nawsEc2Instance.displayName=AWS EC2 örneği\nawsEc2Instance.displayDescription=SSH aracılığıyla bir EC2 örneğine bağlanma\nawsS3Group.displayName=S3 kovaları\nawsS3Group.displayDescription=Bir AWS profilinin S3 kovalarına erişme\nawsS3Bucket.displayName=S3 kova\nawsS3Bucket.displayDescription=Bir AWS profilinin S3 kovasına erişim\nawsEc2Group.displayName=EC2 örnekleri\nawsEc2Group.displayDescription=Bir AWS profilinin EC2 örneklerine erişim\nawsEc2InstanceSsmTerminal=SSM terminalini açın\ngenericS3Bucket.displayName=Genel S3 kovası\ngenericS3Bucket.displayDescription=AWS CLI aracılığıyla genel bir S3 kovasına erişin\naddFileSystem=Dosya sistemi ...\ngenericS3BucketHost=Ev sahibi\ngenericS3BucketHostDescription=S3 sunucusunun ana bilgisayar girişi veya manuel adresi\ngenericS3BucketPortDescription=S3 sunucusunun dinlediği bağlantı noktası\ngenericS3BucketAccessKeyId=Erişim anahtarı kimliği\ngenericS3BucketAccessKeyIdDescription=IAM kullanıcı erişim anahtarı kimliği\ngenericS3BucketSecretAccessKey=Gizli erişim anahtarı\ngenericS3BucketSecretAccessKeyDescription=İlişkili gizli erişim anahtarı\ngenericS3BucketHttps=HTTPS'yi Etkinleştir\ngenericS3BucketHttpsDescription=Sunucuya bağlanmak için HTTPS kullanın. Bazı sağlayıcılar HTTPS gerektirebilir\ntunnelled=Tünellenmiş\nawsInstallSync=Yapılandırma senkronizasyonu\nawsInstallSyncDescription=AWS CLI yapılandırma dosyalarını git kasasına senkronize etme\nawsInstallLocation=Kullanıcı veri konumu\nawsInstallLocationDescription=AWS CLI yapılandırma dosyalarının kaynaklandığı yol\ninstanceActions=Örnek eylemler\nopenSplit=Bölünmüş terminalde açık\nterminalSplitStrategy=Bölünmüş görünüm yönü\nterminalSplitStrategyDescription=Birden fazla terminal oturumunu yan yana açmak için toplu modda bölünmüş görünüm işlevi kullanılırken terminal sekmelerinin nasıl bölüneceğini kontrol eder.\nterminalSplitStrategyDisabledDescription=Birden fazla terminal oturumunu yan yana açmak için toplu modda bölünmüş görünüm işlevi kullanılırken terminal sekmelerinin nasıl bölüneceğini kontrol eder.\\n\\nMevcut terminal yapılandırmanız bölünmüş görünümleri desteklemiyor.\nhorizontal=Yatay\nvertical=Dikey\nbalanced=Dengeli\nclose=Kapat\nhelpButton=$TOPIC$ dokümantasyon bağlantısı\nquickAccess=Hızlı erişim\ntoggleEnabled=Durum değiştir\ncurrentPath=Geçerli yol\ndirectoryContents=Dizin içeriği\ndirectoryOptions=Dizin seçenekleri\nchooseConnectionType=Bağlantı türünü seçin\nbatchMode=Toplu iş modu\ntoggleButton=Geçiş düğmesi\ntailscaleUseSsh=Kuyruk ölçekli SSH kimlik doğrulamasını kullanma\ntailscaleUseSshDescription=Herhangi bir SSH yetkilendirmesi olmadan tailscale SSH sunucusunun kendisi üzerinden oturum açın\nportDescription=SSH sunucusunun üzerinde çalıştığı bağlantı noktası\nloginAs=Olarak giriş yapın\nsshGatewayType=Ağ geçidi tipi\nsshGatewayTypeDescription=Hedefe bir tünel üzerinden mi yoksa ProxyJump seçeneği ile mi bağlanılacağı\ngatewayTunnel=Ağ geçidi tüneli\nproxyJump=Proxy atlama\ncommandTypeAsyncBackground=Arka planda müstakil çalıştır\ncommandTypeSyncBackground=Arka planda çalışın ve bitmesini bekleyin\ncommandTypeTerminalBackground=Terminalde aç\nasyncBackgroundCommand=Arka plan komutu\nsyncBackgroundCommand=Arka plan komutunu engelleme\nterminalBackgroundCommand=Terminal komutu\ntestingConnection=Test bağlantısı ...\nopenManagementConsole=Açık yönetim konsolu\nopenLxcTerminal=LXC terminalini açın\nopenContainerConsole=Seri konsolu açın\nkeeper2fa=2FA yöntemi\nkeeper2faDescription=Hesabınız için yapılandırılmış olan birincil iki faktörlü kimlik doğrulama yöntemi. Keeper hesabınız parolalara erişmek için iki faktörlü kimlik doğrulama gerektiriyorsa bunu etkinleştirin.\nkeeperTotpDuration=Özel 2FA kodu süresi\nkeeperTotpDurationDescription=Bir 2FA kodunun ne kadar süreyle geçerli olacağına ilişkin varsayılan süreyi geçersiz kılın. Yalnızca kuruluş politikanız sürenin değiştirilmesine izin veriyorsa geçerlidir.\\n\\nOlası değerler şunlardır: $VALUES$\nkeeperOtherAuth=Diğer (RSA SecurID, Duo Security, Keeper DNA, vb.)\nextractReusableIdentities=Yeniden kullanılabilir kimlikleri ayıklayın\nidentitiesAdded=Kimlikler eklendi\nsyncMode=Senkronizasyon modu\nsyncModeDescription=Değişikliklerin nasıl senkronize edileceğini kontrol eder.\\n\\nAnlık mod değişiklikleri mümkün olan en kısa sürede itecek ve çekecek, başlangıç ve çıkış modu bir oturum sırasında yapılan tüm değişiklikleri bir kerede senkronize edecek ve manuel mod yalnızca siz başlattığınızda senkronize edecektir.\ntoggleTerminalDock=Terminal yuvasını değiştir\nscriptDirectory=Dizin konumu\nscriptDirectoryDescription=Kabuk betik dosyalarını içeren yerel dizin\nscriptSourceUrl=Depo URL'si\nscriptSourceUrlDescription=Kabuk betik dosyalarını içeren uzak bir git deposunun URL'si\nscriptCollectionSourceType=Kaynak türü\nscriptCollectionSourceTypeDescription=Kabuk betiklerinin yüklenmesi gereken kaynak türü\nscriptCollectionSourceEntry=Kaynak girişi\nscriptCollectionSourceEntryDescription=Kabuk betiklerinin yüklenmesi gereken kaynak\ngitRepository=Git deposu\nscriptCollectionSource.displayName=Senaryo kaynağı\nscriptCollectionSource.displayDescription=Kabuk komut dosyalarını mevcut bir kaynaktan otomatik olarak içe aktarma\ndirectorySource=Dizin kaynağı\ngitRepositorySource=Git deposu kaynağı\nrefreshSource=Kaynağı yenileyin\nscriptTextSourceUrl=Komut Dosyası URL'si\nscriptTextSourceUrlDescription=Kod dosyasının alınacağı URL\nscriptSourceType=Senaryo kaynağı\nscriptSourceTypeDescription=Senaryonun nereden kaynaklanacağı\nscriptSourceTypeInPlace=Yerinde komut dosyası\nscriptSourceTypeUrl=Harici URL\nscriptSourceTypeSource=Mevcut kaynak\nimportScripts=Komut dosyalarını içe aktarma\nscriptsContained=$NUMBER$ yAZILAR\nscriptSourceCollectionImportTitle=Kodları kaynaktan içe aktarma ($SELECTED$/$COUNT$)\nnoScriptsFound=Komut dosyası bulunamadı\ntunnel=Tünel\nnotInitialized=Başlatılmadı\nselectCategory=Kategori seçin ...\nscriptSourceName=Komut dosyası adı\nscriptSourceNameDescription=Kaynaktaki betiğin dosya adı\nworkspaceRestartTitle=Çalışma alanı hazır\nworkspaceRestartContent=Yeni çalışma alanı için $PATH$ adresinde bir kısayol oluşturulmuştur. Yeni çalışma alanını otomatik olarak açmak için kısayola gidebilir veya XPipe'ı şimdi yeniden başlatabilirsiniz.\nbrowseShortcut=Dosyaya göz at\nsyncModeInstant=Anında senkronize edin\nsyncModeSession=Başlangıçta ve çıkışta senkronizasyon\nsyncModeManual=Manuel olarak senkronize edin\npushChanges=Değişiklikleri itin\npullChanges=Çekme değişiklikleri\nsourcedFrom=Kaynak $SOURCE$\ninPlaceScript=Yerinde komut dosyası\ngeneric=Jenerik\nsyncToPlainDirectory=Düz dizine senkronize et\nsyncToPlainDirectoryDescription=Yerel bir dizinle senkronizasyon yaparken, bu dizini başka bir git deposu olarak ya da sadece düz bir dizin olarak ele alabilirsiniz. Düz dizin ayarı etkinleştirilirse, dizin bir git deposu olarak başlatılmaz.\nopenSpiceSession=SPICE oturumunu açın\nterminalBehaviour=Terminal davranışı\nnoScanPossible=Desteklenen bağlantı bulunamadı\nnetworkSwitchPorts=Ağ bağlantı noktaları\nnswitchGroup.displayName=Ağ bağlantı noktaları\nnswitchGroup.displayDescription=Bir ağ cihazındaki kullanılabilir bağlantı noktalarını listeleme\nnswitchPort.displayName=Ağ bağlantı noktası\nnswitchPort.displayDescription=Bir ağ anahtarı cihazındaki tek bir bağlantı noktasını kontrol etme\nenablePort=Bağlantı noktasını etkinleştir\nshutdownPort=Limanı kapatın\nresetPort=Bağlantı noktasını sıfırla\nuseSystemDefault=Sistem varsayılanını kullan\nportStatus=Liman durumu\nclearCounters=Sayaçları temizleyin\nshowStatus=Durumu göster\nshowAllPorts=Tüm bağlantı noktalarını göster\nactiveLicense=Lisans\nactiveLicenseDescription=XPipe lisans anahtarını etkinleştirme\nauthenticatorApp=Authenticator uygulaması\nsecurityKey=Güvenlik anahtarı\nmcpAdditionalContext=Ek MCP bağlamı\nmcpAdditionalContextDescription=MCP istemcisine iletilecek ek talimatlar. Aracı davranışını kontrol etmek ve bireysel kurulumunuz için ek bağlam sağlamak için bunu kullanın.\nmcpAdditionalContextSample=- Önce onaylamadan hiçbir hizmeti ve daemon'u otomatik olarak yeniden başlatmayın\\n- Bir ağ arayüzünü yapılandırırken, ağ geçidi olarak her zaman 192.168.1.1/24 adresini kullanın\nprefsRestartTitle=Yeniden başlatma gerekli\nprefsRestartContent=Değiştirdiğiniz bazı seçeneklerin uygulanması için uygulamanın yeniden başlatılması gerekir. XPipe'ı şimdi yeniden başlatmak istiyor musunuz?\nbashShell=Bash kabuğu\n"
  },
  {
    "path": "lang/strings/translations_vi.properties",
    "content": "delete=Xóa\nproperties=Thuộc tính\nusedDate=Được sử dụng $DATE$\nopenDir=Thư mục mở\nsortLastUsed=Sắp xếp theo ngày sử dụng gần đây nhất\nsortAlphabetical=Sắp xếp theo thứ tự alphabet theo tên\nsortIndexed=Sắp xếp theo chỉ số thứ tự\nrestartDescription=Khởi động lại thường là giải pháp nhanh chóng\nreportIssue=Báo cáo sự cố\nreportIssueDescription=Mở trình báo cáo sự cố tích hợp\nusefulActions=Các thao tác hữu ích\nstored=Đã lưu\ntroubleshootingOptions=Công cụ khắc phục sự cố\ntroubleshoot=Khắc phục sự cố\nremote=Tệp từ xa\naddShellStore=Thêm Shell ...\naddShellTitle=Thêm kết nối vỏ\nsavedConnections=Kết nối đã lưu\nsave=Lưu\nclean=Làm sạch\nmoveTo=Di chuyển đến ...\naddDatabase=Cơ sở dữ liệu ...\nbrowseInternalStorage=Duyệt bộ nhớ trong\naddTunnel=Đường hầm ...\naddService=Dịch vụ ...\naddScript=Kịch bản ...\naddHost=Máy chủ từ xa ...\naddShell=Môi trường Shell ...\naddCommand=Lệnh ...\naddAutomatically=Thêm tự động ...\naddOther=Thêm khác ...\nconnectionAdd=Thêm kết nối\nscriptAdd=Thêm kịch bản\nscriptGroupAdd=Thêm nhóm kịch bản\nidentityAdd=Thêm thông tin nhận dạng\nnew=Mới\nselectType=Chọn loại\nselectTypeDescription=Chọn loại kết nối\nselectShellType=Loại vỏ\nselectShellTypeDescription=Chọn loại kết nối vỏ\nname=Tên\nstoreIntroHeader=Trung tâm kết nối\nstoreIntroContent=Tại đây, cậu có thể quản lý tất cả các kết nối shell cục bộ và từ xa của mình tại một nơi duy nhất. Để bắt đầu, cậu có thể nhanh chóng phát hiện các kết nối khả dụng một cách tự động và chọn những kết nối nào cần thêm.\nstoreIntroButton=Tìm kiếm kết nối ...\ndragAndDropFilesHere=Hoặc chỉ cần kéo và thả tệp vào đây\nconfirmDsCreationAbortTitle=Xác nhận hủy bỏ\nconfirmDsCreationAbortHeader=Cậu có muốn hủy việc tạo nguồn dữ liệu không?\nconfirmDsCreationAbortContent=Mọi tiến trình tạo nguồn dữ liệu sẽ bị mất.\nconfirmInvalidStoreTitle=Bỏ qua xác thực\nconfirmInvalidStoreContent=Bạn có muốn bỏ qua quá trình xác thực kết nối không? Bạn có thể thêm kết nối này ngay cả khi nó không thể được xác thực và khắc phục các vấn đề kết nối sau này.\nexpand=Mở rộng\naccessSubConnections=Truy cập các kết nối con\ncommon=Thông dụng\ncolor=Màu\nalwaysConfirmElevation=Luôn xác nhận quyền truy cập cao hơn\nalwaysConfirmElevationDescription=Kiểm soát cách xử lý các trường hợp yêu cầu quyền truy cập cao hơn để thực thi một lệnh trên hệ thống, ví dụ như khi sử dụng sudo.\\n\\nTheo mặc định, thông tin đăng nhập sudo sẽ được lưu trữ trong phiên làm việc và tự động cung cấp khi cần thiết. Nếu tùy chọn này được bật, hệ thống sẽ yêu cầu bạn xác nhận quyền truy cập cao hơn mỗi lần.\nallow=Cho phép\nask=Hỏi\ndeny=Từ chối\nshare=Thêm vào kho lưu trữ Git\nunshare=Xóa khỏi kho lưu trữ Git\nremove=Xóa\ncreateNewCategory=Danh mục con mới\nprompt=Hướng dẫn\ncustomCommand=Lệnh tùy chỉnh\nother=Khác\nsetLock=Đặt khóa\nselectConnection=Chọn kết nối\nselectEntry=Chọn mục\ncreateLock=Tạo mật khẩu\nchangeLock=Thay đổi mật khẩu\ntest=Kiểm tra\nfinish=Hoàn thành\nerror=Đã xảy ra lỗi\ndownloadStageDescription=Di chuyển các tệp đã tải xuống vào thư mục tải xuống của hệ thống và mở nó.\nok=Ok\nsearch=Tìm kiếm\nrepeatPassword=Nhập lại mật khẩu\naskpassAlertTitle=Askpass\nunsupportedOperation=Hoạt động không được hỗ trợ: $MSG$\nfileConflictAlertTitle=Giải quyết xung đột\nfileConflictAlertContent=Đã xảy ra xung đột. Tệp $FILE$ đã tồn tại trên hệ thống đích.\\n\\nBạn muốn tiếp tục như thế nào?\nfileConflictAlertContentMultiple=Đã xảy ra xung đột. Tệp $FILE$ đã tồn tại.\\n\\nBạn muốn tiếp tục như thế nào? Có thể có thêm các xung đột khác mà bạn có thể giải quyết tự động bằng cách chọn tùy chọn áp dụng cho tất cả.\nmoveAlertTitle=Xác nhận di chuyển\nmoveAlertHeader=Bạn có muốn di chuyển các phần tử đã chọn ($COUNT$) vào \" $TARGET$\" không?\ndeleteAlertTitle=Xác nhận xóa\ndeleteAlertHeader=Bạn có muốn xóa các phần tử đã chọn ($COUNT$) không?\nselectedElements=Các thành phần được chọn:\nmustNotBeEmpty=$VALUE$ không được để trống\nvalueMustNotBeEmpty=Giá trị không được để trống\ntransferDescription=Kéo tệp vào đây để tải xuống\ndragLocalFiles=Kéo tệp tải xuống từ đây\nnull=$VALUE$ phải là không rỗng\nroots=Gốc\nscripts=Kịch bản\nsearchFilter=Tìm kiếm ...\nrecent=Gần đây\nshortcut=Phím tắt\nbrowserWelcomeEmptyHeader=Trình duyệt tệp\nbrowserWelcomeEmptyContent=Bạn có thể chọn ở bên trái các hệ thống muốn mở trong trình duyệt tệp. XPipe sẽ ghi nhớ các hệ thống và thư mục mà bạn đã truy cập trước đó và hiển thị chúng trong menu truy cập nhanh ở đây trong tương lai.\nbrowserWelcomeEmptyButton=Mở trình duyệt tệp cục bộ\nbrowserWelcomeSystems=Mày vừa được kết nối với các hệ thống sau:\nbrowserWelcomeDocsHeader=Tài liệu\nbrowserWelcomeDocsContent=Nếu cậu muốn tìm hiểu XPipe một cách có hướng dẫn chi tiết hơn, hãy truy cập trang web tài liệu.\nbrowserWelcomeDocsButton=Mở tài liệu\nhostFeatureUnsupported=$FEATURE$ không được cài đặt trên máy chủ\nmissingStore=$NAME$ không tồn tại\nconnectionName=Tên kết nối\nconnectionNameDescription=Đặt tên tùy chỉnh cho kết nối này\nopenFileTitle=Mở tệp\nunknown=Không rõ\nscanAlertTitle=Thêm kết nối\nscanAlertChoiceHeader=Mục tiêu\nscanAlertChoiceHeaderDescription=Chọn nơi để tìm kiếm kết nối. Tính năng này sẽ tìm kiếm tất cả các kết nối có sẵn trước tiên.\nscanAlertHeader=Các loại kết nối\nscanAlertHeaderDescription=Chọn các loại kết nối mà cậu muốn tự động thêm vào hệ thống.\nnoInformationAvailable=Không có thông tin sẵn có\nyes=Có\nno=Không\nerrorOccured=Đã xảy ra lỗi\nterminalErrorOccured=Đã xảy ra lỗi thiết bị đầu cuối\nerrorTypeOccured=Một ngoại lệ loại \" $TYPE$ \" đã được ném ra\npermissionsAlertTitle=Quyền truy cập cần thiết\npermissionsAlertHeader=Cần có quyền truy cập bổ sung để thực hiện thao tác này.\npermissionsAlertContent=Vui lòng làm theo hướng dẫn trên cửa sổ pop-up để cấp cho XPipe các quyền cần thiết trong menu cài đặt.\nerrorDetails=Chi tiết lỗi\nupdateReadyAlertTitle=Sẵn sàng cập nhật\nupdateReadyAlertHeader=Bản cập nhật cho phiên bản $VERSION$ đã sẵn sàng để cài đặt\nupdateReadyAlertContent=Điều này sẽ cài đặt phiên bản mới và khởi động lại XPipe sau khi quá trình cài đặt hoàn tất.\nerrorNoDetail=Không có chi tiết lỗi nào có sẵn\nerrorNoExceptionMessage=Một lỗi loại \" $TYPE$ \" đã được ném ra\nupdateAvailableTitle=Có bản cập nhật\nupdateAvailableContent=Bản cập nhật XPipe lên phiên bản $VERSION$ hiện có sẵn để cài đặt. Mặc dù XPipe không thể khởi động, cậu có thể thử cài đặt bản cập nhật để có thể khắc phục sự cố.\nclipboardActionDetectedTitle=Hành động Clipboard đã được phát hiện\nclipboardActionDetectedContent=XPipe đã phát hiện nội dung trong khay nhớ tạm của cậu có thể được mở. Cậu có muốn mở nó ngay bây giờ không? Cậu có muốn nhập nội dung khay nhớ tạm của mình không?\ninstall=Cài đặt ...\nignore=Bỏ qua\npossibleActions=Các thao tác có thể thực hiện\nreportError=Báo cáo lỗi\nreportOnGithub=Tạo báo cáo sự cố trên GitHub\nreportOnGithubDescription=Mở một vấn đề mới trong kho GitHub\nreportErrorDescription=Gửi báo cáo lỗi kèm theo phản hồi của người dùng (nếu có) và thông tin chẩn đoán\nignoreError=Bỏ qua lỗi\nignoreErrorDescription=Bỏ qua lỗi này và tiếp tục như không có gì xảy ra\nprovideEmail=Làm thế nào chúng tôi có thể liên hệ với cậu (tùy chọn, chỉ khi cậu muốn nhận phản hồi). Báo cáo của cậu là ẩn danh theo mặc định, vì vậy cậu có thể cung cấp thông tin liên hệ như địa chỉ email ở đây.\nadditionalErrorInfo=Cung cấp thông tin bổ sung (tùy chọn)\nadditionalErrorAttachments=Chọn tệp đính kèm (tùy chọn)\ndataHandlingPolicies=Chính sách bảo mật\nsendReport=Gửi báo cáo\nerrorHandler=Bộ xử lý lỗi\nevents=Sự kiện\nvalidate=Xác thực\nstackTrace=Dấu vết ngăn xếp\npreviousStep=&lt; Trước đó\nnextStep=Tiếp theo &gt;\nfinishStep=Hoàn thành\nselect=Chọn\nbrowseInternal=Duyệt nội bộ\ncheckOutUpdate=Xem cập nhật\nquit=Thoát\nnoTerminalSet=Chưa có ứng dụng terminal nào được thiết lập tự động. Bạn có thể thực hiện điều này thủ công trong menu cài đặt.\nconnections=Kết nối\nconnectionHub=Trung tâm kết nối\nsettings=Cài đặt\nexplorePlans=Giấy phép\nhelp=Trợ giúp\nabout=Giới thiệu\ndeveloper=Nhà phát triển\nbrowseFileTitle=Duyệt tệp\nbrowser=Trình duyệt tệp\nselectFileFromComputer=Chọn một tệp từ máy tính này\nlinks=Liên kết\nwebsite=Trang web\ndiscordDescription=Tham gia máy chủ Discord\nredditDescription=Tham gia subreddit XPipe\nsecurity=Bảo mật\nsecurityPolicy=Thông tin bảo mật\nsecurityPolicyDescription=Đọc chính sách bảo mật chi tiết\nprivacy=Chính sách bảo mật\nprivacyDescription=Đọc chính sách bảo mật của ứng dụng XPipe\nslackDescription=Tham gia không gian làm việc Slack\nsupport=Hỗ trợ\ngithubDescription=Xem kho lưu trữ GitHub\nopenSourceNotices=Thông báo về phần mềm nguồn mở\ncheckForUpdates=Kiểm tra cập nhật\ncheckForUpdatesDescription=Tải xuống bản cập nhật nếu có\nlastChecked=Đã kiểm tra lần cuối\nversion=Phiên bản\nbuild=Phiên bản xây dựng\nruntimeVersion=Phiên bản chạy\nvirtualMachine=Máy ảo\nupdateReady=Cài đặt bản cập nhật\nupdateReadyPortable=Xem cập nhật\nupdateReadyDescription=Một bản cập nhật đã được tải xuống và sẵn sàng để cài đặt\nupdateReadyDescriptionPortable=Có bản cập nhật sẵn sàng để tải xuống\nupdateRestart=Khởi động lại để cập nhật\nnever=Không bao giờ\nupdateAvailableTooltip=Có bản cập nhật\nptbAvailableTooltip=Phiên bản thử nghiệm công khai có sẵn\nvisitGithubRepository=Thăm kho lưu trữ GitHub\nupdateAvailable=Có bản cập nhật sẵn sàng: $VERSION$\ndownloadUpdate=Tải xuống bản cập nhật\nlegalAccept=Tớ chấp nhận Thỏa thuận Giấy phép Người dùng Cuối cùng\nconfirm=Xác nhận\nprint=In\nwhatsNew=Có gì mới trong phiên bản $VERSION$ ($DATE$)\nantivirusNoticeTitle=Ghi chú về các chương trình diệt virus\nupdateChangelogAlertTitle=Nhật ký thay đổi\ngreetingsAlertTitle=Chào mừng đến với XPipe\neula=Thỏa thuận cấp phép người dùng cuối\nnews=Tin tức\nintroduction=Giới thiệu\nprivacyPolicy=Chính sách bảo mật\nagree=Đồng ý\ndisagree=Không đồng ý\ndirectories=Thư mục\nlogFile=Tệp nhật ký\nlogFiles=Tệp nhật ký\nlogFilesAttachment=Tệp nhật ký\nissueReporter=Người báo cáo sự cố\nopenCurrentLogFile=Tệp nhật ký\nopenCurrentLogFileDescription=Mở tệp nhật ký của phiên hiện tại\nopenLogsDirectory=Mở thư mục nhật ký\ninstallationFiles=Tệp cài đặt\nopenInstallationDirectory=Tệp cài đặt\nopenInstallationDirectoryDescription=Mở thư mục cài đặt XPipe\nlaunchDebugMode=Chế độ gỡ lỗi\nlaunchDebugModeDescription=Khởi động lại XPipe ở chế độ gỡ lỗi\nextensionInstallTitle=Tải xuống\nextensionInstallDescription=Hành động này yêu cầu các thư viện của bên thứ ba bổ sung không được phân phối bởi XPipe. Bạn có thể cài đặt chúng tự động tại đây. Các thành phần sau đó sẽ được tải xuống từ trang web của nhà cung cấp:\nextensionInstallLicenseNote=Bằng cách thực hiện tải xuống và cài đặt tự động, cậu đồng ý với các điều khoản của giấy phép của bên thứ ba:\nlicense=Giấy phép\ninstallRequired=Yêu cầu cài đặt\nrestore=Khôi phục\nrestoreAllSessions=Khôi phục tất cả các phiên\nlimitedTouchscreenMode=Chế độ màn hình cảm ứng giới hạn\nlimitedTouchscreenModeDescription=Khi sử dụng ứng dụng này trên giao diện cảm ứng đặc biệt như màn hình điện thoại, một số menu có thể không hoạt động đúng cách. Khi tùy chọn này được bật, việc triển khai menu sẽ sử dụng chức năng hạn chế hơn để xử lý các sự kiện chuột/cảm ứng được gửi thưa thớt.\nappearance=Giao diện\ndisplay=Hiển thị\npersonalization=Cá nhân hóa\ndisplayOptions=Tùy chọn hiển thị\ntheme=Chủ đề\nrdpConfiguration=Cấu hình máy tính từ xa\nrdpClient=Khách hàng RDP\nrdpClientDescription=Chương trình khách RDP cần gọi khi khởi chạy kết nối RDP.\\n\\nLưu ý rằng các chương trình khách khác nhau có mức độ khả năng và tích hợp khác nhau. Một số chương trình khách không hỗ trợ truyền mật khẩu tự động, vì vậy bạn vẫn phải nhập mật khẩu khi khởi chạy.\nlocalShell=Vỏ lệnh cục bộ\nthemeDescription=Chủ đề hiển thị ưa thích của cậu.\ndontAutomaticallyStartVmSshServer=Không tự động khởi động máy chủ SSH cho các máy ảo khi cần thiết\ndontAutomaticallyStartVmSshServerDescription=Mọi kết nối shell đến một máy ảo (VM) đang chạy trong một hypervisor đều được thực hiện qua SSH. XPipe có thể tự động khởi động máy chủ SSH đã cài đặt khi cần thiết. Nếu cậu không muốn điều này vì lý do bảo mật, cậu có thể tắt tính năng này bằng tùy chọn này.\nconfirmGitShareTitle=Đồng bộ hóa Git\nconfirmGitShareContent=Bạn có muốn thêm tệp đã chọn vào kho lưu trữ Git Vault của mình không? Điều này sẽ sao chép phiên bản đã mã hóa của tệp vào kho Git Vault và cam kết các thay đổi của bạn. Sau đó, bạn sẽ có thể truy cập tệp trên tất cả các máy tính đã đồng bộ hóa.\ngitShareFileTooltip=Thêm tệp vào thư mục dữ liệu git vault để nó được đồng bộ hóa tự động.\\n\\nHành động này chỉ có thể sử dụng khi git vault đã được kích hoạt trong cài đặt.\nperformanceMode=Chế độ hoạt động\nperformanceModeDescription=Vô hiệu hóa tất cả các hiệu ứng hình ảnh không cần thiết để cải thiện hiệu suất của ứng dụng.\ndontAcceptNewHostKeys=Không tự động chấp nhận khóa máy chủ SSH mới\ndontAcceptNewHostKeysDescription=XPipe sẽ tự động chấp nhận khóa máy chủ theo mặc định từ các hệ thống mà trình khách SSH của cậu chưa lưu khóa máy chủ đã biết. Tuy nhiên, nếu bất kỳ khóa máy chủ đã biết nào thay đổi, nó sẽ từ chối kết nối trừ khi cậu chấp nhận khóa mới.\\n\\nVô hiệu hóa hành vi này cho phép cậu kiểm tra tất cả các khóa máy chủ, ngay cả khi không có xung đột ban đầu.\nuiScale=Tỷ lệ giao diện người dùng\nuiScaleDescription=Giá trị thu phóng tùy chỉnh có thể được thiết lập độc lập với tỷ lệ hiển thị hệ thống của bạn. Giá trị được biểu thị bằng phần trăm, ví dụ: giá trị 150 sẽ tương ứng với tỷ lệ giao diện người dùng là 150%.\neditorProgram=Chương trình soạn thảo\neditorProgramDescription=Trình soạn thảo văn bản mặc định được sử dụng khi chỉnh sửa bất kỳ loại dữ liệu văn bản nào.\nwindowOpacity=Độ trong suốt của cửa sổ\nwindowOpacityDescription=Thay đổi độ trong suốt của cửa sổ để theo dõi những gì đang diễn ra ở nền.\nuseSystemFont=Sử dụng phông chữ hệ thống\nopenDataDir=Thư mục dữ liệu an toàn\nopenDataDirButton=Mở thư mục dữ liệu\nopenDataDirDescription=Nếu cậu muốn đồng bộ hóa các tệp bổ sung, chẳng hạn như khóa SSH, giữa các hệ thống với kho lưu trữ Git của mình, cậu có thể đặt chúng vào thư mục dữ liệu lưu trữ. Tất cả các tệp được tham chiếu tại đó sẽ có đường dẫn tệp được tự động điều chỉnh trên bất kỳ hệ thống đồng bộ hóa nào.\nupdates=Cập nhật\nselectAll=Chọn tất cả\nadvanced=Nâng cao\nthirdParty=Thông báo nguồn mở\neulaDescription=Đọc Thỏa thuận cấp phép người dùng cuối cho ứng dụng XPipe\nthirdPartyDescription=Xem các giấy phép nguồn mở của các thư viện của bên thứ ba\nworkspaceLock=Mật khẩu chính\nenableGitStorage=Bật đồng bộ hóa\nsharing=Chia sẻ\ngitSync=Đồng bộ hóa Git\nenableGitStorageDescription=Khi được kích hoạt, XPipe sẽ tạo một kho lưu trữ Git cho kho lưu trữ cục bộ và lưu bất kỳ thay đổi nào vào đó. Lưu ý rằng điều này yêu cầu Git phải được cài đặt và có thể làm chậm quá trình tải và lưu.\\n\\nBất kỳ danh mục nào cần đồng bộ hóa phải được đánh dấu rõ ràng là đồng bộ hóa.\nstorageGitRemote=URL đồng bộ từ xa\nstorageGitRemoteDescription=Khi được thiết lập, XPipe sẽ tự động tải xuống bất kỳ thay đổi nào khi mở và đẩy bất kỳ thay đổi nào lên kho lưu trữ từ xa khi lưu.\\n\\nĐiều này cho phép cậu chia sẻ kho lưu trữ của mình giữa nhiều cài đặt XPipe. Nó hỗ trợ URL HTTP và SSH, cũng như các thư mục cục bộ.\nvault=Kho\nworkspaceLockDescription=Đặt mật khẩu tùy chỉnh để mã hóa bất kỳ thông tin nhạy cảm nào được lưu trữ trong XPipe.\\n\\nĐiều này giúp tăng cường bảo mật bằng cách cung cấp thêm một lớp mã hóa cho thông tin nhạy cảm của cậu. Cậu sẽ được yêu cầu nhập mật khẩu khi XPipe khởi động.\nuseSystemFontDescription=Kiểm soát việc sử dụng phông chữ hệ thống mặc định hay phông chữ Inter, được tích hợp sẵn trong XPipe.\ntooltipDelay=Thời gian hiển thị tooltip\ntooltipDelayDescription=Số mili giây cần chờ trước khi hiển thị hộp thoại trợ giúp.\nfontSize=Kích thước phông chữ\nwindowOptions=Tùy chọn cửa sổ\nsaveWindowLocation=Lưu vị trí cửa sổ\nsaveWindowLocationDescription=Kiểm soát xem tọa độ cửa sổ có được lưu và khôi phục khi khởi động lại hay không.\nstartupShutdown=Khởi động / Tắt máy\nshowChildrenConnectionsInParentCategory=Hiển thị các danh mục con trong danh mục cha\nshowChildrenConnectionsInParentCategoryDescription=Có nên hiển thị tất cả các kết nối nằm trong các danh mục con khi chọn một danh mục cha cụ thể hay không.\\n\\nNếu tùy chọn này bị vô hiệu hóa, các danh mục sẽ hoạt động giống như các thư mục truyền thống, chỉ hiển thị nội dung trực tiếp của chúng mà không bao gồm các thư mục con.\ncondenseConnectionDisplay=Tóm tắt hiển thị kết nối\ncondenseConnectionDisplayDescription=Đảm bảo rằng mỗi kết nối cấp cao nhất chiếm ít không gian dọc hơn để cho phép danh sách kết nối được hiển thị gọn gàng hơn.\nopenConnectionSearchWindowOnConnectionCreation=Mở cửa sổ tìm kiếm kết nối khi tạo kết nối\nopenConnectionSearchWindowOnConnectionCreationDescription=Có tự động mở cửa sổ để tìm kiếm các kết nối con có sẵn khi thêm một kết nối vỏ mới hay không.\nworkflow=Quy trình\nsystem=Hệ\napplication=Ứng dụng\nstorage=Lưu trữ\nrunOnStartup=Chạy khi khởi động\ncloseBehaviour=Hành vi thoát\ncloseBehaviourDescription=Điều khiển cách XPipe sẽ hoạt động khi cửa sổ chính của nó được đóng.\nlanguage=Ngôn ngữ\nlanguageDescription=Ngôn ngữ hiển thị cần sử dụng. Các bản dịch được cải thiện thông qua đóng góp của cộng đồng. Bạn có thể góp phần vào nỗ lực dịch thuật bằng cách gửi các bản sửa lỗi dịch thuật trên GitHub.\nlightTheme=Chủ đề sáng\ndarkTheme=Chủ đề tối\nexit=Thoát XPipe\ncontinueInBackground=Tiếp tục chạy ngầm\nminimizeToTray=Thu nhỏ vào khay hệ thống\ncloseBehaviourAlertTitle=Thiết lập hành vi đóng\ncloseBehaviourAlertTitleHeader=Chọn hành động cần thực hiện khi đóng cửa sổ. Tất cả các kết nối đang hoạt động sẽ bị đóng khi ứng dụng được tắt.\nstartupBehaviour=Hành vi khởi động\nstartupBehaviourDescription=Điều khiển hành vi mặc định của ứng dụng desktop khi XPipe được khởi động.\nclearCachesAlertTitle=Xóa bộ nhớ cache\nclearCachesAlertContent=Bạn có muốn xóa tất cả bộ nhớ cache của XPipe không? Điều này sẽ xóa tất cả dữ liệu cache được lưu trữ để cải thiện trải nghiệm người dùng.\nstartGui=Bắt đầu giao diện người dùng đồ họa (GUI)\nstartInTray=Bắt đầu từ khay hệ thống\nstartInBackground=Bắt đầu chạy ngầm\nclearCaches=Xóa bộ nhớ cache ...\nclearCachesDescription=Xóa tất cả dữ liệu bộ nhớ cache\ncancel=Hủy\nnotAnAbsolutePath=Không phải là đường dẫn tuyệt đối\nnotADirectory=Không phải là thư mục\nnotAnEmptyDirectory=Không phải là một thư mục trống\nautomaticallyCheckForUpdates=Kiểm tra cập nhật\nautomaticallyCheckForUpdatesDescription=Khi được kích hoạt, thông tin về bản cập nhật mới sẽ được tải xuống tự động trong khi XPipe đang chạy sau một thời gian. Bạn vẫn phải xác nhận cài đặt bản cập nhật một cách rõ ràng.\nsendAnonymousErrorReports=Gửi báo cáo lỗi ẩn danh\nsendUsageStatistics=Gửi thống kê sử dụng ẩn danh\nstorageDirectory=Thư mục lưu trữ\nstorageDirectoryDescription=Vị trí mà XPipe nên lưu trữ tất cả thông tin kết nối. Khi thay đổi vị trí này, dữ liệu trong thư mục cũ sẽ không được sao chép sang thư mục mới.\nlogLevel=Mức ghi nhật ký\nappBehaviour=Hành vi của ứng dụng\nlogLevelDescription=Mức độ ghi nhật ký cần sử dụng khi ghi tệp nhật ký.\ndeveloperMode=Chế độ nhà phát triển\ndeveloperModeDescription=Khi được kích hoạt, cậu sẽ có quyền truy cập vào nhiều tùy chọn bổ sung hữu ích cho quá trình phát triển.\neditor=Trình soạn thảo\ncustom=Tùy chỉnh\npasswordManager=Quản lý mật khẩu\nexternalPasswordManager=Quản lý mật khẩu bên ngoài\npasswordManagerDescription=Trình quản lý mật khẩu được cài đặt cục bộ để tích hợp.\\n\\nNếu cậu đã cài đặt trình quản lý mật khẩu, cậu có thể cấu hình XPipe để lấy mật khẩu từ trình quản lý đó, giúp XPipe không cần lưu trữ mật khẩu. Khi tính năng này được bật, bất kỳ trường mật khẩu nào cho kết nối đều có thể được cấu hình để sử dụng trình quản lý mật khẩu.\npasswordManagerCommandTest=Kiểm tra trình quản lý mật khẩu\npasswordManagerCommandTestDescription=Cậu có thể kiểm tra tại đây xem kết quả hiển thị có chính xác không nếu cậu đã cài đặt trình quản lý mật khẩu.\npreferTerminalTabs=Ưu tiên mở tab mới\npreferTerminalTabsDescription=Kiểm soát xem XPipe có cố gắng mở các tab mới trong terminal đã chọn của cậu thay vì các cửa sổ mới hay không. Không phải tất cả các terminal đều hỗ trợ tab.\ncustomRdpClientCommand=Lệnh tùy chỉnh\ncustomRdpClientCommandDescription=Lệnh để thực thi để khởi động trình khách RDP tùy chỉnh.\\n\\nDãy ký tự placeholder $FILE sẽ được thay thế bằng tên tệp .rdp tuyệt đối được trích dẫn khi được gọi. Hãy nhớ trích dẫn đường dẫn thực thi nếu nó chứa khoảng trắng.\ncustomEditorCommand=Lệnh chỉnh sửa tùy chỉnh\ncustomEditorCommandDescription=Lệnh để thực thi để khởi động trình soạn thảo tùy chỉnh.\\n\\nDãy ký tự placeholder $FILE sẽ được thay thế bằng tên tệp tuyệt đối được trích dẫn khi được gọi. Hãy nhớ trích dẫn đường dẫn thực thi của trình soạn thảo nếu nó chứa khoảng trắng.\neditorReloadTimeout=Thời gian chờ tải lại trình soạn thảo\neditorReloadTimeoutDescription=Số mili giây cần chờ trước khi đọc tệp sau khi tệp đã được cập nhật. Điều này giúp tránh các vấn đề xảy ra trong trường hợp trình soạn thảo của cậu chậm trong việc ghi hoặc giải phóng khóa tệp.\nencryptAllVaultData=Mã hóa toàn bộ dữ liệu trong két sắt\nencryptAllVaultDataDescription=Khi được kích hoạt, toàn bộ dữ liệu kết nối của kho lưu trữ sẽ được mã hóa bằng khóa mã hóa kho lưu trữ của người dùng, thay vì chỉ mã hóa các thông tin bí mật bên trong dữ liệu đó. Điều này thêm một lớp bảo mật cho các thông số khác như tên người dùng, tên máy chủ, v.v., vốn không được mã hóa mặc định trong kho lưu trữ.\\n\\nTùy chọn này sẽ khiến lịch sử và bản so sánh (diffs) của kho lưu trữ Git trở nên vô dụng, vì bạn không thể xem các thay đổi gốc nữa, chỉ còn lại các thay đổi nhị phân.\nvaultSecurity=Bảo mật kho\ndeveloperDisableUpdateVersionCheck=Tắt kiểm tra phiên bản cập nhật\ndeveloperDisableUpdateVersionCheckDescription=Kiểm soát xem trình kiểm tra cập nhật có bỏ qua số phiên bản khi tìm kiếm bản cập nhật hay không.\ndeveloperDisableGuiRestrictions=Vô hiệu hóa các hạn chế giao diện người dùng đồ họa (GUI)\ndeveloperDisableGuiRestrictionsDescription=Kiểm soát xem một số hành động bị vô hiệu hóa có thể vẫn được thực thi từ giao diện người dùng hay không.\ndeveloperShowHiddenEntries=Hiển thị các mục ẩn\ndeveloperShowHiddenEntriesDescription=Khi được bật, các nguồn dữ liệu ẩn và nội bộ sẽ được hiển thị.\ndeveloperShowHiddenProviders=Hiển thị các nhà cung cấp ẩn\ndeveloperShowHiddenProvidersDescription=Kiểm soát việc hiển thị hay không hiển thị các nhà cung cấp kết nối và nguồn dữ liệu ẩn và nội bộ trong hộp thoại tạo.\ndeveloperDisableConnectorInstallationVersionCheck=Vô hiệu hóa kiểm tra phiên bản kết nối\ndeveloperDisableConnectorInstallationVersionCheckDescription=Kiểm soát xem trình kiểm tra cập nhật có bỏ qua số phiên bản khi kiểm tra phiên bản của kết nối XPipe được cài đặt trên máy tính từ xa hay không.\nshellCommandTest=Kiểm tra lệnh Shell\nshellCommandTestDescription=Chạy một lệnh trong phiên shell được sử dụng nội bộ bởi XPipe.\nterminal=Terminal\nterminalType=Trình mô phỏng terminal\nterminalConfiguration=Cấu hình thiết bị đầu cuối\nterminalCustomization=Tùy chỉnh giao diện người dùng\neditorConfiguration=Cấu hình trình soạn thảo\ndefaultApplication=Ứng dụng mặc định\ninitialSetup=Cài đặt ban đầu\nterminalTypeDescription=Terminal mặc định được sử dụng để mở kết nối shell.\\n\\nMức độ hỗ trợ tính năng khác nhau tùy theo terminal, và mỗi terminal được đánh dấu là được khuyến nghị hoặc không được khuyến nghị. Trải nghiệm của cậu sẽ tốt nhất khi sử dụng terminal được khuyến nghị.\nprogram=Chương trình\ncustomTerminalCommand=Lệnh terminal tùy chỉnh\ncustomTerminalCommandDescription=Lệnh để thực thi để mở terminal tùy chỉnh với lệnh đã cho.\\n\\nXPipe sẽ tạo một tập lệnh shell tạm thời cho terminal của bạn để thực thi. Chuỗi placeholder $CMD trong lệnh bạn cung cấp sẽ được thay thế bằng tập lệnh launcher thực tế khi được gọi. Hãy nhớ đặt dấu ngoặc kép cho đường dẫn thực thi của terminal nếu nó chứa khoảng trắng.\nclearTerminalOnInit=Xóa màn hình terminal khi khởi động\nclearTerminalOnInitDescription=Khi được kích hoạt, XPipe sẽ thực thi một lệnh xóa sau khi một phiên terminal mới được khởi chạy để loại bỏ bất kỳ đầu ra không cần thiết nào được in ra trong quá trình khởi động phiên terminal.\ndontCachePasswords=Đừng lưu trữ mật khẩu được yêu cầu nhập\ndontCachePasswordsDescription=Kiểm soát xem mật khẩu được yêu cầu có nên được lưu trữ tạm thời bên trong XPipe hay không, để cậu không cần nhập lại chúng trong phiên làm việc hiện tại.\\n\\nNếu tính năng này bị vô hiệu hóa, cậu sẽ phải nhập lại tất cả thông tin đăng nhập mỗi khi hệ thống yêu cầu.\ndenyTempScriptCreation=Từ chối tạo kịch bản tạm thời\ndenyTempScriptCreationDescription=Để thực hiện một số chức năng của mình, XPipe đôi khi tạo các tập lệnh shell tạm thời trên hệ thống đích để cho phép thực thi dễ dàng các lệnh đơn giản. Các tập lệnh này không chứa bất kỳ thông tin nhạy cảm nào và chỉ được tạo ra cho mục đích triển khai.\\n\\nNếu tính năng này bị vô hiệu hóa, XPipe sẽ không tạo bất kỳ tệp tạm thời nào trên hệ thống từ xa. Tùy chọn này hữu ích trong các môi trường bảo mật cao nơi mọi thay đổi hệ thống tệp đều được theo dõi. Nếu tùy chọn này bị vô hiệu hóa, một số chức năng, ví dụ: môi trường shell và kịch bản, sẽ không hoạt động như mong đợi.\ndisableCertutilUse=Vô hiệu hóa việc sử dụng certutil trên Windows\nuseLocalFallbackShell=Sử dụng vỏ lệnh dự phòng cục bộ\nuseLocalFallbackShellDescription=Chuyển sang sử dụng một vỏ lệnh cục bộ khác để xử lý các tác vụ cục bộ. Trên Windows, đó là PowerShell, và trên các hệ thống khác là bourne shell.\\n\\nTùy chọn này có thể được sử dụng trong trường hợp vỏ lệnh cục bộ mặc định bị vô hiệu hóa hoặc gặp sự cố. Tuy nhiên, một số tính năng có thể không hoạt động như mong đợi khi tùy chọn này được kích hoạt.\ndisableCertutilUseDescription=Do một số hạn chế và lỗi trong cmd.exe, các tập lệnh vỏ tạm thời được tạo bằng certutil bằng cách sử dụng nó để giải mã đầu vào base64, vì cmd.exe bị lỗi khi gặp đầu vào không phải ASCII. XPipe cũng có thể sử dụng PowerShell cho mục đích này nhưng điều này sẽ chậm hơn.\\n\\nĐiều này vô hiệu hóa việc sử dụng certutil trên hệ thống Windows để thực hiện một số chức năng và chuyển sang sử dụng PowerShell thay thế. Điều này có thể làm hài lòng một số phần mềm chống virus vì một số trong số chúng chặn việc sử dụng certutil.\ndisableTerminalRemotePasswordPreparation=Vô hiệu hóa việc chuẩn bị mật khẩu từ xa cho thiết bị đầu cuối\ndisableTerminalRemotePasswordPreparationDescription=Trong các tình huống cần thiết lập kết nối shell từ xa qua nhiều hệ thống trung gian trong terminal, có thể cần chuẩn bị các mật khẩu cần thiết trên một trong các hệ thống trung gian để cho phép tự động điền vào các yêu cầu nhập mật khẩu.\\n\\nNếu cậu không muốn mật khẩu bao giờ được chuyển đến bất kỳ hệ thống trung gian nào, cậu có thể vô hiệu hóa tính năng này. Mọi mật khẩu trung gian cần thiết sẽ được yêu cầu nhập trực tiếp trong terminal khi mở.\nmore=Thêm\ntranslate=Dịch\nallConnections=Tất cả các kết nối\nallScripts=Tất cả các kịch bản\nallIdentities=Tất cả các danh tính\nsynced=Đồng bộ hóa\npredefined=Được định nghĩa sẵn\nsamples=Ví dụ\ngoodMorning=Chào buổi sáng\ngoodAfternoon=Chào buổi chiều\ngoodEvening=Chào buổi tối\naddVisual=Hình ảnh ...\naddDesktop=Máy tính để bàn ...\nssh=SSH\nsshConfiguration=Cấu hình SSH\nsize=Kích thước\nattributes=Thuộc tính\nmodified=Sửa đổi\nowner=Chủ sở hữu\nupdateReadyTitle=Cập nhật sẵn sàng cho \" $VERSION$ \"\ntemplates=Mẫu\nretry=Thử lại\nretryAll=Thử lại tất cả\nreplace=Thay thế\nreplaceAll=Thay thế tất cả\nhibernateBehaviour=Hành vi ngủ đông\nhibernateBehaviourDescription=Quy định cách ứng dụng hoạt động khi hệ thống của cậu được đưa vào chế độ ngủ đông/ngủ.\noverview=Tổng quan\nhistory=Lịch sử\nskipAll=Bỏ qua tất cả\nnotes=Ghi chú\naddNotes=Thêm ghi chú\norder=Sắp xếp lại\nkeepFirst=Giữ nguyên\nkeepLast=Giữ lại\npinToTop=Ghim lên đầu\nunpinFromTop=Gỡ bỏ khỏi đầu trang\norderAheadOf=Đặt hàng trước ...\nclearIndex=Đặt lại chỉ số\nhttpServer=Máy chủ HTTP\nmcpServer=MCP server\napiKey=Khóa API\napiKeyDescription=Khóa API để xác thực các yêu cầu API của daemon XPipe. Để biết thêm thông tin về cách xác thực, hãy tham khảo tài liệu API chung.\ndisableApiAuthentication=Vô hiệu hóa xác thực API\ndisableApiAuthenticationDescription=Vô hiệu hóa tất cả các phương thức xác thực bắt buộc để bất kỳ yêu cầu không được xác thực nào cũng sẽ được xử lý.\\n\\nViệc vô hiệu hóa xác thực chỉ nên được thực hiện cho mục đích phát triển.\napi=API\nstoreIntroImportContent=Đã sử dụng XPipe trên một hệ thống khác? Đồng bộ hóa các kết nối hiện có của cậu trên nhiều hệ thống thông qua một kho lưu trữ Git từ xa. Cậu cũng có thể đồng bộ hóa sau này bất cứ lúc nào nếu chưa được thiết lập.\nstoreIntroImportButton=Đồng bộ hóa kết nối ...\nstoreIntroImportHeader=Nhập kết nối\nshowNonRunningChildren=Hiển thị các thành phần con không đang chạy\nhttpApi=Giao diện lập trình ứng dụng HTTP\nisOnlySupportedLimit=chỉ được hỗ trợ với giấy phép chuyên nghiệp khi có hơn một kết nối đồng thời ( $COUNT$ )\nareOnlySupportedLimit=chỉ được hỗ trợ với giấy phép chuyên nghiệp khi có hơn $COUNT$ kết nối\nenabled=Đã bật\nenableGitStoragePtbDisabled=Tính năng đồng bộ hóa Git đã bị vô hiệu hóa cho các bản dựng thử nghiệm công khai để ngăn chặn việc sử dụng với các kho lưu trữ Git của bản phát hành chính thức và để khuyến khích không sử dụng bản dựng PTB làm bản dựng chính hàng ngày.\ncopyId=Sao chép ID API\nrequireDoubleClickForConnections=Yêu cầu nhấp đúp để kết nối\nrequireDoubleClickForConnectionsDescription=Nếu được bật, cậu phải nhấp đúp vào các kết nối để khởi chạy chúng. Tính năng này hữu ích nếu cậu đã quen với việc nhấp đúp vào các đối tượng.\nclearTransferDescription=Chọn rõ ràng\nselectTab=Chọn tab\ncloseTab=Đóng tab\ncloseOtherTabs=Đóng các tab khác\ncloseAllTabs=Đóng tất cả các tab\ncloseLeftTabs=Đóng các tab ở bên trái\ncloseRightTabs=Đóng các tab ở bên phải\naddSerial=Nối tiếp ...\nconnect=Kết nối\nworkspaces=Không gian làm việc\nmanageWorkspaces=Quản lý không gian làm việc\naddWorkspace=Thêm không gian làm việc ...\nworkspaceAdd=Thêm một không gian làm việc mới\nworkspaceAddDescription=Các không gian làm việc là các cấu hình riêng biệt để chạy XPipe. Mỗi không gian làm việc có một thư mục dữ liệu nơi tất cả dữ liệu được lưu trữ cục bộ. Điều này bao gồm dữ liệu kết nối, cài đặt và nhiều hơn nữa.\\n\\nNếu bạn sử dụng tính năng đồng bộ hóa, bạn cũng có thể chọn đồng bộ hóa mỗi không gian làm việc với một kho lưu trữ Git khác nhau.\nworkspaceName=Tên không gian làm việc\nworkspaceNameDescription=Tên hiển thị của không gian làm việc\nworkspacePath=Đường dẫn không gian làm việc\nworkspacePathDescription=Vị trí của thư mục dữ liệu không gian làm việc\nworkspaceCreationAlertTitle=Tạo không gian làm việc\ndeveloperForceSshTty=Bắt buộc SSH TTY\ndeveloperForceSshTtyDescription=Đảm bảo tất cả các kết nối SSH được gán một pty để kiểm tra khả năng hỗ trợ cho stderr thiếu và pty.\ndeveloperDisableSshTunnelGateways=Vô hiệu hóa đường hầm cổng SSH\ndeveloperDisableSshTunnelGatewaysDescription=Không sử dụng phiên kết nối qua đường hầm cho các cổng và thay vào đó hãy kết nối trực tiếp với hệ thống.\nttyWarning=Kết nối đã được phân bổ bắt buộc một pty/tty và không cung cấp luồng stderr riêng biệt.\\n\\nĐiều này có thể dẫn đến một số vấn đề.\\n\\nNếu có thể, hãy xem xét việc điều chỉnh lệnh kết nối để không phân bổ pty.\nxshellSetup=Cài đặt Xshell\ntermiusSetup=Cài đặt Termius\ntryPtbDescription=Thử nghiệm các tính năng mới trong các phiên bản phát triển XPipe\nconfirmVaultUnencryptTitle=Xác nhận giải mã kho lưu trữ\nconfirmVaultUnencryptContent=Bạn có thực sự muốn vô hiệu hóa mã hóa kho lưu trữ nâng cao? Điều này sẽ loại bỏ mã hóa bổ sung cho dữ liệu được lưu trữ và ghi đè lên dữ liệu hiện có.\nenableHttpApi=Kích hoạt API HTTP\nenableHttpApiDescription=Cho phép API, cho phép các chương trình bên ngoài gọi daemon XPipe để thực hiện các thao tác với các kết nối được quản lý của bạn.\nchooseCustomIcon=Chọn biểu tượng tùy chỉnh\ngitVault=Kho lưu trữ Git\nfileBrowser=Trình duyệt tệp\nconfirmAllDeletions=Xác nhận tất cả các thao tác xóa\nconfirmAllDeletionsDescription=Có hiển thị hộp thoại xác nhận cho tất cả các thao tác xóa hay không. Theo mặc định, chỉ các thư mục yêu cầu xác nhận.\nyesterday=Hôm qua\ngreen=Xanh\nyellow=Vàng\nblue=Xanh\nred=Đỏ\ncyan=Cyan\npurple=Màu tím\nasktextAlertTitle=Hướng dẫn\nfileWriteSudoTitle=Viết tệp Sudo\nfileWriteSudoContent=Tệp cậu đang cố gắng ghi không cho phép quyền ghi cho tài khoản người dùng của cậu. Cậu có muốn ghi tệp này với quyền root bằng sudo không? Điều này sẽ tự động nâng quyền lên root bằng thông tin đăng nhập hiện có hoặc thông qua một hộp thoại yêu cầu.\ndontAllowTerminalRestart=Không cho phép khởi động lại thiết bị đầu cuối\ndontAllowTerminalRestartDescription=Theo mặc định, các phiên terminal có thể được khởi động lại sau khi kết thúc từ bên trong terminal. Để cho phép điều này, XPipe sẽ chấp nhận các yêu cầu từ terminal để khởi động lại phiên.\\n\\nXPipe không có quyền kiểm soát terminal và nguồn gốc của yêu cầu này, do đó các ứng dụng độc hại trên hệ thống có thể tận dụng tính năng này để thiết lập kết nối qua XPipe. Vô hiệu hóa tính năng này sẽ ngăn chặn tình huống này xảy ra.\nopenDocumentation=Tài liệu mở\nopenDocumentationDescription=Tham khảo trang tài liệu XPipe cho vấn đề này\nrenameAll=Đổi tên tất cả\nlogging=Ghi nhật ký\nenableTerminalLogging=Bật ghi nhật ký terminal\nenableTerminalLoggingDescription=Cho phép ghi nhật ký phía client cho tất cả các phiên terminal. Tất cả các đầu vào và đầu ra của phiên terminal được ghi vào tệp nhật ký phiên. Lưu ý rằng bất kỳ thông tin nhạy cảm nào như yêu cầu nhập mật khẩu sẽ không được ghi lại.\nterminalLoggingDirectory=Nhật ký phiên làm việc trên terminal\nterminalLoggingDirectoryDescription=Tất cả các bản ghi nhật ký được lưu trữ trong thư mục dữ liệu XPipe trên hệ thống cục bộ của cậu.\nopenSessionLogs=Mở nhật ký phiên\nsessionLogging=Ghi nhật ký thiết bị đầu cuối\nsessionActive=Một phiên nền đang chạy cho kết nối này.\\n\\nĐể dừng phiên này thủ công, hãy nhấp vào chỉ báo trạng thái.\nskipValidation=Bỏ qua xác thực\nscriptsIntroHeader=Về các kịch bản\nscriptsIntroContent=Cậu có thể chạy các skript trên shell init, trong trình duyệt tệp và theo yêu cầu. Cậu có thể tạo các skript của riêng mình trong XPipe hoặc nhập các skript hiện có từ hệ thống cục bộ của cậu hoặc từ một kho lưu trữ Git từ xa.\nscriptsIntroBottomHeader=Sử dụng kịch bản\nscriptsIntroBottomContent=Có nhiều mẫu kịch bản khác nhau để bắt đầu. Cậu có thể nhấp vào nút chỉnh sửa của từng kịch bản để xem cách chúng được thực hiện. Kịch bản phải được kích hoạt để chạy và hiển thị trong menu, mỗi kịch bản đều có công tắc bật/tắt cho mục đích đó.\nscriptsIntroBottomButton=Bắt đầu\nscriptSourcesIntroHeader=Nguồn kịch bản\nscriptSourcesIntroContent=Cậu có thể thêm các nguồn kịch bản tùy chỉnh để có thể truy cập ngay lập tức vào toàn bộ bộ sưu tập kịch bản shell. Cả nguồn cục bộ và kho lưu trữ Git từ xa đều được hỗ trợ làm nguồn. Tất cả các kịch bản được phát hiện từ nguồn sẽ tự động có sẵn.\nscriptSourcesIntroButton=Thêm nguồn ...\ncheckForSecurityUpdates=Kiểm tra các bản cập nhật bảo mật\ncheckForSecurityUpdatesDescription=XPipe có thể kiểm tra các bản cập nhật bảo mật tiềm ẩn một cách độc lập với các bản cập nhật tính năng thông thường. Khi tính năng này được bật, ít nhất các bản cập nhật bảo mật quan trọng sẽ được đề xuất cài đặt ngay cả khi kiểm tra cập nhật thông thường bị tắt.\\n\\nViệc tắt cài đặt này sẽ khiến không thực hiện yêu cầu phiên bản từ bên ngoài, và cậu sẽ không nhận được thông báo về bất kỳ bản cập nhật bảo mật nào.\nclickToDock=Nhấp để ghim terminal\nterminalStarting=Đang chờ khởi động terminal ...\npinTab=Ghim tab\nunpinTab=Gỡ ghim tab\npinned=Đã ghim\nenableConnectionHubTerminalDocking=Kích hoạt kết nối đầu cuối trung tâm\nenableConnectionHubTerminalDockingDescription=Cậu có thể ghim các cửa sổ terminal vào cửa sổ ứng dụng XPipe trong trung tâm kết nối để mô phỏng một terminal tích hợp một phần. Các cửa sổ terminal sau đó sẽ được XPipe quản lý để luôn vừa vặn với khung ghim.\nenableFileBrowserTerminalDocking=Kích hoạt tính năng gắn cửa sổ duyệt tệp vào thanh công cụ\nenableFileBrowserTerminalDockingDescription=Cậu có thể ghim các cửa sổ terminal vào cửa sổ ứng dụng XPipe trong trình duyệt tệp để mô phỏng một terminal tích hợp một phần. Các cửa sổ terminal sau đó sẽ được XPipe quản lý để luôn vừa vặn với khung ghim.\ndownloadsDirectory=Thư mục tải xuống tùy chỉnh\ndownloadsDirectoryDescription=Thư mục tùy chỉnh để lưu các tệp đã tải xuống khi nhấp vào nút \"Di chuyển đến thư mục tải xuống\". Theo mặc định, XPipe sẽ sử dụng thư mục tải xuống của người dùng.\npinLocalMachineOnStartup=Ghim tab máy cục bộ khi khởi động\npinLocalMachineOnStartupDescription=Tự động mở tab máy tính cục bộ và ghim nó. Tính năng này hữu ích nếu cậu thường xuyên sử dụng trình duyệt tệp chia đôi với cả hệ thống tệp cục bộ và hệ thống tệp từ xa đang mở.\nterminalErrorDescription=Lỗi này là lỗi nghiêm trọng và XPipe không thể tiếp tục hoạt động nếu không được khắc phục.\ngroupName=Tên nhóm\nchmodPermissions=Quyền truy cập mới\neditFilesWithDoubleClick=Chỉnh sửa tệp bằng cách nhấp đúp chuột\neditFilesWithDoubleClickDescription=Khi được kích hoạt, nhấp đúp vào tệp sẽ mở trực tiếp tệp đó trong trình soạn thảo văn bản của bạn thay vì hiển thị menu ngữ cảnh.\ncensorMode=Chế độ kiểm duyệt\ncensorModeDescription=Che mờ bất kỳ thông tin nào như tên máy chủ, tên người dùng, tên kết nối và hơn thế nữa.\\n\\nTính năng này hữu ích nếu cậu có ý định chụp màn hình hoặc chia sẻ màn hình XPipe và không muốn lộ bất kỳ thông tin nào.\naddIdentity=Tên...\nidentities=Thông tin nhận dạng\naddMacro=Hành động ...\nidentitiesIntroHeader=Về danh tính\nidentitiesIntroContent=Nếu cậu đang tái sử dụng các tổ hợp thông dụng của tên người dùng, mật khẩu và khóa, việc tạo các danh tính có thể tái sử dụng có thể là một giải pháp hợp lý. Điều này cho phép cậu nhanh chóng tham chiếu chúng khi thêm các kết nối mới.\nidentitiesIntroBottomHeader=Chia sẻ danh tính\nidentitiesIntroBottomContent=Cậu có thể thêm danh tính cục bộ hoặc đồng bộ hóa chúng trong kho lưu trữ Git khi tính năng này được kích hoạt. Điều này cho phép chia sẻ chọn lọc danh tính giữa các hệ thống khác nhau và với các thành viên khác trong nhóm.\nidentitiesIntroBottomButton=Cài đặt đồng bộ hóa\nidentitiesIntroButton=Tạo danh tính\nuserName=Tên người dùng\nuserAuth=Xác thực mật khẩu dựa trên người dùng\ngroupAuth=Xác thực bí mật dựa trên nhóm\nteam=Nhóm\nteamSettings=Cài đặt nhóm\nteamVaults=Kho lưu trữ của nhóm\nvaultTypeNameDefault=Kho lưu trữ mặc định\nvaultTypeNameLegacy=Kho lưu trữ cá nhân cũ\nvaultTypeNamePersonal=Kho lưu trữ cá nhân\nvaultTypeNameTeam=Kho lưu trữ của nhóm\nteamVaultsDescription=Các kho lưu trữ nhóm cho phép nhiều người dùng và nhóm truy cập an toàn vào một kho lưu trữ chung. Cậu có thể cấu hình kết nối và danh tính để chia sẻ cho tất cả người dùng hoặc chỉ cho phép truy cập cho từng người dùng và nhóm bằng cách mã hóa chúng bằng khóa riêng của họ. Các người dùng khác của kho lưu trữ không thể truy cập vào kết nối và danh tính cá nhân hoặc của nhóm nếu họ không có quyền truy cập vào khóa.\nvaultTypeContentDefault=Hiện tại, cậu đang sử dụng một kho lưu trữ mặc định không có tài khoản người dùng và mật khẩu tùy chỉnh. Các thông tin bí mật được mã hóa bằng khóa kho lưu trữ cục bộ. Cậu có thể nâng cấp lên kho lưu trữ cá nhân bằng cách tạo tài khoản người dùng kho lưu trữ. Điều này cho phép cậu mã hóa các thông tin bí mật trong kho lưu trữ bằng mật khẩu cá nhân của riêng mình, mà cậu phải nhập mỗi khi đăng nhập để mở khóa kho lưu trữ.\nvaultTypeContentLegacy=Hiện tại, cậu đang sử dụng một kho lưu trữ cá nhân cũ cho tài khoản của mình. Các thông tin bí mật được mã hóa bằng mật khẩu cá nhân của cậu. Tính tương thích với phiên bản cũ này có tính năng hạn chế và không thể nâng cấp lên kho lưu trữ nhóm ngay lập tức.\nvaultTypeContentPersonal=Hiện tại, cậu đang sử dụng một kho lưu trữ cá nhân cho tài khoản của mình. Các thông tin bí mật được mã hóa bằng mật khẩu cá nhân của cậu. Cậu có thể nâng cấp lên kho lưu trữ nhóm bằng cách thêm người dùng kho lưu trữ bổ sung hoặc thiết lập cấu hình truy cập dựa trên nhóm.\nvaultTypeContentTeam=Hiện tại, cậu đang sử dụng một kho lưu trữ nhóm, cho phép nhiều người dùng truy cập an toàn vào một kho lưu trữ chung. Cậu có thể cấu hình kết nối và danh tính để chia sẻ cho tất cả người dùng hoặc chỉ cho phép truy cập cho tài khoản cá nhân hoặc nhóm của cậu bằng cách mã hóa chúng bằng khóa cá nhân hoặc nhóm của cậu. Các người dùng khác trong kho lưu trữ không thể truy cập vào kết nối và danh tính cá nhân hoặc nhóm của cậu nếu họ không có quyền truy cập vào khóa.\ngroupManagement=Quản lý nhóm\ngroupManagementEmpty=Quản lý nhóm\ngroupManagementDescription=Quản lý các nhóm kho lưu trữ hiện có hoặc tạo mới. Mỗi nhóm kho lưu trữ có khóa bí mật riêng, được sử dụng để mã hóa kết nối và danh tính, chỉ dành riêng cho nhóm đó và không chia sẻ với bên ngoài.\ngroupManagementEmptyDescription=Quản lý các nhóm kho hiện có hoặc tạo mới. Mỗi nhóm kho có khóa bí mật riêng, được sử dụng để mã hóa kết nối và danh tính chỉ dành cho nhóm đó và không chia sẻ với người khác.\\n\\nTài khoản dựa trên nhóm cho đội ngũ được hỗ trợ trong gói chuyên nghiệp.\nuserManagement=Quản lý người dùng\nuserManagementEmpty=Quản lý người dùng\nuserManagementDescription=Quản lý người dùng kho hiện có hoặc tạo người dùng mới. Mỗi người dùng kho có mật khẩu riêng, được sử dụng để mã hóa kết nối và danh tính, chỉ có thể truy cập bởi chính người dùng đó và không cho phép người khác truy cập.\nuserManagementEmptyDescription=Quản lý người dùng kho hiện có hoặc tạo người dùng mới. Mỗi người dùng kho có mật khẩu riêng, được sử dụng để mã hóa kết nối và danh tính, chỉ có thể truy cập bởi chính người dùng đó và không chia sẻ với người khác. Tạo tài khoản cho bản thân để có thể mã hóa kết nối và danh tính bằng khóa cá nhân của mình.\\n\\nPhiên bản cộng đồng hỗ trợ một tài khoản người dùng duy nhất. Phiên bản chuyên nghiệp hỗ trợ nhiều tài khoản người dùng cho một nhóm.\nuserIntroHeader=Quản lý người dùng\nuserIntroContent=Tạo tài khoản người dùng đầu tiên cho bản thân để bắt đầu. Điều này cho phép cậu khóa không gian làm việc này bằng mật khẩu.\naddReusableIdentity=Thêm danh tính có thể tái sử dụng\nusers=Người dùng\nsyncVault=Đồng bộ hóa kho lưu trữ\nsyncVaultDescription=Để đồng bộ hóa kho lưu trữ của cậu với nhiều hệ thống khác nhau hoặc với nhiều thành viên trong nhóm, hãy kích hoạt tính năng đồng bộ hóa Git cho kho lưu trữ này.\nenableGitSync=Bật đồng bộ hóa Git\nbrowseVault=Dữ liệu kho\nbrowseVaultDescription=Bạn có thể tự mình xem thư mục kho lưu trữ trong trình quản lý tệp gốc của mình. Lưu ý rằng việc chỉnh sửa từ bên ngoài không được khuyến khích và có thể gây ra nhiều vấn đề.\nbrowseVaultButton=Duyệt kho lưu trữ\nvaultUsers=Người dùng kho lưu trữ\ncreateHeapDump=Tạo bản sao lưu bộ nhớ đống\ncreateHeapDumpDescription=Sao chép nội dung bộ nhớ vào tệp để khắc phục sự cố sử dụng bộ nhớ\ninitializingApp=Đang tải kết nối\ncheckingLicense=Kiểm tra giấy phép\nloadingGit=Đồng bộ hóa với kho lưu trữ Git\nloadingGpg=Khởi động dịch vụ GnuPG cho Git\nloadingSettings=Đang tải cài đặt\nloadingConnections=Đang tải kết nối\nunlockingVault=Mở khóa két sắt\nloadingUserInterface=Giao diện người dùng đang tải\nptbNotice=Thông báo về phiên bản thử nghiệm công khai\nuserDeletionTitle=Xóa người dùng\nuserDeletionContent=Cậu có muốn xóa người dùng kho lưu trữ này không? Điều này sẽ mã hóa lại tất cả thông tin cá nhân và thông tin kết nối của cậu bằng khóa kho lưu trữ có sẵn cho tất cả người dùng. Quá trình này sẽ mất một chút thời gian và XPipe sẽ khởi động lại để áp dụng các thay đổi của người dùng.\ngroupDeletionTitle=Xóa nhóm\ngroupDeletionContent=Cậu có muốn xóa nhóm kho này không? Điều này sẽ mã hóa lại tất cả các danh tính và bí mật kết nối chỉ dành cho nhóm bằng khóa kho có sẵn cho tất cả người dùng. Quá trình này sẽ mất một chút thời gian và XPipe sẽ khởi động lại để áp dụng các thay đổi của nhóm.\nkillTransfer=Ngắt kết nối\ndestination=Điểm đến\nconfiguration=Cấu hình\nnewFile=Tệp mới\nnewLink=Liên kết mới\nlinkName=Tên liên kết\nscanConnections=Tìm các kết nối có sẵn ...\nobserve=Bắt đầu quan sát\nstopObserve=Dừng quan sát\ncreateShortcut=Tạo lối tắt trên màn hình desktop\nbrowseFiles=Duyệt tệp\nclone=Sao chép\ntargetPath=Đường dẫn đích\nnewDirectory=Thư mục mới\ncopyShareLink=Sao chép liên kết\nselectStore=Chọn Cửa hàng\nsaveSource=Lưu lại để xem sau\nexecute=Thực thi\ndeleteChildren=Xóa tất cả các phần con\nscriptGroupDescriptionDescription=Cho nhóm này một mô tả tùy chọn\nabstractHostDescriptionDescription=Cho phép cậu thêm mô tả tùy chọn cho máy chủ này\nselectSource=Chọn nguồn\ncommandLineRead=Cập nhật\ncommandLineWrite=Viết\nadditionalOptions=Tùy chọn bổ sung\ninput=Đầu vào\nmachine=Máy\nopen=Mở\nedit=Chỉnh sửa\nscriptContents=Nội dung kịch bản\nscriptContentsDescription=Các lệnh kịch bản cần thực thi\nsnippets=Sự phụ thuộc của kịch bản\nsnippetsDescription=Các kịch bản khác cần chạy trước\nsnippetsDependenciesDescription=Tất cả các kịch bản có thể được thực thi nếu áp dụng\nisDefault=Chạy trên init trong tất cả các vỏ tương thích\nbringToShells=Áp dụng cho tất cả các vỏ tương thích\nisDefaultGroup=Chạy tất cả các skript nhóm trên lệnh khởi động shell\nexecutionType=Loại thực thi\nexecutionTypeDescription=Trong những trường hợp nào nên sử dụng kịch bản này?\nminimumShellDialect=Loại vỏ\nminimumShellDialectDescription=Loại vỏ lệnh để chạy kịch bản này\ndumbOnly=Dumb\nterminalOnly=Terminal\nboth=Cả hai\nshouldElevate=Nên nâng cao\nshouldElevateDescription=Có nên chạy skript này với quyền truy cập cao hơn không?\nscript.displayName=Tập lệnh shell\nscript.displayDescription=Tạo một kịch bản vỏ có thể tái sử dụng\nscriptGroup.displayName=Nhóm kịch bản\nscriptGroup.displayDescription=Nhóm các kịch bản lại với nhau và tổ chức chúng bên trong\nscriptGroup=Nhóm\nscriptGroupDescription=Nhóm cần gán kịch bản này cho\nscriptGroupGroupDescription=Nhóm cha tùy chọn để gán nhóm kịch bản này vào\nopenInNewTab=Mở trong tab mới\nexecuteInBackground=trong phần nền\nexecuteInTerminal=trong $TERM$\nback=Quay lại\nbrowseInWindowsExplorer=Duyệt trong Windows Explorer\nbrowseInDefaultFileManager=Duyệt trong trình quản lý tệp mặc định\nbrowseInFinder=Duyệt trong Finder\ncopy=Sao chép\npaste=Dán\ncopyLocation=Sao chép vị trí\nabsolutePaths=Đường dẫn tuyệt đối\nabsoluteLinkPaths=Đường dẫn liên kết tuyệt đối\nabsolutePathsQuoted=Đường dẫn tuyệt đối được trích dẫn\nfileNames=Tên tệp\nlinkFileNames=Tên tệp liên kết\nfileNamesQuoted=Tên tệp (Được trích dẫn)\ndeleteFile=Xóa $FILE$\neditWithEditor=Chỉnh sửa với $EDITOR$\nfollowLink=Theo liên kết\ngoForward=Tiếp tục\nshowDetails=Hiển thị chi tiết\nshowDetailsDescription=Hiển thị dấu vết lỗi\nopenFileWith=Mở bằng ...\nopenWithDefaultApplication=Mở bằng ứng dụng mặc định\nrename=Đổi tên\nrun=Chạy\nopenInTerminal=Mở trong terminal\nfile=Tệp\ndirectory=Thư mục\nsymbolicLink=Liên kết tượng trưng\ndesktopEnvironment.displayName=Môi trường máy tính để bàn\ndesktopEnvironment.displayDescription=Tạo cấu hình môi trường desktop từ xa có thể tái sử dụng\ndesktopHost=Máy chủ desktop\ndesktopHostDescription=Kết nối máy tính để bàn dùng làm cơ sở\ndesktopShellDialect=Ngôn ngữ vỏ lệnh\ndesktopShellDialectDescription=Ngôn ngữ vỏ (shell dialect) để chạy các skript và ứng dụng\ndesktopSnippets=Mẫu mã nguồn\ndesktopSnippetsDescription=Danh sách các đoạn mã có thể tái sử dụng để chạy trước tiên\ndesktopInitScript=Tập lệnh khởi tạo\ndesktopInitScriptDescription=Các lệnh khởi tạo cụ thể cho môi trường này\ndesktopTerminal=Ứng dụng đầu cuối\ndesktopTerminalDescription=Thiết bị đầu cuối cần sử dụng trên màn hình desktop để khởi chạy các tập lệnh\ndesktopApplication.displayName=Ứng dụng máy tính để bàn\ndesktopApplication.displayDescription=Chạy ứng dụng trên máy tính từ xa\ndesktopBase=Máy tính để bàn\ndesktopBaseDescription=Máy tính để bàn để chạy ứng dụng này\ndesktopEnvironmentBase=Môi trường máy tính để bàn\ndesktopEnvironmentBaseDescription=Môi trường desktop để chạy ứng dụng này\ndesktopApplicationPath=Đường dẫn ứng dụng\ndesktopApplicationPathDescription=Đường dẫn đến tệp thực thi cần chạy\ndesktopApplicationArguments=Tham số\ndesktopApplicationArgumentsDescription=Các tham số tùy chọn để truyền cho ứng dụng\ndesktopCommand.displayName=Lệnh trên màn hình desktop\ndesktopCommand.displayDescription=Chạy một lệnh trong môi trường máy tính từ xa\ndesktopCommandScript=Lệnh\ndesktopCommandScriptDescription=Các lệnh cần thực thi trong môi trường\nservice.displayName=Dịch vụ\nservice.displayDescription=Chuyển tiếp dịch vụ từ xa đến máy tính cục bộ của cậu\nserviceLocalPort=Cổng cục bộ rõ ràng\nserviceLocalPortDescription=Cổng cục bộ cần chuyển tiếp, nếu không sẽ sử dụng cổng ngẫu nhiên\nserviceRemotePort=Cổng từ xa\nserviceRemotePortDescription=Cổng mà dịch vụ đang chạy trên\nserviceHost=Máy chủ dịch vụ\nserviceHostDescription=Máy chủ đang chạy dịch vụ\nopenWebsite=Mở trang web\ncustomServiceGroup.displayName=Nhóm dịch vụ\ncustomServiceGroup.displayDescription=Nhóm nhiều dịch vụ vào một danh mục\ninitScript=Tập lệnh khởi động - Chạy trên shell init\nshellScript=Kịch bản phiên shell - Cho phép chạy kịch bản trong phiên shell\nrunnableScript=Kịch bản có thể chạy - Cho phép kịch bản được chạy trực tiếp từ trung tâm kết nối\nfileScript=Tập tin kịch bản - Cho phép kịch bản được gọi cho các tập tin đã chọn trong trình duyệt tập tin\nrunScript=Chạy kịch bản\ncopyUrl=Sao chép URL\nfixedServiceGroup.displayName=Nhóm dịch vụ\nfixedServiceGroup.displayDescription=Danh sách các dịch vụ có sẵn trên hệ thống\nmappedService.displayName=Dịch vụ\nmappedService.displayDescription=Tương tác với một dịch vụ được cung cấp bởi một container\ncustomService.displayName=Dịch vụ\ncustomService.displayDescription=Tự động mở hoặc tạo đường hầm cho cổng dịch vụ từ xa trên máy tính cục bộ của cậu\nfixedService.displayName=Dịch vụ\nfixedService.displayDescription=Sử dụng dịch vụ đã được định nghĩa sẵn\nnoServices=Không có dịch vụ nào khả dụng\nhasServices=$COUNT$ dịch vụ có sẵn\nhasService=$COUNT$ dịch vụ có sẵn\nnoConnections=Không có kết nối khả dụng\nhasConnections=$COUNT$ các kết nối có sẵn\nhasConnection=$COUNT$ kết nối có sẵn\nopenHttp=Mở dịch vụ HTTP\nopenHttps=Mở dịch vụ HTTPS\nnoScriptsAvailable=Không có kịch bản nào được kích hoạt và tương thích\nscriptsDisabled=Các kịch bản đã bị vô hiệu hóa\nchangeIcon=Thay đổi biểu tượng\ninit=Init\nshell=Shell\nhub=Hub\nscript=script\ngenericScript=Chung chung\ngradleTasks=Các tác vụ Gradle\nrunTask=Chạy tác vụ\narchiveName=Tên tệp lưu trữ\ncompress=Nén\ncompressContents=Nén nội dung\nuntarHere=Giải nén tại đây\nuntarDirectory=Giải nén tệp tar sang $DIR$\nunzipDirectory=Giải nén vào $DIR$\nunzipHere=Giải nén tại đây\nrequiresRestart=Yêu cầu khởi động lại để áp dụng.\ndownload=Tải xuống\nservicePath=Đường dẫn dịch vụ\nservicePathDescription=Đường dẫn phụ tùy chọn khi mở URL trong trình duyệt\nactive=Đang hoạt động\ninactive=Không hoạt động\nstarting=Bắt đầu\nremotePort=Cổng từ xa\nremotePortNumber=Cổng từ xa $PORT$\nuserIdentity=Thông tin cá nhân\nglobalIdentity=Danh tính toàn cầu\nidentityChoice=Thông tin nhận dạng người dùng\nidentityChoiceDescription=Chọn một danh tính đã được định nghĩa sẵn hoặc nhập thông tin đăng nhập chỉ cho kết nối này\ndefineNewIdentityOrSelect=Nhập mới hoặc chọn có sẵn\nlocalIdentity.displayName=Danh tính cục bộ\nlocalIdentity.displayDescription=Tạo một danh tính có thể tái sử dụng cho máy tính để bàn cục bộ này\nsyncedIdentity.displayName=Đồng bộ hóa danh tính\nsyncedIdentity.displayDescription=Tạo một danh tính có thể tái sử dụng và được đồng bộ hóa giữa các hệ thống\nlocalIdentity=Danh tính cục bộ\nkeyNotSynced=Tệp khóa chưa được đồng bộ hóa với kho lưu trữ Git. Hãy sử dụng nút \"Thêm vào Git\" cho tệp khóa để thêm nó.\nusernameDescription=Tên người dùng để đăng nhập\nidentity.displayName=Thông tin nhận dạng\nidentity.displayDescription=Tạo một danh tính có thể tái sử dụng cho các kết nối\nlocal=Địa phương\nshared=Toàn cầu\nuserDescription=Tên người dùng hoặc danh tính đã được định nghĩa trước để đăng nhập\nidentityAccessLevel=Mức độ truy cập\nidentityPerUser=Xác thực danh tính cá nhân\nidentityPerUserDescription=Hạn chế quyền truy cập vào danh tính này và các kết nối liên quan của nó chỉ cho người dùng kho lưu trữ của bạn\nidentityPerUserDisabled=Quyền truy cập danh tính cá nhân (đã tắt)\nidentityPerUserDisabledDescription=Hạn chế quyền truy cập vào danh tính này và các kết nối liên quan chỉ cho người dùng kho lưu trữ của bạn (Yêu cầu nhóm phải được cấu hình)\nidentityPerGroup=Quyền truy cập danh tính chỉ dành cho nhóm\nidentityPerGroupDescription=Hạn chế quyền truy cập vào danh tính này và các kết nối liên quan của nó chỉ trong nhóm kho lưu trữ này\nlibrary=Thư viện\nlocation=Vị trí\nkeyAuthentication=Xác thực dựa trên khóa\nkeyAuthenticationDescription=Phương thức xác thực cần sử dụng nếu yêu cầu xác thực dựa trên khóa\nlocationDescription=Đường dẫn tệp của khóa riêng tương ứng của cậu\nkeyFile=Tệp khóa cục bộ\nkeyPassword=Mật khẩu\nkey=Chìa khóa\nyubikeyPiv=Yubikey PIV\npageant=Cuộc thi sắc đẹp\ngpgAgent=GPG Agent\ncustomPkcs11Library=Thư viện PKCS#11 tùy chỉnh\nsshAgent=OpenSSH agent\nnone=Không có\nindex=Chỉ mục ...\notherExternal=Các tác nhân bên ngoài khác\nsync=Đồng bộ hóa\nvaultSync=Đồng bộ hóa kho lưu trữ\ncustomUsername=Tên người dùng\ncustomUsernameDescription=Tên người dùng thay thế tùy chọn để đăng nhập\ncustomUsernamePassword=Mật khẩu\ncustomUsernamePasswordDescription=Mật khẩu của người dùng để sử dụng khi yêu cầu xác thực sudo\nshowInternalPods=Hiển thị các pod nội bộ\nshowAllNamespaces=Hiển thị tất cả các không gian tên\nshowInternalContainers=Hiển thị các container bên trong\nrefresh=Làm mới\nvmwareGui=Bắt đầu giao diện người dùng đồ họa (GUI)\nmonitorVm=Giám sát máy ảo (VM)\naddCluster=Thêm cụm ...\nshowNonRunningInstances=Hiển thị các phiên bản không đang chạy\nvmwareGuiDescription=Có khởi động máy ảo ở chế độ nền hay trong cửa sổ.\nvmwareEncryptionPassword=Mật khẩu mã hóa\nvmwareEncryptionPasswordDescription=Mật khẩu tùy chọn được sử dụng để mã hóa máy ảo (VM).\nvmPasswordDescription=Mật khẩu yêu cầu cho người dùng khách.\nvmPassword=Mật khẩu của người dùng\nvmUser=Người dùng khách\nrunTempContainer=Chạy container tạm thời\nvmUserDescription=Tên người dùng của tài khoản khách chính của cậu\ndockerTempRunAlertTitle=Chạy container tạm thời\ndockerTempRunAlertHeader=Điều này sẽ chạy một quá trình shell trong một container tạm thời, sẽ tự động bị xóa sau khi quá trình đó dừng lại.\nimageName=Tên hình ảnh\nimageNameDescription=Mã định danh hình ảnh container để sử dụng\ncontainerName=Tên container\ncontainerNameDescription=Tên container tùy chỉnh (tùy chọn)\nvm=Máy ảo\nvmDescription=Tệp cấu hình liên quan.\nvmwareScan=Trình ảo hóa desktop VMware\nvmwareMachine.displayName=Máy ảo VMware\nvmwareMachine.displayDescription=Kết nối với máy ảo qua SSH\nvmwareInstallation.displayName=Cài đặt trình ảo hóa desktop VMware\nvmwareInstallation.displayDescription=Tương tác với các máy ảo đã cài đặt thông qua giao diện dòng lệnh (CLI) của chúng\nstart=Bắt đầu\nstop=Dừng\npause=Tạm dừng\nrdpTunnelHost=Máy chủ đích\nrdpTunnelHostDescription=Kết nối SSH để tạo đường hầm cho kết nối RDP đến\nrdpTunnelUsername=Tên người dùng\nrdpTunnelUsernameDescription=Tên người dùng tùy chỉnh để đăng nhập, sử dụng người dùng SSH nếu để trống\nrdpFileLocation=Vị trí tệp\nrdpFileLocationDescription=Đường dẫn tệp của tệp .rdp\nrdpPasswordAuthentication=Xác thực mật khẩu\nrdpFiles=Tệp RDP\nrdpPasswordAuthenticationDescription=Mật khẩu để nhập hoặc sao chép vào khay nhớ tạm, tùy thuộc vào hỗ trợ của ứng dụng khách\nrdpFile.displayName=Tệp RDP\nrdpFile.displayDescription=Kết nối với hệ thống thông qua tệp .rdp hiện có\nrequiredSshServerAlertTitle=Cài đặt máy chủ SSH\nrequiredSshServerAlertHeader=Không thể tìm thấy máy chủ SSH đã cài đặt trong máy ảo.\nrequiredSshServerAlertContent=Để kết nối với máy ảo (VM), XPipe đang tìm kiếm một máy chủ SSH đang chạy nhưng không phát hiện thấy máy chủ SSH nào khả dụng cho máy ảo.\ncomputerName=Tên máy tính\npssComputerNameDescription=Tên máy tính để kết nối\ncredentialUser=Người dùng có chứng chỉ\ncredentialUserDescription=Tên người dùng để đăng nhập.\ncredentialPassword=Mật khẩu xác thực\ncredentialPasswordDescription=Mật khẩu của người dùng.\nsshConfig=Tệp cấu hình SSH\nautostart=Tự động kết nối khi khởi động XPipe\nacceptHostKey=Chấp nhận khóa máy chủ\nmodifyHostKeyPermissions=Thay đổi quyền truy cập khóa máy chủ\nattachContainer=Đính kèm\ncontainerLogs=Hiển thị nhật ký\nopenSftpClient=Mở trong trình khách SFTP bên ngoài\nopenTermius=Mở trong Termius\nshowInternalInstances=Hiển thị các thực thể bên trong\neditPod=Chỉnh sửa pod\nacceptHostKeyDescription=Tin tưởng khóa máy chủ mới và tiếp tục\nmodifyHostKeyPermissionsDescription=Thử xóa quyền truy cập của tệp gốc để OpenSSH hoạt động bình thường\npsSession.displayName=Phiên làm việc từ xa PowerShell\npsSession.displayDescription=Kết nối qua New-PSSession và Enter-PSSession\nsshLocalTunnel.displayName=Kênh SSH cục bộ\nsshLocalTunnel.displayDescription=Thiết lập một đường hầm SSH đến một máy chủ từ xa\nsshRemoteTunnel.displayName=Kênh SSH từ xa\nsshRemoteTunnel.displayDescription=Tạo một đường hầm SSH ngược từ một máy chủ từ xa\nsshDynamicTunnel.displayName=Kênh SSH động\nsshDynamicTunnel.displayDescription=Thiết lập một proxy SOCKS thông qua kết nối SSH\nshellEnvironmentGroup.displayName=Môi trường vỏ lệnh\nshellEnvironmentGroup.displayDescription=Môi trường vỏ lệnh\nshellEnvironment.displayName=Môi trường vỏ lệnh\nshellEnvironment.displayDescription=Tạo môi trường khởi động tùy chỉnh cho vỏ lệnh\nshellEnvironment.informationFormat=$TYPE$ môi trường\nshellEnvironment.elevatedInformationFormat=$ELEVATION$ $TYPE$ môi trường\nenvironmentConnectionDescription=Kết nối cơ bản để tạo môi trường cho\nenvironmentScriptDescription=Tập lệnh khởi động tùy chỉnh tùy chọn để chạy trong vỏ lệnh\nenvironmentSnippets=Các tập lệnh shell\ncommandSnippetsDescription=Các tập lệnh vỏ (shell scripts) được định nghĩa sẵn và có thể tùy chọn để chạy trước\nenvironmentSnippetsDescription=Các tập lệnh shell được định nghĩa sẵn (tùy chọn) để chạy khi khởi động\nshellTypeDescription=Loại vỏ lệnh cụ thể để khởi chạy\noriginPort=Cổng nguồn\noriginAddress=Địa chỉ nguồn\nremoteAddress=Địa chỉ từ xa\nremoteSourceAddress=Địa chỉ nguồn từ xa\nremoteSourcePort=Cổng nguồn từ xa\noriginDestinationPort=Cổng nguồn và cổng đích\noriginDestinationAddress=Địa chỉ nguồn và đích\norigin=Nguồn gốc\nremoteHost=Máy chủ từ xa\naddress=Địa chỉ\nproxmox.displayName=Proxmox\nproxmox.displayDescription=Kết nối với các hệ thống trong môi trường ảo Proxmox\nproxmoxVm.displayName=Proxmox VM\nproxmoxVm.displayDescription=Kết nối với một máy ảo trong Proxmox VE qua SSH\nproxmoxContainer.displayName=Proxmox Container\nproxmoxContainer.displayDescription=Kết nối với một container trong Proxmox VE\nsshDynamicTunnel.hostDescription=Hệ thống được sử dụng làm proxy SOCKS\nsshDynamicTunnel.bindingDescription=Các địa chỉ cần gán cho đường hầm\nsshRemoteTunnel.hostDescription=Hệ thống để bắt đầu đường hầm từ xa đến nguồn gốc\nsshRemoteTunnel.bindingDescription=Các địa chỉ cần gán cho đường hầm\nsshLocalTunnel.hostDescription=Hệ thống mở đường hầm đến\nsshLocalTunnel.bindingDescription=Các địa chỉ cần gán cho đường hầm\nsshLocalTunnel.localAddressDescription=Địa chỉ cục bộ để kết nối\nsshLocalTunnel.remoteAddressDescription=Địa chỉ từ xa để kết nối\ncmd.displayName=Lệnh\ncmd.displayDescription=Thực thi một lệnh tùy ý trên hệ thống\nk8sPod.displayName=Pod Kubernetes\nk8sPod.displayDescription=Kết nối với pod và các container của nó thông qua kubectl\nk8sContainer.displayName=Kubernetes Container\nk8sContainer.displayDescription=Mở giao diện dòng lệnh vào container\nk8sCluster.displayName=Cụm Kubernetes\nk8sCluster.displayDescription=Kết nối với cụm và các pod của nó thông qua kubectl\nsshTunnelGroup.displayName=Kênh SSH\nsshTunnelGroup.displayCategory=Tất cả các loại đường hầm SSH\nlocal.displayName=Máy tính cục bộ\nlocal.displayDescription=Vỏ của máy tính cục bộ\ncygwin=Cygwin\nmsys2=MSYS2\ngitWindows=Git cho Windows\ngitForWindows.displayName=Git cho Windows\ngitForWindows.displayDescription=Truy cập môi trường Git For Windows cục bộ của cậu\nmsys2.displayName=MSYS2\nmsys2.displayDescription=Các giao diện dòng lệnh của môi trường MSYS2 của cậu\ncygwin.displayName=Cygwin\ncygwin.displayDescription=Các giao diện dòng lệnh của môi trường Cygwin của cậu\nnamespace=Không gian tên\ngitVaultIdentityStrategy=Thông tin nhận dạng Git SSH\ngitVaultIdentityStrategyDescription=Nếu cậu chọn sử dụng URL Git SSH làm kho lưu trữ từ xa và kho lưu trữ từ xa yêu cầu xác thực SSH, hãy thiết lập tùy chọn này.\\n\\nNếu cậu cung cấp URL HTTP, có thể bỏ qua tùy chọn này.\ndockerContainers=Các container Docker\ndockerCmd.displayName=khách hàng giao diện dòng lệnh Docker\ndockerCmd.displayDescription=Truy cập các container Docker thông qua trình khách CLI Docker\nwslCmd.displayName=Cài đặt WSL\nwslCmd.displayDescription=Truy cập các phiên bản WSL thông qua trình khách CLI wsl\nk8sCmd.displayName=kubectl client\nk8sCmd.displayDescription=Truy cập cụm Kubernetes thông qua kubectl\nk8sClusters=Cụm Kubernetes\nshells=Các vỏ lệnh có sẵn\ninspectContainer=Kiểm tra\ninspectContext=Kiểm tra\nk8sClusterNameDescription=Tên của bối cảnh mà cụm nằm trong đó.\npod=Pod\npodName=Tên pod\nk8sClusterContext=Bối cảnh\nk8sClusterContextDescription=Tên của bối cảnh mà cụm nằm trong\nk8sClusterNamespace=Không gian tên\nk8sClusterNamespaceDescription=Không gian tên tùy chỉnh hoặc không gian tên mặc định nếu trống\nk8sConfigLocation=Tệp cấu hình\nk8sConfigLocationDescription=Tệp kubeconfig tùy chỉnh hoặc tệp mặc định nếu để trống\ninspectPod=Kiểm tra\nshowAllContainers=Hiển thị các container không đang chạy\nshowAllPods=Hiển thị các pod không đang chạy\nk8sPodHostDescription=Máy chủ mà pod được đặt trên đó\nk8sContainerDescription=Tên của container Kubernetes\nk8sPodDescription=Tên của pod Kubernetes\npodDescription=Pod mà container được đặt trên đó\nk8sClusterHostDescription=Máy chủ mà qua đó cụm máy chủ cần được truy cập. Phải cài đặt và cấu hình kubectl để có thể truy cập cụm máy chủ.\nconnection=Kết nối\nshellCommand.displayName=Lệnh shell tùy chỉnh\nshellCommand.displayDescription=Mở một cửa sổ lệnh tiêu chuẩn thông qua một lệnh tùy chỉnh\nssh.displayName=Kết nối SSH\nssh.displayDescription=Kết nối với hệ thống từ xa thông qua trình khách dòng lệnh SSH\nsshConfig.displayName=Tệp cấu hình SSH\nsshConfig.displayDescription=Kết nối với các máy chủ được định nghĩa trong tệp cấu hình SSH\nsshConfigHost.displayName=Tệp cấu hình SSH cho máy chủ\nsshConfigHost.displayDescription=Kết nối với một máy chủ được định nghĩa trong tệp cấu hình SSH\nsshConfigHost.password=Mật khẩu\nsshConfigHost.passwordDescription=Nhập mật khẩu tùy chọn cho việc đăng nhập của người dùng.\nsshConfigHost.identityPassphrase=Mật khẩu chính\nsshConfigHost.identityPassphraseDescription=Nhập mật khẩu tùy chọn cho khóa của cậu.\nshellCommand.hostDescription=Máy chủ để thực thi lệnh\nshellCommand.commandDescription=Lệnh sẽ mở một cửa sổ lệnh\ncommandType=Loại lệnh\ncommandTypeDescription=Cách thực hiện lệnh\ncommandDescription=Các lệnh tùy chỉnh để thực thi trên máy chủ\ncommandHostDescription=Máy chủ để thực thi lệnh\ncommandDataFlowDescription=Cách lệnh này xử lý đầu vào và đầu ra\ncommandElevationDescription=Chạy lệnh này với quyền quản trị\ncommandShellTypeDescription=Vỏ lệnh để sử dụng cho lệnh này\nlimitedSystem=Đây là một hệ thống giới hạn hoặc nhúng\nlimitedSystemDescription=Đừng cố xác định loại vỏ lệnh, điều này chỉ cần thiết cho các hệ thống nhúng có tài nguyên hạn chế hoặc các thiết bị IoT\nsshForwardX11=Chuyển tiếp X11\nsshForwardX11Description=Cho phép chuyển tiếp X11 cho kết nối\ncustomAgent=Đại lý tùy chỉnh\nidentityAgent=Đại lý nhận dạng\nssh.proxyDescription=Máy chủ proxy tùy chọn để sử dụng khi thiết lập kết nối SSH. Phải cài đặt trình khách SSH.\nusage=Cách sử dụng\nwslHostDescription=Máy chủ mà phiên bản WSL được cài đặt. Phải cài đặt wsl.\nwslDistributionDescription=Tên của phiên bản WSL\nwslUsernameDescription=Tên người dùng rõ ràng để đăng nhập. Nếu không được chỉ định, tên người dùng mặc định sẽ được sử dụng.\nwslPasswordDescription=Mật khẩu của người dùng có thể được sử dụng cho các lệnh sudo.\ndockerHostDescription=Máy chủ mà container Docker được triển khai trên đó. Phải cài đặt Docker.\ndockerContainerDescription=Tên của container Docker\nlocalMachine=Máy tính cục bộ\nrootScan=Môi trường vỏ lệnh Sudo\nloginEnvironmentScan=Môi trường đăng nhập tùy chỉnh\nk8sScan=Cụm Kubernetes\noptions=Tùy chọn\ndockerRunningScan=Chạy các container Docker\ndockerAllScan=Tất cả các container Docker\nwslScan=Các phiên bản WSL\nsshScan=Cấu hình kết nối SSH\nrunAsUser=Chạy với tư cách người dùng\nrunAsUserDescription=Khởi động môi trường shell này với tư cách là một người dùng khác\ndefault=Mặc định\nadministrator=Quản trị viên\nwslHost=Máy chủ WSL\ntimeout=Thời gian chờ\ninstallLocation=Vị trí cài đặt\ninstallLocationDescription=Vị trí nơi môi trường máy chủ ảo ( $NAME$ ) của cậu được cài đặt\nwsl.displayName=Hệ thống con Windows cho Linux\nwsl.displayDescription=Kết nối với một phiên bản WSL đang chạy trên Windows\ndocker.displayName=Container Docker\ndocker.displayDescription=Kết nối với một container Docker\nport=Cổng\nuser=Người dùng\npassword=Mật khẩu\nmethod=Phương pháp\nuri=URL\nproxy=Proxy\ndistribution=Phân phối\nusername=Tên người dùng\nshellType=Loại vỏ\nbrowseFile=Duyệt tệp\nopenShell=Mở cửa sổ lệnh trong terminal\nopenCommand=Thực thi lệnh trong terminal\neditFile=Chỉnh sửa tệp\ndescription=Mô tả\nfurtherCustomization=Tùy chỉnh thêm\nfurtherCustomizationDescription=Để xem thêm các tùy chọn cấu hình, hãy sử dụng các tệp cấu hình SSH\nbrowse=Duyệt\nconfigHost=Máy chủ\nconfigHostDescription=Máy chủ chứa cấu hình\nconfigLocation=Vị trí cấu hình\nconfigLocationDescription=Đường dẫn tệp của tệp cấu hình\ngateway=Cổng\ngatewayDescription=Cổng kết nối tùy chọn để sử dụng khi kết nối\nconnectionInformation=Thông tin kết nối\nconnectionInformationDescription=Hệ thống nào cần kết nối\npasswordAuthentication=Xác thực mật khẩu\npasswordAuthenticationDescription=Mật khẩu tùy chọn để xác thực\nsshConfigString.displayName=Kết nối SSH dựa trên cấu hình\nsshConfigString.displayDescription=Tạo kết nối SSH tùy chỉnh hoàn toàn theo định dạng cấu hình SSH\nsshConfigStringContent=Cấu hình\nsshConfigStringContentDescription=Các tùy chọn SSH cho kết nối trong định dạng cấu hình OpenSSH\nvnc.displayName=Kết nối VNC qua SSH\nvnc.displayDescription=Mở một phiên VNC qua kết nối được mã hóa\nbinding=Liên kết\nvncPortDescription=Cổng mà máy chủ VNC đang lắng nghe\nrdpPortDescription=Cổng mà máy chủ RDP đang lắng nghe\nvncUsername=Tên người dùng\nvncUsernameDescription=Tên người dùng VNC tùy chọn\nvncPassword=Mật khẩu\nvncPasswordDescription=Mật khẩu VNC\nx11WslInstance=Chuyển tiếp X11 sang phiên bản WSL\nx11WslInstanceDescription=Hệ điều hành Linux trong Windows (WSL) được sử dụng làm máy chủ X11 khi sử dụng chuyển tiếp X11 trong kết nối SSH. Hệ điều hành này phải là phiên bản WSL2.\nopenAsRoot=Mở với quyền root\nopenInWSL=Mở trong WSL\nlaunch=Khởi chạy\nsshTrustKeyContent=Khóa máy chủ không được biết, và cậu đã bật xác minh khóa máy chủ thủ công. $CONTENT$\nsshTrustKeyTitle=Khóa máy chủ không xác định\nrdpTunnel.displayName=Kết nối RDP qua SSH\nrdpTunnel.displayDescription=Kết nối qua RDP qua kết nối được mã hóa\nrdpEnableDesktopIntegration=Bật tích hợp desktop\nrdpEnableDesktopIntegrationDescription=Chạy các ứng dụng từ xa giả sử rằng danh sách cho phép RDP cho phép điều đó\nrdpSetupAdminTitle=Cần thiết lập RDP\nrdpSetupAllowTitle=Ứng dụng điều khiển từ xa RDP\nrdpSetupAllowContent=Hiện tại, việc khởi chạy các ứng dụng từ xa trực tiếp không được phép trên hệ thống này. Bạn có muốn kích hoạt tính năng này không? Việc này sẽ cho phép bạn khởi chạy các ứng dụng từ xa trực tiếp từ XPipe bằng cách vô hiệu hóa danh sách cho phép cho các ứng dụng RDP từ xa.\nrdpServerEnableTitle=Máy chủ RDP\nrdpServerEnableContent=Máy chủ RDP đã bị vô hiệu hóa trên hệ thống đích. Bạn có muốn kích hoạt nó trong sổ đăng ký để cho phép kết nối RDP từ xa không?\nrdp=RDP\nrdpScan=Kênh RDP qua SSH\nwslX11SetupTitle=Cài đặt WSL X11\nwslX11SetupContent=XPipe có thể sử dụng phân phối WSL cục bộ của cậu để hoạt động như một máy chủ hiển thị X11. Cậu có muốn cài đặt X11 trên $DIST$ không? Điều này sẽ cài đặt các gói cơ bản của X11 trên phân phối WSL và có thể mất một chút thời gian. Cậu cũng có thể thay đổi phân phối được sử dụng trong menu cài đặt.\ncommand=Lệnh\ncommandGroup=Nhóm lệnh\nvncSystem=Hệ thống đích VNC\nvncSystemDescription=Hệ thống thực tế để tương tác. Đây thường là cùng một hệ thống với máy chủ đường hầm\nvncHost=Máy chủ VNC đích\nvncHostDescription=Hệ thống mà máy chủ VNC đang chạy trên đó\nvncDirectHost=Máy chủ\nvncDirectHostDescription=Địa chỉ máy chủ hoặc địa chỉ được nhập thủ công của máy chủ mà trên đó máy chủ VNC đang chạy\nrdpDirectHost=Máy chủ\nrdpDirectHostDescription=Địa chỉ máy chủ hoặc địa chỉ được chỉ định thủ công của máy chủ mà trên đó máy chủ RDP đang chạy\ngitVaultTitle=Kho lưu trữ Git\ngitVaultForcePushContent=Bạn có muốn đẩy bắt buộc lên kho lưu trữ từ xa không? Điều này sẽ thay thế hoàn toàn nội dung của kho lưu trữ từ xa bằng nội dung cục bộ của bạn, bao gồm cả lịch sử.\ngitVaultOverwriteLocalContent=Bạn có muốn ghi đè các thay đổi trong kho lưu trữ cục bộ của mình không? Điều này sẽ áp dụng tất cả các thay đổi từ kho lưu trữ từ xa vào kho lưu trữ cục bộ của bạn.\nrdpSimple.displayName=Kết nối RDP trực tiếp\nrdpSimple.displayDescription=Kết nối với máy chủ qua RDP\nrdpUsername=Tên người dùng\nrdpUsernameDescription=Tên người dùng để đăng nhập. Có thể bao gồm tiền tố miền\naddressDescription=Nơi kết nối đến\nrdpAdditionalOptions=Các tùy chọn RDP bổ sung\nrdpAdditionalOptionsDescription=Các tùy chọn RDP thô cần bao gồm, định dạng giống như trong các tệp .rdp\nproxmoxVncConfirmTitle=Truy cập VNC\nproxmoxVncConfirmContent=Bạn có muốn kích hoạt truy cập VNC cho máy ảo (VM) không? Điều này sẽ cho phép truy cập trực tiếp từ client VNC trong tệp cấu hình của VM và khởi động lại máy ảo.\ndockerContext.displayName=Bối cảnh Docker\ndockerContext.displayDescription=Tương tác với các container nằm trong một bối cảnh cụ thể\nvmActions=Các hành động của máy ảo (VM)\ndockerContextActions=Các hành động trong ngữ cảnh\nk8sPodActions=Hành động của pod\nopenVnc=Bật quyền truy cập VNC\naddVnc=Thêm kết nối VNC\ncommandGroup.displayName=Nhóm lệnh\ncommandGroup.displayDescription=Nhóm các lệnh có sẵn cho hệ thống\nserial.displayName=Kết nối nối tiếp\nserial.displayDescription=Mở kết nối serial trong terminal\nserialPort=Cổng nối tiếp\nserialPortDescription=Cổng nối tiếp / thiết bị cần kết nối\nbaudRate=Tốc độ truyền\ndataBits=Bit dữ liệu\nstopBits=Bit dừng\nparity=Parity\nflowControlWindow=Kiểm soát luồng\nserialImplementation=Thực hiện theo thứ tự\nserialImplementationDescription=Công cụ dùng để kết nối với cổng nối tiếp\nserialHost=Máy chủ\nserialHostDescription=Hệ thống truy cập cổng nối tiếp trên\nserialPortConfiguration=Cấu hình cổng nối tiếp\nserialPortConfigurationDescription=Thông số cấu hình của thiết bị nối tiếp được kết nối\nserialInformation=Thông tin nối tiếp\nopenXShell=Mở trong XShell\ntsh.displayName=Dịch chuyển tức thời\ntsh.displayDescription=Kết nối với các nút teleport của cậu qua tsh\ntshNode.displayName=Nút truyền tải\ntshNode.displayDescription=Kết nối với một nút teleport trong một cụm\nteleportCluster=Cluster\nteleportClusterDescription=Cụm mà nút thuộc về\nteleportProxy=Proxy\nteleportProxyDescription=Máy chủ proxy được sử dụng để kết nối với nút\nteleportHost=Máy chủ\nteleportHostDescription=Tên máy chủ của nút\nteleportUser=Người dùng\nteleportUserDescription=Người dùng đăng nhập với tư cách là\nlogin=Đăng nhập\nhyperVInstall.displayName=Hyper-V\nhyperVInstall.displayDescription=Kết nối với các máy ảo (VMs) được quản lý bởi Hyper-V\nhyperVVm.displayName=Máy ảo Hyper-V\nhyperVVm.displayDescription=Kết nối với máy ảo Hyper-V qua SSH hoặc PSSession\ntrustHost=Máy chủ tin cậy\ntrustHostDescription=Thêm Tên máy tính vào danh sách máy chủ tin cậy\ncopyIp=Sao chép địa chỉ IP\nvncDirect.displayName=Kết nối VNC trực tiếp\nvncDirect.displayDescription=Kết nối trực tiếp với hệ thống qua VNC\neditConfiguration=Chỉnh sửa cấu hình\nviewInDashboard=Xem trong bảng điều khiển\nsetDefault=Đặt mặc định\nremoveDefault=Xóa mặc định\nconnectAsOtherUser=Đăng nhập với tư cách người dùng khác\nprovideUsername=Cung cấp tên người dùng thay thế để đăng nhập\nvmIdentity=Thông tin người dùng\nvmIdentityDescription=Phương thức xác thực danh tính SSH để sử dụng khi cần thiết để kết nối\nvmPort=Cổng\nvmPortDescription=Cổng để kết nối qua SSH\nforwardAgent=Đại lý chuyển tiếp\nforwardAgentDescription=Cho phép sử dụng danh tính SSH agent trên hệ thống từ xa\nvirshUri=URI\nvirshUriDescription=URI của hypervisor, các tên gọi khác cũng được hỗ trợ\nvirshDomain.displayName=libvirt domain\nvirshDomain.displayDescription=Kết nối với miền libvirt\nvirshHypervisor.displayName=libvirt hypervisor\nvirshHypervisor.displayDescription=Kết nối với trình điều khiển hypervisor được hỗ trợ bởi libvirt\nvirshInstall.displayName=khách hàng dòng lệnh libvirt\nvirshInstall.displayDescription=Kết nối với tất cả các hypervisor libvirt có sẵn thông qua virsh\naddHypervisor=Thêm hypervisor\ninteractiveTerminal=Terminal tương tác\neditDomain=Chỉnh sửa miền\nlibvirt=các miền libvirt\ncustomIp=IP tùy chỉnh\ncustomIpDescription=Vượt qua phát hiện địa chỉ IP cục bộ mặc định nếu cậu sử dụng mạng nâng cao\nautomaticallyDetect=Tự động phát hiện\nuserAddDialogTitle=Tạo tài khoản người dùng\ngroupAddDialogTitle=Tạo nhóm\npassphrase=Mật khẩu\nrepeatPassphrase=Nhập lại mật khẩu\ngroupSecret=Mật khẩu nhóm\nrepeatGroupSecret=Mật khẩu nhóm lặp lại\nvaultGroup=Nhóm kho lưu trữ\nloginAlertTitle=Yêu cầu đăng nhập\nloginAlertHeader=Mở khóa két sắt để truy cập các kết nối cá nhân của bạn\nvaultUser=Người dùng kho lưu trữ\nme=Me\naddGroup=Thêm nhóm ...\naddGroupDescription=Tạo một nhóm mới cho kho lưu trữ này\naddUser=Thêm người dùng ...\naddUserDescription=Tạo một người dùng mới cho kho lưu trữ này\nskip=Bỏ qua\nuserChangePasswordAlertTitle=Thay đổi mật khẩu\ngroupChangeSecretAlertTitle=Thay đổi mật khẩu\ndocs=Tài liệu\nlxd.displayName=LXD Container\nlxd.displayDescription=Kết nối với container LXD thông qua lxc\nlxdCmd.displayName=Khách hàng giao diện dòng lệnh LXD\nlxdCmd.displayDescription=Truy cập các container LXD thông qua trình khách dòng lệnh lxc\npodman.displayName=Podman Container\npodman.displayDescription=Kết nối với container Podman\nincusInstall.displayName=Quản lý máy Incus\nincusInstall.displayDescription=Truy cập các container Incus thông qua trình khách CLI Incus\nincusContainer.displayName=Container Incus\nincusContainer.displayDescription=Kết nối với một container Incus\npodmanCmd.displayName=Podman CLI\npodmanCmd.displayDescription=Truy cập các container Podman thông qua trình khách CLI\nlxdHostDescription=Máy chủ chứa container LXD. Phải cài đặt lxc.\nlxdContainerDescription=Tên của container LXD\npodmanContainers=Podman container\nlxdContainers=Container LXD\nincusContainers=Các container Incus\ncontainer=Container\nhost=Máy chủ\ncontainerActions=Các hành động của container\nserialConsole=Bảng điều khiển nối tiếp\neditRunConfiguration=Chỉnh sửa cấu hình chạy\ncommunityDescription=Một công cụ kết nối mạnh mẽ, hoàn hảo cho các nhu cầu sử dụng cá nhân của bạn.\nupgradeDescription=Quản lý kết nối chuyên nghiệp cho toàn bộ hạ tầng máy chủ của bạn.\ndiscoverPlans=Khám phá các tùy chọn nâng cấp\nextendProfessional=Nâng cấp lên các tính năng chuyên nghiệp mới nhất\ncommunityItem1=Kết nối không giới hạn với các hệ thống và công cụ không thương mại\ncommunityItem2=Tích hợp mượt mà với các thiết bị đầu cuối và trình soạn thảo đã cài đặt của cậu\ncommunityItem3=Trình duyệt tệp từ xa đầy đủ tính năng\ncommunityItem4=Hệ thống kịch bản mạnh mẽ cho tất cả các vỏ lệnh\ncommunityItem5=Tích hợp Git để đồng bộ hóa và chia sẻ thông tin kết nối\nupgradeItem1=Bao gồm tất cả các tính năng của phiên bản cộng đồng\nupgradeItem2=Gói Homelab hỗ trợ số lượng không giới hạn hypervisor và các tính năng SSH nâng cao\nupgradeItem3=Gói Professional hỗ trợ thêm các hệ điều hành và công cụ dành cho doanh nghiệp\nupgradeItem4=Gói Enterprise cung cấp sự linh hoạt hoàn toàn cho các trường hợp sử dụng cụ thể của cậu\nupgrade=Nâng cấp\nupgradeTitle=Các gói dịch vụ có sẵn\nstatus=Trạng thái\ntype=Loại\nlicenseAlertTitle=Yêu cầu giấy phép\nuseCommunity=Tiếp tục với cộng đồng\npreviewDescription=Hãy thử nghiệm các tính năng mới trong vài tuần sau khi phát hành.\ntryPreview=Kích hoạt xem trước\npreviewItem1=Quyền truy cập đầy đủ vào các tính năng chuyên nghiệp mới được phát hành trong vòng 2 tuần sau khi phát hành\npreviewItem2=Thử nghiệm các tính năng mới mà không cần cam kết\nlicensedTo=Được cấp phép cho\nemail=Địa chỉ email\napply=Áp dụng\nclear=Xóa\nactivate=Kích hoạt\nvalidUntil=Hiệu lực đến\nlicenseActivated=Giấy phép đã được kích hoạt\nrestart=Khởi động lại\nlockVault=Khoá két sắt\nrestartApp=Khởi động lại XPipe\nfree=Miễn phí\nupgradeInfo=Cậu có thể tìm thấy thông tin về việc nâng cấp lên giấy phép ở phần dưới đây.\nupgradeInfoPreview=Cậu có thể tìm thông tin về việc nâng cấp lên bản quyền bên dưới hoặc thử nghiệm bản xem trước.\nenterLicenseKey=Nhập khóa cấp phép để nâng cấp\nisOnlySupported=chỉ được hỗ trợ với giấy phép $TYPE$\nareOnlySupported=chỉ được hỗ trợ với giấy phép sử dụng phần mềm không thương mại ( $TYPE$ )\nlegacyLicense=Giấy phép này chỉ bao gồm các tính năng chuyên nghiệp mới được phát hành trong vòng một năm kể từ ngày mua.\npreviewExpiredLicense=Tính năng này gần đây đã được cung cấp miễn phí trong giai đoạn thử nghiệm, nhưng giai đoạn này hiện đã kết thúc.\nopenApiDocs=Tài liệu API\nopenApiDocsDescription=Tài liệu API HTTP có sẵn trực tuyến, bao gồm cả thông số kỹ thuật OpenAPI .yaml. Bạn có thể mở nó trong trình duyệt web của mình hoặc trình khách HTTP ưa thích.\nopenApiDocsButton=Mở tài liệu\npythonApi=Giao diện lập trình ứng dụng (API) Python\npersonalConnection=Kết nối này và tất cả các kết nối con của nó chỉ khả dụng cho người dùng của bạn vì chúng phụ thuộc vào danh tính cá nhân.\ndeveloperPrintInitFiles=In tệp khởi tạo\ndeveloperPrintInitFilesDescription=In tất cả các tập lệnh khởi động shell được chạy khi một terminal được khởi động.\ndeveloperShowSensitiveCommands=Ghi lại các lệnh nhạy cảm\ndeveloperShowSensitiveCommandsDescription=Bao gồm các lệnh nhạy cảm trong đầu ra nhật ký để gỡ lỗi.\ncheckingForUpdates=Kiểm tra cập nhật\ncheckingForUpdatesDescription=Lấy thông tin phiên bản mới nhất\ndownloadingUpdate=Lấy phiên bản ( $VERSION$)\ndownloadingUpdateDescription=Tải xuống gói phát hành\nupdateNag=Bạn chưa cập nhật XPipe trong một thời gian. Bạn có thể đang bỏ lỡ các tính năng mới và bản sửa lỗi của các phiên bản mới hơn.\nupdateNagTitle=Nhắc nhở cập nhật\nupdateNagButton=Xem các phiên bản\nrefreshServices=Làm mới dịch vụ\nserviceProtocolType=Loại giao thức dịch vụ\nserviceProtocolTypeDescription=Kiểm soát cách mở dịch vụ\nserviceCommand=Lệnh để thực thi sau khi dịch vụ đã được kích hoạt\nserviceCommandDescription=Biến $PORT sẽ được thay thế bằng cổng cục bộ thực tế được chuyển tiếp\nvalue=Giá trị\nshowAdvancedOptions=Hiển thị tùy chọn nâng cao\nsshAdditionalConfigOptions=Các tùy chọn cấu hình bổ sung\nremoteFileManager=Quản lý tệp từ xa\nclearUserData=Xóa dữ liệu người dùng\nclearUserDataDescription=Xóa tất cả dữ liệu cấu hình của người dùng, bao gồm cả kết nối\nclearUserDataTitle=Xóa dữ liệu người dùng\nclearUserDataContent=Thao tác này sẽ xóa toàn bộ dữ liệu người dùng cục bộ của xpipe và khởi động lại. Nếu cậu quan tâm đến các kết nối của mình, hãy đảm bảo đồng bộ hóa chúng trước với kho lưu trữ Git.\nundefined=Chưa được định nghĩa\ncopyAddress=Sao chép địa chỉ\nnetbirdDeviceScan=Kết nối Netbird\nnetbirdId=Khóa công khai của đối tác\nnetbirdIdDescription=ID khóa công khai nội bộ Netbird của đối tác\ntailscaleDeviceScan=Kết nối Tailscale\ntailscaleInstall.displayName=Cài đặt Tailscale\ntailscaleInstall.displayDescription=Kết nối với các thiết bị trong mạng tailnet của cậu qua SSH\ntailscaleDevice.displayName=Thiết bị Tailscale\ntailscaleDevice.displayDescription=Kết nối với một thiết bị trong mạng tailnet của cậu qua SSH\ntailscaleId=ID thiết bị\ntailscaleIdDescription=ID thiết bị Tailscale nội bộ\ntailscaleHostName=Tên máy chủ\ntailscaleHostNameDescription=Tên máy chủ của thiết bị trong mạng tailnet\ntailscaleUsername=Tên người dùng\ntailscaleUsernameDescription=Người dùng đăng nhập với tư cách là\ntailscalePassword=Mật khẩu\ntailscalePasswordDescription=Mật khẩu người dùng tùy chọn có thể được sử dụng cho sudo\nscriptName=Tên kịch bản\nscriptNameDescription=Đặt tên tùy chỉnh cho kịch bản này\nscriptGroupName=Tên nhóm kịch bản\nscriptGroupNameDescription=Đặt tên tùy chỉnh cho nhóm kịch bản này\nidentityName=Tên nhận dạng\nidentityNameDescription=Đặt tên tùy chỉnh cho danh tính này\ntailscaleTailnet.displayName=Tailnet\ntailscaleTailnet.displayDescription=Kết nối với một mạng tailnet cụ thể bằng tài khoản của cậu\nputtyConnections=Kết nối PuTTY\nkittyConnections=Kết nối KiTTY\nicons=Biểu tượng\ncustomIcons=Biểu tượng tùy chỉnh\niconSources=Nguồn biểu tượng\niconSourcesDescription=Bạn có thể thêm nguồn biểu tượng của riêng mình tại đây. XPipe sẽ tự động nhận các tệp .svg tại vị trí đã thêm và thêm chúng vào bộ biểu tượng có sẵn.\\n\\nCả thư mục cục bộ và kho lưu trữ Git từ xa đều được hỗ trợ làm vị trí lưu trữ biểu tượng.\nrefreshSources=Làm mới biểu tượng\nrefreshSourcesDescription=Cập nhật tất cả các biểu tượng từ các nguồn có sẵn\naddDirectoryIconSource=Thêm nguồn thư mục ...\naddDirectoryIconSourceDescription=Thêm biểu tượng từ thư mục cục bộ\naddGitIconSource=Thêm nguồn Git ...\naddGitIconSourceDescription=Thêm biểu tượng nằm trong kho lưu trữ Git từ xa\nrepositoryUrl=URL kho lưu trữ Git\niconDirectory=Thư mục biểu tượng\naddUnsupportedKexMethod=Thêm phương thức trao đổi khóa không được hỗ trợ\naddUnsupportedKexMethodDescription=Cho phép sử dụng phương thức trao đổi khóa $VAL$ cho kết nối này\naddUnsupportedHostKeyType=Thêm loại khóa máy chủ không được hỗ trợ\naddUnsupportedHostKeyTypeDescription=Cho phép sử dụng loại khóa máy chủ \" $VAL$ \" cho kết nối này\naddUnsupportedMacType=Thêm loại MAC không được hỗ trợ\naddUnsupportedMacTypeDescription=Cho phép sử dụng loại MAC \" $VAL$ \" cho kết nối này\nrunSilent=chạy ngầm trong nền\nrunInFileBrowser=trong trình duyệt tệp\nrunInConnectionHub=trong bộ chuyển mạch\ncommandOutput=Kết quả thực thi lệnh\niconSourceDeletionTitle=Nguồn biểu tượng xóa\niconSourceDeletionContent=Bạn có muốn xóa nguồn biểu tượng này và tất cả các biểu tượng liên quan đến nó không?\nrefreshIcons=Làm mới biểu tượng\nrefreshIconsDescription=Tải xuống, hiển thị và lưu trữ tất cả hơn 1000 biểu tượng có sẵn từ các nguồn bên ngoài dưới dạng tệp .png. Quá trình này có thể mất một chút thời gian ...\nvaultUserLegacy=Người dùng kho lưu trữ (Chế độ tương thích ngược giới hạn)\nupgradeInstructions=Hướng dẫn nâng cấp\nexternalActionTitle=Yêu cầu thực hiện hành động bên ngoài\nexternalActionContent=Một hành động từ bên ngoài đã được yêu cầu. Bạn có muốn cho phép thực thi các hành động từ bên ngoài XPipe không?\nnoScriptStateAvailable=Làm mới để xác định tính tương thích của kịch bản ...\ndocumentationDescription=Xem tài liệu hướng dẫn\ncustomEditorCommandInTerminal=Chạy lệnh tùy chỉnh trong terminal\ncustomEditorCommandInTerminalDescription=Nếu trình soạn thảo của cậu là trình soạn thảo dựa trên terminal, cậu có thể bật tùy chọn này để tự động mở terminal và chạy lệnh trong phiên terminal thay vì trong trình soạn thảo.\\n\\nCậu có thể sử dụng tùy chọn này cho các trình soạn thảo như vi, vim, nvim và các trình soạn thảo khác.\ndisableHttpsTlsCheck=Vô hiệu hóa xác minh chứng chỉ yêu cầu HTTPS\ndisableHttpsTlsCheckDescription=Nếu tổ chức của cậu đang giải mã lưu lượng HTTPS trong tường lửa bằng cách sử dụng tính năng chặn SSL, các kiểm tra cập nhật hoặc kiểm tra giấy phép sẽ thất bại do chứng chỉ không khớp nhau. Cậu có thể khắc phục điều này bằng cách bật tùy chọn này và tắt tính năng xác thực chứng chỉ TLS.\nconnectionsSelected=$NUMBER$ các kết nối đã chọn\naddConnections=Thêm kết nối\nbrowseDirectory=Duyệt thư mục\nopenTerminal=Mở terminal\ndocumentation=Tài liệu\nreport=Báo cáo lỗi\nkeePassXcNotAssociated=Liên kết KeePassXC\nkeePassXcNotAssociatedDescription=XPipe không được liên kết với cơ sở dữ liệu KeePassXC cục bộ của cậu. Nhấp vào bên dưới để thực hiện bước liên kết XPipe với cơ sở dữ liệu KeePassXC một lần duy nhất, để XPipe có thể truy vấn mật khẩu.\nkeePassXcAssociateMore=Kết nối nhiều cơ sở dữ liệu hơn\nkeePassXcAssociateMoreDescription=Cậu có thể kết nối với nhiều cơ sở dữ liệu KeePassXC cùng một lúc\nkeePassXcAssociated=KeePassXC liên kết\nkeePassXcAssociatedDescription=XPipe được kết nối với các cơ sở dữ liệu KeePassXC cục bộ sau:\nkeePassXcNotAssociatedButton=Kết nối cơ sở dữ liệu\nidentifier=Mã định danh\npasswordManagerCommand=Lệnh tùy chỉnh\npasswordManagerCommandDescription=Lệnh tùy chỉnh để thực thi nhằm lấy mật khẩu. Chuỗi placeholder $KEY sẽ được thay thế bằng khóa mật khẩu được trích dẫn khi lệnh được gọi. Lệnh này nên gọi trình quản lý mật khẩu CLI của bạn để in mật khẩu ra stdout, ví dụ: mypassmgr get $KEY.\nchooseTemplate=Chọn mẫu\nkeePassXcPlaceholder=URL mục KeePassXC\nterminalEnvironment=Môi trường terminal\nterminalEnvironmentDescription=Nếu cậu muốn sử dụng các tính năng của môi trường WSL dựa trên Linux cục bộ cho việc tùy chỉnh terminal, cậu có thể sử dụng chúng làm môi trường terminal.\\n\\nTất cả các lệnh khởi động terminal tùy chỉnh và cấu hình trình đa nhiệm terminal sẽ được thực thi trong phân phối WSL này.\nterminalInitScript=Tập lệnh khởi động terminal\nterminalInitScriptDescription=Các lệnh cần thực thi trong môi trường terminal trước khi kết nối được thiết lập. Bạn có thể sử dụng điều này để cấu hình môi trường terminal khi khởi động.\nterminalMultiplexer=Bộ ghép kênh đầu cuối\nterminalMultiplexerDescription=Bộ multiplexer terminal được sử dụng như một giải pháp thay thế cho tab trong terminal. Điều này sẽ thay thế một số tính năng xử lý terminal, ví dụ như xử lý tab, bằng chức năng của bộ multiplexer.\\n\\nYêu cầu tệp thực thi của bộ multiplexer tương ứng phải được cài đặt trên hệ thống.\nterminalMultiplexerWindowsDescription=Bộ multiplexer terminal để sử dụng thay thế cho tab trong terminal. Điều này sẽ thay thế một số tính năng xử lý terminal, ví dụ như xử lý tab, bằng chức năng của bộ multiplexer.\\n\\nYêu cầu sử dụng môi trường terminal WSL trên Windows và tệp thực thi của bộ multiplexer phải được cài đặt trên hệ thống WSL.\nterminalAlwaysPauseOnExit=Luôn tạm dừng khi thoát\nterminalAlwaysPauseOnExitDescription=Khi được kích hoạt, việc thoát khỏi phiên terminal sẽ luôn yêu cầu bạn chọn giữa khởi động lại hoặc đóng phiên. Nếu bị vô hiệu hóa, XPipe chỉ thực hiện điều này đối với các kết nối bị lỗi và thoát với thông báo lỗi.\nquerying=Tra cứu ...\nretrievedPassword=Được lấy từ: $PASSWORD$\nrefreshOpenpubkey=Cập nhật thông tin nhận dạng khóa công khai đang mở\nrefreshOpenpubkeyDescription=Chạy lệnh opkssh refresh để làm cho danh tính openpubkey hợp lệ lại\nall=Tất cả\nterminalPrompt=Lệnh đầu cuối\nterminalPromptDescription=Công cụ nhắc lệnh terminal để sử dụng trên các terminal từ xa của cậu. Kích hoạt nhắc lệnh terminal sẽ tự động cài đặt và cấu hình công cụ nhắc lệnh trên hệ thống đích khi mở phiên terminal.\\n\\nĐiều này không thay đổi bất kỳ cấu hình nhắc lệnh hiện có hoặc tệp cấu hình nào trên hệ thống. Điều này sẽ làm tăng thời gian tải terminal lần đầu tiên trong quá trình cài đặt nhắc lệnh trên hệ thống từ xa. Terminal của cậu có thể cần thêm phông chữ để hiển thị nhắc lệnh chính xác.\nterminalPromptConfiguration=Cấu hình lời nhắc terminal\nterminalPromptConfig=Tệp cấu hình\nterminalPromptConfigDescription=Tệp cấu hình tùy chỉnh để áp dụng cho lời nhắc. Tệp cấu hình này sẽ được tự động thiết lập trên hệ thống đích khi terminal được khởi tạo và được sử dụng làm cấu hình lời nhắc mặc định.\\n\\nNếu cậu muốn sử dụng tệp cấu hình mặc định hiện có trên mỗi hệ thống, cậu có thể để trống trường này.\npasswordManagerKey=Khóa quản lý mật khẩu\npasswordManagerKeyDescription=Mã định danh của trình quản lý mật khẩu cho thông tin bí mật\npasswordManagerAgent=Trình quản lý mật khẩu\ndockerComposeProject.displayName=Dự án Docker Compose\ndockerComposeProject.displayDescription=Nhóm các container của một dự án compose lại với nhau\nsshVerboseOutput=Bật chế độ hiển thị chi tiết đầu ra SSH\nsshVerboseOutputDescription=Điều này sẽ hiển thị nhiều thông tin gỡ lỗi khi kết nối qua SSH. Có ích cho việc khắc phục sự cố với kết nối SSH.\ndontUseGateway=Không sử dụng cổng\ndontUseGatewayDescription=Đừng sử dụng máy chủ hypervisor làm cổng và kết nối trực tiếp đến địa chỉ IP\ncategoryColor=Màu sắc danh mục\ncategoryColorDescription=Màu mặc định được sử dụng cho các kết nối trong danh mục này\ncategorySync=Đồng bộ hóa với kho lưu trữ Git\ncategorySyncDescription=Tự động đồng bộ hóa tất cả kết nối với kho lưu trữ Git. Tất cả thay đổi cục bộ đối với kết nối sẽ được đẩy lên kho lưu trữ từ xa.\ncategorySyncSpecial=Đồng bộ hóa với kho lưu trữ Git\\n(Không thể cấu hình cho danh mục đặc biệt \"$NAME$\")\ncategoryDontAllowScripts=Vô hiệu hóa tất cả các thay đổi\ncategoryDontAllowScriptsDescription=Vô hiệu hóa việc thực thi lệnh và các thao tác khác trên các hệ thống thuộc danh mục này để ngăn chặn bất kỳ sự thay đổi nào. Điều này sẽ vô hiệu hóa toàn bộ chức năng kịch bản, lệnh môi trường shell, lời nhắc và nhiều tính năng khác.\ncategoryConfirmAllModifications=Xác nhận tất cả các thay đổi\ncategoryConfirmAllModificationsDescription=Xác nhận bất kỳ thay đổi nào đối với kết nối hoặc hệ thống tệp trước khi thực hiện. Điều này có thể ngăn chặn các thao tác vô ý trên các hệ thống quan trọng.\ncategoryDefaultIdentity=Danh tính mặc định\ncategoryDefaultIdentityDescription=Nếu cậu thường xuyên sử dụng một danh tính cụ thể trên nhiều hệ thống trong danh mục này, việc thiết lập danh tính mặc định sẽ cho phép cậu chọn sẵn nó khi tạo kết nối mới.\ncategoryConfigTitle=$NAME$ cấu hình\nconfigure=Cấu hình\naddConnection=Thêm kết nối\nnoCompatibleConnection=Không tìm thấy kết nối tương thích\nnoCompatibleIdentity=Không tìm thấy danh tính tương thích\nnewCategory=Danh mục mới\ndockerComposeRestricted=Dự án compose bị giới hạn bởi $NAME$ và không thể được sửa đổi từ bên ngoài. Vui lòng sử dụng $NAME$ để quản lý dự án compose này.\nrestricted=Hạn chế\ndisableSshPinCaching=Vô hiệu hóa lưu trữ mã PIN SSH\ndisableSshPinCachingDescription=XPipe sẽ tự động lưu trữ các mã PIN đã nhập cho một khóa khi sử dụng một hình thức xác thực dựa trên phần cứng.\\n\\nViệc vô hiệu hóa tính năng này sẽ khiến bạn phải nhập lại mã PIN mỗi khi cố gắng kết nối.\ngitSyncPull=Kéo để đồng bộ hóa các thay đổi từ kho lưu trữ Git từ xa\nenpassVaultFile=Tệp kho lưu trữ\nenpassVaultFileDescription=Tệp kho lưu trữ Enpass cục bộ.\nflat=Phẳng\nrecursive=Đệ quy\nrdpAllowListBlocked=Ứng dụng RemoteApp đã chọn dường như không có trong danh sách cho phép RDP của máy chủ.\npsonoServerUrl=URL máy chủ\npsonoServerUrlDescription=Địa chỉ URL của máy chủ backend psono\npsonoApiKey=Khóa API\npsonoApiKeyDescription=Khóa API cần sử dụng, được định dạng dưới dạng UUID\npsonoApiSecretKey=Khóa bí mật API\npsonoApiSecretKeyDescription=Khóa bí mật API dưới dạng chuỗi hex 64 byte\npassboltServerUrl=URL máy chủ\npassboltServerUrlDescription=URL của máy chủ backend Passbolt\npassboltPassphrase=Mật khẩu\npassboltPassphraseDescription=Mật khẩu cho khóa riêng của két sắt\npassboltPrivateKey=Khóa riêng\npassboltPrivateKeyDescription=Tệp khóa GPG riêng tư cho kho lưu trữ\nfocusWindowOnNotifications=Cửa sổ tập trung vào thông báo\nfocusWindowOnNotificationsDescription=Hiển thị XPipe ở phía trước khi có thông báo hoặc thông báo lỗi xuất hiện, ví dụ như khi kết nối hoặc đường hầm bị ngắt kết nối đột ngột.\ngitUsername=Tên người dùng Git tùy chỉnh\ngitUsernameDescription=Người dùng tùy chỉnh để xác thực với kho lưu trữ Git từ xa. Theo mặc định, XPipe sẽ sử dụng thông tin đăng nhập hiện tại của giao diện dòng lệnh Git (Git CLI).\\n\\nCài đặt này sẽ ghi đè lên bất kỳ thông tin đăng nhập mặc định nào đã được cấu hình cho khách hàng Git CLI cục bộ của cậu.\ngitPassword=Mật khẩu Git tùy chỉnh / Token truy cập cá nhân\ngitPasswordDescription=Mật khẩu hoặc mã truy cập cá nhân dùng để xác thực. Việc cậu cần sử dụng mật khẩu hay mã truy cập cá nhân phụ thuộc vào nhà cung cấp git remote. Cài đặt này sẽ ghi đè lên bất kỳ thông tin đăng nhập mặc định nào đã được cấu hình cho trình khách git CLI cục bộ của cậu.\nsetReadOnly=Đặt chế độ chỉ đọc\nunsetReadOnly=Bỏ chọn chế độ chỉ đọc\nreadOnlyStoreError=Cấu hình của mục này đã bị khóa. Hãy chọn một tên khác để lưu các thay đổi của cậu vào một bản sao mới.\ncategoryFreeze=Đóng băng cấu hình kết nối\ncategoryFreezeDescription=Đánh dấu cấu hình kết nối là chỉ đọc. Điều này có nghĩa là không thể sửa đổi bất kỳ cấu hình kết nối hiện có nào trong danh mục này. Tuy nhiên, vẫn có thể thêm kết nối mới.\nupdateFail=Quá trình cập nhật cài đặt không thành công\nupdateFailAction=Cài đặt bản cập nhật thủ công\nupdateFailActionDescription=Xem các bản phát hành mới nhất trên GitHub\nonePasswordPlaceholder=Tên mục hoặc URL op://\ncomputeDirectorySizes=Tính kích thước thư mục\ncomputeSize=Tính toán kích thước\ncustomSpiceCommand=Lệnh tùy chỉnh\ncustomSpiceCommandDescription=Lệnh tùy chỉnh để thực thi khi khởi chạy các phiên SPICE. Chuỗi placeholder $FILE sẽ được thay thế bằng đường dẫn tệp được trích dẫn đến tệp .vv khi được gọi.\nvncClient=Khách hàng VNC\nvncClientDescription=Khách hàng VNC sẽ được khởi chạy khi mở kết nối VNC trong XPipe.\\n\\nBạn có thể chọn sử dụng khách hàng VNC tích hợp sẵn trong XPipe hoặc khởi chạy một khách hàng VNC bên ngoài đã được cài đặt trên máy tính của bạn nếu bạn muốn tùy chỉnh nhiều hơn.\nintegratedXPipeVncClient=Khách hàng XPipe VNC tích hợp\ncustomVncCommand=Lệnh tùy chỉnh\ncustomVncCommandDescription=Lệnh tùy chỉnh để thực thi khi khởi chạy các phiên VNC. Chuỗi placeholder $ADDRESS sẽ được thay thế bằng địa chỉ được trích dẫn khi được gọi.\nvncConnections=Kết nối VNC\npasswordManagerIdentity=Quản lý mật khẩu\npasswordManagerIdentity.displayName=Quản lý mật khẩu\npasswordManagerIdentity.displayDescription=Lấy tên người dùng và mật khẩu của một tài khoản từ trình quản lý mật khẩu của cậu\npasswordCopied=Mật khẩu kết nối đã được sao chép vào khay nhớ tạm\nerrorOccurred=Xảy ra lỗi\nactionMacro.displayName=Hành động macro\nactionMacro.displayDescription=Chạy trong thực tế bằng cách sử dụng các trình kích hoạt tùy chỉnh\nmacroAdd=Thêm macro\nmacroName=Tên macro\nmacroNameDescription=Đặt tên tùy chỉnh cho macro này\nactionId=ID hành động\nactionIdDescription=Hành động cần thực hiện với macro này\nmacroRefs=Các kết nối liên quan\nmacroRefsDescription=Các kết nối cần thiết để thực hiện hành động\nconnectionCopy=Sao chép\nactionPickerTitle=Chọn hành động\nactionPickerDescription=Nhấp vào một mục để thực hiện một hành động. Thay vì thực hiện hành động, cậu có thể tạo và chỉnh sửa các phím tắt cho hành động đó trong chế độ chọn phím tắt hành động.\ncancelActionPicker=Hủy thao tác chọn\nactionShortcut=Phím tắt hành động\nactionShortcuts=Phím tắt\nactionStore=Lưu trữ hành động\nactionStoreDescription=Mục nhập trong cửa hàng để thực hiện hành động trên\nactionStores=Hành động lưu trữ\nactionStoresDescription=Các mục trong kho để thực thi hành động trên\nactionDesktopShortcut=Lối tắt trên màn hình\nactionDesktopShortcutDescription=Tạo một lối tắt cho hành động này trên màn hình desktop của cậu\nactionUrlShortcut=Liên kết rút gọn URL\nactionUrlShortcutDescription=Sao chép một URL có thể kích hoạt các hành động này khi được mở\nactionUrlShortcutDisabled=Liên kết rút gọn URL (Không khả dụng)\nactionUrlShortcutDisabledDescription=Loại cài đặt \" $TYPE$ \" không hỗ trợ mở các liên kết URL\nactionApiCall=Yêu cầu API\nactionApiCallDescription=Gọi hành động này từ API HTTP\nactionMacro=Hành động macro\nactionMacroDescription=Tạo một macro có chức năng nâng cao cho thao tác này\ncreateMacro=Tạo macro\nactionConfiguration=Tham số\nactionConfigurationDescription=Các tham số cần truyền cho hành động được thực thi\nconfirmAction=Xác nhận hành động\nactionConnections=Kết nối hành động\nactionConnectionsDescription=Các kết nối để thực hiện hành động trên\nactionConnection=Kết nối hành động\nactionConnectionDescription=Kết nối để thực hiện hành động trên\nappleContainerInstall.displayName=Apple containers\nappleContainerInstall.displayDescription=Truy cập các bản sao container của Apple thông qua giao diện dòng lệnh container (CLI)\nappleContainer.displayName=Apple container\nappleContainer.displayDescription=Truy cập các bản sao container của Apple thông qua giao diện dòng lệnh container (CLI)\nappleContainerHostDescription=Máy chủ mà container Apple được triển khai trên đó\nappleContainerDescription=Tên của container Apple\nappleContainers=Apple containers\nchangeOrderIndexTitle=Thay đổi thứ tự\norderIndex=Chỉ mục\norderIndexDescription=Chỉ mục rõ ràng để sắp xếp mục này so với các mục khác. Các chỉ mục thấp nhất được hiển thị ở trên cùng, các chỉ mục cao nhất ở dưới cùng\nmoveToFirst=Di chuyển lên đầu tiên\nmoveToLast=Di chuyển đến cuối\ncategory=Thể loại\nincludeRoot=Bao gồm gốc\nexcludeRoot=Loại trừ thư mục gốc\nfreezeConfiguration=Đóng băng cấu hình\nunfreezeConfiguration=Bỏ khóa cấu hình\nwaylandScalingTitle=Tỷ lệ thu phóng Wayland\nactionApiUrl=$URL$ (Sao chép nội dung JSON)\ncopyBody=Sao chép nội dung yêu cầu\ngitRepoTerminalOpen=Mở kho lưu trữ trong terminal\ngitRepoTerminalOpenDescription=Hãy tự mình xem kho lưu trữ bằng lệnh dòng lệnh\ngitRepoOverwriteLocal=Ghi đè kho lưu trữ cục bộ\ngitRepoOverwriteLocalDescription=Thay thế tất cả các thay đổi cục bộ bằng các thay đổi từ máy chủ từ xa\ngitRepoForcePush=Ghi đè kho lưu trữ từ xa\ngitRepoForcePushDescription=Sử dụng lệnh git push --force để áp dụng các thay đổi cục bộ của cậu lên máy chủ từ xa\ngitRepoDontWarn=Đừng cảnh báo nữa\ngitRepoDontWarnDescription=Nếu điều này được mong đợi, hãy làm cho XPipe bỏ qua lỗi này trong tương lai\ngitRepoTryAgain=Thử lại\ngitRepoTryAgainDescription=Thử thực hiện lại thao tác tương tự\ngitRepoEnablePlain=Sử dụng đồng bộ thư mục thông thường\ngitRepoEnablePlainDescription=Đừng khởi tạo kho lưu trữ Git để đồng bộ hóa các thay đổi vào thư mục\ngitRepoCreateBare=Sử dụng git sync\ngitRepoCreateBareDescription=Tạo một kho lưu trữ Git trống mới trong thư mục đồng bộ hóa\ngitRepoDisable=Tạm thời vô hiệu hóa Git Vault\ngitRepoDisableDescription=Không thực hiện bất kỳ thay đổi nào trong phiên này\ngitRepoPullRefresh=Kéo các thay đổi và làm mới\ngitRepoPullRefreshDescription=Ghép các thay đổi từ xa và tải lại dữ liệu\nbreakOutCategory=Phân loại\nmergeCategory=Ghép danh mục\nopenWinScp=Mở trong WinSCP\nuninstallApplication=Gỡ cài đặt\nuninstallApplicationDescription=Chạy tệp .pkg để thực thi skript cài đặt và gỡ cài đặt hoàn toàn XPipe\nk8sEditPodTitle=Áp dụng các thay đổi\nk8sEditPodContent=Bạn có muốn áp dụng các thay đổi đã thực hiện thông qua lệnh kubectl apply không? Có thể cần khởi động lại để các thay đổi có hiệu lực.\nvirshEditDomainTitle=Áp dụng các thay đổi\nvirshEditDomainContent=Cậu có muốn áp dụng các thay đổi cho miền không? Có thể cần khởi động lại để các thay đổi có hiệu lực.\npkcs11Library=Thư viện PKCS#11\npkcs11LibraryDescription=Đường dẫn của tệp thư viện liên kết động\nsshAgentSocket=Cổng socket đại lý SSH tùy chỉnh\nsshAgentSocketDescription=Cổng tùy chỉnh để sử dụng để giao tiếp với trình đại lý SSH. Trình đại lý tùy chỉnh này có thể được sử dụng cho kết nối bằng cách chọn tùy chọn trình đại lý tùy chỉnh cho nó.\npublicKey=Mã định danh khóa công khai\npublicKeyDescription=Khóa công khai tùy chọn để buộc đại lý chỉ cung cấp khóa riêng tư khớp\nactions=Các thao tác\nhcloudServer.displayName=Máy chủ đám mây Hetzner\nhcloudServer.displayDescription=Kết nối đến máy chủ được lưu trữ trên đám mây Hetzner thông qua SSH\nhcloudInstall.displayName=Giao diện dòng lệnh Hetzner Cloud (CLI)\nhcloudInstall.displayDescription=Truy cập các máy chủ được lưu trữ trên đám mây Hetzner thông qua hcloud\nhcloudContext.displayName=bối cảnh hcloud\nhcloudContext.displayDescription=Truy cập các máy chủ trong bối cảnh hcloud\nmetrics=Chỉ số\nopenInVsCode=Mở trong VsCode\naddCloud=Đám mây ...\nhcloudToken=hcloud token\nhcloudTokenDescription=Token đám mây Hetzner cần sử dụng. Để biết thêm thông tin, xem tài liệu hướng dẫn\nhcloudLogin=Đăng nhập Hetzner Cloud\nclearHcloudToken=Xóa token hcloud\nclearHcloudTokenDescription=Xóa token hiện có để có thể đăng nhập lại\nselectIdentity=Chọn danh tính\nenableMcpServer=Bật máy chủ MCP\nenableMcpServerDescription=Kích hoạt máy chủ XPipe MCP, cho phép các khách hàng MCP bên ngoài gửi yêu cầu đến máy chủ MCP. Xem chi tiết cấu hình bên dưới.\\n\\nLưu ý rằng API HTTP không cần phải được kích hoạt để sử dụng chức năng MCP.\nenableMcpMutationTools=Bật công cụ biến đổi MCP\nenableMcpMutationToolsDescription=Theo mặc định, chỉ các công cụ chỉ đọc được kích hoạt trên máy chủ MCP. Điều này nhằm đảm bảo rằng không có thao tác vô tình nào có thể thực hiện và tiềm ẩn nguy cơ thay đổi hệ thống.\\n\\nNếu cậu có kế hoạch thực hiện thay đổi trên hệ thống thông qua các client MCP, hãy đảm bảo rằng client MCP của cậu đã được cấu hình để xác nhận bất kỳ hành động tiềm ẩn nguy hiểm nào trước khi kích hoạt tùy chọn này. Yêu cầu kết nối lại tất cả các client MCP để áp dụng thay đổi.\nmcpClientConfigurationDetails=Cấu hình client MCP\nmcpClientConfigurationDetailsDescription=Sử dụng dữ liệu cấu hình này để kết nối với máy chủ XPipe MCP từ ứng dụng khách MCP mà cậu chọn.\nswitchHostAddress=Thay đổi địa chỉ máy chủ\naddAnotherHostName=Thêm tên máy chủ khác\naddNetwork=Quét mạng ...\nnetworkScan=Quét mạng\nnetworkScanStore=Máy chủ đích\nnetworkScanStoreDescription=Máy chủ cần quét mạng cục bộ\nuseAsGateway=Sử dụng máy chủ làm cổng mặc định\nuseAsGatewayDescription=Có nên sử dụng máy chủ đích làm cổng cho các kết nối được tạo ra hay không\nnetworkScanPorts=Các cổng cần quét\nnetworkScanPortsDescription=Danh sách các cổng được phân tách bằng dấu phẩy để bao gồm trong quá trình quét\nnetworkScanType=Loại kết nối\nnetworkScanTypeDescription=Loại máy chủ cần tìm kiếm\nemptyDirectory=Thư mục này dường như trống rỗng\nhcloudConfigFile=tệp cấu hình hcloud\nhcloudConfigFileDescription=Vị trí của tệp cấu hình .toml của hcloud CLI\npreferMonochromeIcons=Ưu tiên sử dụng biểu tượng đơn sắc\npreferMonochromeIconsDescription=Khi được bật, các biến biểu tượng đơn sắc sẽ được ưu tiên sử dụng thay vì các phiên bản màu mặc định của biểu tượng, giả sử rằng có sẵn một biến thể biểu tượng chế độ sáng hoặc tối riêng biệt cho biểu tượng từ nguồn.\\n\\nYêu cầu làm mới biểu tượng để áp dụng.\nalwaysShowSshMotd=Luôn hiển thị MOTD\nalwaysShowSshMotdDescription=Có hiển thị thông báo hàng ngày được cấu hình trên hệ thống từ xa khi đăng nhập vào phiên terminal mới hay không. Lưu ý rằng việc thay đổi này có thể ảnh hưởng đến hành vi khởi tạo của kết nối SSH.\nmanageSubscription=Quản lý đăng ký\nnoListeningServer=Không có máy chủ nghe\nnetworkScanResults=Kết quả quét\nnetworkScanResultsDescription=Danh sách các hệ thống được tìm thấy trong mạng\nlocalShellDialect=Vỏ lệnh cục bộ\nlocalShellDialectDescription=Vỏ lệnh được sử dụng cho các thao tác cục bộ. Trong trường hợp vỏ lệnh cục bộ mặc định bị vô hiệu hóa hoặc hỏng một phần, tùy chọn này có thể được sử dụng để chuyển sang một vỏ lệnh thay thế khác.\\n\\nMột số cấu hình như các mục PATH tùy chỉnh có thể không áp dụng với vỏ lệnh thay thế nếu chúng chưa được cấu hình trong các tệp cấu hình vỏ lệnh tương ứng.\nagentSocketNotFound=Không tìm thấy socket đại lý hoạt động\nagentSocket=Vị trí socket\nagentSocketDescription=Đường dẫn của tệp socket của agent\nagentSocketNotConfigured=Chưa có socket tùy chỉnh nào được cấu hình\ndownloadInProgress=$NAME$ đang tải xuống\nenableTerminalStartupBell=Bật chuông khởi động thiết bị đầu cuối\nenableTerminalStartupBellDescription=Phát lệnh tiếng bíp/chuông trong phiên terminal mới. Nếu trình giả lập terminal của cậu hỗ trợ chuông, tính năng này có thể được sử dụng để dễ dàng nhận diện các phiên terminal mới được khởi chạy.\ninvalidSshGatewayChain=Cấu hình chuỗi cổng trung gian không hợp lệ bao gồm cả cổng trung gian và cổng không trung gian.\nsyncFileExists=Tệp đã đồng bộ $FILE$ đã tồn tại\nreplaceFile=Thay thế tệp\nreplaceFileDescription=Đã thay thế tệp hiện có bằng tệp này\nrenameFile=Đổi tên tệp\nrenameFileDescription=Đổi tên tệp này để đồng bộ hóa\nnewFileName=Tên tệp mới\nparentHostDoesNotSupportTunneling=Máy chủ cha $NAME$ không hỗ trợ tunneling\nconnectionNotesTemplate=Mẫu ghi chú\nconnectionNotesTemplateDescription=Mẫu Markdown cần sử dụng khi thêm một mục ghi chú mới vào kết nối.\nconnectionNotesButton=Chỉnh sửa ghi chú\nrdpSmartSizing=Bật tính năng điều chỉnh kích thước thông minh\nrdpSmartSizingDescription=Khi được kích hoạt, mstsc sẽ thu nhỏ kích thước màn hình desktop nếu cửa sổ quá nhỏ để hiển thị ở độ phân giải đầy đủ. Tỷ lệ khung hình của màn hình desktop được giữ nguyên khi thu nhỏ.\ndisableStartOnInit=Tắt tính năng khởi động tự động\nenableStartOnInit=Bật khởi động tự động\nfileReadSudoTitle=Đọc tệp Sudo\nfileReadSudoContent=Tệp cậu đang cố gắng đọc không cấp quyền đọc cho người dùng hiện tại. Cậu có muốn đọc tệp này với tư cách người dùng root bằng sudo không? Điều này sẽ tự động nâng cấp lên quyền root bằng thông tin đăng nhập hiện có hoặc thông qua một lời nhắc.\nnetbirdInstall.displayName=Cài đặt Netbird\nnetbirdInstall.displayDescription=Kết nối với các thiết bị khác trong mạng Netbird của cậu\nnetbirdProfile.displayName=Hồ sơ Netbird\nnetbirdProfile.displayDescription=Danh sách các đối tác trong một hồ sơ cụ thể\nnetbirdPeer.displayName=Netbird peer\nnetbirdPeer.displayDescription=Kết nối với một máy tính khác qua SSH\nnetbirdPublicKey=Khóa công khai\nnetbirdPublicKeyDescription=Khóa công khai nội bộ của đối tác\nnetbirdHostName=Tên máy chủ\nnetbirdHostNameDescription=Tên máy chủ của đối tác trong mạng\nvncRefSystem=Hệ thống liên quan\nvncRefSystemDescription=Mục kết nối để liên kết kết nối VNC này với. Để trống nếu không có\nabstractHost.displayName=Máy chủ trừu tượng\nabstractHost.displayDescription=Tạo một mục nhập cho một máy chủ không hỗ trợ kết nối shell\nabstractHostAddress=Địa chỉ máy chủ\nabstractHostAddressDescription=Địa chỉ của máy chủ\nabstractHostGateway=Cổng kết nối\nabstractHostGatewayDescription=Hệ thống cổng tùy chọn để kết nối với máy chủ này\nabstractHostConvert=Chuyển đổi thành mục nhập máy chủ trừu tượng\nhostNoConnections=Không có kết nối khả dụng\nhostHasConnections=$COUNT$ các kết nối có sẵn\nhostHasConnection=$COUNT$ kết nối có sẵn\nlargeFileWarningTitle=Chỉnh sửa tệp tin lớn\nlargeFileWarningContent=Tệp cậu muốn chỉnh sửa có kích thước khá lớn ( $SIZE$). Cậu có chắc chắn muốn mở tệp này trong trình soạn thảo văn bản của mình không?\nrdpAskpassUser=Tên người dùng RDP cho máy chủ $HOST$\nrdpAskpassPassword=Mật khẩu cho người dùng $USER$\ninPlaceKey=Chìa khóa\ninPlaceKeyText=Nội dung khóa riêng tư\ninPlaceKeyTextDescription=Nội dung của khóa riêng tư\nnetbirdSelfhosted=Phiên bản Netbird tự lưu trữ\nnetbirdSelfhostedDescription=Cung cấp một URL tùy chỉnh thay vì sử dụng phiên bản được lưu trữ trên đám mây\nnetbirdManagementUrl=URL quản lý Netbird\nnetbirdManagementUrlDescription=Địa chỉ URL quản lý của phiên bản tự lưu trữ của cậu\nnetbirdSetupKey=Khóa cài đặt\nnetbirdSetupKeyDescription=Nếu cậu đang sử dụng khóa cài đặt, cậu có thể sử dụng một khóa để đăng nhập\nnetbirdLogin=Đăng nhập Netbird\naddProfile=Thêm hồ sơ\nnetbirdProfileNameAsktext=Tên của hồ sơ Netbird mới\nopenSftp=Mở trong phiên SFTP\ncapslockWarning=Cậu đã bật chế độ Caps Lock\ninherit=Kế thừa\nsshConfigStringSelected=Máy chủ đích\nsshConfigStringSelectedDescription=Đối với nhiều máy chủ, máy chủ đầu tiên được sử dụng làm mục tiêu. Sắp xếp lại các máy chủ của cậu để thay đổi mục tiêu\ntunnelToLocalhost=Tunnel đến localhost\ntunnelToLocalhostDescription=Tự động chuyển hướng cổng từ xa đến localhost\ntags=Thẻ\ntag=Thẻ\naddNewTag=Tạo thẻ mới\ncreateTag=Tạo thẻ ...\ninPlacePublicKey=Khóa công khai\ninPlacePublicKeyDescription=Khóa công khai liên quan đến khóa riêng tư đã chỉ định\nsshKeygenTitle=Tạo khóa SSH mới\nsshKeygenAlgorithm=Thuật toán\nsshKeygenAlgorithmDescription=Thuật toán tạo khóa bất đối xứng để sử dụng cho khóa\nrsaBits=Bit\nrsaBitsDescription=Số bit trong khóa được tạo ra\nsshKeygenComment=Bình luận\nsshKeygenCommentDescription=Lời bình luận tùy chọn cho khóa này\nsshKeygenPassphrase=Mật khẩu\nsshKeygenPassphraseDescription=Mật khẩu tùy chọn cho khóa này\ned25519SkResident=Tạo khóa thường trú\ned25519SkResidentDescription=Lưu trữ khóa riêng trên khóa bảo mật phần cứng\ned25519SkResidentKeyName=Nhãn khóa thường trú\ned25519SkResidentKeyNameDescription=Gán nhãn cho khóa. Cần thiết khi lưu trữ nhiều khóa trên khóa bảo mật\ned25519SkPinRequired=Yêu cầu mã PIN\ned25519SkPinRequiredDescription=Yêu cầu nhập mã PIN khi sử dụng\ned25519SkUserPresenceRequired=Yêu cầu sự hiện diện của người dùng\ned25519SkUserPresenceRequiredDescription=Yêu cầu thao tác chạm hoặc tương tự khi sử dụng. Một số khóa bảo mật yêu cầu tính năng này phải được kích hoạt\ncopyPublicKey=Sao chép khóa công khai\ngeneratePublicKey=Tạo khóa công khai\npublicKeyGenerateNotice=Có thể được tạo ra từ khóa riêng tư\nidentityApplyTargetHost=Mục tiêu\nidentityApplyTargetHostDescription=Hệ thống áp dụng danh tính cho\nidentityApplyAuthorizedHost=Khóa SSH được ủy quyền\nidentityApplyAuthorizedHostDescription=Khóa SSH được thêm vào tệp máy chủ được ủy quyền\nidentityApplyAuthorizedHostButton=Thêm khóa vào tệp\napplyIdentityToHost=Áp dụng danh tính cho máy chủ ...\nidentityApplyMissingPublicKeyTitle=Không có khóa công khai\nidentityApplyMissingPublicKeyContent=Khóa SSH của tài khoản không có khóa công khai liên kết với nó. Hãy kiểm tra cấu hình để biết chi tiết.\nvalid=Hợp lệ\nnotValid=Không hợp lệ\nwarning=Cảnh báo\nidentityApplyTitle=Áp dụng danh tính\nidentityApplyConfigPasswordEnabled=Bật xác thực mật khẩu\nidentityApplyConfigPasswordEnabledDescription=Xác thực mật khẩu vẫn được kích hoạt trong cấu hình sshd\nidentityApplyConfigPasswordDisabled=Tính năng xác thực mật khẩu đã bị vô hiệu hóa\nidentityApplyConfigPasswordDisabledDescription=Xác thực mật khẩu vẫn bị vô hiệu hóa trong cấu hình sshd\nidentityApplyConfigKeyEnabled=Bật xác thực khóa\nidentityApplyConfigKeyEnabledDescription=Xác thực dựa trên khóa vẫn được kích hoạt trong cấu hình sshd\nidentityApplyConfigKeyDisabled=Tính năng xác thực khóa đã bị vô hiệu hóa\nidentityApplyConfigKeyDisabledDescription=Xác thực dựa trên khóa vẫn bị vô hiệu hóa trong cấu hình sshd\nidentityApplyConfigRootDisabledWarning=Tài khoản root bị vô hiệu hóa\nidentityApplyConfigRootDisabledWarningDescription=Tài khoản người dùng root không được kích hoạt trong cấu hình sshd\nidentityApplyConfigAdminWarning=Các khóa quản trị viên đã được cấu hình\nidentityApplyConfigAdminWarningDescription=Khóa có thể cần được thêm vào tệp `administrators_authorized_keys` thay vì cho người dùng quản trị\nidentityApplyEditConfig=Chỉnh sửa cấu hình\nidentityApplyEditConfigDescription=Mở tệp cấu hình sshd trong trình soạn thảo để khắc phục các vấn đề\nidentityApplyEditAuthorizedKeys=Chỉnh sửa các khóa được ủy quyền\nidentityApplyEditAuthorizedKeysDescription=Mở tệp authorized_keys trong trình soạn thảo để chỉnh sửa hoặc xóa các khóa khác\nidentityApplyEditConfigButton=Mở tệp sshd_config\nidentityApplyEditAuthorizedKeysButton=Mở tệp authorized_keys\nidentityApplySetStoreIdentity=Bộ nhận dạng kết nối\nidentityApplySetStoreIdentityDescription=Danh tính được cấu hình để sử dụng cho kết nối\nidentityApplySetStoreIdentityButton=Áp dụng danh tính\ngenerateKey=Tạo khóa\ngroupSecretStrategy=Kiểm soát truy cập dựa trên nhóm\ngroupSecretStrategyDescription=Cách lấy khóa bí mật của nhóm được sử dụng cho việc mã hóa và giải mã cho nhóm. Phương pháp lấy khóa cậu chọn sẽ được thực thi khi người dùng đăng nhập vào kho lưu trữ khi khởi động.\\n\\nCài đặt này được cấu hình theo từng nhóm. Để thay đổi cài đặt này cho một nhóm khác ngoài nhóm hiện đang hoạt động, cậu phải đăng nhập vào kho lưu trữ với tư cách là thành viên của nhóm đó.\nfileSecret=Bí mật dựa trên tệp\ncommandSecret=Lệnh\nhttpRequestSecret=Phản hồi HTTP\nfileSecretChoice=Vị trí tệp\nfileSecretChoiceDescription=Đường dẫn đến tệp chứa khóa mã hóa nhóm. Vì tệp này có thể được truy vấn trên tất cả các nền tảng, cậu có thể sử dụng ~ trong đường dẫn để tham chiếu đến thư mục home. Tệp này phải có sẵn trên tất cả các hệ thống mà cậu mở khóa kho lưu trữ, nếu không quá trình đăng nhập sẽ thất bại.\ncommandSecretField=Kịch bản truy xuất\ncommandSecretFieldDescription=Lệnh sẽ trả về khóa mã hóa bí mật cho nhóm hiện tại. Lệnh được thực thi trong vỏ lệnh mặc định của hệ thống cục bộ và khóa nên được in ra stdout.\nhttpRequestSecretField=URI yêu cầu\nhttpRequestSecretFieldDescription=URI để gửi yêu cầu HTTP. Khóa bí mật nhóm được lấy từ nội dung phản hồi HTTP.\nvaultAuthentication=Xác thực kho lưu trữ\nvaultAuthenticationDescription=Cách xác thực / mở khóa dữ liệu trong kho dữ liệu. Có nhiều phương pháp khác nhau để mã hóa và mở khóa dữ liệu trong kho, tùy thuộc vào đối tượng mà cậu muốn chia sẻ dữ liệu kho với.\ngroupAuthFailed=Xác thực bí mật không thành công\nuserAuthFailed=Xác thực mật khẩu thất bại\nsavingChanges=Lưu thay đổi\nawsDeviceScan=AWS\naws=AWS\nawsCliInstallTitle=Yêu cầu AWS CLI\nawsCliInstallContent=Tích hợp AWS yêu cầu AWS CLI phải được cài đặt trên hệ thống cục bộ của cậu\nawsProfileCreateTitle=Hồ sơ AWS mới\nawsProfileAccessKey=Phím tắt\nawsProfileName=Tên hồ sơ\nawsProfileNameDescription=Tên hiển thị của hồ sơ mới\nawsProfileRegion=Khu vực\nawsProfileRegionDescription=Khu vực AWS liên kết với hồ sơ\nawsProfileAccessKeyId=Mã truy cập\nawsProfileAccessKeyIdDescription=ID khóa truy cập người dùng IAM\nawsProfileSecretAccessKey=Khóa truy cập bí mật\nawsProfileSecretAccessKeyDescription=Khóa truy cập bí mật liên quan\nawsInstall.displayName=Cài đặt AWS CLI\nawsInstall.displayDescription=Kết nối với hệ thống AWS của cậu thông qua AWS CLI\nawsProfile.displayName=Hồ sơ AWS CLI\nawsProfile.displayDescription=Truy cập AWS thông qua một hồ sơ cụ thể\nawsInstanceId=ID thực thể\nawsInstanceIdDescription=Mã định danh nội bộ của phiên bản này\nawsInstanceUseSsm=Kết nối qua SSM\nawsInstanceUseSsmDescription=Sử dụng công cụ SSM để kết nối với instance thông qua SSH\nawsEc2Instance.displayName=Máy chủ ảo AWS EC2\nawsEc2Instance.displayDescription=Kết nối với một instance EC2 qua SSH\nawsS3Group.displayName=Thùng S3\nawsS3Group.displayDescription=Truy cập các thùng S3 của một hồ sơ AWS\nawsS3Bucket.displayName=Thùng S3\nawsS3Bucket.displayDescription=Truy cập thùng S3 của một hồ sơ AWS\nawsEc2Group.displayName=Các phiên bản EC2\nawsEc2Group.displayDescription=Truy cập các phiên bản EC2 của một hồ sơ AWS\nawsEc2InstanceSsmTerminal=Mở cửa sổ SSM\ngenericS3Bucket.displayName=Thùng S3 chung\ngenericS3Bucket.displayDescription=Truy cập một thùng S3 chung thông qua AWS CLI\naddFileSystem=Hệ thống tệp ...\ngenericS3BucketHost=Máy chủ\ngenericS3BucketHostDescription=Địa chỉ máy chủ hoặc địa chỉ thủ công của máy chủ S3\ngenericS3BucketPortDescription=Cổng mà máy chủ S3 đang lắng nghe\ngenericS3BucketAccessKeyId=Mã truy cập\ngenericS3BucketAccessKeyIdDescription=ID khóa truy cập người dùng IAM\ngenericS3BucketSecretAccessKey=Khóa truy cập bí mật\ngenericS3BucketSecretAccessKeyDescription=Khóa truy cập bí mật liên quan\ngenericS3BucketHttps=Bật HTTPS\ngenericS3BucketHttpsDescription=Sử dụng HTTPS để kết nối với máy chủ. Một số nhà cung cấp có thể yêu cầu HTTPS\ntunnelled=Được truyền qua đường hầm\nawsInstallSync=Đồng bộ hóa cấu hình\nawsInstallSyncDescription=Đồng bộ hóa các tệp cấu hình AWS CLI với kho lưu trữ Git\nawsInstallLocation=Vị trí dữ liệu của cậu\nawsInstallLocationDescription=Đường dẫn nơi các tệp cấu hình AWS CLI được lấy từ\ninstanceActions=Các hành động của đối tượng\nopenSplit=Mở trong cửa sổ terminal chia đôi\nterminalSplitStrategy=Chế độ xem chia đôi\nterminalSplitStrategyDescription=Quy định cách chia tab terminal khi sử dụng tính năng chia màn hình trong chế độ batch để mở nhiều phiên terminal cạnh nhau.\nterminalSplitStrategyDisabledDescription=Điều khiển cách chia tab terminal khi sử dụng chức năng chia màn hình trong chế độ batch để mở nhiều phiên terminal cạnh nhau.\\n\\nCấu hình terminal hiện tại của cậu không hỗ trợ chế độ chia màn hình.\nhorizontal=Ngang\nvertical=Dọc\nbalanced=Cân bằng\nclose=Đóng\nhelpButton=$TOPIC$ liên kết tài liệu\nquickAccess=Truy cập nhanh\ntoggleEnabled=Chuyển đổi trạng thái\ncurrentPath=Đường dẫn hiện tại\ndirectoryContents=Nội dung thư mục\ndirectoryOptions=Tùy chọn thư mục\nchooseConnectionType=Chọn loại kết nối\nbatchMode=Chế độ hàng loạt\ntoggleButton=Nút chuyển đổi\ntailscaleUseSsh=Sử dụng xác thực SSH của Tailscale\ntailscaleUseSshDescription=Đăng nhập trực tiếp qua máy chủ SSH của Tailscale mà không cần xác thực SSH\nportDescription=Cổng mà máy chủ SSH đang chạy trên\nloginAs=Đăng nhập với tư cách là\nsshGatewayType=Loại cổng\nsshGatewayTypeDescription=Có nên kết nối với mục tiêu thông qua một đường hầm hay sử dụng tùy chọn ProxyJump\ngatewayTunnel=Cổng kết nối\nproxyJump=Proxy nhảy\ncommandTypeAsyncBackground=Chạy độc lập trong nền\ncommandTypeSyncBackground=Chạy ngầm và chờ hoàn tất\ncommandTypeTerminalBackground=Mở trong terminal\nasyncBackgroundCommand=Lệnh nền\nsyncBackgroundCommand=Lệnh nền chặn\nterminalBackgroundCommand=Lệnh terminal\ntestingConnection=Kiểm tra kết nối ...\nopenManagementConsole=Mở bảng điều khiển quản lý\nopenLxcTerminal=Mở cửa sổ terminal LXC\nopenContainerConsole=Mở giao diện điều khiển serial\nkeeper2fa=phương pháp xác thực hai yếu tố (2FA)\nkeeper2faDescription=Phương thức xác thực hai yếu tố chính được cấu hình cho tài khoản của cậu. Bật tính năng này nếu tài khoản Keeper của cậu yêu cầu xác thực hai yếu tố để truy cập mật khẩu.\nkeeperTotpDuration=Thời gian hiệu lực của mã 2FA tùy chỉnh\nkeeperTotpDurationDescription=Thay đổi thời gian mặc định mà mã xác thực hai yếu tố (2FA) có hiệu lực. Tính năng này chỉ áp dụng nếu chính sách của tổ chức cho phép thay đổi thời gian.\\n\\nCác giá trị có thể là: $VALUES$\nkeeperOtherAuth=Khác (RSA SecurID, Duo Security, Keeper DNA, v.v.)\nextractReusableIdentities=Trích xuất các danh tính có thể tái sử dụng\nidentitiesAdded=Thêm danh tính\nsyncMode=Chế độ đồng bộ hóa\nsyncModeDescription=Quy định cách thức đồng bộ hóa các thay đổi.\\n\\nChế độ tức thì sẽ đẩy và kéo các thay đổi ngay lập tức, chế độ khởi động và thoát sẽ đồng bộ hóa tất cả các thay đổi được thực hiện trong một phiên làm việc cùng một lúc, và chế độ thủ công chỉ đồng bộ hóa khi cậu khởi động nó.\ntoggleTerminalDock=Chuyển đổi chế độ gắn kết terminal\nscriptDirectory=Vị trí thư mục\nscriptDirectoryDescription=Thư mục cục bộ chứa các tệp kịch bản shell\nscriptSourceUrl=URL kho lưu trữ\nscriptSourceUrlDescription=Đường dẫn URL đến kho lưu trữ Git từ xa chứa các tệp kịch bản shell\nscriptCollectionSourceType=Loại nguồn\nscriptCollectionSourceTypeDescription=Loại nguồn mà từ đó các tập lệnh shell nên được tải\nscriptCollectionSourceEntry=Nguồn nhập\nscriptCollectionSourceEntryDescription=Nguồn mà từ đó các tập lệnh shell nên được tải\ngitRepository=Kho lưu trữ Git\nscriptCollectionSource.displayName=Nguồn kịch bản\nscriptCollectionSource.displayDescription=Tự động nhập các tập lệnh shell từ một nguồn hiện có\ndirectorySource=Nguồn thư mục\ngitRepositorySource=Nguồn kho lưu trữ Git\nrefreshSource=Làm mới nguồn\nscriptTextSourceUrl=URL kịch bản\nscriptTextSourceUrlDescription=Đường dẫn URL để tải tệp kịch bản từ\nscriptSourceType=Nguồn kịch bản\nscriptSourceTypeDescription=Lấy kịch bản từ đâu\nscriptSourceTypeInPlace=Kịch bản tại chỗ\nscriptSourceTypeUrl=URL bên ngoài\nscriptSourceTypeSource=Nguồn hiện có\nimportScripts=Nhập các tập lệnh\nscriptsContained=$NUMBER$ kịch bản\nscriptSourceCollectionImportTitle=Nhập các tập lệnh từ nguồn ($SELECTED$/$COUNT$)\nnoScriptsFound=Không tìm thấy kịch bản nào\ntunnel=Đường hầm\nnotInitialized=Chưa được khởi tạo\nselectCategory=Chọn danh mục ...\nscriptSourceName=Tên kịch bản\nscriptSourceNameDescription=Tên tệp của script trong nguồn\nworkspaceRestartTitle=Không gian làm việc đã sẵn sàng\nworkspaceRestartContent=Một lối tắt đến không gian làm việc mới đã được tạo tại $PATH$. Cậu có thể truy cập vào lối tắt này hoặc khởi động lại XPipe ngay bây giờ để mở không gian làm việc mới tự động.\nbrowseShortcut=Duyệt tệp\nsyncModeInstant=Đồng bộ hóa ngay lập tức\nsyncModeSession=Đồng bộ hóa khi khởi động và tắt ứng dụng\nsyncModeManual=Đồng bộ hóa thủ công\npushChanges=Đẩy các thay đổi\npullChanges=Kéo các thay đổi\nsourcedFrom=Nguồn $SOURCE$\ninPlaceScript=Kịch bản tại chỗ\ngeneric=Chung chung\nsyncToPlainDirectory=Đồng bộ hóa với thư mục thông thường\nsyncToPlainDirectoryDescription=Khi đồng bộ hóa với một thư mục cục bộ, cậu có thể xem thư mục này như một kho lưu trữ Git khác hoặc chỉ là một thư mục thông thường. Nếu chế độ thư mục thông thường được bật, thư mục sẽ không được khởi tạo như một kho lưu trữ Git.\nopenSpiceSession=Mở phiên làm việc SPICE\nterminalBehaviour=Hành vi của thiết bị đầu cuối\nnoScanPossible=Không tìm thấy kết nối nào được hỗ trợ\nnetworkSwitchPorts=Cổng mạng\nnswitchGroup.displayName=Cổng mạng\nnswitchGroup.displayDescription=Danh sách các cổng có sẵn trên thiết bị mạng\nnswitchPort.displayName=Cổng mạng\nnswitchPort.displayDescription=Kiểm soát một cổng riêng lẻ trên thiết bị chuyển mạch mạng\nenablePort=Kích hoạt cổng\nshutdownPort=Tắt cổng\nresetPort=Đặt lại cổng\nuseSystemDefault=Sử dụng mặc định của hệ thống\nportStatus=Trạng thái cổng\nclearCounters=Xóa bộ đếm\nshowStatus=Hiển thị trạng thái\nshowAllPorts=Hiển thị tất cả các cổng\nactiveLicense=Giấy phép\nactiveLicenseDescription=Kích hoạt khóa cấp phép XPipe\nauthenticatorApp=Ứng dụng xác thực\nsecurityKey=Khóa bảo mật\nmcpAdditionalContext=Bối cảnh bổ sung của MCP\nmcpAdditionalContextDescription=Hướng dẫn bổ sung để truyền cho khách hàng MCP. Sử dụng điều này để kiểm soát hành vi của đại lý và cung cấp bối cảnh bổ sung cho cấu hình cá nhân của cậu.\nmcpAdditionalContextSample=- Không tự động khởi động lại bất kỳ dịch vụ hoặc daemon nào mà không xác nhận trước\\n- Khi cấu hình giao diện mạng, luôn sử dụng 192.168.1.1/24 làm cổng mặc định\nprefsRestartTitle=Cần khởi động lại\nprefsRestartContent=Một số tùy chọn cậu đã thay đổi yêu cầu khởi động lại ứng dụng để áp dụng. Cậu có muốn khởi động lại XPipe ngay bây giờ không?\nbashShell=Vỏ lệnh Bash\n"
  },
  {
    "path": "lang/strings/translations_zh-Hans.properties",
    "content": "delete=删除\nproperties=属性\n#custom\nusedDate=使用日期 $DATE$\nopenDir=打开目录\nsortLastUsed=按最后使用日期排序\n#custom\nsortAlphabetical=按名称字母顺序排序\n#custom\nsortIndexed=按索引顺序排序\n#custom\nrestartDescription=重启通常可以快速解决问题\nreportIssue=报告问题\n#custom\nreportIssueDescription=打开内置问题报告工具\n#custom\nusefulActions=常用操作\n#custom\nstored=已保存\n#custom\ntroubleshootingOptions=故障排查工具\n#custom\ntroubleshoot=排查问题\nremote=远程文件\n#custom\naddShellStore=添加 Shell ...\n#custom\naddShellTitle=添加 Shell 连接\n#custom\nsavedConnections=已保存的连接\nsave=保存\n#custom\nclean=清理\nmoveTo=移动到 ...\n#custom\naddDatabase=数据库 ...\nbrowseInternalStorage=浏览内部存储\naddTunnel=隧道 ...\naddService=服务 ...\naddScript=脚本 ...\naddHost=远程主机 ...\n#custom\naddShell=添加 Shell 环境 ...\naddCommand=命令 ...\naddAutomatically=自动添加...\naddOther=添加其他 ...\nconnectionAdd=添加连接\nscriptAdd=添加脚本\nscriptGroupAdd=添加脚本组\nidentityAdd=添加身份\n#custom\nnew=新建\nselectType=选择类型\nselectTypeDescription=选择连接类型\n#custom\nselectShellType=Shell 类型\n#custom\nselectShellTypeDescription=选择 Shell 连接的类型\nname=名称\n#custom\nstoreIntroHeader=连接中心\n#custom\nstoreIntroContent=在此，您可以在同一位置管理所有本地和远程的 Shell 连接。首先，您可以快速自动检测可用的连接，并选择要添加的项。\nstoreIntroButton=搜索连接 ...\ndragAndDropFilesHere=或直接将文件拖放到此处\nconfirmDsCreationAbortTitle=确认中止\n#custom\nconfirmDsCreationAbortHeader=确定要放弃创建数据源吗？\n#custom\nconfirmDsCreationAbortContent=数据源创建进度都将丢失。\nconfirmInvalidStoreTitle=跳过验证\n#custom\nconfirmInvalidStoreContent=是否跳过连接验证？即使当前无法验证，你仍可先添加该连接，并在稍后修复连接问题。\nexpand=扩展\naccessSubConnections=访问子连接\ncommon=常见\ncolor=颜色\nalwaysConfirmElevation=始终确认权限提升\n#custom\nalwaysConfirmElevationDescription=控制在系统上运行命令需要提升权限（例如使用 sudo）时的处理方式。\\n\\n默认情况下，会在会话期间缓存 sudo 凭据，并在需要时自动提供。启用此选项后，每次提升权限都会要求你确认。\nallow=允许\nask=询问\ndeny=拒绝\n#custom\nshare=添加到 Git 仓库\n#custom\nunshare=从 Git 仓库移除\n#custom\nremove=移除\ncreateNewCategory=新子类别\nprompt=提示\ncustomCommand=自定义命令\nother=其他\nsetLock=设置锁定\nselectConnection=选择连接\nselectEntry=选择条目\ncreateLock=创建口令\n#custom\nchangeLock=更改锁定密码\ntest=测试\nfinish=完成\nerror=发生错误\n#custom\ndownloadStageDescription=将已下载文件移入系统下载目录后再打开。\n#custom\nok=确定\nsearch=搜索\nrepeatPassword=重复密码\n#custom\naskpassAlertTitle=输入密码\nunsupportedOperation=不支持的操作：$MSG$\n#custom\nfileConflictAlertTitle=文件冲突\n#custom\nfileConflictAlertContent=目标系统已存在文件 $FILE$。\n#custom\nfileConflictAlertContentMultiple=文件 $FILE$ 已经存在。可能还有更多冲突，您可以选择一个适用于所有冲突的选项来自动解决。\nmoveAlertTitle=确认移动\n#custom\nmoveAlertHeader=确定要将 $COUNT$ 个选定元素移动到 $TARGET$ 吗？\ndeleteAlertTitle=确认删除\n#custom\ndeleteAlertHeader=确定要删除 $COUNT$ 个选定元素吗？\n#custom\nselectedElements=选定的元素：\n#custom\nmustNotBeEmpty=$VALUE$ 不能为空\n#custom\nvalueMustNotBeEmpty=值不能为空\n#custom\ntransferDescription=拖动文件到此处下载至本地\n#custom\ndragLocalFiles=从这里拖出下载文件\n#custom\nnull=$VALUE$ 不得为空\n#custom\nroots=根目录\nscripts=脚本\nsearchFilter=搜索...\nrecent=最近使用\nshortcut=快捷方式\n#custom\nbrowserWelcomeEmptyHeader=文件管理器\n#custom\nbrowserWelcomeEmptyContent=您可以在左侧选择要在文件管理器中打开的系统。XPipe 会记住您以前访问过的系统和目录，并在以后的快速访问菜单中显示出来。\n#custom\nbrowserWelcomeEmptyButton=打开本地文件管理器\nbrowserWelcomeSystems=您最近连接了以下系统：\nbrowserWelcomeDocsHeader=文档\nbrowserWelcomeDocsContent=如果您更喜欢在指导下熟悉 XPipe，请访问文档网站。\n#custom\nbrowserWelcomeDocsButton=打开文档\nhostFeatureUnsupported=$FEATURE$ 主机上未安装\nmissingStore=$NAME$ 不存在\nconnectionName=连接名称\nconnectionNameDescription=为该连接自定义名称\nopenFileTitle=打开文件\nunknown=未知\nscanAlertTitle=添加连接\nscanAlertChoiceHeader=目标\n#custom\nscanAlertChoiceHeaderDescription=选择搜索连接的位置，系统会先查找所有可用连接。\nscanAlertHeader=连接类型\n#custom\nscanAlertHeaderDescription=选择为系统自动添加的连接类型。\nnoInformationAvailable=无可用信息\nyes=是\n#custom\nno=否\nerrorOccured=发生错误\n#custom\nterminalErrorOccured=终端发生错误\n#custom\nerrorTypeOccured=抛出了类型为 $TYPE$ 的异常\npermissionsAlertTitle=所需权限\npermissionsAlertHeader=执行此操作需要额外权限。\npermissionsAlertContent=请根据弹出窗口在设置菜单中赋予 XPipe 所需的权限。\nerrorDetails=错误详情\n#custom\nupdateReadyAlertTitle=更新可安装\n#custom\nupdateReadyAlertHeader=$VERSION$ 版本更新已准备好安装\n#custom\nupdateReadyAlertContent=将安装新版本，安装完成后 XPipe 将自动重启。\n#custom\nerrorNoDetail=无可用错误详细信息\nerrorNoExceptionMessage=抛出了一个类型为$TYPE$ 的错误\n#custom\nupdateAvailableTitle=发现新版本\n#custom\nupdateAvailableContent=可更新至 $VERSION$。即使 XPipe 无法启动，也可尝试安装以解决潜在问题。\n#custom\nclipboardActionDetectedTitle=检测到剪贴板内容\n#custom\nclipboardActionDetectedContent=XPipe 检测到可处理的剪贴板内容。你希望现在打开还是导入？\ninstall=安装 ...\nignore=忽略\npossibleActions=可用操作\n#custom\nreportError=提交错误报告\n#custom\nreportOnGithub=在 GitHub 上提交问题报告\n#custom\nreportOnGithubDescription=在 GitHub 仓库中创建一个新问题\n#custom\nreportErrorDescription=发送包含可选用户反馈与诊断信息的错误报告\n#custom\nignoreError=忽略该错误\n#custom\nignoreErrorDescription=忽略该错误并继续运行\nprovideEmail=我们如何与您联系（可选，仅在您希望得到回复时）。您的报告默认为匿名，因此您可以在此处提供电子邮件地址等联系信息。\nadditionalErrorInfo=提供附加信息（可选）\nadditionalErrorAttachments=选择附件（可选）\ndataHandlingPolicies=隐私政策\n#custom\nsendReport=提交报告\nerrorHandler=错误处理程序\nevents=活动\nvalidate=验证\n#custom\nstackTrace=堆栈追踪\n#custom\npreviousStep=< 上一步\n#custom\nnextStep=下一步 >\nfinishStep=完成\nselect=选择\n#custom\nbrowseInternal=浏览内部\n#custom\ncheckOutUpdate=查看更新\nquit=退出\n#custom\nnoTerminalSet=未检测到终端程序，可在设置中手动指定。\nconnections=连接\nconnectionHub=连接中心\nsettings=设置\n#custom\nexplorePlans=查看许可证选项\nhelp=帮助\nabout=关于\n#custom\ndeveloper=开发者设置\nbrowseFileTitle=浏览文件\nbrowser=文件浏览器\nselectFileFromComputer=从本计算机选择文件\nlinks=链接\nwebsite=网站\ndiscordDescription=加入 Discord 服务器\nredditDescription=加入 XPipe subreddit\nsecurity=安全性\nsecurityPolicy=安全信息\nsecurityPolicyDescription=阅读详细的安全策略\nprivacy=隐私政策\nprivacyDescription=阅读 XPipe 应用程序的隐私政策\nslackDescription=加入 Slack 工作区\nsupport=支持\ngithubDescription=查看 GitHub 仓库\nopenSourceNotices=开放源代码公告\ncheckForUpdates=检查更新\n#custom\ncheckForUpdatesDescription=检查并下载可用更新\n#custom\nlastChecked=上次检查时间\nversion=版本\nbuild=构建版本\n#custom\nruntimeVersion=运行时版本号\nvirtualMachine=虚拟机\nupdateReady=安装更新\n#custom\nupdateReadyPortable=查看更新\nupdateReadyDescription=已下载更新并准备安装\nupdateReadyDescriptionPortable=可下载更新\n#custom\nupdateRestart=重启以更新\nnever=从不\n#custom\nupdateAvailableTooltip=有可用更新\n#custom\nptbAvailableTooltip=公测版可用\nvisitGithubRepository=访问 GitHub 仓库\n#custom\nupdateAvailable=有可用更新：$VERSION$\ndownloadUpdate=下载更新\n#custom\nlegalAccept=我已阅读并接受最终用户许可协议\nconfirm=确认\nprint=打印\n#custom\nwhatsNew=版本 $VERSION$ 新内容（$DATE$）\nantivirusNoticeTitle=关于杀毒软件的说明\nupdateChangelogAlertTitle=更新日志\n#custom\ngreetingsAlertTitle=欢迎使用 XPipe\neula=最终用户许可协议\nnews=新闻\nintroduction=简介\nprivacyPolicy=隐私政策\nagree=同意\ndisagree=不同意\ndirectories=目录\nlogFile=日志文件\nlogFiles=日志文件\nlogFilesAttachment=日志文件\nissueReporter=问题报告器\n#custom\nopenCurrentLogFile=打开当前日志文件\nopenCurrentLogFileDescription=打开当前会话的日志文件\nopenLogsDirectory=打开日志目录\ninstallationFiles=安装文件\nopenInstallationDirectory=安装文件\nopenInstallationDirectoryDescription=打开 XPipe 安装目录\nlaunchDebugMode=调试模式\nlaunchDebugModeDescription=在调试模式下重启 XPipe\nextensionInstallTitle=下载\n#custom\nextensionInstallDescription=此操作需要额外的第三方库，这些库不由 XPipe 分发。你可以在此自动安装，它们将从各个网站下载：\n#custom\nextensionInstallLicenseNote=执行下载与自动安装即表示你同意相关第三方许可条款：\nlicense=许可证\ninstallRequired=安装要求\nrestore=恢复\nrestoreAllSessions=恢复所有会话\nlimitedTouchscreenMode=有限触摸屏模式\nlimitedTouchscreenModeDescription=在手机屏幕等较为特殊的触摸屏界面上使用本程序时，某些菜单可能无法正常工作。启用该选项后，菜单会使用更有限的功能来处理稀少的鼠标/触摸事件。\nappearance=外观\ndisplay=显示\npersonalization=个性化\ndisplayOptions=显示选项\ntheme=主题\nrdpConfiguration=远程桌面配置\nrdpClient=RDP 客户端\nrdpClientDescription=启动 RDP 连接时调用的 RDP 客户端程序。\\n\\n请注意，各种客户端具有不同程度的能力和集成。有些客户端不支持自动传递密码，因此仍需在启动时填写。\n#custom\nlocalShell=本地 Shell\n#custom\nthemeDescription=选择您的首选主题\n#custom\ndontAutomaticallyStartVmSshServer=禁止按需自动启动虚拟机 SSH 服务器\n#custom\ndontAutomaticallyStartVmSshServerDescription=对运行在虚拟机管理程序中的虚拟机进行任何 Shell 连接都通过 SSH。XPipe 可按需自动启动该虚拟机的 SSH 服务器；若出于安全考虑不希望如此，可勾选此项禁用。\nconfirmGitShareTitle=Git 同步\n#custom\nconfirmGitShareContent=确定将所选文件添加到 Git 仓库吗？将复制其加密版本并提交，以便在所有已同步的桌面上访问。\n#custom\ngitShareFileTooltip=将文件添加到 Git 保险库数据目录以自动同步。\\n\\n此操作仅在设置中已启用 Git 保险库时可用。\nperformanceMode=性能模式\n#custom\nperformanceModeDescription=关闭非必要视觉效果以提升性能\n#custom\ndontAcceptNewHostKeys=禁止自动信任新的 SSH 主机密钥\n#custom\ndontAcceptNewHostKeysDescription=若 SSH 客户端中不存在已知主机密钥，XPipe 默认自动接受系统返回的初始主机密钥；若已有主机密钥发生变化，则需显式确认，否则拒绝连接。\\n\\n禁用自动接受可强制对所有陌生主机密钥执行人工核实。\n#custom\nuiScale=界面缩放\n#custom\nuiScaleDescription=自定义界面缩放百分比（独立于系统缩放）。例如 150 表示放大到 150%。\n#custom\neditorProgram=默认编辑器程序\n#custom\neditorProgramDescription=用于编辑文本内容的默认编辑器程序\n#custom\nwindowOpacity=窗口不透明度\n#custom\nwindowOpacityDescription=调整不透明度以便同时关注后台内容\n#custom\nuseSystemFont=使用系统字体\n#custom\nopenDataDir=保险库数据目录\n#custom\nopenDataDirButton=打开数据目录\n#custom\nopenDataDirDescription=可将需同步的额外文件（如 SSH 密钥）放入此目录；在其他系统上路径引用会自动适配。\nupdates=更新\n#custom\nselectAll=全选\nadvanced=高级\n#custom\nthirdParty=开源声明\neulaDescription=阅读 XPipe 应用程序的最终用户许可协议\nthirdPartyDescription=查看第三方库的开源许可证\n#custom\nworkspaceLock=工作区密码\n#custom\nenableGitStorage=启用 Git 同步\nsharing=共享\ngitSync=Git 同步\n#custom\nenableGitStorageDescription=启用后将为本地保险库初始化 Git 仓库并自动提交更改。需要已安装 Git，可能略微增加加载/保存时间。\\n\\n需要同步的类别需显式标记。\nstorageGitRemote=远程同步 URL\n#custom\nstorageGitRemoteDescription=配置远程仓库后：加载时自动拉取远程更改，保存时推送本地更改，实现多设备间共享保险库。支持 HTTP 与 SSH URL。\\n\\n可能略微降低加载/保存性能。\nvault=保险库\n#custom\nworkspaceLockDescription=设置自定义口令以加密存储在 XPipe 的敏感数据。\\n\\n提供额外一层保护，启动时需要输入口令才能解锁。\nuseSystemFontDescription=控制使用默认系统字体还是 XPipe 附带的 Inter 字体。\n#custom\ntooltipDelay=提示显示延迟\n#custom\ntooltipDelayDescription=等待提示显示的毫秒数。\nfontSize=字体大小\nwindowOptions=窗口选项\nsaveWindowLocation=保存窗口位置\nsaveWindowLocationDescription=控制是否保存窗口坐标并在重启时恢复。\nstartupShutdown=启动/关闭\nshowChildrenConnectionsInParentCategory=在父类别中显示子类别\nshowChildrenConnectionsInParentCategoryDescription=当选择某个父类别时，是否包括位于子类别中的所有连接。\\n\\n如果禁用，类别的行为更像传统的文件夹，只显示其直接内容而不包括子文件夹。\ncondenseConnectionDisplay=压缩连接显示\ncondenseConnectionDisplayDescription=减少每个顶级连接的垂直空间，使连接列表更加简洁。\nopenConnectionSearchWindowOnConnectionCreation=在创建连接时打开连接搜索窗口\n#custom\nopenConnectionSearchWindowOnConnectionCreationDescription=是否在添加新 Shell 连接时自动打开子连接搜索窗口。\nworkflow=工作流程\nsystem=系统\napplication=应用程序\nstorage=存储\nrunOnStartup=启动时运行\n#custom\ncloseBehaviour=关闭行为\n#custom\ncloseBehaviourDescription=控制 XPipe 关闭主窗口后的处理方式。\nlanguage=语言\n#custom\nlanguageDescription=设置界面语言。\\n\\n当前翻译基于自动生成，并由社区贡献者优化。您可以在 GitHub 上提交改进建议。\n#custom\nlightTheme=浅色主题\ndarkTheme=深色主题\nexit=退出 XPipe\n#custom\ncontinueInBackground=在后台继续\n#custom\nminimizeToTray=最小化至系统托盘\ncloseBehaviourAlertTitle=设置关闭行为\ncloseBehaviourAlertTitleHeader=选择关闭窗口时应发生的情况。关闭程序时，任何活动连接都将被关闭。\n#custom\nstartupBehaviour=启动设置\nstartupBehaviourDescription=控制 XPipe 启动时桌面应用程序的默认行为。\nclearCachesAlertTitle=清除缓存\n#custom\nclearCachesAlertContent=您想清除所有 XPipe 缓存数据吗？这将所有缓存数据(用来提升用户体验的)。\nstartGui=启动图形用户界面\n#custom\nstartInTray=最小化至托盘启动\nstartInBackground=后台启动\nclearCaches=清除缓存...\nclearCachesDescription=删除所有缓存数据\ncancel=取消\nnotAnAbsolutePath=非绝对路径\nnotADirectory=不是目录\nnotAnEmptyDirectory=不是空目录\n#custom\nautomaticallyCheckForUpdates=自动检查更新\n#custom\nautomaticallyCheckForUpdatesDescription=启用后，XPipe 会在运行一段时间后自动获取新版本信息。若有更新需要安装则仍需手动确认。\nsendAnonymousErrorReports=发送匿名错误报告\nsendUsageStatistics=发送匿名使用统计数据\nstorageDirectory=存储目录\nstorageDirectoryDescription=XPipe 存储所有连接信息的位置。更改时，旧目录中的数据不会复制到新目录。\nlogLevel=日志级别\n#custom\nappBehaviour=应用程序设置\n#custom\nlogLevelDescription=设置日志文件记录的级别。\n#custom\ndeveloperMode=开发者模式\n#custom\ndeveloperModeDescription=启用后，可访问开发相关的高级选项。\neditor=编辑\ncustom=自定义\npasswordManager=密码管理器\nexternalPasswordManager=外部密码管理器\n#custom\npasswordManagerDescription=与本地已安装的密码管理器集成。\\n\\n配置后，XPipe 在需要时调用命令检索密码而非本地保存；连接的密码字段可切换为“使用密码管理器”。\npasswordManagerCommandTest=测试密码管理器\n#custom\npasswordManagerCommandTestDescription=测试命令输出是否仅包含密码本身（stdout 纯文本，无多余格式）。\n#custom\npreferTerminalTabs=优先使用新标签页\n#custom\npreferTerminalTabsDescription=控制 XPipe 是否尝试在所选终端中新建标签页而非新窗口。并非所有终端都支持标签。\ncustomRdpClientCommand=自定义命令\ncustomRdpClientCommandDescription=启动自定义 RDP 客户端时要执行的命令。\\n\\n调用时，占位符字符串 $FILE 将被带引号的 .rdp 绝对文件名替换。如果可执行文件路径包含空格，请记住使用引号。\ncustomEditorCommand=自定义编辑器命令\ncustomEditorCommandDescription=启动自定义编辑器时要执行的命令。\\n\\n调用时，占位符字符串 $FILE 将被带引号的绝对文件名替换。如果编辑器的执行路径包含空格，请记住一定要加上引号。\neditorReloadTimeout=编辑器重载超时\neditorReloadTimeoutDescription=文件更新后读取前的等待毫秒数。这样可以避免编辑器在写入或释放文件锁时出现问题。\n#custom\nencryptAllVaultData=加密全部保险库数据\n#custom\nencryptAllVaultDataDescription=启用后，连接数据的所有字段（而非仅机密项）都会用保险库主密钥加密，为用户名、主机名等原本明文参数增加保护。\\n\\n代价：Git 保险库的历史与 diff 不再可读，仅呈现二进制块。\nvaultSecurity=保险库安全\ndeveloperDisableUpdateVersionCheck=禁用更新版本检查\ndeveloperDisableUpdateVersionCheckDescription=控制更新检查程序在查找更新时是否忽略版本号。\ndeveloperDisableGuiRestrictions=禁用图形用户界面限制\ndeveloperDisableGuiRestrictionsDescription=控制是否仍可从用户界面执行某些已禁用的操作。\ndeveloperShowHiddenEntries=显示隐藏条目\ndeveloperShowHiddenEntriesDescription=启用后，将显示隐藏数据源和内部数据源。\n#custom\ndeveloperShowHiddenProviders=显示隐藏的提供程序\n#custom\ndeveloperShowHiddenProvidersDescription=控制是否在创建对话框中显示隐藏和内部的连接与数据源提供程序。\ndeveloperDisableConnectorInstallationVersionCheck=禁用连接器版本检查\n#custom\ndeveloperDisableConnectorInstallationVersionCheckDescription=控制更新检查器在检测远程机器上已安装的 XPipe 连接器版本时是否忽略版本号。\\\n#custom\nshellCommandTest=Shell 命令测试\n#custom\nshellCommandTestDescription=在 XPipe 内部使用的 Shell 会话中运行一条命令。\nterminal=终端\n#custom\nterminalType=终端类型\nterminalConfiguration=终端配置\n#custom\nterminalCustomization=终端自定义\neditorConfiguration=编辑器配置\ndefaultApplication=默认应用程序\ninitialSetup=初始设置\n#custom\nterminalTypeDescription=用于打开 Shell 连接的默认终端。\\n\\n不同终端的功能支持程度各异，并带有推荐/不推荐标识。选择推荐终端可获得更佳体验。\nprogram=程序\ncustomTerminalCommand=自定义终端命令\n#custom\ncustomTerminalCommandDescription=使用给定命令启动自定义终端时执行的命令。\\n\\nXPipe 会生成临时启动器脚本供终端调用，命令中的占位符 $CMD 会被实际脚本路径替换。若终端可执行路径含空格需加引号。\n#custom\nclearTerminalOnInit=启动时清屏\n#custom\nclearTerminalOnInitDescription=启用后，新终端会话启动后将执行清屏命令，移除启动时的多余输出。\n#custom\ndontCachePasswords=不缓存已输入的密码\n#custom\ndontCachePasswordsDescription=开启后，XPipe 会在本会话内缓存已输入的密码，后续无需重复输入。\\n\\n关闭后，每当系统再次需要这些密码时都会重新提示你输入。\ndenyTempScriptCreation=拒绝创建临时脚本\n#custom\ndenyTempScriptCreationDescription=为了实现某些功能，XPipe 有时会在目标系统上创建临时 Shell 脚本，以便轻松执行简单命令。这些脚本不包含任何敏感信息，只是为了实现目的而创建的。\\n\\n如果禁用该行为，XPipe 将不会在远程系统上创建任何临时文件。该选项在高度安全的情况下非常有用，因为在这种情况下，文件系统的每次更改都会受到监控。如果禁用，某些功能（如 Shell 环境和脚本）将无法正常工作。\n#custom\ndisableCertutilUse=禁用 Windows certutil\n#custom\nuseLocalFallbackShell=使用本地备用 Shell\n#custom\nuseLocalFallbackShellDescription=改用其他本地 Shell 来处理本地操作。在 Windows 系统上是 PowerShell，在其他系统上是 Bourne Shell。\\n\\n如果正常的本地默认 Shell 在某种程度上被禁用或损坏，则可以使用此选项。启用该选项后，某些功能可能无法正常工作。\n#custom\ndisableCertutilUseDescription=由于 cmd.exe 存在若干缺陷和 bug（在处理非 ASCII 输入时尤其容易出错），XPipe 会把临时脚本内容先做 Base64 编码，再借助 certutil 解码写出脚本文件。也可以改用 PowerShell 完成该步骤，但速度会更慢。\\n\\n启用本选项后，将在 Windows 上完全禁用 certutil，相关功能一律回退为使用 PowerShell。某些杀毒软件会拦截 certutil，禁用后可能减少误报或阻断。\n#custom\ndisableTerminalRemotePasswordPreparation=禁用远程终端密码预置\n#custom\ndisableTerminalRemotePasswordPreparationDescription=建立经多级跳板的远程 Shell 连接时，可能需在中间系统暂存密码以自动响应提示。\\n\\n若不希望密码流经中间系统，可禁用此功能；所需密码将于终端打开时现场输入。\nmore=更多信息\ntranslate=翻译\n#custom\nallConnections=全部连接\nallScripts=所有脚本\n#custom\nallIdentities=全部身份\n#custom\nsynced=已同步\npredefined=预定义\nsamples=样本\ngoodMorning=早上好\ngoodAfternoon=下午好\ngoodEvening=晚上好\n#custom\naddVisual=桌面会话 ...\naddDesktop=桌面 ...\n#custom\nssh=SSH 连接\nsshConfiguration=SSH 配置\n#custom\nsize=文件大小\nattributes=属性\nmodified=已修改\nowner=所有者\n#custom\nupdateReadyTitle=可更新至 $VERSION$\ntemplates=模板\nretry=重试\nretryAll=全部重试\nreplace=替换\nreplaceAll=全部替换\nhibernateBehaviour=休眠行为\nhibernateBehaviourDescription=控制应用程序在系统进入休眠/睡眠状态时的行为。\noverview=概述\nhistory=历史\nskipAll=全部跳过\nnotes=说明\naddNotes=添加注释\n#custom\norder=调整顺序\n#custom\nkeepFirst=保持在首位\n#custom\nkeepLast=保持在末位\n#custom\npinToTop=固定到顶部\n#custom\nunpinFromTop=取消固定到顶部\n#custom\norderAheadOf=排在…之前\nclearIndex=重置索引\nhttpServer=HTTP 服务器\n#custom\nmcpServer=MCP 服务端\n#custom\napiKey=API Key\n#custom\napiKeyDescription=用于对 XPipe 守护进程 API 请求进行认证的密钥。认证方式见 API 文档。\n#custom\ndisableApiAuthentication=禁用 API 认证\n#custom\ndisableApiAuthenticationDescription=停用所有必需的认证方式，允许处理任意未认证请求。\\n\\n仅限开发调试场景使用，生产环境请保持开启。\n#custom\napi=API\nstoreIntroImportContent=已经在其他系统上使用 XPipe？通过远程 git 仓库在多个系统上同步您的现有连接。如果尚未设置，您也可以稍后随时进行同步。\nstoreIntroImportButton=同步连接...\nstoreIntroImportHeader=导入连接\n#custom\nshowNonRunningChildren=显示未运行的子项\nhttpApi=HTTP API\nisOnlySupportedLimit=只有在连接数超过$COUNT$ 时才支持专业许可证\nareOnlySupportedLimit=只有在连接数超过$COUNT$ 时才支持专业许可证\nenabled=已启用\nenableGitStoragePtbDisabled=公共测试版已禁用 Git 同步功能，以防止与常规发布版本的 git 仓库一起使用，并避免将公共测试版作为日常驱动程序使用。\n#custom\ncopyId=复制 API 标识\nrequireDoubleClickForConnections=要求双击连接\n#custom\nrequireDoubleClickForConnectionsDescription=如果启用，则必须双击连接才能启动。如果您习惯双击打开软件，这将非常有用。\nclearTransferDescription=清除选择\nselectTab=选择选项卡\ncloseTab=关闭选项卡\ncloseOtherTabs=关闭其他标签页\ncloseAllTabs=关闭所有标签页\ncloseLeftTabs=向左关闭标签\ncloseRightTabs=向右关闭标签页\naddSerial=串行 ...\nconnect=连接\n#custom\nworkspaces=工作区\nmanageWorkspaces=管理工作区\naddWorkspace=添加工作区 ...\nworkspaceAdd=添加新工作区\nworkspaceAddDescription=工作区是运行 XPipe 的不同配置。每个工作区都有一个数据目录，本地存储所有数据。其中包括连接数据、设置等。\\n\\n如果使用同步功能，您还可以选择将每个工作区与不同的 git 仓库同步。\nworkspaceName=工作区名称\nworkspaceNameDescription=工作区的显示名称\nworkspacePath=工作区路径\n#custom\nworkspacePathDescription=工作区数据目录路径\nworkspaceCreationAlertTitle=创建工作区\n#custom\ndeveloperForceSshTty=强制 SSH 分配 TTY\n#custom\ndeveloperForceSshTtyDescription=让所有 SSH 连接都分配一个伪终端 (pty)，用于测试在缺少独立 stderr 以及强制 pty 情况下的支持。\ndeveloperDisableSshTunnelGateways=禁用 SSH 网关隧道\n#custom\ndeveloperDisableSshTunnelGatewaysDescription=对网关不使用隧道会话，而是直接连接到系统。\n#custom\nttyWarning=该连接被强制分配了 pty/TTY，且未提供单独的 stderr 流。\\n\\n这可能会导致一些问题。\\n\\n如有可能，请尝试使连接命令不再强制分配 pty。\nxshellSetup=Xshell 设置\ntermiusSetup=Termius 设置\n#custom\ntryPtbDescription=在 XPipe 开发构建版本中提前试用新功能\n#custom\nconfirmVaultUnencryptTitle=确认停用高级加密\n#custom\nconfirmVaultUnencryptContent=确定要禁用高级保险库加密吗？将移除额外加密层并重写现有数据。\nenableHttpApi=启用 HTTP API\n#custom\nenableHttpApiDescription=开启后，外部程序可通过 HTTP API 调用 XPipe 守护进程，对已管理的连接执行操作。\nchooseCustomIcon=选择自定义图标\ngitVault=Git 保险库\n#custom\nfileBrowser=文件管理器\n#custom\nconfirmAllDeletions=删除前确认\n#custom\nconfirmAllDeletionsDescription=是否为所有删除操作显示确认对话框（默认仅对目录启用）。\nyesterday=昨天\ngreen=绿色\nyellow=黄色\nblue=蓝色\nred=红色\ncyan=青色\npurple=紫色\nasktextAlertTitle=提示\n#custom\nfileWriteSudoTitle=以 Sudo 权限写入文件\nfileWriteSudoContent=您尝试写入的文件没有授予您的用户写入权限。您想以 root 用户身份使用 sudo 写入该文件吗？这将通过现有凭证或提示自动提升为 root 用户。\n#custom\ndontAllowTerminalRestart=禁止终端请求重启\n#custom\ndontAllowTerminalRestartDescription=默认允许会话在终端内部结束后再次请求启动，XPipe 会响应这些外部重启请求。\\n\\n由于无法验证请求来源，恶意本地程序也可能借此发起连接。禁用后将拒绝此类重启请求以提升安全性。\n#custom\nopenDocumentation=查看文档\nopenDocumentationDescription=访问 XPipe 文档页面了解这一问题\n#custom\nrenameAll=全部重命名\nlogging=日志\n#custom\nenableTerminalLogging=启用终端日志记录\n#custom\nenableTerminalLoggingDescription=为所有终端会话启用客户端日志；输入与输出写入日志文件。敏感信息（如密码提示）不会被记录。\n#custom\nterminalLoggingDirectory=终端日志目录\n#custom\nterminalLoggingDirectoryDescription=所有终端日志存放于本机 XPipe 数据目录。\n#custom\nopenSessionLogs=查看会话日志\n#custom\nsessionLogging=终端日志记录\n#custom\nsessionActive=该连接当前在后台运行。\\n\\n单击状态指示器以手动停止会话。\nskipValidation=跳过验证\n#custom\nscriptsIntroHeader=脚本简介\n#custom\nscriptsIntroContent=你可以在 Shell 初始化时、在文件浏览器中或按需运行脚本。你的自定义提示符、别名和其他自定义功能可自动应用到所有系统，无需在远程主机上手动配置——XPipe 的脚本系统会为你全部处理。\nscriptsIntroBottomHeader=使用脚本\nscriptsIntroBottomContent=这里有各种示例脚本供您开始使用。你可以点击各个脚本的编辑按钮，查看它们是如何实现的。首先必须启用脚本才能运行并显示在菜单中，每个脚本上都有一个切换按钮。\n#custom\nscriptsIntroBottomButton=立即开始\nscriptSourcesIntroHeader=脚本源\nscriptSourcesIntroContent=您可以添加自定义脚本源，以便即时访问整个 shell 脚本集合。本地源和远程 git 仓库都支持作为源。源中所有检测到的脚本都将自动可用。\nscriptSourcesIntroButton=添加源 ...\n#custom\ncheckForSecurityUpdates=检查重要安全更新\ncheckForSecurityUpdatesDescription=XPipe 可与正常功能更新分开检查潜在的安全更新。启用此功能后，即使正常的更新检查被禁用，至少也会推荐安装重要的安全更新。\\n\\n禁用此设置后，将不会执行外部版本请求，也不会通知您任何安全更新。\n#custom\nclickToDock=点击以停靠终端\n#custom\nterminalStarting=正在等待终端启动\n#custom\npinTab=固定选项卡\nunpinTab=取消固定选项卡\n#custom\npinned=已固定\nenableConnectionHubTerminalDocking=启用连接集线器终端对接\nenableConnectionHubTerminalDockingDescription=您可以将终端窗口停靠到连接集线器中的 XPipe 应用程序窗口，以模拟集成终端。XPipe会对终端窗口进行管理，使其始终适合停靠。\nenableFileBrowserTerminalDocking=启用文件浏览器终端对接\nenableFileBrowserTerminalDockingDescription=您可以在文件浏览器中将终端窗口停靠在 XPipe 应用程序窗口上，以模拟集成终端。XPipe会对终端窗口进行管理，使其始终适合停靠。\n#custom\ndownloadsDirectory=下载保存目录\n#custom\ndownloadsDirectoryDescription=点击“移至下载”时文件会放入此目录。默认使用系统用户下载目录，可自定义覆盖。\n#custom\npinLocalMachineOnStartup=启动时固定本地机器标签页\n#custom\npinLocalMachineOnStartupDescription=自动打开并固定本地机器标签页；在分屏同时浏览本地与远程文件系统时很实用。\n#custom\nterminalErrorDescription=这是一个致命错误，未修复前 XPipe 无法继续运行。\n#custom\ngroupName=分组名称\n#custom\nchmodPermissions=更改权限为\neditFilesWithDoubleClick=双击编辑文件\neditFilesWithDoubleClickDescription=启用后，双击文件将直接在文本编辑器中打开，而不是显示上下文菜单。\n#custom\ncensorMode=脱敏模式\n#custom\ncensorModeDescription=模糊主机名、用户名、连接名等信息。\\n\\n如果您打算截屏或屏幕共享 XPipe 界面，但又不想泄露任何信息，这将非常有用。\n#custom\naddIdentity=添加身份 ...\n#custom\nidentities=身份列表\n#custom\naddMacro=动作宏 ...\n#custom\nidentitiesIntroHeader=身份功能简介\n#custom\nidentitiesIntroContent=若经常复用同一用户名、密码或密钥，建议创建可重复使用的身份。\n#custom\nidentitiesIntroBottomHeader=共享身份\n#custom\nidentitiesIntroBottomContent=身份可仅本地使用，也可通过 Git 同步选择性地与其他系统或团队成员共享。\n#custom\nidentitiesIntroBottomButton=配置同步\n#custom\nidentitiesIntroButton=创建身份\nuserName=用户名\nuserAuth=基于用户密码的身份验证\ngroupAuth=基于组的秘密身份验证\nteam=团队\n#custom\nteamSettings=团队管理\nteamVaults=团队保险库\nvaultTypeNameDefault=默认保险库\n#custom\nvaultTypeNameLegacy=旧版个人保险库\nvaultTypeNamePersonal=个人保险库\nvaultTypeNameTeam=团队保险库\n#custom\nteamVaultsDescription=团队保险库支持多用户安全访问。连接与身份可设置为全员共享，或用个人密钥加密仅自用；其他用户无法访问您的个人项目。\n#custom\nvaultTypeContentDefault=当前为默认保险库：未设置用户与自定义口令，机密以本地库密钥加密。可创建库用户升级为个人库，以个人口令加密并在登录时解锁。\n#custom\nvaultTypeContentLegacy=当前为旧版个人保险库：机密以个人口令加密，功能受限且无法原地转换为团队库。\n#custom\nvaultTypeContentPersonal=当前为个人保险库：机密采用您的个人口令加密。可添加其它库用户升级为团队库。\n#custom\nvaultTypeContentTeam=当前为团队保险库：支持多用户共享。连接/身份可设为共享或仅个人加密私有；他人无法访问您的私有条目。\ngroupManagement=群组管理\ngroupManagementEmpty=群组管理\ngroupManagementDescription=管理现有的信息库组或创建新的信息库组。每个信息库群组都有自己的密钥，用于加密连接和身份信息，只有该群组可以使用，其他群组不能使用。\ngroupManagementEmptyDescription=管理现有的信息库组或创建新的信息库组。每个信息库群组都有自己的密钥，用于加密连接和身份信息，只有该群组可以使用，其他群组不能使用。\\n\\n专业计划支持团队的群组账户。\nuserManagement=用户管理\nuserManagementEmpty=用户管理\nuserManagementDescription=管理现有的信息库用户或创建新用户。每个保险库用户都有自己的密码，用于加密连接和身份，只有该用户才能使用，其他人无法使用。\nuserManagementEmptyDescription=管理现有的信息库用户或创建新用户。每个信息库用户都有自己的密码，用于加密连接和身份信息，只有该用户可以使用，其他人无法使用。为自己创建一个用户，以便使用个人密钥对连接和身份进行加密。\\n\\n社区版支持单个用户账户。专业版支持一个团队的多个用户账户。\nuserIntroHeader=用户管理\nuserIntroContent=为自己创建第一个用户账户，以便开始使用。这样就可以用密码锁定该工作区。\n#custom\naddReusableIdentity=添加可复用身份\nusers=用户\nsyncVault=保险库同步\nsyncVaultDescription=要跨多个系统或与多个团队成员同步信息库，请启用此信息库的 git 同步功能。\n#custom\nenableGitSync=启用 Git 同步\n#custom\nbrowseVault=查看保险库数据\n#custom\nbrowseVaultDescription=保险库存放所有连接信息与凭据，可在本地文件管理器查看。\\n\\n不建议外部编辑，可能导致数据不一致。\n#custom\nbrowseVaultButton=打开保险库目录\nvaultUsers=保险库用户\ncreateHeapDump=创建堆转储\n#custom\ncreateHeapDumpDescription=将内存内容转存为文件，以分析内存使用问题。\n#custom\ninitializingApp=正在加载连接...\ncheckingLicense=检查许可证\n#custom\nloadingGit=正在同步 Git 仓库...\nloadingGpg=为 git 启动 GnuPG 守护进程\n#custom\nloadingSettings=正在加载设置...\n#custom\nloadingConnections=加载连接中...\nunlockingVault=开锁保险库\n#custom\nloadingUserInterface=正在加载用户界面...\n#custom\nptbNotice=公测版通知\n#custom\nuserDeletionTitle=删除用户\n#custom\nuserDeletionContent=确认删除该用户？其所有个人身份与连接将被移除。XPipe 将重启以应用更改。\ngroupDeletionTitle=组删除\ngroupDeletionContent=您要删除此保险库组吗？这将使用所有用户都可用的保险库密钥重新加密所有组专用身份和连接秘密。这需要一段时间，XPipe 将重新启动以应用组更改。\n#custom\nkillTransfer=中止传输\n#custom\ndestination=目标路径\nconfiguration=配置\n#custom\nnewFile=创建新文件\n#custom\nnewLink=创建新链接\nlinkName=链接名称\nscanConnections=查找可用连接 ...\n#custom\nobserve=开始监控\n#custom\nstopObserve=停止监控\n#custom\ncreateShortcut=创建快捷方式\nbrowseFiles=浏览文件\nclone=克隆\ntargetPath=目标路径\nnewDirectory=新目录\ncopyShareLink=复制链接\n#custom\nselectStore=选择存储位置\nsaveSource=稍后保存\n#custom\nexecute=运行\n#custom\ndeleteChildren=删除所有子项\nscriptGroupDescriptionDescription=为该组提供可选描述\nabstractHostDescriptionDescription=为该主机提供可选描述\nselectSource=选择来源\n#custom\ncommandLineRead=读取命令行\n#custom\ncommandLineWrite=写入命令行\nadditionalOptions=附加选项\ninput=输入\nmachine=机器\nopen=打开\nedit=编辑\nscriptContents=脚本内容\nscriptContentsDescription=要执行的脚本命令\n#custom\nsnippets=脚本片段\n#custom\nsnippetsDescription=优先运行的其他脚本片段\n#custom\nsnippetsDependenciesDescription=（可选）可能运行的脚本片段列表\n#custom\nisDefault=在所有兼容的 Shell 初始化时运行\n#custom\nbringToShells=适用于所有兼容的 Shell\n#custom\nisDefaultGroup=Shell 启动时运行该组内全部脚本\nexecutionType=执行类型\n#custom\nexecutionTypeDescription=在哪些场景/阶段使用此脚本\n#custom\nminimumShellDialect=Shell 类型\n#custom\nminimumShellDialectDescription=运行此脚本所需的 Shell 类型\n#custom\ndumbOnly=简化模式\n#custom\nterminalOnly=仅终端\n#custom\nboth=两者皆可\n#custom\nshouldElevate=需要提权\n#custom\nshouldElevateDescription=是否以提升权限（如 sudo/管理员）运行该脚本\n#custom\nscript.displayName=Shell 脚本\n#custom\nscript.displayDescription=创建可重复使用的 Shell 脚本\nscriptGroup.displayName=脚本组\nscriptGroup.displayDescription=将脚本分组并组织在\nscriptGroup=组别\nscriptGroupDescription=要将此脚本分配给的组\nscriptGroupGroupDescription=指定此脚本组的可选父组\nopenInNewTab=在新标签页中打开\n#custom\nexecuteInBackground=在后台运行\n#custom\nexecuteInTerminal=在 $TERM$ 中执行\nback=返回\n#custom\nbrowseInWindowsExplorer=在资源管理器中打开\n#custom\nbrowseInDefaultFileManager=在默认文件管理器中打开\n#custom\nbrowseInFinder=在 Finder 中打开\ncopy=复制\npaste=粘贴\n#custom\ncopyLocation=复制路径\nabsolutePaths=绝对路径\nabsoluteLinkPaths=绝对链接路径\nabsolutePathsQuoted=绝对引用路径\nfileNames=文件名\nlinkFileNames=链接文件名\nfileNamesQuoted=文件名（引用）\n#custom\ndeleteFile=删除 $FILE$\n#custom\neditWithEditor=使用 $EDITOR$ 编辑\n#custom\nfollowLink=打开链接\ngoForward=前进\nshowDetails=显示详细信息\nshowDetailsDescription=显示错误的堆栈跟踪\n#custom\nopenFileWith=用其他程序打开\nopenWithDefaultApplication=用默认应用程序打开\nrename=重命名\nrun=运行\nopenInTerminal=在终端中打开\nfile=文件\ndirectory=目录\nsymbolicLink=符号链接\n#custom\ndesktopEnvironment.displayName=远程桌面环境\ndesktopEnvironment.displayDescription=创建可重复使用的远程桌面环境配置\ndesktopHost=桌面主机\ndesktopHostDescription=作为基础的桌面连接\n#custom\ndesktopShellDialect=Shell 类型\n#custom\ndesktopShellDialectDescription=运行脚本和应用程序所用 Shell 类型\ndesktopSnippets=脚本片段\n#custom\ndesktopSnippetsDescription=优先运行的可复用脚本片段列表\n#custom\ndesktopInitScript=初始化脚本\ndesktopInitScriptDescription=该环境专用的初始化命令\n#custom\ndesktopTerminal=桌面终端程序\n#custom\ndesktopTerminalDescription=桌面上用于启动脚本的终端程序。\ndesktopApplication.displayName=桌面应用程序\ndesktopApplication.displayDescription=在远程桌面上运行应用程序\ndesktopBase=桌面\ndesktopBaseDescription=运行此应用程序的桌面\ndesktopEnvironmentBase=桌面环境\ndesktopEnvironmentBaseDescription=运行此应用程序的桌面环境\ndesktopApplicationPath=应用路径\ndesktopApplicationPathDescription=要运行的可执行文件的路径\ndesktopApplicationArguments=参数\ndesktopApplicationArgumentsDescription=传递给应用程序的可选参数\ndesktopCommand.displayName=桌面命令\ndesktopCommand.displayDescription=在远程桌面环境中运行命令\ndesktopCommandScript=命令\ndesktopCommandScriptDescription=在环境中运行的命令\nservice.displayName=服务\nservice.displayDescription=将远程服务转发到本地计算机\n#custom\nserviceLocalPort=指定本地端口\nserviceLocalPortDescription=要转发到的本地端口，否则使用随机端口\nserviceRemotePort=远程端口\nserviceRemotePortDescription=服务运行的端口\nserviceHost=服务主机\nserviceHostDescription=服务运行的主机\nopenWebsite=打开网站\ncustomServiceGroup.displayName=服务组\ncustomServiceGroup.displayDescription=将多项服务归为一类\n#custom\ninitScript=启动脚本 - 在 Shell 启动时运行\n#custom\nshellScript=Shell 会话脚本 - 在 Shell 会话中运行脚本\n#custom\nrunnableScript=可运行脚本 - 允许从连接中心直接运行脚本\nfileScript=文件脚本 - 允许为文件浏览器中选定的文件调用脚本\nrunScript=运行脚本\ncopyUrl=复制 URL\nfixedServiceGroup.displayName=服务组\nfixedServiceGroup.displayDescription=列出系统中可用的服务\nmappedService.displayName=服务\nmappedService.displayDescription=与容器暴露的服务交互\ncustomService.displayName=服务\ncustomService.displayDescription=在本地计算机上自动打开或隧道远程服务端口\nfixedService.displayName=服务\nfixedService.displayDescription=使用预定义服务\nnoServices=无可用服务\nhasServices=$COUNT$ 可用服务\nhasService=$COUNT$ 可用服务\nnoConnections=无可用连接\nhasConnections=$COUNT$ 可用连接\nhasConnection=$COUNT$ 可用连接\n#custom\nopenHttp=打开 HTTP 服务\n#custom\nopenHttps=打开 HTTPS 服务\n#custom\nnoScriptsAvailable=无可用脚本\nscriptsDisabled=禁用脚本\nchangeIcon=更改图标\ninit=启动\n#custom\nshell=Shell\n#custom\nhub=连接中心\nscript=脚本\ngenericScript=通用\ngradleTasks=Gradle 任务\nrunTask=运行任务\n#custom\narchiveName=压缩包名称\ncompress=压缩\ncompressContents=压缩内容\n#custom\nuntarHere=在此解压\nuntarDirectory=到$DIR$\nunzipDirectory=解压缩为$DIR$\n#custom\nunzipHere=解压到当前目录\n#custom\nrequiresRestart=更改将在重新启动后生效。\ndownload=下载\nservicePath=服务路径\nservicePathDescription=在浏览器中打开 URL 时可选择的子路径\n#custom\nactive=已激活\n#custom\ninactive=未激活\n#custom\nstarting=开启中\n#custom\nremotePort=远程端口\n#custom\nremotePortNumber=远程端口号：$PORT$\nuserIdentity=个人身份\n#custom\nglobalIdentity=全局身份\n#custom\nidentityChoice=选择身份\n#custom\nidentityChoiceDescription=选择已有身份或创建一个可复用身份用于该连接\n#custom\ndefineNewIdentityOrSelect=输入用户名或选择已有身份\nlocalIdentity.displayName=本地身份\n#custom\nlocalIdentity.displayDescription=为此本地桌面创建一个可复用的身份\nsyncedIdentity.displayName=同步身份\n#custom\nsyncedIdentity.displayDescription=创建跨系统同步的可复用身份\nlocalIdentity=本地身份\nkeyNotSynced=密钥文件尚未同步到 git 仓库。请使用添加到 git 按钮添加密钥文件。\nusernameDescription=登录的用户名\nidentity.displayName=身份\n#custom\nidentity.displayDescription=为连接创建可复用身份\nlocal=本地\n#custom\nshared=全局\n#custom\nuserDescription=登录使用的用户名或可复用身份\nidentityAccessLevel=访问级别\n#custom\nidentityPerUser=仅个人可见\n#custom\nidentityPerUserDescription=限制仅当前保险库用户可访问此身份及其连接\n#custom\nidentityPerUserDisabled=个人访问（已禁用）\n#custom\nidentityPerUserDisabledDescription=限制仅当前保险库用户访问（需已配置团队）\nidentityPerGroup=仅限群组的身份访问\nidentityPerGroupDescription=只允许该保险库组访问此身份及其相关连接\n#custom\nlibrary=资源库\nlocation=地点\n#custom\nkeyAuthentication=密钥认证\n#custom\nkeyAuthenticationDescription=需要密钥登录时采用的认证方式\n#custom\nlocationDescription=私钥文件路径\nkeyFile=本地密钥文件\n#custom\nkeyPassword=密钥口令\n#custom\nkey=密钥\nyubikeyPiv=Yubikey PIV\n#custom\npageant=Pageant\n#custom\ngpgAgent=GPG Agent\ncustomPkcs11Library=自定义 PKCS#11 库\n#custom\nsshAgent=OpenSSH Agent\nnone=无\n#custom\nindex=自定义顺序\n#custom\notherExternal=其他外部 Agent\nsync=同步\nvaultSync=保险库同步\n#custom\ncustomUsername=自定义用户名\ncustomUsernameDescription=用于登录的可选备用用户\n#custom\ncustomUsernamePassword=自定义密码\n#custom\ncustomUsernamePasswordDescription=执行 sudo 验证时使用的用户密码\n#custom\nshowInternalPods=显示内部 Pod\nshowAllNamespaces=显示所有命名空间\nshowInternalContainers=显示内部容器\nrefresh=刷新\nvmwareGui=启动图形用户界面\nmonitorVm=监控虚拟机\naddCluster=添加集群 ...\nshowNonRunningInstances=显示非运行实例\n#custom\nvmwareGuiDescription=选择在后台启动虚拟机，还是以窗口方式启动。\n#custom\nvmwareEncryptionPassword=虚拟机加密口令\n#custom\nvmwareEncryptionPasswordDescription=（可选）用于解锁已加密虚拟机的口令。\nvmPasswordDescription=访客用户所需的密码。\nvmPassword=用户密码\nvmUser=访客用户\nrunTempContainer=运行临时容器\nvmUserDescription=主要访客用户的用户名\ndockerTempRunAlertTitle=运行临时容器\n#custom\ndockerTempRunAlertHeader=此操作将在临时容器中运行一个 Shell 进程，停止后自动移除。\n#custom\nimageName=镜像名称\n#custom\nimageNameDescription=要使用的容器镜像标识符\ncontainerName=容器名称\n#custom\ncontainerNameDescription=（可选）自定义容器名称\nvm=虚拟机\nvmDescription=相关的配置文件。\nvmwareScan=VMware 桌面管理程序\nvmwareMachine.displayName=VMware 虚拟机\n#custom\nvmwareMachine.displayDescription=通过 SSH 连接该虚拟机\nvmwareInstallation.displayName=安装 VMware 桌面管理程序\n#custom\nvmwareInstallation.displayDescription=通过 CLI 管理已安装的虚拟机\nstart=启动\nstop=停止\npause=暂停\nrdpTunnelHost=目标主机\nrdpTunnelHostDescription=将 RDP 连接隧道化的 SSH 连接\nrdpTunnelUsername=用户名\nrdpTunnelUsernameDescription=登录时使用的自定义用户，如果留空，则使用 SSH 用户\nrdpFileLocation=文件位置\nrdpFileLocationDescription=.rdp 文件的文件路径\n#custom\nrdpPasswordAuthentication=RDP 密码认证\nrdpFiles=RDP 文件\nrdpPasswordAuthenticationDescription=要填写或复制到剪贴板的密码，取决于客户端支持\nrdpFile.displayName=RDP 文件\nrdpFile.displayDescription=通过现有 .rdp 文件连接系统\nrequiredSshServerAlertTitle=设置 SSH 服务器\n#custom\nrequiredSshServerAlertHeader=虚拟机中未检测到已安装的 SSH 服务器。\n#custom\nrequiredSshServerAlertContent=XPipe 尝试连接该虚拟机，但没有发现正在运行的 SSH 服务器。\ncomputerName=计算机名称\n#custom\npssComputerNameDescription=目标计算机名称\n#custom\ncredentialUser=认证用户名\n#custom\ncredentialUserDescription=用于登录的用户名。\n#custom\ncredentialPassword=认证密码\n#custom\ncredentialPasswordDescription=该用户的登录密码。\n#custom\nsshConfig=SSH 配置\nautostart=在 XPipe 启动时自动连接\n#custom\nacceptHostKey=信任此主机密钥\n#custom\nmodifyHostKeyPermissions=修正主机密钥权限\n#custom\nattachContainer=附加到容器\n#custom\ncontainerLogs=查看日志\nopenSftpClient=在外部 SFTP 客户端中打开\nopenTermius=在 Termius 中打开\nshowInternalInstances=显示内部实例\n#custom\neditPod=编辑 Pod\n#custom\nacceptHostKeyDescription=信任该主机密钥并继续\n#custom\nmodifyHostKeyPermissionsDescription=尝试移除原始文件的多余权限以满足 OpenSSH 的要求\npsSession.displayName=PowerShell 远程会话\npsSession.displayDescription=通过 New-PSSession 和 Enter-PSSession 连接\nsshLocalTunnel.displayName=本地 SSH 通道\nsshLocalTunnel.displayDescription=建立连接远程主机的 SSH 通道\nsshRemoteTunnel.displayName=远程 SSH 通道\nsshRemoteTunnel.displayDescription=从远程主机建立反向 SSH 通道\nsshDynamicTunnel.displayName=动态 SSH 通道\nsshDynamicTunnel.displayDescription=通过 SSH 连接建立 SOCKS 代理\n#custom\nshellEnvironmentGroup.displayName=Shell 环境\n#custom\nshellEnvironmentGroup.displayDescription=Shell 环境\n#custom\nshellEnvironment.displayName=Shell 环境\n#custom\nshellEnvironment.displayDescription=创建自定义 Shell 启动环境\nshellEnvironment.informationFormat=$TYPE$ 环境\nshellEnvironment.elevatedInformationFormat=$ELEVATION$ $TYPE$ 环境\nenvironmentConnectionDescription=基础连接，为\n#custom\nenvironmentScriptDescription=在 Shell 中运行的可选自定义启动脚本\n#custom\nenvironmentSnippets=Shell 脚本片段\n#custom\ncommandSnippetsDescription=优先运行的可选预定义 Shell 脚本片段\n#custom\nenvironmentSnippetsDescription=初始化时运行的可选预定义 Shell 脚本片段\n#custom\nshellTypeDescription=指定 Shell 类型\noriginPort=源端口\noriginAddress=源地址\nremoteAddress=远程地址\nremoteSourceAddress=远程源地址\nremoteSourcePort=远程源端口\n#custom\noriginDestinationPort=源端目标端口\n#custom\noriginDestinationAddress=源端目标地址\n#custom\norigin=源端\nremoteHost=远程主机\naddress=地址\nproxmox.displayName=Proxmox\nproxmox.displayDescription=连接 Proxmox 虚拟环境中的系统\nproxmoxVm.displayName=Proxmox 虚拟机\nproxmoxVm.displayDescription=通过 SSH 连接 Proxmox VE 中的虚拟机\nproxmoxContainer.displayName=Proxmox 容器\nproxmoxContainer.displayDescription=连接到 Proxmox VE 中的容器\nsshDynamicTunnel.hostDescription=用作 SOCKS 代理的系统\n#custom\nsshDynamicTunnel.bindingDescription=要将该隧道绑定到的地址\n#custom\nsshRemoteTunnel.hostDescription=在此系统上启动指向源端的反向 SSH 隧道。\n#custom\nsshRemoteTunnel.bindingDescription=要将远程隧道绑定到的地址\n#custom\nsshLocalTunnel.hostDescription=要建立隧道连接的远程系统\n#custom\nsshLocalTunnel.bindingDescription=要将本地隧道绑定到的地址\n#custom\nsshLocalTunnel.localAddressDescription=要绑定的本地地址\nsshLocalTunnel.remoteAddressDescription=要绑定的远程地址\ncmd.displayName=指令\n#custom\ncmd.displayDescription=在终端中运行预设系统命令\nk8sPod.displayName=Kubernetes Pod\n#custom\nk8sPod.displayDescription=通过 kubectl 连接 Pod 及其容器\nk8sContainer.displayName=Kubernetes 容器\n#custom\nk8sContainer.displayDescription=打开容器 Shell 会话\nk8sCluster.displayName=Kubernetes 集群\n#custom\nk8sCluster.displayDescription=通过 kubectl 连接集群及其 Pod\nsshTunnelGroup.displayName=SSH 隧道\nsshTunnelGroup.displayCategory=所有类型的 SSH 隧道\nlocal.displayName=本地机器\n#custom\nlocal.displayDescription=本地计算机的 Shell\ncygwin=Cygwin\nmsys2=MSYS2\n#custom\ngitWindows=Git for Windows\n#custom\ngitForWindows.displayName=Git for Windows\ngitForWindows.displayDescription=访问本地 Git for Windows 环境\nmsys2.displayName=MSYS2\n#custom\nmsys2.displayDescription=MSYS2 环境的访问 Shell\ncygwin.displayName=Cygwin\n#custom\ncygwin.displayDescription=访问 Cygwin 环境的 Shell\n#custom\nnamespace=命名空间\ngitVaultIdentityStrategy=Git SSH 身份\n#custom\ngitVaultIdentityStrategyDescription=当远程仓库使用 SSH URL 且需 SSH 身份时配置此项。\\n\\n若使用 HTTP URL 可忽略。\ndockerContainers=Docker 容器\n#custom\ndockerCmd.displayName=Docker CLI 客户端\n#custom\ndockerCmd.displayDescription=通过 Docker CLI 客户端访问 Docker 容器\nwslCmd.displayName=WSL 安装\n#custom\nwslCmd.displayDescription=通过 WSL CLI 客户端访问 WSL 实例\nk8sCmd.displayName=kubectl 客户端\nk8sCmd.displayDescription=通过 kubectl 访问 Kubernetes 集群\nk8sClusters=Kubernetes 集群\n#custom\nshells=可用的 Shell\n#custom\ninspectContainer=查看容器信息\ninspectContext=检查\n#custom\nk8sClusterNameDescription=集群所在的上下文名称\n#custom\npod=Pod\npodName=Pod 名称\nk8sClusterContext=上下文\n#custom\nk8sClusterContextDescription=集群所在的上下文名称\n#custom\nk8sClusterNamespace=命名空间\n#custom\nk8sClusterNamespaceDescription=自定义命名空间；留空则使用默认命名空间\nk8sConfigLocation=配置文件\nk8sConfigLocationDescription=自定义的 kubeconfig 文件或默认文件（如果留空）。\n#custom\ninspectPod=查看 Pod 详情\nshowAllContainers=显示未运行的容器\n#custom\nshowAllPods=显示未运行的 Pod\n#custom\nk8sPodHostDescription=Pod 所在节点\nk8sContainerDescription=Kubernetes 容器的名称\n#custom\nk8sPodDescription=Kubernetes Pod 名称\n#custom\npodDescription=容器所在的 Pod\n#custom\nk8sClusterHostDescription=访问集群的主机；需已安装并配置 kubectl。\nconnection=连接\n#custom\nshellCommand.displayName=自定义 Shell 命令\n#custom\nshellCommand.displayDescription=通过自定义命令打开标准 Shell\nssh.displayName=SSH 连接\nssh.displayDescription=通过 SSH 命令行客户端连接远程系统\nsshConfig.displayName=SSH 配置文件\nsshConfig.displayDescription=连接 SSH 配置文件中定义的主机\n#custom\nsshConfigHost.displayName=SSH 配置主机\nsshConfigHost.displayDescription=连接到 SSH 配置文件中定义的主机\nsshConfigHost.password=密码\nsshConfigHost.passwordDescription=为用户登录提供可选密码。\n#custom\nsshConfigHost.identityPassphrase=身份密钥口令\n#custom\nsshConfigHost.identityPassphraseDescription=身份私钥的可选口令。\nshellCommand.hostDescription=执行命令的主机\n#custom\nshellCommand.commandDescription=打开 Shell 的命令\ncommandType=命令类型\ncommandTypeDescription=如何执行命令\n#custom\ncommandDescription=主机上 Shell 脚本中要执行的命令。\ncommandHostDescription=运行命令的主机\ncommandDataFlowDescription=该命令如何处理输入和输出\ncommandElevationDescription=以提升的权限运行此命令\n#custom\ncommandShellTypeDescription=该命令使用的 Shell\n#custom\nlimitedSystem=这是一个资源受限或嵌入式系统\n#custom\nlimitedSystemDescription=跳过 Shell 类型检测（适用于资源受限的嵌入式 / IoT 设备）\n#custom\nsshForwardX11=启用 X11 转发\nsshForwardX11Description=为连接启用 X11 转发\n#custom\ncustomAgent=自定义 Agent\n#custom\nidentityAgent=身份 Agent\n#custom\nssh.proxyDescription=SSH 连接可使用代理主机（需安装 SSH 客户端）。\nusage=使用方法\nwslHostDescription=WSL 实例所在的主机。必须已安装 wsl。\nwslDistributionDescription=WSL 实例的名称\nwslUsernameDescription=要登录的明确用户名。如果未指定，将使用默认用户名。\nwslPasswordDescription=用户密码，可用于执行 sudo 命令。\n#custom\ndockerHostDescription=运行 Docker 容器的主机；需已安装 Docker。\n#custom\ndockerContainerDescription=Docker 容器名称\nlocalMachine=本地机器\n#custom\nrootScan=Sudo Shell 环境\nloginEnvironmentScan=自定义登录环境\nk8sScan=Kubernetes 集群\noptions=选项\n#custom\ndockerRunningScan=运行中的 Docker 容器\n#custom\ndockerAllScan=所有 Docker 容器\nwslScan=WSL 实例\nsshScan=SSH 配置连接\n#custom\nrunAsUser=以其他用户运行\n#custom\nrunAsUserDescription=以指定其他用户身份启动该 Shell 环境\ndefault=默认值\nadministrator=管理员\nwslHost=WSL 主机\ntimeout=超时\ninstallLocation=安装位置\ninstallLocationDescription=$NAME$ 环境的安装位置\nwsl.displayName=Linux 的 Windows 子系统\nwsl.displayDescription=连接到 Windows 上运行的 WSL 实例\ndocker.displayName=Docker 容器\n#custom\ndocker.displayDescription=连接到 Docker 容器\nport=端口\nuser=用户\npassword=密码\nmethod=方法\nuri=网址\nproxy=代理\n#custom\ndistribution=安装方式\nusername=用户名\n#custom\nshellType=Shell 类型\n#custom\nbrowseFile=查看文件\n#custom\nopenShell=在终端中打开 Shell\nopenCommand=在终端中执行命令\neditFile=编辑文件\ndescription=说明\nfurtherCustomization=进一步定制\nfurtherCustomizationDescription=有关更多配置选项，请使用 ssh 配置文件\nbrowse=浏览\nconfigHost=主机\nconfigHostDescription=配置所在的主机\nconfigLocation=配置位置\nconfigLocationDescription=配置文件的文件路径\ngateway=网关\ngatewayDescription=连接时使用的可选网关\n#custom\nconnectionInformation=连接详情\nconnectionInformationDescription=连接哪个系统\npasswordAuthentication=密码验证\n#custom\npasswordAuthenticationDescription=用于身份验证的可选密码\nsshConfigString.displayName=基于配置的 SSH 连接\nsshConfigString.displayDescription=以 SSH 配置格式创建完全自定义的 SSH 连接\nsshConfigStringContent=配置\n#custom\nsshConfigStringContentDescription=以 OpenSSH 配置格式指定此连接的 SSH 选项\nvnc.displayName=通过 SSH 进行 VNC 连接\nvnc.displayDescription=通过隧道连接打开 VNC 会话\n#custom\nbinding=绑定\nvncPortDescription=VNC 服务器监听的端口\nrdpPortDescription=RDP 服务器监听的端口\nvncUsername=用户名\nvncUsernameDescription=可选的 VNC 用户名\nvncPassword=密码\n#custom\nvncPasswordDescription=VNC 访问密码\n#custom\nx11WslInstance=用于 X11 转发的 WSL 实例\n#custom\nx11WslInstanceDescription=在使用 SSH 的 X11 转发时作为 X11 服务器的本地 WSL（必须是 WSL2\n#custom\nopenAsRoot=以 Root 用户权限打开\nopenInWSL=在 WSL 中打开\nlaunch=启动\n#custom\nsshTrustKeyContent=未知主机密钥，已启用手动验证：$CONTENT$\nsshTrustKeyTitle=未知主机密钥\nrdpTunnel.displayName=通过 SSH 进行 RDP 连接\nrdpTunnel.displayDescription=通过 RDP 在隧道连接上进行连接\nrdpEnableDesktopIntegration=启用桌面集成\nrdpEnableDesktopIntegrationDescription=假设 RDP 允许列表允许运行远程应用程序\nrdpSetupAdminTitle=需要 RDP 设置\nrdpSetupAllowTitle=RDP 远程应用程序\nrdpSetupAllowContent=本系统目前不允许直接启动远程应用程序。您想启用它吗？通过禁用 RDP 远程应用程序的允许列表，这将允许您直接从 XPipe 运行远程应用程序。\nrdpServerEnableTitle=RDP 服务器\nrdpServerEnableContent=目标系统上的 RDP 服务器已禁用。您想在注册表中启用它以允许远程 RDP 连接吗？\n#custom\nrdp=远程桌面（RDP）\nrdpScan=通过 SSH 的 RDP 隧道\nwslX11SetupTitle=WSL X11 设置\n#custom\nwslX11SetupContent=XPipe 可以使用你的本地 WSL 作为 X11 显示服务器。要在 $DIST$ 上设置 X11 吗？这将安装基础 X11 软件包，可能需要一些时间。你也可以稍后在设置菜单中更改所使用的版本。\ncommand=指令\ncommandGroup=命令组\nvncSystem=VNC 目标系统\nvncSystemDescription=实际交互系统。通常与隧道主机相同\nvncHost=目标 VNC 主机\nvncHostDescription=运行 VNC 服务器的系统\nvncDirectHost=主机\nvncDirectHostDescription=运行 VNC 服务器的服务器的主机条目或手动地址\nrdpDirectHost=主机\nrdpDirectHostDescription=运行 RDP 服务器的服务器的主机条目或手动地址\ngitVaultTitle=Git 保险库\n#custom\ngitVaultForcePushContent=要执行强制推送到远程仓库吗？这将用本地仓库的全部内容（包括历史记录）完全替换远程仓库。\ngitVaultOverwriteLocalContent=您想覆盖本地仓库的改动吗？这会将所有远程更改应用到本地仓库。\nrdpSimple.displayName=直接 RDP 连接\nrdpSimple.displayDescription=通过 RDP 连接到主机\nrdpUsername=用户名\n#custom\nrdpUsernameDescription=登录所用的用户名\naddressDescription=连接到哪里\nrdpAdditionalOptions=其他 RDP 选项\nrdpAdditionalOptionsDescription=要包含的原始 RDP 选项，格式与 .rdp 文件相同\nproxmoxVncConfirmTitle=VNC 访问\nproxmoxVncConfirmContent=您要为虚拟机启用 VNC 访问吗？这将在虚拟机配置文件中直接启用 VNC 客户端访问，并重新启动虚拟机。\ndockerContext.displayName=Docker 上下文\n#custom\ndockerContext.displayDescription=访问指定 Docker 上下文中的容器\nvmActions=虚拟机操作\ndockerContextActions=上下文操作\nk8sPodActions=Pod 操作\nopenVnc=启用 VNC 访问\naddVnc=添加 VNC 连接\ncommandGroup.displayName=命令组\ncommandGroup.displayDescription=系统可用命令组\nserial.displayName=串行连接\n#custom\nserial.displayDescription=在终端中打开串口连接\nserialPort=串行端口\nserialPortDescription=要连接的串行端口/设备\nbaudRate=波特率\ndataBits=数据位\nstopBits=停止位\nparity=奇偶校验\nflowControlWindow=流量控制\nserialImplementation=串行实施\nserialImplementationDescription=用于连接串行端口的工具\nserialHost=主机\nserialHostDescription=访问串行端口的系统\nserialPortConfiguration=串行端口配置\nserialPortConfigurationDescription=所连接串行设备的配置参数\nserialInformation=序列信息\nopenXShell=在 XShell 中打开\n#custom\ntsh.displayName=Teleport\n#custom\ntsh.displayDescription=通过 tsh 连接到你的 Teleport 节点\n#custom\ntshNode.displayName=Teleport 节点\n#custom\ntshNode.displayDescription=连接到集群中的 Teleport 节点\nteleportCluster=集群\n#custom\nteleportClusterDescription=该节点所在的集群\nteleportProxy=代理\nteleportProxyDescription=用于连接节点的代理服务器\nteleportHost=主机\nteleportHostDescription=节点的主机名\nteleportUser=用户\nteleportUserDescription=登录用户\nlogin=登录\nhyperVInstall.displayName=Hyper-V\nhyperVInstall.displayDescription=连接由 Hyper-V 管理的虚拟机\nhyperVVm.displayName=Hyper-V 虚拟机\nhyperVVm.displayDescription=通过 SSH 或 PSSession 连接到 Hyper-V 虚拟机\ntrustHost=信任主机\ntrustHostDescription=将 ComputerName 添加到受信任主机列表\n#custom\ncopyIp=复制 IP 地址\nvncDirect.displayName=直接 VNC 连接\nvncDirect.displayDescription=直接通过 VNC 连接到系统\neditConfiguration=编辑配置\n#custom\nviewInDashboard=在控制面板中查看\nsetDefault=设置默认值\nremoveDefault=删除默认值\n#custom\nconnectAsOtherUser=使用其他用户连接\nprovideUsername=提供其他登录用户名\nvmIdentity=访客身份\nvmIdentityDescription=必要时用于连接的 SSH 身份验证方法\nvmPort=端口\nvmPortDescription=通过 SSH 连接的端口\n#custom\nforwardAgent=前向代理（SSH 代理转发）\nforwardAgentDescription=在远程系统上提供 SSH 代理身份\n#custom\nvirshUri=通用资源标识符 (URI)\nvirshUriDescription=管理程序 URI，也支持别名\nvirshDomain.displayName=libvirt 域\nvirshDomain.displayDescription=连接到 libvirt 域\nvirshHypervisor.displayName=libvirt 虚拟机管理程序\nvirshHypervisor.displayDescription=连接到支持 libvirt 的管理程序驱动程序\nvirshInstall.displayName=libvirt 命令行客户端\nvirshInstall.displayDescription=通过 virsh 连接到所有可用的 libvirt 虚拟机管理程序\naddHypervisor=添加管理程序\n#custom\ninteractiveTerminal=交互式终端\n#custom\neditDomain=编辑Domain\n#custom\nlibvirt=libvirt Domain\ncustomIp=自定义 IP\n#custom\ncustomIpDescription=当使用高级网络配置时，可在此覆盖默认的本地虚拟机 IP 自动检测结果\nautomaticallyDetect=自动检测\nuserAddDialogTitle=创建用户\ngroupAddDialogTitle=创建群组\npassphrase=密码\n#custom\nrepeatPassphrase=重复密码\ngroupSecret=组秘密\nrepeatGroupSecret=重复群组秘密\nvaultGroup=保险库组\nloginAlertTitle=需要登录\nloginAlertHeader=解锁保险库以访问您的个人连接\nvaultUser=保险库用户\nme=我\naddGroup=添加组...\naddGroupDescription=为该保险库创建一个新组\naddUser=添加用户 ...\naddUserDescription=为该保险库创建新用户\nskip=跳过\n#custom\nuserChangePasswordAlertTitle=修改密码\ngroupChangeSecretAlertTitle=秘密更改\ndocs=文档\nlxd.displayName=LXD 容器\nlxd.displayDescription=通过 lxc 连接到 LXD 容器\nlxdCmd.displayName=LXD CLI 客户端\nlxdCmd.displayDescription=通过 lxc CLI 客户端访问 LXD 容器\npodman.displayName=Podman 容器\npodman.displayDescription=连接到 Podman 容器\n#custom\nincusInstall.displayName=Incus 容器管理器\nincusInstall.displayDescription=通过 incus CLI 客户端访问 incus 容器\nincusContainer.displayName=Incus 容器\nincusContainer.displayDescription=连接到 incus 容器\npodmanCmd.displayName=Podman CLI\npodmanCmd.displayDescription=通过 CLI 客户端访问 Podman 容器\nlxdHostDescription=LXD 容器所在的主机。必须安装了 lxc。\nlxdContainerDescription=LXD 容器的名称\npodmanContainers=Podman 容器\nlxdContainers=LXD 容器\nincusContainers=Incus 容器\n#custom\ncontainer=容器\nhost=主机\n#custom\ncontainerActions=容器操作\n#custom\nserialConsole=串口控制台\neditRunConfiguration=编辑运行配置\n#custom\ncommunityDescription=适配你各类个人用例的多功能连接工具。\nupgradeDescription=为您的整个服务器基础设施提供专业的连接管理。\n#custom\ndiscoverPlans=查看升级选项\nextendProfessional=升级到最新的专业功能\ncommunityItem1=无限连接非商业系统和工具\ncommunityItem2=与已安装的终端和编辑器无缝集成\ncommunityItem3=功能齐全的远程文件浏览器\n#custom\ncommunityItem4=适用于所有 Shell 的强大脚本系统\ncommunityItem5=Git 集成，用于同步和共享连接信息\nupgradeItem1=包括社区版的所有功能\nupgradeItem2=Homelab 计划支持无限的管理程序和高级 SSH 功能\nupgradeItem3=Professional 计划还支持企业操作系统和工具\nupgradeItem4=Enterprise 计划具有充分的灵活性，可满足您的个性化需求\n#custom\nupgrade=升级\nupgradeTitle=可用计划\nstatus=状态\ntype=类型\nlicenseAlertTitle=所需许可证\n#custom\nuseCommunity=继续使用社区版本\n#custom\npreviewDescription=新功能发布后可试用数周。\ntryPreview=激活预览\npreviewItem1=新发布的专业功能发布后两周内可完全访问\npreviewItem2=无需任何承诺即可试用新功能\nlicensedTo=授权给\nemail=电子邮件地址\napply=应用\nclear=清除\nactivate=激活\n#custom\nvalidUntil=许可证有效期至\n#custom\nlicenseActivated=您的许可证已激活\nrestart=重新启动\nlockVault=锁库\n#custom\nrestartApp=重启 XPipe\nfree=免费\nupgradeInfo=您可以在下面找到有关升级到许可证的信息。\nupgradeInfoPreview=您可以在下面找到有关升级到许可证的信息，或试用预览版。\nenterLicenseKey=输入许可证密钥进行升级\nisOnlySupported=至少要有$TYPE$ 许可证才支持\nareOnlySupported=至少需要$TYPE$ 许可证才能支持\nlegacyLicense=该许可证仅包含购买后一年内发布的新专业功能。\npreviewExpiredLicense=该功能最近还在免费预览阶段，但现在已经过期。\nopenApiDocs=API 文档\nopenApiDocsDescription=HTTP API 文档在线提供，包括 OpenAPI .yaml 规范。你可以在网络浏览器或你喜欢的 HTTP 客户端中打开它。\nopenApiDocsButton=打开文档\npythonApi=Python API\npersonalConnection=此连接及其所有子连接仅对您的用户开放，因为它们取决于个人身份。\ndeveloperPrintInitFiles=打印启动文件的执行\n#custom\ndeveloperPrintInitFilesDescription=打印终端启动时执行的全部 Shell 启动脚本。\ndeveloperShowSensitiveCommands=日志敏感命令\ndeveloperShowSensitiveCommandsDescription=在日志输出中包含敏感命令，以便调试。\n#custom\ncheckingForUpdates=正在检查更新\ncheckingForUpdatesDescription=获取最新版本信息\n#custom\ndownloadingUpdate=获取版本 (v$VERSION$)\n#custom\ndownloadingUpdateDescription=正在下载发布包\nupdateNag=您有一段时间没有更新 XPipe 了。您可能会错过新版本的新功能和修复。\nupdateNagTitle=更新提醒\nupdateNagButton=参见发布\n#custom\nrefreshServices=更新服务列表\nserviceProtocolType=服务协议类型\n#custom\nserviceProtocolTypeDescription=如何打开服务\nserviceCommand=服务激活后运行的命令\nserviceCommandDescription=占位符 $PORT 将被替换为实际的隧道本地端口\nvalue=价值\nshowAdvancedOptions=显示高级选项\nsshAdditionalConfigOptions=附加配置选项\nremoteFileManager=远程文件管理器\n#custom\nclearUserData=清除用户数据\nclearUserDataDescription=删除所有用户配置数据，包括连接\nclearUserDataTitle=用户数据删除\n#custom\nclearUserDataContent=此操作会删除 XPipe 的所有本地用户数据并重启。如果你在乎你的数据，务必先与 Git 仓库同步。\nundefined=未定义\ncopyAddress=复制地址\nnetbirdDeviceScan=网鸟连接\nnetbirdId=对等公开密钥\nnetbirdIdDescription=对等方的内部网鸟公钥 ID\n#custom\ntailscaleDeviceScan=Tailscale 连接\n#custom\ntailscaleInstall.displayName=Tailscale 安装\n#custom\ntailscaleInstall.displayDescription=通过 SSH 连接 Tailnet 中的设备\n#custom\ntailscaleDevice.displayName=Tailscale 设备\n#custom\ntailscaleDevice.displayDescription=通过 SSH 连接 Tailnet 中的设备\ntailscaleId=设备 ID\n#custom\ntailscaleIdDescription=内部的 Tailscale 设备 ID\ntailscaleHostName=主机名\n#custom\ntailscaleHostNameDescription=该设备在 Tailnet 中的主机名\ntailscaleUsername=用户名\n#custom\ntailscaleUsernameDescription=用于登录的用户名\ntailscalePassword=密码\ntailscalePasswordDescription=可用于 sudo 的可选用户密码\nscriptName=脚本名称\nscriptNameDescription=为脚本自定义名称\nscriptGroupName=脚本组名称\nscriptGroupNameDescription=为该脚本组自定义名称\nidentityName=身份名称\nidentityNameDescription=为该标识自定义名称\n#custom\ntailscaleTailnet.displayName=Tailnet\n#custom\ntailscaleTailnet.displayDescription=使用当前账号连接到指定 Tailnet\nputtyConnections=PuTTY 连接\nkittyConnections=KiTTY 连接\nicons=图标\ncustomIcons=自定义图标\niconSources=图标来源\n#custom\niconSourcesDescription=可以在这里添加你自己的图标来源。XPipe 会自动识别该位置的所有 .svg 文件并加入可用图标列表。\\n\\n支持本地目录与远程 Git 仓库。\nrefreshSources=刷新图标\nrefreshSourcesDescription=更新可用资源中的所有图标\naddDirectoryIconSource=添加目录源 ...\naddDirectoryIconSourceDescription=从本地目录添加图标\n#custom\naddGitIconSource=添加 Git 源 ...\n#custom\naddGitIconSourceDescription=添加远程 Git 仓库中的图标\nrepositoryUrl=Git 仓库 URL\niconDirectory=图标目录\naddUnsupportedKexMethod=添加不支持的密钥交换方法\naddUnsupportedKexMethodDescription=允许在此连接中使用密钥交换方法$VAL$\naddUnsupportedHostKeyType=添加不支持的主机密钥类型\naddUnsupportedHostKeyTypeDescription=允许在此连接中使用主机密钥类型$VAL$\naddUnsupportedMacType=添加不支持的 MAC 类型\naddUnsupportedMacTypeDescription=允许在此连接中使用 MAC 类型$VAL$\n#custom\nrunSilent=后台静默运行\nrunInFileBrowser=在文件浏览器中\n#custom\nrunInConnectionHub=连接中心中\ncommandOutput=命令输出\niconSourceDeletionTitle=删除图标源\niconSourceDeletionContent=您想删除此图标源及其所有相关图标吗？\nrefreshIcons=刷新图标\nrefreshIconsDescription=从外部资源检索、渲染和缓存所有可用的 1000 多个图标到 .png 文件。这可能需要一段时间...\n#custom\nvaultUserLegacy=Vault 用户（有限旧版兼容模式）\nupgradeInstructions=升级说明\nexternalActionTitle=外部操作请求\n#custom\nexternalActionContent=检测到一个外部操作请求。是否允许从 XPipe 外部启动操作？\n#custom\nnoScriptStateAvailable=刷新以检测脚本兼容性…\ndocumentationDescription=查看文档\ncustomEditorCommandInTerminal=在终端中运行自定义命令\ncustomEditorCommandInTerminalDescription=如果编辑器是基于终端的，可以启用此选项自动打开终端，并在终端会话中运行命令。\\n\\nvi、vim、nvim 等编辑器都可以使用该选项。\ndisableHttpsTlsCheck=禁用 HTTPS 请求证书验证\ndisableHttpsTlsCheckDescription=如果贵机构在防火墙中使用 SSL 拦截对 HTTPS 流量进行解密，任何更新检查或许可证检查都会因证书不匹配而失败。你可以启用此选项并禁用 TLS 证书验证来解决这个问题。\nconnectionsSelected=$NUMBER$ 选定的连接\naddConnections=添加连接\nbrowseDirectory=浏览目录\nopenTerminal=打开终端\n#custom\ndocumentation=帮助文档\nreport=报告错误\n#custom\nkeePassXcNotAssociated=KeePassXC 未关联\n#custom\nkeePassXcNotAssociatedDescription=XPipe 尚未与本地 KeePassXC 数据库关联。点击下方按钮执行一次性授权，之后即可检索密码。\nkeePassXcAssociateMore=连接更多数据库\nkeePassXcAssociateMoreDescription=可同时连接多个 KeePassXC 数据库\n#custom\nkeePassXcAssociated=KeePassXC 已关联\n#custom\nkeePassXcAssociatedDescription=XPipe 已连接到本地 KeePassXC 数据库。\n#custom\nkeePassXcNotAssociatedButton=关联\nidentifier=标识符\npasswordManagerCommand=自定义命令\n#custom\npasswordManagerCommandDescription=获取密码时执行的命令。占位符 $KEY 会替换为带引号的条目标识。命令需将密码直接打印到 stdout，例如：mypassmgr get $KEY。\nchooseTemplate=选择模板\n#custom\nkeePassXcPlaceholder=KeePassXC Entry URL\nterminalEnvironment=终端环境\n#custom\nterminalEnvironmentDescription=可选本地 Linux WSL 发行版作为终端环境，用其能力定制终端。\\n\\n自定义启动命令与多路复用器配置将于该发行版中执行。\n#custom\nterminalInitScript=终端初始化脚本\n#custom\nterminalInitScriptDescription=在连接建立前于终端环境执行的命令，用于初始化环境。\n#custom\nterminalMultiplexer=终端复用器\n#custom\nterminalMultiplexerDescription=作为终端标签页的替代所使用的终端复用器。启用后，它会用复用器功能替换部分终端处理特性（如标签页处理）。\\n\\n需要在该系统上已安装对应复用器的可执行文件。\n#custom\nterminalMultiplexerWindowsDescription=作为终端标签页的替代所使用的终端复用器。启用后，它会用复用器功能替换部分终端处理特性（如标签页处理）。\\n\\n在 Windows 上需使用 WSL 终端环境，并在对应的 WSL 版本中安装该复用器的可执行文件。\n#custom\nterminalAlwaysPauseOnExit=退出时总是暂停\n#custom\nterminalAlwaysPauseOnExitDescription=启用后，退出终端会话时将始终提示你选择重新启动或关闭该会话。禁用时，XPipe 仅在连接失败并以错误退出时才会提示。\nquerying=查询...\nretrievedPassword=获得：$PASSWORD$\nrefreshOpenpubkey=刷新 openpubkey 身份\n#custom\nrefreshOpenpubkeyDescription=运行 opkssh refresh 以重新使 openpubkey 身份生效\nall=全部\n#custom\nterminalPrompt=终端美化工具\n#custom\nterminalPromptDescription=用于远程终端的美化工具。启用后，首次打开会话会在目标系统自动部署和配置。\\n\\n不会修改已有提示配置，但初次加载时间会稍长。可能需安装 Nerd Fonts 以正确显示符号。\n#custom\nterminalPromptConfiguration=终端美化工具配置\n#custom\nterminalPromptConfig=配置文件内容\n#custom\nterminalPromptConfigDescription=自定义配置文件；终端初始化时自动部署为默认配置。若保留默认行为可留空。\npasswordManagerKey=密码管理器密钥\n#custom\npasswordManagerKeyDescription=密码管理器中此机密项的标识符\npasswordManagerAgent=密码管理器代理\ndockerComposeProject.displayName=Docker compose 项目\ndockerComposeProject.displayDescription=将组成项目的容器组合在一起\n#custom\nsshVerboseOutput=启用 SSH 调试输出\nsshVerboseOutputDescription=通过 SSH 连接时，它会打印大量调试信息。有助于排除 SSH 连接的故障。\ndontUseGateway=不要使用网关\ndontUseGatewayDescription=不要使用管理程序主机作为网关，而应直接连接 IP\ncategoryColor=类别 颜色\ncategoryColorDescription=该类别中的连接使用的默认颜色\n#custom\ncategorySync=与 Git 仓库同步\n#custom\ncategorySyncDescription=自动将该类别下的所有连接与 Git 仓库同步。本地更改会在保存时推送到远程。\ncategorySyncSpecial=与 git 仓库同步\\n(特殊类别 \"$NAME$\" 无法配置）\ncategoryDontAllowScripts=禁用所有修改\n#custom\ncategoryDontAllowScriptsDescription=禁止在此类别内的系统上创建脚本，以避免任何文件系统修改。此操作将禁用全部脚本功能、Shell 环境命令、提示符等。\ncategoryConfirmAllModifications=确认所有修改\n#custom\ncategoryConfirmAllModificationsDescription=在对连接或文件系统执行任何类型的修改前要求确认，可防止在关键系统上发生意外操作。\ncategoryDefaultIdentity=默认身份\ncategoryDefaultIdentityDescription=如果您经常在本类别中的许多系统上使用某个身份，那么设置默认身份可以让您在创建新连接时预先选择该身份。\ncategoryConfigTitle=$NAME$ 配置\nconfigure=配置\naddConnection=添加连接\n#custom\nnoCompatibleConnection=未找到兼容的连接\nnoCompatibleIdentity=未找到兼容身份\nnewCategory=新类别\ndockerComposeRestricted=该 compose 项目受$NAME$ 限制，不能从外部修改。请使用$NAME$ 管理此撰写项目。\nrestricted=限制级\ndisableSshPinCaching=禁用 SSH PIN 缓存\ndisableSshPinCachingDescription=当使用某种形式的基于硬件的身份验证时，XPipe 将自动缓存为密钥输入的任何 PIN 码。\\n\\n禁用此功能将导致每次尝试连接时都必须重新输入 PIN 码。\n#custom\ngitSyncPull=拉取远程 Git 仓库更改以同步\nenpassVaultFile=保险库文件\n#custom\nenpassVaultFileDescription=本地 Enpass Vault 主文件。\n#custom\nflat=仅当前层\n#custom\nrecursive=全部层级\nrdpAllowListBlocked=所选 RemoteApp 似乎不在服务器的 RDP 允许列表中。\npsonoServerUrl=服务器 URL\n#custom\npsonoServerUrlDescription=Psono 后端服务器 URL\n#custom\npsonoApiKey=API Key\n#custom\npsonoApiKeyDescription=用于访问的 API Key（UUID 格式）\n#custom\npsonoApiSecretKey=API Secret Key\n#custom\npsonoApiSecretKeyDescription=以 64 位十六进制字符串表示的 Secret Key\npassboltServerUrl=服务器 URL\npassboltServerUrlDescription=passbolt 后端服务器的 URL\npassboltPassphrase=密码\npassboltPassphraseDescription=保险库私钥的口令\npassboltPrivateKey=私钥\npassboltPrivateKeyDescription=保险库的私人 gpg 密钥文件\n#custom\nfocusWindowOnNotifications=通知时将 XPipe 置于前台\n#custom\nfocusWindowOnNotificationsDescription=当出现通知或错误消息时将 XPipe 前置到最前，例如连接或隧道意外终止时。\ngitUsername=自定义 git 用户名\ngitUsernameDescription=用于验证 git 远程仓库的自定义用户。默认情况下，XPipe 将使用当前配置的 git CLI 认证。\\n\\n此设置将覆盖本地 git CLI 客户端已配置的任何默认凭据。\ngitPassword=自定义 git 密码/个人访问令牌\ngitPasswordDescription=用于身份验证的密码或个人访问令牌。是否需要密码或个人访问令牌取决于 git 远程提供商。此设置将覆盖本地 git CLI 客户端已配置的默认凭据。\nsetReadOnly=设置只读\nunsetReadOnly=未设置只读\n#custom\nreadOnlyStoreError=此条目配置已锁定。使用不同名称另存为副本即可修改。\ncategoryFreeze=冻结连接配置\ncategoryFreezeDescription=将连接配置标记为只读。这意味着不能修改该类别中的任何现有连接条目配置。但可以添加新连接。\n#custom\nupdateFail=更新安装失败\nupdateFailAction=手动安装更新\nupdateFailActionDescription=在 GitHub 上查看最新版本\nonePasswordPlaceholder=项目名称或 op:// URL\ncomputeDirectorySizes=计算目录大小\ncomputeSize=计算大小\ncustomSpiceCommand=自定义命令\ncustomSpiceCommandDescription=启动 SPICE 会话时要执行的自定义命令。调用时，占位符字符串 $FILE 将被 .vv 文件的引号文件路径替换。\nvncClient=VNC 客户端\nvncClientDescription=在 XPipe 中打开 VNC 连接时要启动的 VNC 客户端。\\n\\n您可以选择使用 XPipe 中集成的 VNC 客户端，或者启动一个本地安装的外部 VNC 客户端（如果您需要更多自定义功能）。\nintegratedXPipeVncClient=集成 XPipe VNC 客户端\ncustomVncCommand=自定义命令\ncustomVncCommandDescription=启动 VNC 会话时要执行的自定义命令。调用时，占位符字符串 $ADDRESS 将被带引号的地址替换。\nvncConnections=VNC 连接\npasswordManagerIdentity=密码管理器身份\npasswordManagerIdentity.displayName=密码管理器身份\npasswordManagerIdentity.displayDescription=从密码管理器中读取身份的用户名和密码\npasswordCopied=连接密码复制到剪贴板\nerrorOccurred=发生错误\n#custom\nactionMacro.displayName=动作宏\n#custom\nactionMacro.displayDescription=通过自定义触发器执行该动作\nmacroAdd=添加宏\nmacroName=宏名称\n#custom\nmacroNameDescription=为此宏设置自定义名称\nactionId=动作 ID\n#custom\nactionIdDescription=此宏要运行的动作\n#custom\nmacroRefs=关联的连接\n#custom\nmacroRefsDescription=该宏运行动作时使用的连接\nconnectionCopy=复制\n#custom\nactionPickerTitle=选择动作\n#custom\nactionPickerDescription=点击某项以执行一个动作。你也可以切换到“动作快捷方式选择”模式，创建或编辑该动作的快捷方式，而不是直接执行。\n#custom\ncancelActionPicker=取消动作选择\n#custom\nactionShortcut=动作快捷方式\n#custom\nactionShortcuts=动作快捷方式\n#custom\nactionStore=目标系统\n#custom\nactionStoreDescription=该动作的目标系统\n#custom\nactionStores=目标系统\n#custom\nactionStoresDescription=该动作的目标系统\nactionDesktopShortcut=桌面快捷方式\n#custom\nactionDesktopShortcutDescription=为此动作在桌面创建一个快捷方式\nactionUrlShortcut=URL 快捷方式\n#custom\nactionUrlShortcutDescription=复制一个在打开时可触发此动作的 URL\nactionUrlShortcutDisabled=URL 快捷方式（不可用）\n#custom\nactionUrlShortcutDisabledDescription=该类型不支持打开 URL\n#custom\nactionApiCall=API 请求\n#custom\nactionApiCallDescription=通过 HTTP API 调用此动作\nactionMacro=动作宏\n#custom\nactionMacroDescription=为此动作创建一个具备高级功能的宏\ncreateMacro=创建宏\nactionConfiguration=参数\n#custom\nactionConfigurationDescription=执行该动作时要传递的参数\n#custom\nconfirmAction=确认动作\n#custom\nactionConnections=动作连接\n#custom\nactionConnectionsDescription=要运行该动作的连接\nactionConnection=动作连接\n#custom\nactionConnectionDescription=要运行该动作的连接\n#custom\nappleContainerInstall.displayName=Apple 容器\n#custom\nappleContainerInstall.displayDescription=通过容器 CLI 访问 Apple 容器实例\n#custom\nappleContainer.displayName=Apple 容器\n#custom\nappleContainer.displayDescription=通过容器 CLI 访问 Apple 容器实例\n#custom\nappleContainerHostDescription=Apple 容器所在主机\n#custom\nappleContainerDescription=Apple 容器名称\n#custom\nappleContainers=Apple 容器\nchangeOrderIndexTitle=更改顺序\n#custom\norderIndex=排序\n#custom\norderIndexDescription=为此条目指定一个用于排序的数字；数值越小显示越靠前。\nmoveToFirst=移至首位\nmoveToLast=移至最后\ncategory=类别\nincludeRoot=包括根\nexcludeRoot=排除根\nfreezeConfiguration=冻结配置\nunfreezeConfiguration=解冻配置\nwaylandScalingTitle=Wayland 扩展\nactionApiUrl=$URL$ (复制 json 正文)\ncopyBody=复制请求正文\ngitRepoTerminalOpen=在终端打开仓库\n#custom\ngitRepoTerminalOpenDescription=在命令行中查看该仓库\ngitRepoOverwriteLocal=覆盖本地仓库\ngitRepoOverwriteLocalDescription=用远程更改替换所有本地更改\n#custom\ngitRepoForcePush=强制推送（覆盖）远程仓库\n#custom\ngitRepoForcePushDescription=使用 git push --force 将本地更改强制推送到远程\ngitRepoDontWarn=不再发出警告\ngitRepoDontWarnDescription=如果这是预料之中的，请让 XPipe 今后忽略此错误\ngitRepoTryAgain=再试一次\ngitRepoTryAgainDescription=再次尝试相同的操作\ngitRepoEnablePlain=使用普通目录同步\ngitRepoEnablePlainDescription=不初始化 git 仓库以同步目录中的更改\ngitRepoCreateBare=使用 git 同步\ngitRepoCreateBareDescription=在同步目录中初始化一个新的裸 git 仓库\n#custom\ngitRepoDisable=暂时禁用 Git 保险库\ngitRepoDisableDescription=在此会话期间不要提交任何更改\n#custom\ngitRepoPullRefresh=拉取更改并刷新\ngitRepoPullRefreshDescription=合并远程更改并重新加载数据\n#custom\nbreakOutCategory=按分类拆分出新的连接类别\nmergeCategory=合并类别\nopenWinScp=在 WinSCP 中打开\nuninstallApplication=卸载\nuninstallApplicationDescription=运行 .pkg 安装脚本以完全卸载 XPipe\nk8sEditPodTitle=应用更改\nk8sEditPodContent=您想应用通过 kubectl apply 命令作出的更改吗？可能需要重新启动才能应用更改。\nvirshEditDomainTitle=应用更改\nvirshEditDomainContent=要将更改应用到域中吗？可能需要重新启动才能应用更改。\npkcs11Library=PKCS#11 库\npkcs11LibraryDescription=动态链接库文件的路径\n#custom\nsshAgentSocket=自定义 SSH Agent 套接字\n#custom\nsshAgentSocketDescription=用于与 SSH 代理通信的自定义套接字。在连接配置中选择“自定义 Agent”选项后即可使用该自定义 Agent。\n#custom\npublicKey=公钥标识\n#custom\npublicKeyDescription=指定的可选公钥；填写后将强制 Agent 仅提供与其匹配的私钥。\nactions=行动\n#custom\nhcloudServer.displayName=Hetzner 云服务器\n#custom\nhcloudServer.displayDescription=通过 SSH 访问 Hetzner Cloud 上托管的服务器\n#custom\nhcloudInstall.displayName=Hetzner 云 CLI\n#custom\nhcloudInstall.displayDescription=使用 hcloud CLI 管理 Hetzner Cloud 上的服务器\nhcloudContext.displayName=云上下文\n#custom\nhcloudContext.displayDescription=该云环境内可访问的服务器集合\n#custom\nmetrics=统计\nopenInVsCode=在 VsCode 中打开\naddCloud=云计算 ...\n#custom\nhcloudToken=API Token\n#custom\nhcloudTokenDescription=用于访问 Hetzner Cloud 的 API Token（参见官方文档）\n#custom\nhcloudLogin=Hetzner 云登录\n#custom\nclearHcloudToken=清除 Token\n#custom\nclearHcloudTokenDescription=删除当前 Token 以重新登录\nselectIdentity=选择身份\n#custom\nenableMcpServer=启用 MCP 服务端\n#custom\nenableMcpServerDescription=启动 XPipe 的 MCP 服务端，允许外部 MCP 客户端发送请求。下方提供连接配置。\\n\\n注意：MCP 与 HTTP API 相互独立，无需启用 HTTP API 即可使用。\n#custom\nenableMcpMutationTools=启用 MCP 修改工具\n#custom\nenableMcpMutationToolsDescription=默认仅启用只读工具以避免意外更改系统。\\n\\n若需通过 MCP 客户端执行会修改系统的操作，请先确认客户端具备针对破坏性行为的交互确认，再开启此选项。开启后需让已连接的 MCP 客户端重新连接生效。\nmcpClientConfigurationDetails=MCP 客户端配置\n#custom\nmcpClientConfigurationDetailsDescription=使用以下配置数据从选定 MCP 客户端连接到 XPipe MCP 服务端。\nswitchHostAddress=更改主机地址\naddAnotherHostName=添加另一个主机名\naddNetwork=网络扫描 ...\nnetworkScan=网络扫描\n#custom\nnetworkScanStore=扫描目标\n#custom\nnetworkScanStoreDescription=扫描本地网段以发现运行 SSH 服务的主机\nuseAsGateway=使用主机作为网关\nuseAsGatewayDescription=是否将目标主机用作创建连接的网关\nnetworkScanPorts=扫描端口\n#custom\nnetworkScanPortsDescription=需要包含在扫描中的以逗号分隔的 SSH 端口列表\nnetworkScanType=连接类型\nnetworkScanTypeDescription=需要查找的服务器类型\n#custom\nemptyDirectory=这似乎是个空目录\nhcloudConfigFile=hcloud 配置文件\nhcloudConfigFileDescription=hcloud CLI .toml 配置文件的位置\npreferMonochromeIcons=首选单色图标\npreferMonochromeIconsDescription=启用后，单色图标变量将优先于默认的彩色图标版本，前提是来源图标有单独的浅色或深色模式图标变量。\\n\\n需要刷新图标才能应用。\nalwaysShowSshMotd=始终显示 MOTD\n#custom\nalwaysShowSshMotdDescription=登录新终端会话时始终显示远程系统的每日消息 (MOTD)。更改此项可能影响 SSH 连接的初始化流程。\nmanageSubscription=管理订阅\nnoListeningServer=无监听服务器\nnetworkScanResults=扫描结果\n#custom\nnetworkScanResultsDescription=扫描发现的主机列表\n#custom\nlocalShellDialect=本地 Shell\n#custom\nlocalShellDialectDescription=用于本地操作的 Shell。如果正常的本地默认 Shell 在某种程度上被禁用或损坏，则可使用此选项返回到另一种选择。\\n\\n某些配置（如自定义 PATH 条目）如果尚未在相应的 Shell 配置文件中配置，则可能不适用于备用 shell。\n#custom\nagentSocketNotFound=未找到活动的 Agent 套接字\n#custom\nagentSocket=Agent 套接字位置\nagentSocketDescription=代理套接字文件的路径\n#custom\nagentSocketNotConfigured=尚未设置自定义 Agent 套接字\ndownloadInProgress=$NAME$ 下载中\nenableTerminalStartupBell=启用终端启动铃声\n#custom\nenableTerminalStartupBellDescription=如果你选用的终端支持响铃，会在新建终端会话时播放蜂鸣/响铃提示。可帮助你更容易分辨新启动的终端实例。\ninvalidSshGatewayChain=包含跳转网关和非跳转网关的混合网关链配置无效。\nsyncFileExists=同步文件$FILE$ 已经存在\nreplaceFile=替换文件\nreplaceFileDescription=用这个文件替换现有文件\nrenameFile=重命名文件\nrenameFileDescription=为该文件取一个不同的名称，以便同步\nnewFileName=新文件名\nparentHostDoesNotSupportTunneling=父主机$NAME$ 不支持隧道传输\nconnectionNotesTemplate=备注模板\nconnectionNotesTemplateDescription=为连接添加新备注条目时应使用的 markdown 模板。\nconnectionNotesButton=编辑注释\nrdpSmartSizing=启用智能尺寸\nrdpSmartSizingDescription=启用后，如果窗口太小，无法以全分辨率显示，mstsc 会缩小桌面大小。缩放时将保留桌面的宽高比。\ndisableStartOnInit=禁用自动启动\nenableStartOnInit=启用自动启动\nfileReadSudoTitle=Sudo 文件读取\nfileReadSudoContent=您尝试读取的文件不授予当前用户读取权限。你想以 sudo root 用户身份读取该文件吗？这将使用现有凭证或通过提示自动提升为根用户。\nnetbirdInstall.displayName=安装 Netbird\nnetbirdInstall.displayDescription=连接到您的 Netbird 网络中的对等网络\nnetbirdProfile.displayName=网鸟简介\nnetbirdProfile.displayDescription=特定配置文件中的同行者列表\nnetbirdPeer.displayName=网鸟同行\nnetbirdPeer.displayDescription=通过 SSH 连接到对等网络\nnetbirdPublicKey=公钥\nnetbirdPublicKeyDescription=对等方的内部公开密钥\nnetbirdHostName=主机名\nnetbirdHostNameDescription=网络中对等设备的主机名\nvncRefSystem=关联系统\nvncRefSystemDescription=与此 VNC 连接相关联的连接条目。如果没有，则留空\nabstractHost.displayName=摘要主机\nabstractHost.displayDescription=为不支持 shell 连接的主机创建一个条目\nabstractHostAddress=主机地址\nabstractHostAddressDescription=主机地址\nabstractHostGateway=网关\nabstractHostGatewayDescription=用于连接该主机的可选网关系统\nabstractHostConvert=转换为抽象主机条目\nhostNoConnections=无可用连接\nhostHasConnections=$COUNT$ 可用连接\nhostHasConnection=$COUNT$ 可用连接\nlargeFileWarningTitle=大文件编辑\nlargeFileWarningContent=您要编辑的文件很大，有$SIZE$ 。您真的想在文本编辑器中打开这个文件吗？\nrdpAskpassUser=主机的 RDP 用户名$HOST$\nrdpAskpassPassword=用户密码$USER$\ninPlaceKey=关键字\ninPlaceKeyText=私钥内容\ninPlaceKeyTextDescription=私钥内容\nnetbirdSelfhosted=自托管网鸟实例\nnetbirdSelfhostedDescription=提供自定义 URL，而不是使用云托管版本\nnetbirdManagementUrl=网鸟管理 URL\nnetbirdManagementUrlDescription=自托管实例的管理 URL\nnetbirdSetupKey=设置键\nnetbirdSetupKeyDescription=如果使用设置密钥，可以使用一个用于登录\nnetbirdLogin=网鸟登录\naddProfile=添加配置文件\nnetbirdProfileNameAsktext=新网鸟配置文件的名称\nopenSftp=在 SFTP 会话中打开\ncapslockWarning=您已启用盖帽锁\ninherit=继承\nsshConfigStringSelected=目标主机\nsshConfigStringSelectedDescription=对于多个主机，第一个会被用作目标。重新排列主机顺序以更改目标\ntunnelToLocalhost=隧道到本地主机\ntunnelToLocalhostDescription=自动将远程端口隧道到 localhost\ntags=标签\ntag=标签\naddNewTag=创建新标签\ncreateTag=创建标签...\ninPlacePublicKey=公钥\ninPlacePublicKeyDescription=指定私人密钥的相关公钥\nsshKeygenTitle=生成新的 SSH 密钥\nsshKeygenAlgorithm=算法\nsshKeygenAlgorithmDescription=密钥使用的非对称密钥生成算法\nrsaBits=比特\nrsaBitsDescription=生成密钥的位数\nsshKeygenComment=评论\nsshKeygenCommentDescription=此密钥的可选注释\nsshKeygenPassphrase=密码\nsshKeygenPassphraseDescription=该密钥的可选口令\ned25519SkResident=制作常驻密钥\ned25519SkResidentDescription=在硬件安全密钥上存储私钥\ned25519SkResidentKeyName=常驻密钥标签\ned25519SkResidentKeyNameDescription=给密钥贴标签。在安全密钥中存储多个密钥时需要使用\ned25519SkPinRequired=要求输入密码\ned25519SkPinRequiredDescription=使用时要求输入密码\ned25519SkUserPresenceRequired=要求用户在场\ned25519SkUserPresenceRequiredDescription=使用时需要触摸或类似操作。某些安全密钥要求启用此功能\ncopyPublicKey=复制公开密钥\ngeneratePublicKey=生成公钥\npublicKeyGenerateNotice=可由私人密钥生成\nidentityApplyTargetHost=目标\nidentityApplyTargetHostDescription=应用身份识别的系统\nidentityApplyAuthorizedHost=已授权的 SSH 密钥\nidentityApplyAuthorizedHostDescription=将 SSH 密钥添加到授权主机文件中\nidentityApplyAuthorizedHostButton=将密钥附加到文件\napplyIdentityToHost=对主机应用身份 ...\nidentityApplyMissingPublicKeyTitle=丢失的公钥\nidentityApplyMissingPublicKeyContent=身份的 SSH 密钥没有关联公钥。请查看配置了解详情。\nvalid=有效\nnotValid=无效\nwarning=警告\nidentityApplyTitle=应用身份\nidentityApplyConfigPasswordEnabled=已启用密码验证\nidentityApplyConfigPasswordEnabledDescription=密码验证仍在 sshd 配置中启用\nidentityApplyConfigPasswordDisabled=禁用密码验证\nidentityApplyConfigPasswordDisabledDescription=在 sshd 配置中仍禁用密码验证\nidentityApplyConfigKeyEnabled=已启用密钥验证\nidentityApplyConfigKeyEnabledDescription=基于密钥的身份验证仍在 sshd 配置中启用\nidentityApplyConfigKeyDisabled=禁用密钥验证\nidentityApplyConfigKeyDisabledDescription=sshd 配置中基于密钥的身份验证仍处于禁用状态\nidentityApplyConfigRootDisabledWarning=禁用根登录\nidentityApplyConfigRootDisabledWarningDescription=sshd 配置中未启用根用户登录\nidentityApplyConfigAdminWarning=配置的管理员密钥\nidentityApplyConfigAdminWarningDescription=对于管理员用户，可能需要将密钥添加到 administrators_authorized_keys 中。\nidentityApplyEditConfig=编辑配置\nidentityApplyEditConfigDescription=在编辑器中打开 sshd 配置以修复任何问题\nidentityApplyEditAuthorizedKeys=编辑授权密钥\nidentityApplyEditAuthorizedKeysDescription=在编辑器中打开 authorized_keys 文件，编辑或删除其他密钥\nidentityApplyEditConfigButton=打开 sshd_config\nidentityApplyEditAuthorizedKeysButton=打开授权密钥\nidentityApplySetStoreIdentity=连接标识集\nidentityApplySetStoreIdentityDescription=身份已配置为连接使用\nidentityApplySetStoreIdentityButton=应用身份\ngenerateKey=生成密钥\ngroupSecretStrategy=基于组的访问控制\ngroupSecretStrategyDescription=如何检索用于加密和解密组的组密文。用户登录保险库启动时，将运行您选择的检索方法。\\n\\n此设置按组配置。要为当前活动组以外的其他组更改此设置，必须以该组成员身份登录保管库。\nfileSecret=基于文件的秘密\ncommandSecret=指令\nhttpRequestSecret=HTTP 响应\nfileSecretChoice=文件位置\nfileSecretChoiceDescription=包含组加密密文的文件路径。由于可以在所有平台上查询该文件，因此可以在路径中使用 ~ 来指代主目录。该文件必须在所有解锁金库的系统上都可用，否则登录将失败。\ncommandSecretField=检索脚本\ncommandSecretFieldDescription=返回当前组秘密加密密钥的命令。该命令在本地系统默认 shell 中运行，密钥应打印到 stdout。\nhttpRequestSecretField=请求 URI\nhttpRequestSecretFieldDescription=发送 HTTP 请求的 URI。组密文取自 HTTP 响应体。\nvaultAuthentication=保险库验证\nvaultAuthenticationDescription=如何验证/解锁信息库数据。有多种加密和解锁信息库数据的方法，具体取决于你想与谁共享信息库数据。\ngroupAuthFailed=密文验证失败\nuserAuthFailed=密码验证失败\nsavingChanges=保存更改\nawsDeviceScan=AWS\naws=AWS\nawsCliInstallTitle=需要 AWS CLI\nawsCliInstallContent=AWS 集成要求在本地系统中安装 AWS CLI\nawsProfileCreateTitle=新的 AWS 配置文件\nawsProfileAccessKey=访问键\nawsProfileName=简介名称\nawsProfileNameDescription=新配置文件的显示名称\nawsProfileRegion=区域\nawsProfileRegionDescription=与配置文件相关联的 AWS 区域\nawsProfileAccessKeyId=访问密钥 ID\nawsProfileAccessKeyIdDescription=IAM 用户访问密钥 ID\nawsProfileSecretAccessKey=秘密访问密钥\nawsProfileSecretAccessKeyDescription=相关的秘密访问密钥\nawsInstall.displayName=AWS CLI 安装\nawsInstall.displayDescription=通过 AWS CLI 连接到您的 AWS 系统\nawsProfile.displayName=AWS CLI 配置文件\nawsProfile.displayDescription=通过特定配置文件访问 AWS\nawsInstanceId=实例 ID\nawsInstanceIdDescription=该实例的内部 ID\nawsInstanceUseSsm=通过 SSM 连接\nawsInstanceUseSsmDescription=使用 SSM 工具通过 SSH 连接到实例\nawsEc2Instance.displayName=AWS EC2 实例\nawsEc2Instance.displayDescription=通过 SSH 连接 EC2 实例\nawsS3Group.displayName=S3 存储桶\nawsS3Group.displayDescription=访问 AWS 配置文件的 S3 存储桶\nawsS3Bucket.displayName=S3 邮筒\nawsS3Bucket.displayDescription=访问 AWS 配置文件的 S3 存储桶\nawsEc2Group.displayName=EC2 实例\nawsEc2Group.displayDescription=访问 AWS 配置文件的 EC2 实例\nawsEc2InstanceSsmTerminal=打开 SSM 终端\ngenericS3Bucket.displayName=通用 S3 存储桶\ngenericS3Bucket.displayDescription=通过 AWS CLI 访问通用 S3 存储桶\naddFileSystem=文件系统 ...\ngenericS3BucketHost=主机\ngenericS3BucketHostDescription=S3 服务器的主机条目或手动地址\ngenericS3BucketPortDescription=S3 服务器正在监听的端口\ngenericS3BucketAccessKeyId=访问密钥 ID\ngenericS3BucketAccessKeyIdDescription=IAM 用户访问密钥 ID\ngenericS3BucketSecretAccessKey=秘密访问密钥\ngenericS3BucketSecretAccessKeyDescription=相关的秘密访问密钥\ngenericS3BucketHttps=启用 HTTPS\ngenericS3BucketHttpsDescription=使用 HTTPS 连接服务器。某些提供商可能要求使用 HTTPS\ntunnelled=隧道式\nawsInstallSync=配置同步\nawsInstallSyncDescription=将 AWS CLI 配置文件同步到 git vault\nawsInstallLocation=用户数据位置\nawsInstallLocationDescription=AWS CLI 配置文件的来源路径\ninstanceActions=实例操作\nopenSplit=在分割终端中打开\nterminalSplitStrategy=分割视图方向\nterminalSplitStrategyDescription=在批处理模式下使用分割视图功能打开相邻的多个终端会话时，控制终端选项卡的分割方式。\nterminalSplitStrategyDisabledDescription=在批处理模式下使用分割视图功能打开相邻的多个终端会话时，控制终端选项卡的分割方式。\\n\\n您当前的终端配置不支持分割视图。\nhorizontal=横向\nvertical=纵向\nbalanced=平衡\nclose=关闭\nhelpButton=$TOPIC$ 文档链接\nquickAccess=快速访问\ntoggleEnabled=切换状态\ncurrentPath=当前路径\ndirectoryContents=目录内容\ndirectoryOptions=目录选项\nchooseConnectionType=选择连接类型\nbatchMode=批处理模式\ntoggleButton=切换按钮\ntailscaleUseSsh=使用 tailscale SSH 验证\ntailscaleUseSshDescription=通过 tailscale SSH 服务器本身登录，无需任何 SSH 验证\nportDescription=SSH 服务器运行的端口\nloginAs=登录为\nsshGatewayType=网关类型\nsshGatewayTypeDescription=是否通过隧道或使用 ProxyJump 选项连接目标机\ngatewayTunnel=网关隧道\nproxyJump=代理跳转\ncommandTypeAsyncBackground=在后台独立运行\ncommandTypeSyncBackground=在后台运行并等待完成\ncommandTypeTerminalBackground=在终端中打开\nasyncBackgroundCommand=背景命令\nsyncBackgroundCommand=阻止后台命令\nterminalBackgroundCommand=终端命令\ntestingConnection=测试连接...\nopenManagementConsole=开放式管理控制台\nopenLxcTerminal=打开 LXC 终端\nopenContainerConsole=打开串行控制台\nkeeper2fa=2FA 方法\nkeeper2faDescription=为您的账户配置的主要双因素身份验证方法。如果您的 Keeper 帐户需要双因素身份验证才能访问密码，请启用此选项。\nkeeperTotpDuration=自定义 2FA 代码持续时间\nkeeperTotpDurationDescription=覆盖 2FA 验证码有效期的默认持续时间。仅适用于组织政策允许更改持续时间的情况。\\n\\n可能的值有$VALUES$\nkeeperOtherAuth=其他（RSA SecurID、Duo Security、Keeper DNA 等）\nextractReusableIdentities=提取可重复使用的身份\nidentitiesAdded=添加的身份信息\nsyncMode=同步模式\nsyncModeDescription=控制同步更改的方式。\\n\\n即时模式会尽快推拉更改，启动和退出模式会一次性同步会话中的所有更改，而手动模式只有在你启动时才会同步。\ntoggleTerminalDock=切换终端基座\nscriptDirectory=目录位置\nscriptDirectoryDescription=包含 shell 脚本文件的本地目录\nscriptSourceUrl=仓库 URL\nscriptSourceUrlDescription=指向包含 shell 脚本文件的远程 git 仓库的 URL\nscriptCollectionSourceType=来源类型\nscriptCollectionSourceTypeDescription=加载 shell 脚本的源类型\nscriptCollectionSourceEntry=来源条目\nscriptCollectionSourceEntryDescription=加载 shell 脚本的源代码\ngitRepository=Git 仓库\nscriptCollectionSource.displayName=脚本源\nscriptCollectionSource.displayDescription=从现有源自动导入 shell 脚本\ndirectorySource=目录源\ngitRepositorySource=Git 仓库源代码\nrefreshSource=刷新源\nscriptTextSourceUrl=脚本 URL\nscriptTextSourceUrlDescription=检索脚本文件的 URL\nscriptSourceType=脚本源\nscriptSourceTypeDescription=从何处获取脚本\nscriptSourceTypeInPlace=就地脚本\nscriptSourceTypeUrl=外部 URL\nscriptSourceTypeSource=现有来源\nimportScripts=导入脚本\nscriptsContained=$NUMBER$ 脚本\nscriptSourceCollectionImportTitle=从源代码导入脚本 ($SELECTED$/$COUNT$)\nnoScriptsFound=未找到脚本\ntunnel=隧道\nnotInitialized=未初始化\nselectCategory=选择类别 ...\nscriptSourceName=脚本名称\nscriptSourceNameDescription=源代码中脚本的文件名\nworkspaceRestartTitle=工作区就绪\nworkspaceRestartContent=新工作区的快捷方式已在$PATH$ 上创建。您可以导航到该快捷方式或重新启动 XPipe，以自动打开新工作区。\nbrowseShortcut=浏览文件\nsyncModeInstant=即时同步\nsyncModeSession=启动和退出时同步\nsyncModeManual=手动同步\npushChanges=推送更改\npullChanges=拉动更改\nsourcedFrom=来源于$SOURCE$\ninPlaceScript=就地脚本\ngeneric=通用\nsyncToPlainDirectory=同步到普通目录\nsyncToPlainDirectoryDescription=同步到本地目录时，可以把该目录当作另一个 git 仓库，也可以只当作一个普通目录。如果启用了普通目录设置，该目录就不会被初始化为 git 仓库。\nopenSpiceSession=打开 SPICE 会话\nterminalBehaviour=终端行为\nnoScanPossible=未找到支持的连接\nnetworkSwitchPorts=网络端口\nnswitchGroup.displayName=网络端口\nnswitchGroup.displayDescription=列出网络设备上的可用端口\nnswitchPort.displayName=网络端口\nnswitchPort.displayDescription=控制网络交换设备上的单个端口\nenablePort=启用端口\nshutdownPort=关闭端口\nresetPort=重置端口\nuseSystemDefault=使用系统默认值\nportStatus=端口状态\nclearCounters=清除计数器\nshowStatus=显示状态\nshowAllPorts=显示所有端口\nactiveLicense=许可证\nactiveLicenseDescription=激活 XPipe 许可证密钥\nauthenticatorApp=验证器应用程序\nsecurityKey=安全密钥\nmcpAdditionalContext=其他 MCP 上下文\nmcpAdditionalContextDescription=传递给 MCP 客户端的附加指令。用它来控制代理行为，并为您的个性化设置提供额外的上下文。\nmcpAdditionalContextSample=- 未经确认，请勿自动重启任何服务和守护进程\\n- 配置网络接口时，始终使用 192.168.1.1/24 作为网关\nprefsRestartTitle=需要重新启动\nprefsRestartContent=您更改的某些选项需要重新启动应用程序才能应用。您现在要重新启动 XPipe 吗？\nbashShell=Bash shell\n"
  },
  {
    "path": "lang/strings/translations_zh-Hant.properties",
    "content": "delete=刪除\nproperties=屬性\nusedDate=已使用$DATE$\nopenDir=開放目錄\nsortLastUsed=依最後使用日期排序\nsortAlphabetical=依名稱字母順序排序\nsortIndexed=依序索引排序\nrestartDescription=重新啟動通常可以快速解決問題\nreportIssue=報告問題\nreportIssueDescription=開啟整合式問題報告器\nusefulActions=有用的動作\nstored=儲存\ntroubleshootingOptions=疑難排解工具\ntroubleshoot=疑難排解\nremote=遠端檔案\naddShellStore=新增 Shell ...\naddShellTitle=新增 Shell 連線\nsavedConnections=儲存的連線\nsave=儲存\nclean=清潔\nmoveTo=移動到 ...\naddDatabase=資料庫 ...\nbrowseInternalStorage=瀏覽內部儲存空間\naddTunnel=隧道 ...\naddService=服務 ...\naddScript=腳本 ...\naddHost=遠端主機 ...\naddShell=Shell 環境 ...\naddCommand=指令 ...\naddAutomatically=自動添加 ...\naddOther=添加其他 ...\nconnectionAdd=新增連線\nscriptAdd=新增腳本\nscriptGroupAdd=新增指令碼群組\nidentityAdd=新增身分\nnew=新產品\nselectType=選擇類型\nselectTypeDescription=選擇連線類型\nselectShellType=殼類型\nselectShellTypeDescription=選擇 Shell 連線的類型\nname=名稱\nstoreIntroHeader=連接樞紐\nstoreIntroContent=在這裡，您可以在一個地方管理所有本機和遠端 shell 連線。一開始，您可以快速自動偵測可用的連線，並選擇要新增的連線。\nstoreIntroButton=搜尋連線 ...\ndragAndDropFilesHere=或直接將檔案拖放至此\nconfirmDsCreationAbortTitle=確認中止\nconfirmDsCreationAbortHeader=您要中止資料來源的建立嗎？\nconfirmDsCreationAbortContent=任何資料來源建立進度都會遺失。\nconfirmInvalidStoreTitle=跳過驗證\nconfirmInvalidStoreContent=您想跳過連線驗證嗎？即使無法驗證，您也可以新增此連線，並在稍後修復連線問題。\nexpand=展開\naccessSubConnections=存取子連線\ncommon=常見\ncolor=顏色\nalwaysConfirmElevation=永遠確認權限提升\nalwaysConfirmElevationDescription=控制如何處理需要提升權限才能在系統上執行指令的情況，例如使用 sudo。\\n\\n預設情況下，任何 sudo 認證會在會話期間緩存，並在需要時自動提供。如果啟用此選項，每次都會要求您確認提升存取權限。\nallow=允許\nask=詢問\ndeny=拒絕\nshare=新增至 git 儲存庫\nunshare=從 git 套件庫移除\nremove=移除\ncreateNewCategory=新的子類別\nprompt=提示\ncustomCommand=自訂指令\nother=其他\nsetLock=設定鎖定\nselectConnection=選擇連線\nselectEntry=選擇項目\ncreateLock=建立密碼\nchangeLock=變更密碼\ntest=測試\nfinish=完成\nerror=發生錯誤\ndownloadStageDescription=將下載的檔案移動到您的系統下載目錄中並開啟。\nok=好的\nsearch=搜尋\nrepeatPassword=重複密碼\naskpassAlertTitle=詢問密碼\nunsupportedOperation=不支援的操作：$MSG$\nfileConflictAlertTitle=解決衝突\nfileConflictAlertContent=遇到衝突。檔案$FILE$ 已存在於目標系統上。\\n\\n您想如何繼續？\nfileConflictAlertContentMultiple=遇到衝突。檔案$FILE$ 已經存在。\\n\\n您想如何繼續？可能有更多的衝突，您可以選擇一個適用於所有的選項來自動解決。\nmoveAlertTitle=確認移動\nmoveAlertHeader=您要將 ($COUNT$) 選定的元素移到$TARGET$ 嗎？\ndeleteAlertTitle=確認刪除\ndeleteAlertHeader=您要刪除 ($COUNT$) 選取的元素嗎？\nselectedElements=選取的元素：\nmustNotBeEmpty=$VALUE$ 不得為空\nvalueMustNotBeEmpty=值不可為空\ntransferDescription=將檔案拖曳至此下載\ndragLocalFiles=從這裡拖曳下載\nnull=$VALUE$ 必須為 not null\nroots=根\nscripts=腳本\nsearchFilter=搜尋 ...\nrecent=最近使用\nshortcut=捷徑\nbrowserWelcomeEmptyHeader=檔案瀏覽器\nbrowserWelcomeEmptyContent=您可以在左側選擇要在檔案瀏覽器中開啟的系統。XPipe 會記住您之前存取過的系統和目錄，並在日後顯示在此處的快速存取選單中。\nbrowserWelcomeEmptyButton=開啟本機檔案瀏覽器\nbrowserWelcomeSystems=您最近連線到下列系統：\nbrowserWelcomeDocsHeader=說明文件\nbrowserWelcomeDocsContent=如果您喜歡更有指導性的方式來熟悉 XPipe，請查看說明文件網站。\nbrowserWelcomeDocsButton=開放式文件\nhostFeatureUnsupported=$FEATURE$ 未安裝在主機上\nmissingStore=$NAME$ 不存在\nconnectionName=連線名稱\nconnectionNameDescription=為此連線自訂名稱\nopenFileTitle=開啟檔案\nunknown=未知\nscanAlertTitle=新增連線\nscanAlertChoiceHeader=目標\nscanAlertChoiceHeaderDescription=選擇搜尋連線的位置。這會先尋找所有可用的連線。\nscanAlertHeader=連接類型\nscanAlertHeaderDescription=選取您要為系統自動新增的連線類型。\nnoInformationAvailable=無可用資訊\nyes=是\nno=沒有\nerrorOccured=發生錯誤\nterminalErrorOccured=發生終端機錯誤\nerrorTypeOccured=產生了$TYPE$ 類型的異常\npermissionsAlertTitle=所需的權限\npermissionsAlertHeader=執行此操作需要額外的權限。\npermissionsAlertContent=請依照彈出的設定選單，給予 XPipe 所需的權限。\nerrorDetails=錯誤詳細資訊\nupdateReadyAlertTitle=更新就緒\nupdateReadyAlertHeader=$VERSION$ 版本的更新已準備好安裝\nupdateReadyAlertContent=這將安裝新版本，並在安裝完成後重新啟動 XPipe。\nerrorNoDetail=無錯誤詳細資訊\nerrorNoExceptionMessage=產生了$TYPE$ 類型的錯誤\nupdateAvailableTitle=可更新\nupdateAvailableContent=可安裝 XPipe 更新至$VERSION$ 版本。即使 XPipe 無法啟動，您仍可嘗試安裝更新，以可能修復問題。\nclipboardActionDetectedTitle=偵測到剪貼簿動作\nclipboardActionDetectedContent=XPipe 在您的剪貼簿中偵測到可開啟的內容。您想現在就打開它嗎？您要匯入剪貼簿內容嗎？\ninstall=安裝 ...\nignore=忽略\npossibleActions=可用的動作\nreportError=報告錯誤\nreportOnGithub=在 GitHub 上建立問題報告\nreportOnGithubDescription=在 GitHub 倉庫中開啟一個新問題\nreportErrorDescription=傳送包含可選使用者回饋和診斷資訊的錯誤報告\nignoreError=忽略錯誤\nignoreErrorDescription=忽略此錯誤，繼續若無其事地工作\nprovideEmail=我們如何與您聯繫（可選項，僅在您希望獲得回覆時）。您的報告預設是匿名的，因此您可以在此提供電子郵件地址等聯絡資訊。\nadditionalErrorInfo=提供其他資訊（選擇性）\nadditionalErrorAttachments=選擇附件（選擇性）\ndataHandlingPolicies=隱私權政策\nsendReport=傳送報告\nerrorHandler=錯誤處理程式\nevents=活動\nvalidate=驗證\nstackTrace=堆疊追蹤\npreviousStep=< 上一頁\nnextStep=下一頁 >\nfinishStep=完成\nselect=選擇\nbrowseInternal=瀏覽內部\ncheckOutUpdate=檢查更新\nquit=退出\nnoTerminalSet=未自動設定終端應用程式。您可以在設定功能表中手動設定。\nconnections=連接\nconnectionHub=連接集線器\nsettings=設定\nexplorePlans=許可證\nhelp=說明\nabout=關於\ndeveloper=開發人員\nbrowseFileTitle=瀏覽檔案\nbrowser=檔案瀏覽器\nselectFileFromComputer=從本電腦選擇檔案\nlinks=連結\nwebsite=網站\ndiscordDescription=加入 Discord 伺服器\nredditDescription=加入 XPipe subreddit\nsecurity=安全性\nsecurityPolicy=安全資訊\nsecurityPolicyDescription=閱讀詳細的安全政策\nprivacy=隱私權政策\nprivacyDescription=閱讀 XPipe 應用程式的隱私權政策\nslackDescription=加入 Slack 工作區\nsupport=支援\ngithubDescription=查看 GitHub 倉庫\nopenSourceNotices=開放原始碼通告\ncheckForUpdates=檢查更新\ncheckForUpdatesDescription=如果有更新，請下載更新\nlastChecked=最後檢查\nversion=版本\nbuild=建立版本\nruntimeVersion=執行時間版本\nvirtualMachine=虛擬機器\nupdateReady=安裝更新\nupdateReadyPortable=檢查更新\nupdateReadyDescription=已下載更新並準備安裝\nupdateReadyDescriptionPortable=有更新可供下載\nupdateRestart=重新啟動更新\nnever=從未\nupdateAvailableTooltip=可更新\nptbAvailableTooltip=可用的公開測試版本\nvisitGithubRepository=訪問 GitHub 倉庫\nupdateAvailable=可更新：$VERSION$\ndownloadUpdate=下載更新\nlegalAccept=我接受終端使用者授權合約\nconfirm=確認\nprint=列印\nwhatsNew=$VERSION$ ($DATE$) 中的新功能\nantivirusNoticeTitle=有關防毒程式的說明\nupdateChangelogAlertTitle=更新記錄\ngreetingsAlertTitle=歡迎來到 XPipe\neula=終端使用者授權合約\nnews=新聞\nintroduction=簡介\nprivacyPolicy=隱私權政策\nagree=同意\ndisagree=不同意\ndirectories=目錄\nlogFile=日誌檔案\nlogFiles=日誌檔案\nlogFilesAttachment=日誌檔案\nissueReporter=問題報告器\nopenCurrentLogFile=日誌檔案\nopenCurrentLogFileDescription=開啟目前會話的記錄檔\nopenLogsDirectory=開啟日誌目錄\ninstallationFiles=安裝檔案\nopenInstallationDirectory=安裝檔案\nopenInstallationDirectoryDescription=開啟 XPipe 安裝目錄\nlaunchDebugMode=除錯模式\nlaunchDebugModeDescription=在除錯模式下重新啟動 XPipe\nextensionInstallTitle=下載\nextensionInstallDescription=此動作需要額外的第三方函式庫，XPipe 並未散佈這些函式庫。您可以在此自動安裝它們。然後從廠商的網站下載這些元件：\nextensionInstallLicenseNote=執行下載和自動安裝即表示您同意第三方授權條款：\nlicense=許可證\ninstallRequired=需要安裝\nrestore=還原\nrestoreAllSessions=還原所有會話\nlimitedTouchscreenMode=有限的觸控螢幕模式\nlimitedTouchscreenModeDescription=在電話螢幕等較奇特的觸控螢幕介面上使用本應用程式時，某些功能表可能無法正常運作。啟用此選項時，選單實作會使用較有限的功能來處理稀疏傳送的滑鼠/觸控事件。\nappearance=外觀\ndisplay=顯示\npersonalization=個人化\ndisplayOptions=顯示選項\ntheme=主題\nrdpConfiguration=遠端桌面配置\nrdpClient=RDP 用戶端\nrdpClientDescription=啟動 RDP 連線時要呼叫的 RDP 用戶端程式。\\n\\n請注意，各種用戶端有不同程度的能力和整合。有些用戶端不支援自動傳遞密碼，因此您仍必須在啟動時填入密碼。\nlocalShell=本地 shell\nthemeDescription=您偏好的顯示主題。\ndontAutomaticallyStartVmSshServer=需要時不自動啟動虛擬機器的 SSH 伺服器\ndontAutomaticallyStartVmSshServerDescription=任何與在管理程序中執行的虛擬機器之間的 shell 連線，都是透過 SSH 進行的。XPipe 可以在需要時自動啟動已安裝的 SSH 伺服器。如果您基於安全理由不想這樣做，那麼您可以直接使用此選項停用此行為。\nconfirmGitShareTitle=Git 同步\nconfirmGitShareContent=您是否要將選取的檔案加入您的 git vault 儲存庫？這會將該檔案的加密版本複製到您的 git 儲存庫，並提交您的變更。之後您就可以在所有同步的桌面上存取該檔案。\ngitShareFileTooltip=將檔案加入 git vault 資料目錄，使其自動同步。\\n\\n此動作僅能在設定中啟用 git vault 時使用。\nperformanceMode=效能模式\nperformanceModeDescription=停用所有不需要的視覺效果，以改善應用程式的效能。\ndontAcceptNewHostKeys=不自動接受新的 SSH 主機金鑰\ndontAcceptNewHostKeysDescription=如果您的 SSH 用戶端沒有儲存已知的主機密鑰，XPipe 預設會自動接受來自系統的主機密鑰。但是，如果任何已知的主機金鑰已經改變，除非您接受新的金鑰，否則它會拒絕連線。\\n\\n停用此行為可讓您檢查所有主機金鑰，即使最初沒有衝突。\nuiScale=UI 標度\nuiScaleDescription=自訂縮放值，可獨立於您的系統範圍顯示縮放設定。數值以百分比為單位，例如值為 150 時，使用者介面的縮放比例為 150%。\neditorProgram=編輯程式\neditorProgramDescription=編輯任何類型的文字資料時使用的預設文字編輯器。\nwindowOpacity=視窗不透明度\nwindowOpacityDescription=變更視窗的不透明度，以掌握背景中正在發生的事情。\nuseSystemFont=使用系統字型\nopenDataDir=金庫資料目錄\nopenDataDirButton=開放資料目錄\nopenDataDirDescription=如果您想用 git 倉庫跨系統同步其他檔案，例如 SSH 金鑰，您可以把它們放到儲存資料目錄中。任何在那裡引用的檔案，其檔案路徑都會在任何同步的系統上自動調整。\nupdates=更新\nselectAll=全部選取\nadvanced=進階\nthirdParty=開放原始碼通知\neulaDescription=閱讀 XPipe 應用程式的最終使用者授權合約\nthirdPartyDescription=檢視第三方程式庫的開放原始碼授權證\nworkspaceLock=主密碼\nenableGitStorage=啟用同步\nsharing=分享\ngitSync=Git 同步\nenableGitStorageDescription=啟用時，XPipe 會為本機儲存庫初始化一個 git 儲存庫，並將任何變更提交至該儲存庫。請注意，這需要安裝 git，而且可能會減慢載入和儲存作業的速度。\\n\\n任何應該同步的類別都必須明確標示為同步。\nstorageGitRemote=遠端同步 URL\nstorageGitRemoteDescription=設定時，XPipe 會在載入時自動拉取任何變更，並在儲存時將任何變更推送至遠端儲存庫。\\n\\n這可讓您在多個 XPipe 安裝之間分享您的保險庫。它支援 HTTP 和 SSH URL，以及本機目錄。\nvault=儲存庫\nworkspaceLockDescription=設定自訂密碼，以加密 XPipe 中儲存的任何敏感資訊。\\n\\n這可提高安全性，因為它為您儲存的敏感資訊提供了額外的加密層。當 XPipe 啟動時，系統會提示您輸入密碼。\nuseSystemFontDescription=控制是否使用預設的系統字型或 XPipe 隨附的 Inter 字型。\ntooltipDelay=工具提示延遲\ntooltipDelayDescription=工具提示顯示前的等待毫秒數。\nfontSize=字體大小\nwindowOptions=視窗選項\nsaveWindowLocation=儲存視窗位置\nsaveWindowLocationDescription=控制視窗座標是否應該儲存，並在重新啟動時還原。\nstartupShutdown=啟動 / 關機\nshowChildrenConnectionsInParentCategory=在父類別中顯示子類別\nshowChildrenConnectionsInParentCategoryDescription=當選擇有某個父類別時，是否包含位於子類別中的所有連線。\\n\\n如果停用此功能，類別的行為會更像傳統的資料夾，只顯示其直接內容，而不包括子資料夾。\ncondenseConnectionDisplay=濃縮連接顯示\ncondenseConnectionDisplayDescription=讓每個頂層連線佔用較少的垂直空間，以提供更精簡的連線清單。\nopenConnectionSearchWindowOnConnectionCreation=建立連線時開啟連線搜尋視窗\nopenConnectionSearchWindowOnConnectionCreationDescription=新增 shell 連線時，是否自動開啟搜尋可用子連線的視窗。\nworkflow=工作流程\nsystem=系統\napplication=應用程式\nstorage=儲存\nrunOnStartup=啟動時執行\ncloseBehaviour=退出行為\ncloseBehaviourDescription=控制 XPipe 在關閉其主視窗時應如何進行。\nlanguage=語言\nlanguageDescription=使用的顯示語言。翻譯會透過社群的貢獻來改善。您可以在 GitHub 上提交翻譯修正，協助翻譯工作。\nlightTheme=燈光主題\ndarkTheme=深色主題\nexit=退出 XPipe\ncontinueInBackground=在背景中繼續\nminimizeToTray=最小化至托盤\ncloseBehaviourAlertTitle=設定關閉行為\ncloseBehaviourAlertTitleHeader=選擇關閉視窗時應發生的情況。關閉應用程式時，任何作用中的連線都會關閉。\nstartupBehaviour=啟動行為\nstartupBehaviourDescription=控制 XPipe 啟動時桌面應用程式的預設行為。\nclearCachesAlertTitle=清除快取記憶體\nclearCachesAlertContent=您想清除所有 XPipe 快取記憶體？這將刪除所有儲存的快取記憶體資料，以改善使用者體驗。\nstartGui=啟動 GUI\nstartInTray=在托盤中啟動\nstartInBackground=在背景中啟動\nclearCaches=清除快取 ...\nclearCachesDescription=刪除所有快取資料\ncancel=取消\nnotAnAbsolutePath=不是絕對路徑\nnotADirectory=不是目錄\nnotAnEmptyDirectory=不是空目錄\nautomaticallyCheckForUpdates=檢查更新\nautomaticallyCheckForUpdatesDescription=啟用時，XPipe 會在執行一段時間後自動取得新版本資訊。您仍需明確確認任何更新安裝。\nsendAnonymousErrorReports=傳送匿名錯誤報告\nsendUsageStatistics=傳送匿名使用統計資料\nstorageDirectory=儲存目錄\nstorageDirectoryDescription=XPipe 應儲存所有連線資訊的位置。變更時，舊目錄中的資料不會複製到新目錄中。\nlogLevel=日誌層級\nappBehaviour=應用程式行為\nlogLevelDescription=寫入記錄檔時應該使用的記錄層級。\ndeveloperMode=開發人員模式\ndeveloperModeDescription=啟用後，您就可以存取各種對開發有用的附加選項。\neditor=編輯\ncustom=自訂\npasswordManager=密碼管理員\nexternalPasswordManager=外部密碼管理器\npasswordManagerDescription=本機安裝的密碼管理器進行整合。\\n\\n如果您已經安裝了密碼管理器，您可以設定 XPipe 從中擷取密碼，這樣 XPipe 就不必自行儲存密碼。啟用後，連線的任何密碼欄位便可設定為使用密碼管理器。\npasswordManagerCommandTest=測試密碼管理器\npasswordManagerCommandTestDescription=如果您已設定密碼管理器，您可以在此測試輸出看起來是否正確。\npreferTerminalTabs=偏好開啟新索引標籤\npreferTerminalTabsDescription=控制 XPipe 是否嘗試在您選擇的終端開啟新標籤頁，而不是新視窗。並非每個終端機都支援標籤。\ncustomRdpClientCommand=自訂指令\ncustomRdpClientCommandDescription=啟動自訂 RDP 用戶端要執行的指令。\\n\\n在呼叫時，占位符串 $FILE 將被引號的絕對 .rdp 檔案名稱取代。如果可執行路徑包含空格，請記得加上引號。\ncustomEditorCommand=自訂編輯器指令\ncustomEditorCommandDescription=要執行以啟動自訂編輯器的指令。\\n\\n占位符字串 $FILE 在呼叫時會被引號的絕對檔案名稱取代。如果您的編輯器執行路徑包含空格，請記得加上引號。\neditorReloadTimeout=編輯器重新載入超時\neditorReloadTimeoutDescription=在更新檔案後，讀取檔案前的等待毫秒數。這可避免在您的編輯器寫入或釋放檔案鎖時速度較慢的情況下發生問題。\nencryptAllVaultData=加密所有保險庫資料\nencryptAllVaultDataDescription=啟用時，保險庫連線資料的每一部分都會使用您的使用者保險庫加密金鑰加密，而非僅對資料中的秘密進行加密。這可為儲存庫預設不加密的其他參數，如使用者名稱、主機名稱等，增加另一層安全性。\\n\\n此選項將使您的 git 儲存庫歷史和差異變得無用，因為您再也看不到原始變更，只能看到二進位變更。\nvaultSecurity=金庫安全\ndeveloperDisableUpdateVersionCheck=停用更新版本檢查\ndeveloperDisableUpdateVersionCheckDescription=控制更新檢查器在尋找更新時是否會忽略版本號。\ndeveloperDisableGuiRestrictions=停用 GUI 限制\ndeveloperDisableGuiRestrictionsDescription=控制某些停用的動作是否仍可從使用者介面執行。\ndeveloperShowHiddenEntries=顯示隱藏的項目\ndeveloperShowHiddenEntriesDescription=啟用時，會顯示隱藏和內部資料來源。\ndeveloperShowHiddenProviders=顯示隱藏的提供者\ndeveloperShowHiddenProvidersDescription=控制是否在建立對話框中顯示隱藏和內部連線及資料來源提供者。\ndeveloperDisableConnectorInstallationVersionCheck=停用連接器版本檢查\ndeveloperDisableConnectorInstallationVersionCheckDescription=控制更新檢查器在檢查安裝在遠端機器上的 XPipe 連接器版本時，是否會忽略版本號。\nshellCommandTest=Shell 指令測試\nshellCommandTestDescription=在 XPipe 內部使用的 shell 會話中執行指令。\nterminal=終端\nterminalType=終端模擬器\nterminalConfiguration=終端配置\nterminalCustomization=終端機客製化\neditorConfiguration=編輯器配置\ndefaultApplication=預設應用程式\ninitialSetup=初始設定\nterminalTypeDescription=用於開啟 shell 連線的預設終端機。\\n\\n不同終端機的功能支援程度各異，每種終端機均標示為推薦或不推薦。使用推薦的終端時，您的使用者體驗會最好。\nprogram=程式\ncustomTerminalCommand=自訂終端機指令\ncustomTerminalCommandDescription=用指定的指令開啟自訂終端時要執行的指令。\\n\\nXPipe 會建立一個臨時啟動器 shell 腳本，供您的終端執行。您提供的命令中的占位符字串 $CMD 將會在呼叫時被實際的啟動器腳本取代。如果您的終端可執行路徑包含空格，請記住引用該路徑。\nclearTerminalOnInit=在啟動時清除終端機\nclearTerminalOnInitDescription=啟用時，XPipe 會在啟動新的終端會話後執行清除指令，以移除在啟動終端會話時列印的任何不必要輸出。\ndontCachePasswords=不要快取提示的密碼\ndontCachePasswordsDescription=控制 XPipe 是否應在內部緩存查詢到的密碼，以便您不必在目前會話中再次輸入密碼。\\n\\n如果禁用此行為，則每次系統需要時，您都必須重新輸入任何提示的憑證。\ndenyTempScriptCreation=拒絕建立臨時指令碼\ndenyTempScriptCreationDescription=為了實現其部分功能，XPipe 有時候會在目標系統上建立臨時 shell 腳本，以方便執行簡單的指令。這些腳本不包含任何敏感資訊，只是為了實作目的而建立。\\n\\n如果禁用此行為，XPipe 將不會在遠端系統上建立任何臨時檔案。此選項在高安全性的情況下非常有用，因為檔案系統的每項變更都會受到監控。如果停用此功能，某些功能 (例如 shell 環境與腳本) 將無法正常運作。\ndisableCertutilUse=在 Windows 上停用 certutil 的使用\nuseLocalFallbackShell=使用本機後備 shell\nuseLocalFallbackShellDescription=改用其他本機 shell 來處理本機作業。這會是 Windows 上的 PowerShell 和其他系統上的 bourne shell。\\n\\n此選項可用於正常本機預設 shell 在某種程度上被停用或損壞的情況。啟用此選項時，某些功能可能無法如預期般運作。\ndisableCertutilUseDescription=由於 cmd.exe 的幾個缺點和錯誤，臨時 shell 腳本是使用 certutil 建立的，方法是使用它來解碼 base64 輸入，因為 cmd.exe 在非 ASCII 輸入時會斷開。XPipe 也可以使用 PowerShell 來處理，但速度會較慢。\\n\\n這將禁止在 Windows 系統上使用 certutil 來實現某些功能，而改用 PowerShell。這可能會讓某些防毒軟體感到滿意，因為有些防毒軟體會封鎖 certutil 的使用。\ndisableTerminalRemotePasswordPreparation=停用終端遠端密碼準備\ndisableTerminalRemotePasswordPreparationDescription=在終端建立經過多個中間系統的遠端 shell 連線時，可能需要在其中一個中間系統上準備任何所需的密碼，以便自動填寫任何提示。\\n\\n如果您不希望將密碼傳輸到任何中間系統，可以停用此行為。任何所需的中間系統密碼都會在開啟終端機時被查詢。\nmore=更多資訊\ntranslate=翻譯\nallConnections=所有連線\nallScripts=所有腳本\nallIdentities=所有身分\nsynced=同步\npredefined=預先定義\nsamples=樣本\ngoodMorning=早安\ngoodAfternoon=午安\ngoodEvening=晚上好\naddVisual=Visual ...\naddDesktop=桌上型電腦 ...\nssh=SSH\nsshConfiguration=SSH 設定\nsize=尺寸\nattributes=屬性\nmodified=已修改\nowner=所有者\nupdateReadyTitle=更新至$VERSION$ ready\ntemplates=範本\nretry=重試\nretryAll=全部重試\nreplace=取代\nreplaceAll=全部取代\nhibernateBehaviour=休眠行為\nhibernateBehaviourDescription=控制系統進入休眠/睡眠狀態時應用程式的行為。\noverview=概述\nhistory=歷史\nskipAll=全部跳過\nnotes=注意事項\naddNotes=新增備註\norder=重新排序\nkeepFirst=先保持\nkeepLast=保留最後一次\npinToTop=針對頂端\nunpinFromTop=從頂部解除釘選\norderAheadOf=提前訂購 ...\nclearIndex=重設索引\nhttpServer=HTTP 伺服器\nmcpServer=MCP 伺服器\napiKey=API 金鑰\napiKeyDescription=驗證 XPipe daemon API 請求的 API 金鑰。有關如何驗證的詳細資訊，請參閱一般 API 文件。\ndisableApiAuthentication=停用 API 認證\ndisableApiAuthenticationDescription=停用所有必要的驗證方法，以便處理任何未經驗證的請求。\\n\\n驗證只應為開發目的而停用。\napi=API\nstoreIntroImportContent=已經在其他系統上使用 XPipe？透過遠端 git 資源庫，在多個系統上同步您現有的連線。如果尚未設定，您也可以稍後隨時同步。\nstoreIntroImportButton=同步連線 ...\nstoreIntroImportHeader=匯入連線\nshowNonRunningChildren=顯示未執行的兒童\nhttpApi=HTTP API\nisOnlySupportedLimit=只有在擁有超過$COUNT$ 連線時，才支援專業授權。\nareOnlySupportedLimit=只有當連線超過$COUNT$ 時，才支援專業授權。\nenabled=已啟用\nenableGitStoragePtbDisabled=Git 同步功能已停用於公共測試建置，以防止與一般發行版的 git 套件庫共用，並避免使用 PTB 建置作為您的日常驅動程式。\ncopyId=複製 API ID\nrequireDoubleClickForConnections=連線時需要雙擊\nrequireDoubleClickForConnectionsDescription=如果啟用，您必須雙擊連線才能啟動連線。如果您習慣雙擊東西，這會很有用。\nclearTransferDescription=清除選擇\nselectTab=選擇標籤\ncloseTab=關閉標籤\ncloseOtherTabs=關閉其他標籤頁\ncloseAllTabs=關閉所有標籤頁\ncloseLeftTabs=向左關閉索引標籤\ncloseRightTabs=向右關閉索引標籤\naddSerial=串列 ...\nconnect=連接\nworkspaces=工作區\nmanageWorkspaces=管理工作區\naddWorkspace=新增工作區 ...\nworkspaceAdd=新增工作區\nworkspaceAddDescription=工作區是執行 XPipe 的獨特配置。每個工作區都有一個資料目錄，所有資料都儲存在本機中。這包括連線資料、設定等。\\n\\n如果您使用同步功能，也可以選擇將每個工作區與不同的 git 儲存庫同步。\nworkspaceName=工作區名稱\nworkspaceNameDescription=工作區的顯示名稱\nworkspacePath=工作區路徑\nworkspacePathDescription=工作區資料目錄的位置\nworkspaceCreationAlertTitle=工作區建立\ndeveloperForceSshTty=強制 SSH TTY\ndeveloperForceSshTtyDescription=讓所有 SSH 連線分配一個 pty，以測試是否支援遺失的 stderr 和 pty。\ndeveloperDisableSshTunnelGateways=停用 SSH 閘道隧道\ndeveloperDisableSshTunnelGatewaysDescription=不要使用閘道的隧道會話，而應直接連接到系統。\nttyWarning=連線強行分配了 pty/tty，且未提供獨立的 stderr 串流。\\n\\n這可能會導致一些問題。\\n\\n如果可以的話，請研究讓 connection 指令不分配 pty。\nxshellSetup=Xshell 設定\ntermiusSetup=Termius 設定\ntryPtbDescription=在 XPipe 開發人員版本中提前試用新功能\nconfirmVaultUnencryptTitle=確認金庫解除加密\nconfirmVaultUnencryptContent=您真的要停用進階保管庫加密嗎？這會移除儲存資料的額外加密，並會覆寫現有資料。\nenableHttpApi=啟用 HTTP API\nenableHttpApiDescription=啟用 API，允許外部程式呼叫 XPipe daemon，對您管理的連線執行動作。\nchooseCustomIcon=選擇自訂圖示\ngitVault=Git 保險庫\nfileBrowser=檔案瀏覽器\nconfirmAllDeletions=確認所有刪除\nconfirmAllDeletionsDescription=是否顯示所有刪除作業的確認對話框。預設情況下，只有目錄需要確認。\nyesterday=昨天\ngreen=綠色\nyellow=黃色\nblue=藍色\nred=紅色\ncyan=青色\npurple=紫色\nasktextAlertTitle=提示\nfileWriteSudoTitle=Sudo 檔案寫入\nfileWriteSudoContent=您要寫入的檔案未授予您的使用者寫入權限。您是否要使用 sudo 以 root 身份寫入此檔案？這將使用現有憑證或透過提示自動提升為 root。\ndontAllowTerminalRestart=不允許重新啟動終端機\ndontAllowTerminalRestartDescription=預設情況下，終端會話結束後可以從終端內部重新啟動。為了允許這樣做，XPipe 會接受這些來自終端重新啟動會話的外部請求\\n\\nXPipe無法控制終端以及此呼叫的來源，因此惡意的本機應用程式也可以使用此功能，透過XPipe啟動連線。停用此功能可防止此情況發生。\nopenDocumentation=開啟文件\nopenDocumentationDescription=請造訪 XPipe 相關文件頁面\nrenameAll=全部重新命名\nlogging=記錄\nenableTerminalLogging=啟用終端記錄\nenableTerminalLoggingDescription=啟用所有終端會話的用戶端記錄。終端會話的所有輸入和輸出都會寫入會話記錄檔。請注意，任何敏感資訊（例如密碼提示）都不會被記錄下來。\nterminalLoggingDirectory=終端機會話記錄\nterminalLoggingDirectoryDescription=所有日誌都儲存在本機系統的 XPipe 資料目錄中。\nopenSessionLogs=開啟會話記錄\nsessionLogging=終端日誌\nsessionActive=此連線正在執行背景會話。\\n\\n若要手動停止此會話，請按一下狀態指示器。\nskipValidation=跳過驗證\nscriptsIntroHeader=關於腳本\nscriptsIntroContent=您可以在 shell init、檔案瀏覽器和按需執行腳本。您可以在 XPipe 中自行建立腳本，或從本機系統或遠端 git 倉庫匯入現有的腳本。\nscriptsIntroBottomHeader=使用腳本\nscriptsIntroBottomContent=有多種範例腳本可供開始使用。您可以按一下個別腳本的編輯按鈕，看看它們是如何實作的。腳本首先必須啟用才能執行並顯示在功能表中，每個腳本上都有一個切換按鈕。\nscriptsIntroBottomButton=開始使用\nscriptSourcesIntroHeader=腳本來源\nscriptSourcesIntroContent=您可以新增自訂的指令碼來源，以立即存取整個 shell 指令碼集合。本機來源和遠端 git 套件庫都支援作為來源。來源中所有偵測到的指令碼都會自動變得可用。\nscriptSourcesIntroButton=新增來源 ...\ncheckForSecurityUpdates=檢查安全更新\ncheckForSecurityUpdatesDescription=XPipe 可與一般功能更新分開檢查潛在的安全性更新。啟用此功能時，即使正常的更新檢查已停用，至少也會建議安裝重要的安全更新。\\n\\n停用此設定將不會執行外部版本請求，也不會通知您任何安全性更新。\nclickToDock=按一下以停靠終端機\nterminalStarting=等待終端機啟動 ...\npinTab=針腳標籤\nunpinTab=解除釘選標籤\npinned=釘選\nenableConnectionHubTerminalDocking=啟用連接集線器終端停靠\nenableConnectionHubTerminalDockingDescription=您可以將終端視窗停靠在連線集線器中的 XPipe 應用程式視窗，以模擬一個有點整合的終端。終端視窗隨後會由 XPipe 管理，使其始終適合停靠。\nenableFileBrowserTerminalDocking=啟用檔案瀏覽器終端停靠\nenableFileBrowserTerminalDockingDescription=您可以將終端視窗停靠在檔案瀏覽器中的 XPipe 應用程式視窗，以模擬一個有點整合的終端。終端視窗隨後會由 XPipe 管理，使其始終適合停靠。\ndownloadsDirectory=自訂下載目錄\ndownloadsDirectoryDescription=點選移動到下載按鈕時，將下載檔案放入的自訂目錄。預設情況下，XPipe 會使用您的使用者下載目錄。\npinLocalMachineOnStartup=啟動時釘選本機索引標籤\npinLocalMachineOnStartupDescription=自動開啟本機標籤並將其釘選。如果您經常在開啟本機和遠端檔案系統的情況下使用分割檔案瀏覽器，這將非常有用。\nterminalErrorDescription=此錯誤為終端錯誤，XPipe 不修復此錯誤將無法繼續。\ngroupName=群組名稱\nchmodPermissions=新增權限\neditFilesWithDoubleClick=用雙擊編輯檔案\neditFilesWithDoubleClickDescription=啟用後，雙擊檔案會直接在文字編輯器中開啟，而不會顯示上下文功能表。\ncensorMode=審查模式\ncensorModeDescription=模糊主機名稱、使用者名稱、連線名稱等任何資訊。\\n\\n如果您打算對 XPipe 進行螢幕截圖或螢幕分享，且不想洩漏任何資訊，這將非常有用。\naddIdentity=身份 ...\nidentities=身分資訊\naddMacro=行動 ...\nidentitiesIntroHeader=關於身分\nidentitiesIntroContent=如果您要重複使用使用者名稱、密碼和鑰匙的常見組合，那麼建立可重複使用的身分可能會很有意義。這可讓您在新增連線時快速參考它們。\nidentitiesIntroBottomHeader=分享身分\nidentitiesIntroBottomContent=您可以在本機新增身分，也可以在啟用時將身分同步到 git 儲存庫。這可讓您有選擇性地在多個系統中與其他團隊成員分享身分。\nidentitiesIntroBottomButton=設定同步\nidentitiesIntroButton=建立身分\nuserName=使用者名稱\nuserAuth=基於使用者密碼的驗證\ngroupAuth=基於群組的秘密驗證\nteam=團隊\nteamSettings=團隊設定\nteamVaults=團隊保險庫\nvaultTypeNameDefault=預設保險庫\nvaultTypeNameLegacy=遺傳的個人保險庫\nvaultTypeNamePersonal=個人保險庫\nvaultTypeNameTeam=團隊保險庫\nteamVaultsDescription=團隊保險庫可讓多個使用者和群組安全存取共用的保險庫。您可以將連線和身分設定為對所有使用者共用，或僅對個別使用者和群組可用，方法是用他們自己的金鑰加密。如果其他保管庫使用者無法存取金鑰，他們就無法存取個人和群組的連線和身分。\nvaultTypeContentDefault=您目前使用的是預設保險庫，未設定使用者和自訂密碼。秘密是使用本機儲存庫金鑰加密的。您可以透過建立儲存庫使用者帳戶，升級為個人儲存庫。這可讓您使用個人密碼加密儲存庫的秘密，每次登入時都必須輸入密碼才能解鎖儲存庫。\nvaultTypeContentLegacy=您目前正在使用傳統的使用者個人保險箱。秘密是以您的個人密碼加密的。此傳統相容性的功能有限，無法就地升級為團隊保險庫。\nvaultTypeContentPersonal=您目前正在使用使用者的個人保險箱。秘密會以您的個人密碼加密。您可以透過新增儲存庫使用者或新增基於群組的存取設定，升級為團隊儲存庫。\nvaultTypeContentTeam=您目前使用的是團隊保險庫，可讓多位使用者安全存取共用的保險庫。您可以將連線和身分設定為所有使用者皆可共用，或只供您個人使用者或群組使用，方法是使用您的個人或群組金鑰進行加密。其他保管庫使用者若無法取得金鑰，就無法存取您個人和群組的連線和身分。\ngroupManagement=群組管理\ngroupManagementEmpty=群組管理\ngroupManagementDescription=管理現有的儲存庫群組或建立新的群組。每個儲存庫群組都有自己的個別密碼鑰匙，用來加密連線和身分，而這些連線和身分只應該提供給該群組，不應該提供給其他人。\ngroupManagementEmptyDescription=管理現有的儲存庫群組或建立新的群組。每個儲存庫群組都有自己的個別密碼鑰匙，用來加密連線和身分，而這些連線和身分只應該提供給該群組，不應該提供給其他人。\\n\\n專業方案支援團隊的群組帳號。\nuserManagement=使用者管理\nuserManagementEmpty=使用者管理\nuserManagementDescription=管理現有的儲存庫使用者或建立新使用者。每個儲存庫使用者都有自己的個別密碼，用來加密連線和身分，這些連線和身分只應該提供給該使用者，不應該提供給其他人。\nuserManagementEmptyDescription=管理現有的儲存庫使用者或建立新使用者。每個儲存庫使用者都有自己的個人密碼，用來加密連線和身分，而這些連線和身分只能由該使用者使用，其他人無法使用。為自己建立一個使用者，以便使用個人密鑰加密連線和身分。\\n\\n社群版支援單一使用者帳戶。專業版支援一個團隊的多個使用者帳戶。\nuserIntroHeader=使用者管理\nuserIntroContent=為自己建立第一個使用者帳戶，以便開始使用。這可讓您使用密碼鎖定此工作區。\naddReusableIdentity=新增可重複使用的身分\nusers=使用者\nsyncVault=金庫同步\nsyncVaultDescription=若要與多個系統或多個團隊成員同步您的儲存庫，請啟用此儲存庫的 git 同步。\nenableGitSync=啟用 git 同步\nbrowseVault=保險庫資料\nbrowseVaultDescription=您可以自己在本機檔案管理員中查看儲存庫目錄。請注意，不建議進行外部編輯，可能會導致各種問題。\nbrowseVaultButton=瀏覽保險庫\nvaultUsers=保險庫使用者\ncreateHeapDump=建立堆轉儲\ncreateHeapDumpDescription=將記憶體內容轉存至檔案，以排除記憶體使用的問題\ninitializingApp=載入連線\ncheckingLicense=檢查許可證\nloadingGit=與 git repo 同步\nloadingGpg=啟動 git 的 GnuPG 守护进程\nloadingSettings=載入設定\nloadingConnections=載入連線\nunlockingVault=開鎖金庫\nloadingUserInterface=載入使用者介面\nptbNotice=公開測試建置的通知\nuserDeletionTitle=使用者刪除\nuserDeletionContent=您要刪除這個儲存庫使用者嗎？這將使用所有使用者都可使用的保險庫金鑰，重新加密您所有的個人身分和連線秘密。這將需要一段時間，XPipe 會重新啟動以套用使用者變更。\ngroupDeletionTitle=群組刪除\ngroupDeletionContent=您要刪除這個保管庫群組嗎？這將使用所有使用者都可使用的保險庫金鑰，重新加密所有僅限群組的身分和連線秘密。這需要一段時間，XPipe 會重新啟動以套用群組變更。\nkillTransfer=殺毒傳輸\ndestination=目的地\nconfiguration=配置\nnewFile=新檔案\nnewLink=新連結\nlinkName=連結名稱\nscanConnections=尋找可用的連線 ...\nobserve=開始觀察\nstopObserve=停止觀察\ncreateShortcut=建立桌面捷徑\nbrowseFiles=瀏覽檔案\nclone=複製\ntargetPath=目標路徑\nnewDirectory=新目錄\ncopyShareLink=複製連結\nselectStore=選擇商店\nsaveSource=儲存以備稍後使用\nexecute=執行\ndeleteChildren=移除所有兒童\nscriptGroupDescriptionDescription=給這個群組一個可選的描述\nabstractHostDescriptionDescription=給這台主機一個可選的描述\nselectSource=選擇來源\ncommandLineRead=更新\ncommandLineWrite=撰寫\nadditionalOptions=附加選項\ninput=輸入\nmachine=機器\nopen=開啟\nedit=編輯\nscriptContents=腳本內容\nscriptContentsDescription=要執行的指令碼指令\nsnippets=腳本依賴\nsnippetsDescription=先執行其他腳本\nsnippetsDependenciesDescription=在適用的情況下應執行的所有可能的腳本\nisDefault=在所有相容 shell 的 init 上執行\nbringToShells=帶到所有相容的 shell\nisDefaultGroup=在 shell init 執行所有群組指令碼\nexecutionType=執行類型\nexecutionTypeDescription=在何種情況下使用此腳本\nminimumShellDialect=殼類型\nminimumShellDialectDescription=執行此指令碼的 shell 類型\ndumbOnly=笨\nterminalOnly=終端\nboth=兩者\nshouldElevate=應提升\nshouldElevateDescription=是否以提升的權限執行此指令碼\nscript.displayName=Shell 指令碼\nscript.displayDescription=建立可重複使用的 shell 腳本\nscriptGroup.displayName=腳本群組\nscriptGroup.displayDescription=將指令碼群組並組織在\nscriptGroup=群組\nscriptGroupDescription=將此腳本指派給的群組\nscriptGroupGroupDescription=指定此指令碼群組的可選父群組\nopenInNewTab=在新標籤中開啟\nexecuteInBackground=在背景中\nexecuteInTerminal=在$TERM$\nback=返回\nbrowseInWindowsExplorer=在 Windows 檔案總管中瀏覽\nbrowseInDefaultFileManager=在預設檔案管理器中瀏覽\nbrowseInFinder=在搜尋器中瀏覽\ncopy=複製\npaste=貼上\ncopyLocation=複製位置\nabsolutePaths=絕對路徑\nabsoluteLinkPaths=絕對連結路徑\nabsolutePathsQuoted=絕對引號路徑\nfileNames=檔案名稱\nlinkFileNames=連結檔案名稱\nfileNamesQuoted=檔案名稱 (引號)\ndeleteFile=刪除$FILE$\neditWithEditor=編輯用$EDITOR$\nfollowLink=追蹤連結\ngoForward=前進\nshowDetails=顯示詳細資料\nshowDetailsDescription=顯示錯誤的堆疊追蹤\nopenFileWith=開啟與 ...\nopenWithDefaultApplication=使用預設應用程式開啟\nrename=重新命名\nrun=執行\nopenInTerminal=在終端機中開啟\nfile=檔案\ndirectory=目錄\nsymbolicLink=符號連結\ndesktopEnvironment.displayName=桌面環境\ndesktopEnvironment.displayDescription=建立可重複使用的遠端桌面環境設定\ndesktopHost=桌面主機\ndesktopHostDescription=用作基礎的桌面連線\ndesktopShellDialect=Shell 方言\ndesktopShellDialectDescription=用來執行腳本和應用程式的 shell 方言\ndesktopSnippets=腳本片段\ndesktopSnippetsDescription=先執行的可重用腳本片段清單\ndesktopInitScript=初始化腳本\ndesktopInitScriptDescription=此環境特有的 Init 指令\ndesktopTerminal=終端應用程式\ndesktopTerminalDescription=在桌面上啟動腳本時使用的終端機\ndesktopApplication.displayName=桌面應用程式\ndesktopApplication.displayDescription=在遠端桌面執行應用程式\ndesktopBase=桌上型電腦\ndesktopBaseDescription=執行此應用程式的桌面\ndesktopEnvironmentBase=桌面環境\ndesktopEnvironmentBaseDescription=執行此應用程式的桌面環境\ndesktopApplicationPath=應用程式路徑\ndesktopApplicationPathDescription=執行檔的路徑\ndesktopApplicationArguments=論據\ndesktopApplicationArgumentsDescription=傳給應用程式的可選參數\ndesktopCommand.displayName=桌面指令\ndesktopCommand.displayDescription=在遠端桌面環境中執行指令\ndesktopCommandScript=指令\ndesktopCommandScriptDescription=在環境中執行的指令\nservice.displayName=服務\nservice.displayDescription=將遠端服務轉送到本機\nserviceLocalPort=明確的本地端埠\nserviceLocalPortDescription=要轉送至的本機連接埠，否則會使用隨機連接埠\nserviceRemotePort=遠端連接埠\nserviceRemotePortDescription=服務執行的連接埠\nserviceHost=服務主機\nserviceHostDescription=服務執行的主機\nopenWebsite=開放網站\ncustomServiceGroup.displayName=服務群組\ncustomServiceGroup.displayDescription=將多個服務歸類為一個類別\ninitScript=初始化腳本 - 在 shell 初始化時執行\nshellScript=Shell 會話指令碼 - 在 Shell 會話中提供可執行的指令碼\nrunnableScript=可執行的指令碼 - 允許從連線集線器直接執行指令碼\nfileScript=檔案指令碼 - 允許在檔案瀏覽器中為選取的檔案呼叫指令碼\nrunScript=執行腳本\ncopyUrl=複製 URL\nfixedServiceGroup.displayName=服務群組\nfixedServiceGroup.displayDescription=列出系統上的可用服務\nmappedService.displayName=服務\nmappedService.displayDescription=與容器暴露的服務交互\ncustomService.displayName=服務\ncustomService.displayDescription=在本機上自動開啟或隧道遠端服務連接埠\nfixedService.displayName=服務\nfixedService.displayDescription=使用預先定義的服務\nnoServices=無可用服務\nhasServices=$COUNT$ 可用服務\nhasService=$COUNT$ 可用服務\nnoConnections=無可用連線\nhasConnections=$COUNT$ 可用連接\nhasConnection=$COUNT$ 可用連接\nopenHttp=開放式 HTTP 服務\nopenHttps=開啟 HTTPS 服務\nnoScriptsAvailable=沒有啟用和相容的腳本可用\nscriptsDisabled=停用腳本\nchangeIcon=變更圖示\ninit=啟動\nshell=殼\nhub=樞紐\nscript=腳本\ngenericScript=通用\ngradleTasks=Gradle 任務\nrunTask=執行任務\narchiveName=存檔名稱\ncompress=壓縮\ncompressContents=壓縮內容\nuntarHere=在此下載\nuntarDirectory=直到$DIR$\nunzipDirectory=解壓縮為$DIR$\nunzipHere=在此解壓縮\nrequiresRestart=需要重新啟動才能應用\ndownload=下載\nservicePath=服務路徑\nservicePathDescription=在瀏覽器中開啟 URL 時可選的子路徑\nactive=活躍\ninactive=不動\nstarting=開始\nremotePort=遠端連接埠\nremotePortNumber=遠端連接埠$PORT$\nuserIdentity=個人身分\nglobalIdentity=全球身分\nidentityChoice=使用者身分\nidentityChoiceDescription=選擇預先定義的身分或指定此連線專用的登入詳細資訊\ndefineNewIdentityOrSelect=輸入新的或選擇現有的\nlocalIdentity.displayName=本地身分\nlocalIdentity.displayDescription=為此本機桌面建立可重複使用的身分\nsyncedIdentity.displayName=同步身份\nsyncedIdentity.displayDescription=建立跨系統同步的可重複使用身分\nlocalIdentity=本地身分\nkeyNotSynced=密鑰檔案尚未同步至 git 儲存庫。使用 key 檔案的 add to git 按鈕來新增它。\nusernameDescription=登入的使用者名稱\nidentity.displayName=識別\nidentity.displayDescription=為連線建立可重複使用的身分\nlocal=本地\nshared=全球\nuserDescription=用來登入的使用者名稱或預先定義的身分\nidentityAccessLevel=存取等級\nidentityPerUser=個人身分存取\nidentityPerUserDescription=限制只有您的保管庫使用者才能存取此身分及其相關連線\nidentityPerUserDisabled=個人身分存取 (禁用)\nidentityPerUserDisabledDescription=限制只有您的保管庫使用者才能存取此身分及其相關連線 (需要設定團隊)\nidentityPerGroup=僅限群組的身分存取\nidentityPerGroupDescription=僅限制此保管庫群組存取此身分及其相關連線\nlibrary=圖書館\nlocation=地點\nkeyAuthentication=基於金鑰的驗證\nkeyAuthenticationDescription=如果需要基於金鑰的驗證，使用的驗證方法\nlocationDescription=對應的私人密碼匙的檔案路徑\nkeyFile=本機金鑰檔案\nkeyPassword=密碼\nkey=關鍵字\nyubikeyPiv=Yubikey PIV\npageant=盛會\ngpgAgent=GPG 代理\ncustomPkcs11Library=自訂 PKCS#11 函式庫\nsshAgent=OpenSSH 代理\nnone=無\nindex=索引 ...\notherExternal=其他外部代理\nsync=同步\nvaultSync=保險庫同步\ncustomUsername=使用者名稱\ncustomUsernameDescription=登入時可選擇的替代使用者\ncustomUsernamePassword=密碼\ncustomUsernamePasswordDescription=需要 sudo 驗證時使用的使用者密碼\nshowInternalPods=顯示內部 pod\nshowAllNamespaces=顯示所有命名空間\nshowInternalContainers=顯示內部容器\nrefresh=刷新\nvmwareGui=啟動 GUI\nmonitorVm=監控虛擬機器\naddCluster=新增群集 ...\nshowNonRunningInstances=顯示未執行的實例\nvmwareGuiDescription=是否在背景或視窗中啟動虛擬機。\nvmwareEncryptionPassword=加密密碼\nvmwareEncryptionPasswordDescription=用於加密 VM 的選用密碼。\nvmPasswordDescription=訪客使用者所需的密碼。\nvmPassword=使用者密碼\nvmUser=訪客使用者\nrunTempContainer=執行臨時容器\nvmUserDescription=主要訪客使用者的使用者名稱\ndockerTempRunAlertTitle=執行臨時容器\ndockerTempRunAlertHeader=這會在臨時容器中執行 shell 進程，一旦停止就會自動移除。\nimageName=圖片名稱\nimageNameDescription=要使用的容器映像識別碼\ncontainerName=容器名稱\ncontainerNameDescription=可選的自訂容器名稱\nvm=虛擬機器\nvmDescription=相關的組態檔案。\nvmwareScan=VMware 桌面管理程序\nvmwareMachine.displayName=VMware 虛擬機\nvmwareMachine.displayDescription=透過 SSH 連線至虛擬機器\nvmwareInstallation.displayName=VMware 桌面管理程序安裝\nvmwareInstallation.displayDescription=透過其 CLI 與已安裝的虛擬機器互動\nstart=開始\nstop=停止\npause=暫停\nrdpTunnelHost=目標主機\nrdpTunnelHostDescription=將 RDP 連線隧道至的 SSH 連線\nrdpTunnelUsername=使用者名稱\nrdpTunnelUsernameDescription=登入的自訂使用者，如果留空會使用 SSH 使用者\nrdpFileLocation=檔案位置\nrdpFileLocationDescription=.rdp 檔案的檔案路徑\nrdpPasswordAuthentication=密碼認證\nrdpFiles=RDP 檔案\nrdpPasswordAuthenticationDescription=要填入或複製到剪貼簿的密碼，視客戶端支援而定\nrdpFile.displayName=RDP 檔案\nrdpFile.displayDescription=透過現有的 .rdp 檔案連線到系統\nrequiredSshServerAlertTitle=設定 SSH 伺服器\nrequiredSshServerAlertHeader=無法在虛擬機器中找到已安裝的 SSH 伺服器。\nrequiredSshServerAlertContent=若要連線至虛擬機器，XPipe 會尋找執行中的 SSH 伺服器，但未偵測到虛擬機器有可用的 SSH 伺服器。\ncomputerName=電腦名稱\npssComputerNameDescription=要連線的電腦名稱\ncredentialUser=憑證使用者\ncredentialUserDescription=要登入的使用者。\ncredentialPassword=憑證密碼\ncredentialPasswordDescription=使用者的密碼。\nsshConfig=SSH 配置檔案\nautostart=在 XPipe 啟動時自動連線\nacceptHostKey=接受主機密鑰\nmodifyHostKeyPermissions=修改主機金鑰權限\nattachContainer=附加檔案\ncontainerLogs=顯示記錄\nopenSftpClient=在外部 SFTP 客戶端中開啟\nopenTermius=在 Termius 中開啟\nshowInternalInstances=顯示內部實例\neditPod=編輯 pod\nacceptHostKeyDescription=信任新的主機金鑰並繼續\nmodifyHostKeyPermissionsDescription=嘗試移除原始檔案的權限，讓 OpenSSH 滿意\npsSession.displayName=PowerShell 遠端會話\npsSession.displayDescription=透過 New-PSSession 和 Enter-PSSession 連線\nsshLocalTunnel.displayName=本機 SSH 通道\nsshLocalTunnel.displayDescription=建立 SSH 通道到遠端主機\nsshRemoteTunnel.displayName=遠端 SSH 通道\nsshRemoteTunnel.displayDescription=從遠端主機建立反向 SSH 通道\nsshDynamicTunnel.displayName=動態 SSH 通道\nsshDynamicTunnel.displayDescription=透過 SSH 連線建立 SOCKS 代理\nshellEnvironmentGroup.displayName=Shell 環境\nshellEnvironmentGroup.displayDescription=Shell 環境\nshellEnvironment.displayName=Shell 環境\nshellEnvironment.displayDescription=建立自訂的 shell 啟動環境\nshellEnvironment.informationFormat=$TYPE$ 環境\nshellEnvironment.elevatedInformationFormat=$ELEVATION$ $TYPE$ 環境\nenvironmentConnectionDescription=的基礎連線，以建立\nenvironmentScriptDescription=在 shell 中執行的選用自訂 init 腳本\nenvironmentSnippets=Shell 腳本\ncommandSnippetsDescription=可選擇先執行的預先定義 shell 腳本\nenvironmentSnippetsDescription=在初始化時執行的選用預先定義 shell 腳本\nshellTypeDescription=要啟動的顯式 shell 類型\noriginPort=原始連接埠\noriginAddress=來源位址\nremoteAddress=遠端位址\nremoteSourceAddress=遠端來源位址\nremoteSourcePort=遠端來源連接埠\noriginDestinationPort=起始目的地連接埠\noriginDestinationAddress=來源目的地位址\norigin=起源\nremoteHost=遠端主機\naddress=地址\nproxmox.displayName=Proxmox\nproxmox.displayDescription=連接至 Proxmox 虛擬環境中的系統\nproxmoxVm.displayName=Proxmox VM\nproxmoxVm.displayDescription=透過 SSH 連線至 Proxmox VE 中的虛擬機器\nproxmoxContainer.displayName=Proxmox 容器\nproxmoxContainer.displayDescription=連接至 Proxmox VE 中的容器\nsshDynamicTunnel.hostDescription=用作 SOCKS 代理的系統\nsshDynamicTunnel.bindingDescription=將隧道綁定到哪些位址\nsshRemoteTunnel.hostDescription=從哪個系統啟動到原點的遠端隧道\nsshRemoteTunnel.bindingDescription=將隧道綁定到哪些位址\nsshLocalTunnel.hostDescription=打開隧道的系統\nsshLocalTunnel.bindingDescription=將隧道綁定到哪些位址\nsshLocalTunnel.localAddressDescription=要綁定的本機位址\nsshLocalTunnel.remoteAddressDescription=要綁定的遠端位址\ncmd.displayName=指令\ncmd.displayDescription=在系統上執行任意指令\nk8sPod.displayName=Kubernetes Pod\nk8sPod.displayDescription=透過 kubectl 連線到 Pod 及其容器\nk8sContainer.displayName=Kubernetes 容器\nk8sContainer.displayDescription=開啟容器的 shell\nk8sCluster.displayName=Kubernetes 集群\nk8sCluster.displayDescription=透過 kubectl 連線至群集及其 Pod\nsshTunnelGroup.displayName=SSH 通道\nsshTunnelGroup.displayCategory=所有類型的 SSH 通道\nlocal.displayName=本機\nlocal.displayDescription=本機的 shell\ncygwin=Cygwin\nmsys2=MSYS2\ngitWindows=Windows 版 Git\ngitForWindows.displayName=Windows 版 Git\ngitForWindows.displayDescription=存取您的本機 Git For Windows 環境\nmsys2.displayName=MSYS2\nmsys2.displayDescription=您 MSYS2 環境的存取 shell\ncygwin.displayName=Cygwin\ncygwin.displayDescription=存取 Cygwin 環境的 shell\nnamespace=名稱空間\ngitVaultIdentityStrategy=Git SSH 身分識別\ngitVaultIdentityStrategyDescription=如果您選擇使用 SSH git URL 作為遠端，且您的遠端儲存庫需要 SSH 身份，則設定此選項。\\n\\n如果您提供的是 HTTP URL，則可以忽略此選項。\ndockerContainers=Docker 容器\ndockerCmd.displayName=docker CLI 用戶端\ndockerCmd.displayDescription=透過 docker CLI 用戶端存取 Docker 容器\nwslCmd.displayName=WSL 安裝\nwslCmd.displayDescription=透過 wsl CLI 用戶端存取 WSL 實例\nk8sCmd.displayName=kubectl 用戶端\nk8sCmd.displayDescription=透過 kubectl 存取 Kubernetes 叢集\nk8sClusters=Kubernetes 集群\nshells=可用的外殼\ninspectContainer=檢查\ninspectContext=檢查\nk8sClusterNameDescription=群集所在上下文的名稱。\npod=Pod\npodName=Pod 名稱\nk8sClusterContext=內容\nk8sClusterContextDescription=群集所在上下文的名稱\nk8sClusterNamespace=名稱空間\nk8sClusterNamespaceDescription=自訂命名空間或預設命名空間 (若為空)\nk8sConfigLocation=組態檔案\nk8sConfigLocationDescription=自訂的 kubeconfig 檔案或預設檔案 (如果留空)\ninspectPod=檢查\nshowAllContainers=顯示未執行的容器\nshowAllPods=顯示未執行的 Pod\nk8sPodHostDescription=pod 所在的主機\nk8sContainerDescription=Kubernetes 容器的名稱\nk8sPodDescription=Kubernetes pod 的名稱\npodDescription=容器所在的 pod\nk8sClusterHostDescription=應透過其存取群集的主機。必須安裝並設定 kubectl 才能存取群集。\nconnection=連線\nshellCommand.displayName=自訂 shell 指令\nshellCommand.displayDescription=透過自訂指令開啟標準 shell\nssh.displayName=SSH 連線\nssh.displayDescription=透過 SSH 命令列用戶端連接到遠端系統\nsshConfig.displayName=SSH 配置檔案\nsshConfig.displayDescription=連接至 SSH 配置檔案中定義的主機\nsshConfigHost.displayName=SSH 配置檔案主機\nsshConfigHost.displayDescription=連接至 SSH 配置檔案中定義的主機\nsshConfigHost.password=密碼\nsshConfigHost.passwordDescription=提供使用者登入的選用密碼。\nsshConfigHost.identityPassphrase=金鑰密碼\nsshConfigHost.identityPassphraseDescription=提供金鑰的選用密碼。\nshellCommand.hostDescription=執行指令的主機\nshellCommand.commandDescription=開啟 shell 的指令\ncommandType=指令類型\ncommandTypeDescription=如何執行指令\ncommandDescription=在主機上執行的自訂指令\ncommandHostDescription=執行指令的主機\ncommandDataFlowDescription=此指令如何處理輸入和輸出\ncommandElevationDescription=以提升的權限執行此指令\ncommandShellTypeDescription=此指令要使用的 shell\nlimitedSystem=這是有限系統或嵌入式系統\nlimitedSystemDescription=不要嘗試識別外殼類型，對於有限的嵌入式系統或 IOT 裝置是必要的\nsshForwardX11=轉寄 X11\nsshForwardX11Description=啟用連線的 X11 轉送\ncustomAgent=自訂代理\nidentityAgent=身分代理\nssh.proxyDescription=建立 SSH 連線時要使用的可選代理主機。必須已安裝 ssh 用戶端。\nusage=使用方式\nwslHostDescription=WSL 實例所在的主機。必須已安裝 wsl。\nwslDistributionDescription=WSL 實例的名稱\nwslUsernameDescription=要登入的明確使用者名稱。如果未指定，將使用預設使用者名稱。\nwslPasswordDescription=使用者的密碼，可用於 sudo 指令。\ndockerHostDescription=docker 容器所在的主機。必須安裝了 docker。\ndockerContainerDescription=docker 容器的名稱\nlocalMachine=本機\nrootScan=Sudo shell 環境\nloginEnvironmentScan=自訂登入環境\nk8sScan=Kubernetes 集群\noptions=選項\ndockerRunningScan=執行 docker 容器\ndockerAllScan=所有的 docker 容器\nwslScan=WSL 實例\nsshScan=SSH 配置連接\nrunAsUser=以使用者身份執行\nrunAsUserDescription=以不同使用者的身份啟動此 shell 環境\ndefault=預設\nadministrator=管理員\nwslHost=WSL 主機\ntimeout=超時\ninstallLocation=安裝位置\ninstallLocationDescription=$NAME$ 環境的安裝位置\nwsl.displayName=Windows Linux 子系統\nwsl.displayDescription=連接至 Windows 上執行的 WSL 實例\ndocker.displayName=Docker 容器\ndocker.displayDescription=連接至 docker 容器\nport=連接埠\nuser=使用者\npassword=密碼\nmethod=方法\nuri=網址\nproxy=代理\ndistribution=分發\nusername=使用者名稱\nshellType=殼類型\nbrowseFile=瀏覽檔案\nopenShell=在終端機中開啟 shell\nopenCommand=在終端機執行指令\neditFile=編輯檔案\ndescription=說明\nfurtherCustomization=進一步的客製化\nfurtherCustomizationDescription=如需更多設定選項，請使用 ssh 設定檔案\nbrowse=瀏覽\nconfigHost=主機\nconfigHostDescription=配置所在的主機\nconfigLocation=設定位置\nconfigLocationDescription=設定檔案的檔案路徑\ngateway=閘道\ngatewayDescription=連線時要使用的選購閘道\nconnectionInformation=連線資訊\nconnectionInformationDescription=要連接到哪個系統\npasswordAuthentication=密碼認證\npasswordAuthenticationDescription=用於驗證的選用密碼\nsshConfigString.displayName=基於組態的 SSH 連線\nsshConfigString.displayDescription=以 SSH 配置格式建立完全自訂的 SSH 連線\nsshConfigStringContent=配置\nsshConfigStringContentDescription=OpenSSH 配置格式中連線的 SSH 選項\nvnc.displayName=透過 SSH 進行 VNC 連線\nvnc.displayDescription=透過隧道連線開啟 VNC 會話\nbinding=綁定\nvncPortDescription=VNC 伺服器正在聆聽的連接埠\nrdpPortDescription=RDP 伺服器正在聆聽的連接埠\nvncUsername=使用者名稱\nvncUsernameDescription=可選的 VNC 使用者名稱\nvncPassword=密碼\nvncPasswordDescription=VNC 密碼\nx11WslInstance=X11 Forward WSL 範例\nx11WslInstanceDescription=在 SSH 連線中使用 X11 轉送時，要用作 X11 伺服器的本機 Windows Subsystem for Linux 發行版。此套件必須是 WSL2 套件。\nopenAsRoot=以根使用者身份開啟\nopenInWSL=在 WSL 中開啟\nlaunch=啟動\nsshTrustKeyContent=不知道主機金鑰，且您已啟用手動主機金鑰驗證。$CONTENT$\nsshTrustKeyTitle=未知主機密鑰\nrdpTunnel.displayName=透過 SSH 的 RDP 連線\nrdpTunnel.displayDescription=透過隧道連線的 RDP 連線\nrdpEnableDesktopIntegration=啟用桌面整合\nrdpEnableDesktopIntegrationDescription=假設 RDP 允許清單允許執行遠端應用程式\nrdpSetupAdminTitle=需要 RDP 設定\nrdpSetupAllowTitle=RDP 遠端應用程式\nrdpSetupAllowContent=目前此系統不允許直接啟動遠端應用程式。您要啟用嗎？這將允許您停用 RDP 遠端應用程式的允許清單，從 XPipe 直接執行遠端應用程式。\nrdpServerEnableTitle=RDP 伺服器\nrdpServerEnableContent=目標系統上的 RDP 伺服器已停用。您是否要在註冊表中啟用它，以便允許遠端 RDP 連線？\nrdp=RDP\nrdpScan=透過 SSH 的 RDP 通道\nwslX11SetupTitle=WSL X11 設定\nwslX11SetupContent=XPipe 可以使用您本機的 WSL 發行版來作為 X11 顯示伺服器。您想在$DIST$ 上設定 X11 嗎？這將會在 WSL 發行版上安裝基本的 X11 套件，可能需要一些時間。您也可以在設定選單中變更使用哪個發行版。\ncommand=指令\ncommandGroup=指令群組\nvncSystem=VNC 目標系統\nvncSystemDescription=實際要互動的系統。這通常與隧道主機相同\nvncHost=目標 VNC 主機\nvncHostDescription=VNC 伺服器執行的系統\nvncDirectHost=主機\nvncDirectHostDescription=VNC 伺服器執行所在伺服器的主機項目或手動位址\nrdpDirectHost=主機\nrdpDirectHostDescription=RDP 伺服器執行所在伺服器的主機項目或手動位址\ngitVaultTitle=Git 保險庫\ngitVaultForcePushContent=要強制推送到遠端儲存庫？這會用您的本地版本庫完全取代所有遠端版本庫的內容，包括歷史記錄。\ngitVaultOverwriteLocalContent=要覆蓋您本地儲存庫的變更？這會將所有遠端變更套用到您的本地儲存庫。\nrdpSimple.displayName=直接 RDP 連線\nrdpSimple.displayDescription=透過 RDP 連接到主機\nrdpUsername=使用者名稱\nrdpUsernameDescription=要登入的使用者。可包含網域前綴\naddressDescription=連接至何處\nrdpAdditionalOptions=其他 RDP 選項\nrdpAdditionalOptionsDescription=要包含的原始 RDP 選項，格式與 .rdp 檔案相同\nproxmoxVncConfirmTitle=VNC 存取\nproxmoxVncConfirmContent=您要啟用虛擬機器的 VNC 存取權限嗎？這會在虛擬機器設定檔中啟用直接 VNC 用戶端存取，並重新啟動虛擬機器。\ndockerContext.displayName=Docker 上下文\ndockerContext.displayDescription=與位於特定上下文中的容器互動\nvmActions=虛擬機器動作\ndockerContextActions=上下文動作\nk8sPodActions=Pod 動作\nopenVnc=啟用 VNC 存取\naddVnc=新增 VNC 連線\ncommandGroup.displayName=指令群組\ncommandGroup.displayDescription=系統的群組可用指令\nserial.displayName=序列連接\nserial.displayDescription=在終端機中開啟序列連接\nserialPort=序列埠\nserialPortDescription=連接的序列埠/裝置\nbaudRate=波特率\ndataBits=資料位元\nstopBits=停止位元\nparity=奇偶校驗\nflowControlWindow=流量控制\nserialImplementation=串列實作\nserialImplementationDescription=用來連接串列埠的工具\nserialHost=主機\nserialHostDescription=上存取序列埠的系統\nserialPortConfiguration=串列埠配置\nserialPortConfigurationDescription=連接的序列裝置的設定參數\nserialInformation=序列資訊\nopenXShell=在 XShell 中開啟\ntsh.displayName=遠距傳輸\ntsh.displayDescription=透過 tsh 連接到您的遠距傳送節點\ntshNode.displayName=遠距傳送節點\ntshNode.displayDescription=連接至群集中的遠端傳送節點\nteleportCluster=集群\nteleportClusterDescription=節點所在的群集\nteleportProxy=代理\nteleportProxyDescription=用來連接節點的代理伺服器\nteleportHost=主機\nteleportHostDescription=節點的主機名稱\nteleportUser=使用者\nteleportUserDescription=以下列身分登入的使用者\nlogin=登入\nhyperVInstall.displayName=Hyper-V\nhyperVInstall.displayDescription=連接至 Hyper-V 管理的虛擬機器\nhyperVVm.displayName=Hyper-V 虛擬機器\nhyperVVm.displayDescription=透過 SSH 或 PSSession 連線至 Hyper-V 虛擬機器\ntrustHost=信任主機\ntrustHostDescription=將 ComputerName 加入受信任的主機清單\ncopyIp=複製 IP\nvncDirect.displayName=直接 VNC 連線\nvncDirect.displayDescription=直接透過 VNC 連接到系統\neditConfiguration=編輯設定\nviewInDashboard=在儀表板中檢視\nsetDefault=設定預設值\nremoveDefault=移除預設值\nconnectAsOtherUser=以其他使用者身分連線\nprovideUsername=提供登入的替代使用者名稱\nvmIdentity=訪客身份\nvmIdentityDescription=必要時連線時使用的 SSH 身份驗證方法\nvmPort=連接埠\nvmPortDescription=透過 SSH 連線的連接埠\nforwardAgent=前向代理\nforwardAgentDescription=在遠端系統上提供 SSH 代理身份\nvirshUri=URI\nvirshUriDescription=管理程序 URI，也支援別名\nvirshDomain.displayName=libvirt 網域\nvirshDomain.displayDescription=連接至 libvirt 網域\nvirshHypervisor.displayName=libvirt 管理程序\nvirshHypervisor.displayDescription=連接至 libvirt 支援的管理程序驅動程式\nvirshInstall.displayName=libvirt 指令行用戶端\nvirshInstall.displayDescription=透過 virsh 連線至所有可用的 libvirt 管理程序\naddHypervisor=新增管理程序\ninteractiveTerminal=互動終端機\neditDomain=編輯網域\nlibvirt=libvirt 網域\ncustomIp=自訂 IP\ncustomIpDescription=如果使用進階網路，覆寫預設的本機 VM IP 偵測\nautomaticallyDetect=自動偵測\nuserAddDialogTitle=使用者建立\ngroupAddDialogTitle=群組建立\npassphrase=密碼\nrepeatPassphrase=重複密碼\ngroupSecret=群組秘密\nrepeatGroupSecret=重複群組秘密\nvaultGroup=保險庫群組\nloginAlertTitle=需要登入\nloginAlertHeader=解鎖保險庫以存取您的個人連線\nvaultUser=保險庫使用者\nme=我\naddGroup=新增群組 ...\naddGroupDescription=為此保險庫建立新的群組\naddUser=新增使用者 ...\naddUserDescription=為此保險庫建立新使用者\nskip=跳過\nuserChangePasswordAlertTitle=密碼變更\ngroupChangeSecretAlertTitle=秘密變更\ndocs=說明文件\nlxd.displayName=LXD 容器\nlxd.displayDescription=透過 lxc 連接到 LXD 容器\nlxdCmd.displayName=LXD CLI 用戶端\nlxdCmd.displayDescription=透過 lxc CLI 用戶端存取 LXD 容器\npodman.displayName=Podman 集裝箱\npodman.displayDescription=連接到 Podman 容器\nincusInstall.displayName=Incus 機器管理員\nincusInstall.displayDescription=透過 incus CLI 用戶端存取 incus 容器\nincusContainer.displayName=Incus 容器\nincusContainer.displayDescription=連接至incus 容器\npodmanCmd.displayName=Podman CLI\npodmanCmd.displayDescription=透過 CLI 用戶端存取 Podman 容器\nlxdHostDescription=LXD 容器所在的主機。必須已安裝 lxc。\nlxdContainerDescription=LXD 容器的名稱\npodmanContainers=Podman 集裝箱\nlxdContainers=LXD 容器\nincusContainers=Incus 容器\ncontainer=容器\nhost=主機\ncontainerActions=容器動作\nserialConsole=序列控制端\neditRunConfiguration=編輯執行組態\ncommunityDescription=最適合您個人使用個案的連接電源工具。\nupgradeDescription=為您的整個伺服器基礎架構提供專業的連線管理。\ndiscoverPlans=發現升級選項\nextendProfessional=升級至最新的專業功能\ncommunityItem1=與非商業系統和工具的無限制連接\ncommunityItem2=與您已安裝的終端機和編輯器無縫整合\ncommunityItem3=功能齊全的遠端檔案瀏覽器\ncommunityItem4=適用於所有 shell 的強大指令碼系統\ncommunityItem5=用於同步和共享連接資訊的 Git 整合\nupgradeItem1=包含所有社群版功能\nupgradeItem2=Homelab 計劃支援無限制的管理程序和進階 SSH 功能\nupgradeItem3=專業方案額外支援企業作業系統和工具\nupgradeItem4=企業方案具有充分的彈性，可滿足您個別的使用情況\nupgrade=升級\nupgradeTitle=可用的計劃\nstatus=狀態\ntype=類型\nlicenseAlertTitle=所需許可證\nuseCommunity=繼續使用社群\npreviewDescription=在發佈後的幾個星期內試用新功能。\ntryPreview=啟動預覽\npreviewItem1=發佈後 2 週內可完全存取新發佈的專業功能\npreviewItem2=無需承諾即可試用新功能\nlicensedTo=授權給\nemail=電子郵件地址\napply=套用\nclear=清除\nactivate=啟動\nvalidUntil=有效期至\nlicenseActivated=已啟用的授權\nrestart=重新啟動\nlockVault=鎖金庫\nrestartApp=重新啟動 XPipe\nfree=免費\nupgradeInfo=您可以在下方找到升級為授權的相關資訊。\nupgradeInfoPreview=您可以在下方找到升級為授權的相關資訊，或試用預覽。\nenterLicenseKey=輸入授權金鑰進行升級\nisOnlySupported=僅支援至少$TYPE$ 授權\nareOnlySupported=僅支援至少$TYPE$ 授權\nlegacyLicense=此授權僅包含購買後一年內發佈的新專業功能。\npreviewExpiredLicense=此功能最近在預覽中免費供應，但此期已過。\nopenApiDocs=API 文件\nopenApiDocsDescription=HTTP API 說明文件可於線上取得，包括 OpenAPI .yaml 規格。您可以在 Web 瀏覽器或您偏好的 HTTP 用戶端中開啟它。\nopenApiDocsButton=開啟文件\npythonApi=Python API\npersonalConnection=此連線及其所有子連線僅供您的使用者使用，因為它們取決於個人身分。\ndeveloperPrintInitFiles=列印啟始檔執行\ndeveloperPrintInitFilesDescription=列印啟動終端機時執行的所有 shell init 腳本。\ndeveloperShowSensitiveCommands=日誌敏感指令\ndeveloperShowSensitiveCommandsDescription=在日誌輸出中包含敏感指令，以便除錯。\ncheckingForUpdates=檢查更新\ncheckingForUpdatesDescription=擷取最新版本資訊\ndownloadingUpdate=擷取版本 (版本$VERSION$)\ndownloadingUpdateDescription=下載發行套件\nupdateNag=您有一段時間沒有更新 XPipe 了。您可能會錯過更新版本的新功能和修正。\nupdateNagTitle=更新提醒\nupdateNagButton=查看發佈\nrefreshServices=刷新服務\nserviceProtocolType=服務通訊協定類型\nserviceProtocolTypeDescription=控制如何開啟服務\nserviceCommand=服務啟動後執行的指令\nserviceCommandDescription=佔位符 $PORT 將被實際的隧道本地端埠取代\nvalue=價值\nshowAdvancedOptions=顯示進階選項\nsshAdditionalConfigOptions=其他設定選項\nremoteFileManager=遠端檔案管理員\nclearUserData=刪除使用者資料\nclearUserDataDescription=刪除所有使用者設定資料，包括連線\nclearUserDataTitle=使用者資料刪除\nclearUserDataContent=這將刪除 xpipe 的所有本機使用者資料，並重新啟動。如果您關心您的連線，請務必先與 git 儲存庫同步。\nundefined=未定義\ncopyAddress=複製位址\nnetbirdDeviceScan=Netbird 連線\nnetbirdId=對等公開金鑰\nnetbirdIdDescription=對等方的內部 netbird 公鑰 id\ntailscaleDeviceScan=尾標連接\ntailscaleInstall.displayName=Tailscale 安裝\ntailscaleInstall.displayDescription=透過 SSH 連線至尾網中的裝置\ntailscaleDevice.displayName=Tailscale 裝置\ntailscaleDevice.displayDescription=透過 SSH 連線至尾網中的裝置\ntailscaleId=裝置 ID\ntailscaleIdDescription=內部 tailscale 裝置 ID\ntailscaleHostName=主機名稱\ntailscaleHostNameDescription=尾網中裝置的主機名稱\ntailscaleUsername=使用者名稱\ntailscaleUsernameDescription=以下列身分登入的使用者\ntailscalePassword=密碼\ntailscalePasswordDescription=可用於 sudo 的可選使用者密碼\nscriptName=腳本名稱\nscriptNameDescription=為此腳本自訂名稱\nscriptGroupName=腳本群組名稱\nscriptGroupNameDescription=為此腳本群組自訂名稱\nidentityName=身分名稱\nidentityNameDescription=賦予此身分自訂名稱\ntailscaleTailnet.displayName=尾端網路\ntailscaleTailnet.displayDescription=以帳號連線至特定的尾隨網路\nputtyConnections=PuTTY 連線\nkittyConnections=KiTTY 連線\nicons=圖示\ncustomIcons=自訂圖示\niconSources=圖示來源\niconSourcesDescription=您可以在此新增自己的圖示來源。XPipe 會擷取新增位置的任何 .svg 檔案，並將其新增至可用的圖示集。\\n\\n本機目錄和遠端 git 套件庫均可作為圖示位置。\nrefreshSources=刷新圖示\nrefreshSourcesDescription=從可用來源更新所有圖示\naddDirectoryIconSource=新增目錄來源 ...\naddDirectoryIconSourceDescription=從本機目錄新增圖示\naddGitIconSource=新增 git 原始碼 ...\naddGitIconSourceDescription=新增位於遠端 git 套件庫的圖示\nrepositoryUrl=Git 儲存庫 URL\niconDirectory=圖示目錄\naddUnsupportedKexMethod=新增不支援的金鑰交換方法\naddUnsupportedKexMethodDescription=允許此連線使用金鑰交換方法$VAL$\naddUnsupportedHostKeyType=新增不支援的主機金鑰類型\naddUnsupportedHostKeyTypeDescription=允許此連線使用主機金鑰類型$VAL$\naddUnsupportedMacType=新增不支援的 MAC 類型\naddUnsupportedMacTypeDescription=允許 MAC 類型$VAL$ 用於此連線\nrunSilent=默默地在背景中\nrunInFileBrowser=在檔案瀏覽器中\nrunInConnectionHub=連接集線器中\ncommandOutput=指令輸出\niconSourceDeletionTitle=刪除圖示來源\niconSourceDeletionContent=您要刪除此圖示來源及其所有相關圖示嗎？\nrefreshIcons=刷新圖示\nrefreshIconsDescription=從外部來源擷取、渲染和快取所有可用的 1000 多個圖示為 .png 檔案。這可能需要一段時間 ...\nvaultUserLegacy=保險庫使用者 (有限的傳統相容性模式)\nupgradeInstructions=升級說明\nexternalActionTitle=外部動作請求\nexternalActionContent=要求執行外部動作。您要允許從 XPipe 外部啟動動作嗎？\nnoScriptStateAvailable=刷新以確定腳本相容性 ...\ndocumentationDescription=查看說明文件\ncustomEditorCommandInTerminal=在終端機執行自訂指令\ncustomEditorCommandInTerminalDescription=如果您的編輯器是以終端機為基礎，您可以啟用此選項，以自動開啟終端機，並取代在終端機會話中執行指令。\\n\\n您可以對 vi、vim、nvim 等編輯器使用此選項。\ndisableHttpsTlsCheck=停用 HTTPS 請求證書驗證\ndisableHttpsTlsCheckDescription=如果您的組織在防火牆中使用 SSL 攔截來解密 HTTPS 流量，任何更新檢查或授權檢查都會因憑證不符而失敗。您可以啟用此選項並關閉 TLS 證書驗證，以修復此問題。\nconnectionsSelected=$NUMBER$ 選取的連線\naddConnections=新增連線\nbrowseDirectory=瀏覽目錄\nopenTerminal=開啟終端機\ndocumentation=說明文件\nreport=報告錯誤\nkeePassXcNotAssociated=KeePassXC 連結\nkeePassXcNotAssociatedDescription=XPipe未與您本地的KeePassXC資料庫聯繫。點擊下方執行一次性步驟，將XPipe與KeePassXC資料庫聯繫，以便XPipe可以查詢密碼。\nkeePassXcAssociateMore=連接更多資料庫\nkeePassXcAssociateMoreDescription=您可以同時連接多個 KeePassXC 資料庫\nkeePassXcAssociated=KeePassXC 連結\nkeePassXcAssociatedDescription=XPipe 連接到下列本機 KeePassXC 資料庫：\nkeePassXcNotAssociatedButton=連結資料庫\nidentifier=識別碼\npasswordManagerCommand=自訂指令\npasswordManagerCommandDescription=要執行以取得密碼的自訂指令。在呼叫時，占位符字串 $KEY 將被引號密碼鍵所取代。這應該呼叫您的密碼管理器 CLI，將密碼列印到 stdout，例如：mypassmgr get $KEY。\nchooseTemplate=選擇範本\nkeePassXcPlaceholder=KeePassXC 輸入 URL\nterminalEnvironment=終端環境\nterminalEnvironmentDescription=如果您想要使用本機 Linux-based WSL 環境的功能來自訂您的終端機，您可以使用它們作為終端機環境。\\n\\n任何自訂的終端啟動指令和終端多工器組態就會在此 WSL 發行版中執行。\nterminalInitScript=終端啟動腳本\nterminalInitScriptDescription=連線啟動前在終端機環境中執行的指令。您可以用它在啟動時設定終端機環境。\nterminalMultiplexer=終端多工器\nterminalMultiplexerDescription=中使用終端多工器來替代制表符。這將以多工器功能取代某些終端處理特性，例如標籤處理。\\n\\n需要在系統上安裝相關的 multiplexer 可執行檔。\nterminalMultiplexerWindowsDescription=中使用終端多工器來替代制表符。這將以多工器功能取代某些終端處理特性，例如標籤處理。\\n\\n需要在 Windows 上使用 WSL 終端環境，並在 WSL 系統上安裝 multiplexer 可執行檔。\nterminalAlwaysPauseOnExit=總是在退出時暫停\nterminalAlwaysPauseOnExitDescription=啟用時，退出終端會話時會一直提示您重新啟動或關閉該會話。如果停用，XPipe 只會在連線失敗且退出時出現錯誤時才會這樣做。\nquerying=查詢 ...\nretrievedPassword=獲得：$PASSWORD$\nrefreshOpenpubkey=刷新 openpubkey 身份\nrefreshOpenpubkeyDescription=執行 opkssh refresh 使 openpubkey 身分證重新有效\nall=全部\nterminalPrompt=終端提示\nterminalPromptDescription=在遠端終端中使用的終端提示工具。啟用終端提示工具會在開啟終端會話時，自動在目標系統上設定和配置提示工具。\\n\\n這不會修改系統上任何現有的提示設定或設定檔檔案。當提示工具正在遠端系統上設定時，這會增加首次載入終端的時間。您的終端可能需要額外的字型才能正確顯示提示。\nterminalPromptConfiguration=終端提示配置\nterminalPromptConfig=組態檔案\nterminalPromptConfigDescription=應用於提示符的自訂組態檔案。當終端初始化時，此組態會在目標系統上自動設定，並作為預設的提示組態。\\n\\n如果要使用每個系統上現有的預設組態檔案，可以將此欄位留空。\npasswordManagerKey=密碼管理器按鍵\npasswordManagerKeyDescription=秘密的密碼管理員識別碼\npasswordManagerAgent=密碼管理員代理\ndockerComposeProject.displayName=Docker compose 專案\ndockerComposeProject.displayDescription=將編譯專案的容器群組在一起\nsshVerboseOutput=啟用冗長的 SSH 輸出\nsshVerboseOutputDescription=透過 SSH 連線時會列印大量除錯資訊。對於排解 SSH 連線的問題非常有用。\ndontUseGateway=請勿使用閘道\ndontUseGatewayDescription=請勿使用管理程序主機作為閘道，並直接連線至 IP\ncategoryColor=分類顏色\ncategoryColorDescription=此類別內連線使用的預設顏色\ncategorySync=與 git 倉庫同步\ncategorySyncDescription=自動將所有連線與 git 套件庫同步。所有本地連線的變更都會推送到遠端。\ncategorySyncSpecial=與 git 儲存庫同步\\n(特殊類別 \"$NAME$\" 不可設定)\ncategoryDontAllowScripts=停用所有修改\ncategoryDontAllowScriptsDescription=停用此類別內系統的任何指令執行和其他操作，以防止任何修改。這將停用所有指令碼功能、shell 環境指令、提示等。\ncategoryConfirmAllModifications=確認所有修改\ncategoryConfirmAllModificationsDescription=先確認對連線或檔案系統進行的任何類型的修改。這樣可以防止對重要系統進行意外操作。\ncategoryDefaultIdentity=預設身分\ncategoryDefaultIdentityDescription=如果您經常在此類別中的許多系統上使用某個身分，那麼設定預設身分將允許您在建立新連線時預先選擇該身分。\ncategoryConfigTitle=$NAME$ 配置\nconfigure=設定\naddConnection=新增連線\nnoCompatibleConnection=找不到相容的連線\nnoCompatibleIdentity=未找到相容的身分\nnewCategory=新類別\ndockerComposeRestricted=這個 compose 專案受到$NAME$ 的限制，無法從外部修改。請使用$NAME$ 管理這個 compose 專案。\nrestricted=受限制\ndisableSshPinCaching=停用 SSH PIN 快取\ndisableSshPinCachingDescription=XPipe 在使用某種形式的基於硬體的驗證時，會自動快取任何為金鑰輸入的 PIN。\\n\\n禁用此功能將導致每次嘗試連線時都必須重新輸入 PIN。\ngitSyncPull=拉動同步遠端 git 變更\nenpassVaultFile=保險庫檔案\nenpassVaultFileDescription=本機 Enpass 保險庫檔案。\nflat=扁平\nrecursive=遞歸\nrdpAllowListBlocked=選取的 RemoteApp 似乎沒有包含在伺服器的 RDP 允許清單中。\npsonoServerUrl=伺服器 URL\npsonoServerUrlDescription=psono 後端伺服器的 URL\npsonoApiKey=API 金鑰\npsonoApiKeyDescription=要使用的 API 金鑰，格式為 uuid\npsonoApiSecretKey=API 密鑰\npsonoApiSecretKeyDescription=API 秘鑰為 64 位元組十六進位字串\npassboltServerUrl=伺服器 URL\npassboltServerUrlDescription=passbolt 後端伺服器的 URL\npassboltPassphrase=密碼\npassboltPassphraseDescription=保管庫私人密碼匙的口令\npassboltPrivateKey=私密金鑰\npassboltPrivateKeyDescription=保管庫的私人 gpg 金鑰檔案\nfocusWindowOnNotifications=通知上的焦點視窗\nfocusWindowOnNotificationsDescription=當顯示通知或錯誤訊息時，例如連線或隧道意外終止時，將 XPipe 移至前台。\ngitUsername=自訂 git 使用者名稱\ngitUsernameDescription=的自訂使用者，以驗證 git 遠端儲存庫。預設情況下，XPipe 會使用目前設定的 git CLI 認證。\\n\\n此設定將覆寫您本機 git CLI 用戶端已設定的任何預設認證。\ngitPassword=自訂 git 密碼 / 個人存取標記\ngitPasswordDescription=用於驗證的密碼或個人存取權標。是否需要密碼或個人存取標記取決於 git 遠端提供者。此設定將覆寫本地 git CLI 用戶端已設定的預設認證。\nsetReadOnly=設定唯讀\nunsetReadOnly=未設定唯讀\nreadOnlyStoreError=此項目的設定已凍結。選擇不同的名稱，將您的變更儲存到新副本。\ncategoryFreeze=凍結連線設定\ncategoryFreezeDescription=將連線設定標記為唯讀。這表示無法修改此類別中的現有連線項目設定。但可以新增連線。\nupdateFail=更新安裝不成功\nupdateFailAction=手動安裝更新\nupdateFailActionDescription=在 GitHub 上查看最新版本\nonePasswordPlaceholder=項目名稱或 op:// URL\ncomputeDirectorySizes=計算目錄大小\ncomputeSize=計算大小\ncustomSpiceCommand=自訂指令\ncustomSpiceCommandDescription=要執行以啟動 SPICE 會話的自訂指令。調用時，占位符串 $FILE 將被帶引號的 .vv 檔案路徑取代。\nvncClient=VNC 用戶端\nvncClientDescription=在 XPipe 中開啟 VNC 連線時要啟動的 VNC 用戶端。\\n\\n您可以選擇使用 XPipe 中的整合式 VNC 用戶端，或者啟動一個外部本機安裝的 VNC 用戶端，如果您需要更多的自訂功能。\nintegratedXPipeVncClient=整合式 XPipe VNC 客戶端\ncustomVncCommand=自訂指令\ncustomVncCommandDescription=要執行以啟動 VNC 會話的自訂指令。調用時，占位符串 $ADDRESS 將被引號地址取代。\nvncConnections=VNC 連線\npasswordManagerIdentity=密碼管理員身份\npasswordManagerIdentity.displayName=密碼管理員身份\npasswordManagerIdentity.displayDescription=從密碼管理員中擷取身分的使用者名稱和密碼\npasswordCopied=連線密碼複製到剪貼簿\nerrorOccurred=發生錯誤\nactionMacro.displayName=動作巨集\nactionMacro.displayDescription=使用自訂觸發器在行動中執行\nmacroAdd=新增巨集\nmacroName=巨集名稱\nmacroNameDescription=自訂巨集名稱\nactionId=動作 ID\nactionIdDescription=使用此巨集執行的動作\nmacroRefs=相關連線\nmacroRefsDescription=執行動作的連線\nconnectionCopy=複製\nactionPickerTitle=挑選動作\nactionPickerDescription=按一下某個東西以執行動作。您可以在動作捷徑選取模式中建立和編輯動作的捷徑，而不是執行動作。\ncancelActionPicker=取消動作選取\nactionShortcut=動作捷徑\nactionShortcuts=動作捷徑\nactionStore=動作儲存\nactionStoreDescription=要執行動作的商店項目\nactionStores=動作儲存\nactionStoresDescription=要執行動作的儲存項目\nactionDesktopShortcut=桌面捷徑\nactionDesktopShortcutDescription=在桌面上建立此動作的捷徑\nactionUrlShortcut=URL 捷徑\nactionUrlShortcutDescription=複製開啟時可觸發此動作的 URL\nactionUrlShortcutDisabled=URL 捷徑 (無法使用)\nactionUrlShortcutDisabledDescription=$TYPE$ 安裝類型不支援開啟 URL\nactionApiCall=API 請求\nactionApiCallDescription=從 HTTP API 中呼叫此動作\nactionMacro=動作巨集\nactionMacroDescription=為此動作建立具有進階功能的巨集\ncreateMacro=建立巨集\nactionConfiguration=參數\nactionConfigurationDescription=傳給執行動作的參數\nconfirmAction=確認動作\nactionConnections=動作連接\nactionConnectionsDescription=執行動作的連線\nactionConnection=動作連接\nactionConnectionDescription=執行動作的連線\nappleContainerInstall.displayName=Apple 容器\nappleContainerInstall.displayDescription=透過容器 CLI 存取 apple 容器實例\nappleContainer.displayName=Apple 集裝箱\nappleContainer.displayDescription=透過容器 CLI 存取 apple 容器實例\nappleContainerHostDescription=apple 容器所在的主機\nappleContainerDescription=蘋果容器的名稱\nappleContainers=Apple 容器\nchangeOrderIndexTitle=變更順序\norderIndex=索引\norderIndexDescription=顯式索引，用來排列此項目相對於其他項目的順序。最低的索引顯示在上方，最高的顯示在下方\nmoveToFirst=移至第一位\nmoveToLast=移至最後\ncategory=類別\nincludeRoot=包括根\nexcludeRoot=排除根\nfreezeConfiguration=凍結設定\nunfreezeConfiguration=解除凍結設定\nwaylandScalingTitle=Wayland 擴充\nactionApiUrl=$URL$ (複製 json 正文)\ncopyBody=複製請求正文\ngitRepoTerminalOpen=在終端機打開儲存庫\ngitRepoTerminalOpenDescription=使用命令列查看倉庫\ngitRepoOverwriteLocal=覆寫本機儲存庫\ngitRepoOverwriteLocalDescription=以遠端的變更取代所有本機的變更\ngitRepoForcePush=覆寫遠端儲存庫\ngitRepoForcePushDescription=使用 git push --force 將本地的變更套用到遠端\ngitRepoDontWarn=不再警告\ngitRepoDontWarnDescription=如果這是預期發生的，請讓 XPipe 在未來忽略此錯誤\ngitRepoTryAgain=再試一次\ngitRepoTryAgainDescription=再次嘗試相同的操作\ngitRepoEnablePlain=使用純目錄同步\ngitRepoEnablePlainDescription=不要初始化 git 倉庫以同步目錄的變更\ngitRepoCreateBare=使用 git 同步\ngitRepoCreateBareDescription=在同步目錄中初始化一個新的裸 git 倉庫\ngitRepoDisable=暫時停用 git 保險庫\ngitRepoDisableDescription=在此階段不要提交任何變更\ngitRepoPullRefresh=拉動變更和刷新\ngitRepoPullRefreshDescription=合併遠端變更並重新載入資料\nbreakOutCategory=突破類別\nmergeCategory=合併類別\nopenWinScp=在 WinSCP 中開啟\nuninstallApplication=卸載\nuninstallApplicationDescription=執行 .pkg 安裝腳本，以完全解除安裝 XPipe\nk8sEditPodTitle=套用變更\nk8sEditPodContent=您要套用透過 kubectl apply 指令所做的變更嗎？可能需要重新啟動才能套用變更。\nvirshEditDomainTitle=套用變更\nvirshEditDomainContent=您要將變更套用到網域嗎？可能需要重新啟動才能套用變更。\npkcs11Library=PKCS#11 函式庫\npkcs11LibraryDescription=動態連結函式庫檔案的路徑\nsshAgentSocket=自訂 SSH 代理插座\nsshAgentSocketDescription=用於與 SSH 代理通訊的自訂套接字。選擇此自訂代理選項，即可將此自訂代理用於連線。\npublicKey=公開金鑰識別碼\npublicKeyDescription=可選的公開金鑰，強制代理只提供匹配的私人金鑰\nactions=行動\nhcloudServer.displayName=赫茲納雲端伺服器\nhcloudServer.displayDescription=透過 SSH 存取 Hetzner 雲端託管的伺服器\nhcloudInstall.displayName=Hetzner Cloud CLI\nhcloudInstall.displayDescription=透過 hcloud 存取 Hetzner 雲端託管的伺服器\nhcloudContext.displayName=雲端上下文\nhcloudContext.displayDescription=雲端上下文的存取伺服器\nmetrics=度量\nopenInVsCode=在 VsCode 中開啟\naddCloud=雲 ...\nhcloudToken=雲端代碼\nhcloudTokenDescription=要使用的 Hetzner 雲端代碼。如需詳細資訊，請參閱說明文件\nhcloudLogin=赫茲納雲端登入\nclearHcloudToken=清除 hcloud 令牌\nclearHcloudTokenDescription=刪除現有的代碼，以便再次登入\nselectIdentity=選擇身分\nenableMcpServer=啟用 MCP 伺服器\nenableMcpServerDescription=啟用 XPipe MCP 伺服器，允許外部 MCP 用戶端向 MCP 伺服器傳送請求。配置詳情請參閱下文。\\n\\n請注意，MCP 功能不一定要啟用 HTTP API。\nenableMcpMutationTools=啟用 MCP 變異工具\nenableMcpMutationToolsDescription=預設情況下，MCP 伺服器只啟用唯讀工具。這是為了確保不會發生可能修改系統的意外操作。\\n\\n如果您計劃透過 MCP 用戶端對系統進行變更，請務必檢查您的 MCP 用戶端是否已設定，以便在啟用此選項之前確認任何可能的破壞性操作。需要重新連接任何 MCP 用戶端才能套用。\nmcpClientConfigurationDetails=MCP 用戶端配置\nmcpClientConfigurationDetailsDescription=使用此設定資料，從您所選擇的 MCP 用戶端連線至 XPipe MCP 伺服器。\nswitchHostAddress=變更主機位址\naddAnotherHostName=新增另一個主機名稱\naddNetwork=網路掃描 ...\nnetworkScan=網路掃描\nnetworkScanStore=目標主機\nnetworkScanStoreDescription=要掃描本機網路的主機\nuseAsGateway=使用主機作為閘道\nuseAsGatewayDescription=是否使用目標主機作為建立連線的閘道\nnetworkScanPorts=要掃描的連接埠\nnetworkScanPortsDescription=要包含在掃描的逗號分隔連接埠清單\nnetworkScanType=連線類型\nnetworkScanTypeDescription=要尋找的伺服器類型\nemptyDirectory=此目錄看起來是空的\nhcloudConfigFile=雲端配置檔案\nhcloudConfigFileDescription=hcloud CLI .toml 配置文件的位置\npreferMonochromeIcons=偏好單色圖示\npreferMonochromeIconsDescription=啟用時，假設來源的圖示有獨立的淺色或深色模式圖示變體，則會選擇單色圖示變體，而非預設的彩色圖示版本。\\n\\n需要刷新圖示才能套用。\nalwaysShowSshMotd=總是顯示 MOTD\nalwaysShowSshMotdDescription=是否在新終端會話登入時顯示遠端系統上設定的每日訊息。請注意，變更這項可能會改變 SSH 連線的初始化行為。\nmanageSubscription=管理訂閱\nnoListeningServer=無監聽伺服器\nnetworkScanResults=掃描結果\nnetworkScanResultsDescription=網路中找到的系統清單\nlocalShellDialect=本地 shell\nlocalShellDialectDescription=用於本機操作的 shell。如果正常的本機預設 shell 在某種程度上被停用或損壞，則可使用此選項來回退到另一個選擇。\\n\\n某些設定，例如自訂 PATH 輸入，如果尚未在各自的 shell 設定檔中設定，則可能不適用於備用 shell。\nagentSocketNotFound=未找到作用中的代理插座\nagentSocket=插座位置\nagentSocketDescription=代理套接字檔案的路徑\nagentSocketNotConfigured=尚未設定自訂套接字\ndownloadInProgress=$NAME$ 下載中\nenableTerminalStartupBell=啟用終端啟動鈴聲\nenableTerminalStartupBellDescription=在新啟動的終端會話中播放嗶聲/鈴聲指令。如果您的終端模擬器支援鈴聲，這可以用來更容易識別新啟動的終端實例。\ninvalidSshGatewayChain=具有跳轉閘道和非跳轉閘道的無效混合閘道鏈組態。\nsyncFileExists=同步檔案$FILE$ 已經存在\nreplaceFile=取代檔案\nreplaceFileDescription=用這個檔案取代現有檔案\nrenameFile=重新命名檔案\nrenameFileDescription=給這個檔案一個不同的名稱來同步\nnewFileName=新檔案名稱\nparentHostDoesNotSupportTunneling=父主機$NAME$ 不支援隧道功能\nconnectionNotesTemplate=備註範本\nconnectionNotesTemplateDescription=為連線新增備註項目時應使用的 markdown 模版。\nconnectionNotesButton=編輯備註\nrdpSmartSizing=啟用智慧型大小\nrdpSmartSizingDescription=啟用時，如果視窗太小，無法以完整解析度顯示，mstsc 會縮小桌面大小。縮小時會保留桌面的長寬比。\ndisableStartOnInit=停用自動啟動\nenableStartOnInit=啟用自動啟動\nfileReadSudoTitle=Sudo 檔案讀取\nfileReadSudoContent=您嘗試讀取的檔案未授予您目前使用者的讀取權限。您是否要以 root 使用者身份使用 sudo 讀取此檔案？這將使用現有憑證或透過提示自動升級為 root。\nnetbirdInstall.displayName=安裝 Netbird\nnetbirdInstall.displayDescription=連接至您 Netbird 網路中的對等網路\nnetbirdProfile.displayName=網鳥簡介\nnetbirdProfile.displayDescription=列出特定設定檔中的對等人\nnetbirdPeer.displayName=網鳥對等\nnetbirdPeer.displayDescription=透過 SSH 連線至對等電腦\nnetbirdPublicKey=公開金鑰\nnetbirdPublicKeyDescription=對等機構的內部公開金鑰\nnetbirdHostName=主機名稱\nnetbirdHostNameDescription=網路中對等電腦的主機名稱\nvncRefSystem=相關系統\nvncRefSystemDescription=要與此 VNC 連線關聯的連線項目。如果沒有則留空\nabstractHost.displayName=摘要主機\nabstractHost.displayDescription=為不支援 shell 連線的主機建立項目\nabstractHostAddress=主機位址\nabstractHostAddressDescription=主機的位址\nabstractHostGateway=閘道\nabstractHostGatewayDescription=連接此主機的選用閘道系統\nabstractHostConvert=轉換為抽象主機項目\nhostNoConnections=無可用連線\nhostHasConnections=$COUNT$ 可用連接\nhostHasConnection=$COUNT$ 可用連接\nlargeFileWarningTitle=大型檔案編輯\nlargeFileWarningContent=您要編輯的檔案相當大，有$SIZE$ 。您真的要在文字編輯器中打開這個檔案？\nrdpAskpassUser=主機的 RDP 使用者名稱$HOST$\nrdpAskpassPassword=使用者的密碼$USER$\ninPlaceKey=關鍵字\ninPlaceKeyText=私密金鑰內容\ninPlaceKeyTextDescription=私人密碼匙的內容\nnetbirdSelfhosted=自託管的 netbird 實例\nnetbirdSelfhostedDescription=提供自訂 URL 而非使用雲端託管版本\nnetbirdManagementUrl=Netbird 管理 URL\nnetbirdManagementUrlDescription=您自行託管的實例的管理 URL\nnetbirdSetupKey=設定按鍵\nnetbirdSetupKeyDescription=如果您使用設定鍵，您可以使用其中一個登入\nnetbirdLogin=網鳥登入\naddProfile=新增設定檔\nnetbirdProfileNameAsktext=新 netbird 配置文件的名稱\nopenSftp=在 SFTP 會話中開啟\ncapslockWarning=您已啟用上限鎖定功能\ninherit=繼承\nsshConfigStringSelected=目標主機\nsshConfigStringSelectedDescription=對於多台主機，第一台會作為目標。重新排列主機以變更目標\ntunnelToLocalhost=隧道到 localhost\ntunnelToLocalhostDescription=自動將遠端連接埠隧道到 localhost\ntags=標籤\ntag=標籤\naddNewTag=建立新標籤\ncreateTag=建立標籤 ...\ninPlacePublicKey=公開金鑰\ninPlacePublicKeyDescription=指定私人密碼匙的相關公開密碼匙\nsshKeygenTitle=產生新的 SSH 金鑰\nsshKeygenAlgorithm=演算法\nsshKeygenAlgorithmDescription=金鑰使用的非對稱密鑰原始演算法\nrsaBits=位元\nrsaBitsDescription=產生的金鑰中的位元數\nsshKeygenComment=評論\nsshKeygenCommentDescription=此密鑰的選用註解\nsshKeygenPassphrase=密碼\nsshKeygenPassphraseDescription=此金鑰的可選密碼\ned25519SkResident=製作常駐鑰匙\ned25519SkResidentDescription=在硬體安全金鑰上儲存私人金鑰\ned25519SkResidentKeyName=駐留按鍵標籤\ned25519SkResidentKeyNameDescription=給金鑰一個標籤在安全金鑰上儲存多個金鑰時需要使用\ned25519SkPinRequired=要求 PIN 碼\ned25519SkPinRequiredDescription=使用時要求輸入 PIN 碼\ned25519SkUserPresenceRequired=要求使用者在場\ned25519SkUserPresenceRequiredDescription=使用時需要觸控或類似功能。某些安全鑰匙需要啟用此功能\ncopyPublicKey=複製公開金鑰\ngeneratePublicKey=產生公開金鑰\npublicKeyGenerateNotice=可由私人密碼匙產生\nidentityApplyTargetHost=目標\nidentityApplyTargetHostDescription=應用身分的系統\nidentityApplyAuthorizedHost=已授權的 SSH 金鑰\nidentityApplyAuthorizedHostDescription=SSH 金鑰會新增至授權主機檔案\nidentityApplyAuthorizedHostButton=將按鍵附加到檔案\napplyIdentityToHost=將身分套用至主機 ...\nidentityApplyMissingPublicKeyTitle=遺失公開金鑰\nidentityApplyMissingPublicKeyContent=身份的 SSH 金鑰沒有關聯公開金鑰。查看配置以瞭解詳細資訊。\nvalid=有效\nnotValid=無效\nwarning=警告\nidentityApplyTitle=應用身分\nidentityApplyConfigPasswordEnabled=已啟用密碼認證\nidentityApplyConfigPasswordEnabledDescription=在 sshd 配置中仍啟用密碼驗證\nidentityApplyConfigPasswordDisabled=停用密碼認證\nidentityApplyConfigPasswordDisabledDescription=在 sshd 配置中仍然停用密碼驗證\nidentityApplyConfigKeyEnabled=啟用金鑰驗證\nidentityApplyConfigKeyEnabledDescription=基於金鑰的驗證仍在 sshd 配置中啟用\nidentityApplyConfigKeyDisabled=關閉金鑰認證\nidentityApplyConfigKeyDisabledDescription=在 sshd 配置中，基於金鑰的驗證仍處於停用狀態\nidentityApplyConfigRootDisabledWarning=禁用 Root 登入\nidentityApplyConfigRootDisabledWarningDescription=在 sshd 配置中未啟用 Root 使用者登入\nidentityApplyConfigAdminWarning=配置的管理員按鍵\nidentityApplyConfigAdminWarningDescription=對於 admin 使用者，可能必須將金鑰加入 administrators_authorized_keys 中。\nidentityApplyEditConfig=編輯設定\nidentityApplyEditConfigDescription=在編輯器中開啟 sshd 配置以修正任何問題\nidentityApplyEditAuthorizedKeys=編輯授權按鍵\nidentityApplyEditAuthorizedKeysDescription=在編輯器中開啟 authorized_keys 檔案，編輯或移除其他金鑰\nidentityApplyEditConfigButton=開啟 sshd_config\nidentityApplyEditAuthorizedKeysButton=開啟 authorized_keys\nidentityApplySetStoreIdentity=連線識別集\nidentityApplySetStoreIdentityDescription=身份設定為連線使用\nidentityApplySetStoreIdentityButton=應用身分\ngenerateKey=產生金鑰\ngroupSecretStrategy=基於群組的存取控制\ngroupSecretStrategyDescription=如何擷取用於群組加密和解密的群組秘密。當使用者在啟動時登入保管庫，您所選擇的擷取方法將會執行。\\n\\n此設定是依每個群組設定。若要為目前活動群組以外的其他群組變更此設定，您必須以該群組的成員登入保管庫。\nfileSecret=檔案型機密\ncommandSecret=指令\nhttpRequestSecret=HTTP 回應\nfileSecretChoice=檔案位置\nfileSecretChoiceDescription=包含群組加密秘密的檔案路徑。由於此檔案可在所有平台上查詢，您可以在路徑中使用 ~ 來指代主目錄。該檔案必須在您解除保險庫的所有系統上都可用，否則登入將會失敗。\ncommandSecretField=檢索腳本\ncommandSecretFieldDescription=命令，可傳回當前群組的秘密加密金鑰。該命令在本機系統預設 shell 中執行，金鑰應列印到 stdout。\nhttpRequestSecretField=請求 URI\nhttpRequestSecretFieldDescription=要傳送 HTTP 請求的 URI。群組秘密取自 HTTP 回應正文。\nvaultAuthentication=保險庫認證\nvaultAuthenticationDescription=如何認證/解鎖儲存庫資料。有多種不同的方式來加密和解鎖儲存庫資料，這取決於您想與誰分享儲存庫資料。\ngroupAuthFailed=秘密驗證失敗\nuserAuthFailed=密碼驗證失敗\nsavingChanges=儲存變更\nawsDeviceScan=AWS\naws=AWS\nawsCliInstallTitle=需要 AWS CLI\nawsCliInstallContent=AWS 整合需要在本機系統安裝 AWS CLI\nawsProfileCreateTitle=新的 AWS 設定檔\nawsProfileAccessKey=存取鍵\nawsProfileName=設定檔名稱\nawsProfileNameDescription=新設定檔的顯示名稱\nawsProfileRegion=區域\nawsProfileRegionDescription=與設定檔相關聯的 AWS 區域\nawsProfileAccessKeyId=存取金鑰 ID\nawsProfileAccessKeyIdDescription=IAM 使用者存取金鑰 ID\nawsProfileSecretAccessKey=秘密存取金鑰\nawsProfileSecretAccessKeyDescription=相關的秘密存取金鑰\nawsInstall.displayName=AWS CLI 安裝\nawsInstall.displayDescription=透過 AWS CLI 連線至您的 AWS 系統\nawsProfile.displayName=AWS CLI 設定檔\nawsProfile.displayDescription=透過特定設定檔存取 AWS\nawsInstanceId=實例 ID\nawsInstanceIdDescription=這個實例的內部 ID\nawsInstanceUseSsm=透過 SSM 連線\nawsInstanceUseSsmDescription=使用 SSM 工具透過 SSH 連線至實體\nawsEc2Instance.displayName=AWS EC2 實例\nawsEc2Instance.displayDescription=透過 SSH 連線至 EC2 實例\nawsS3Group.displayName=S3 收納桶\nawsS3Group.displayDescription=存取 AWS 設定檔的 S3 資料桶\nawsS3Bucket.displayName=S3 水桶\nawsS3Bucket.displayDescription=存取 AWS 設定檔的 S3 桶\nawsEc2Group.displayName=EC2 實體\nawsEc2Group.displayDescription=存取 AWS 設定檔的 EC2 實體\nawsEc2InstanceSsmTerminal=開啟 SSM 終端\ngenericS3Bucket.displayName=一般 S3 儲存桶\ngenericS3Bucket.displayDescription=透過 AWS CLI 存取一般 S3 水桶\naddFileSystem=檔案系統 ...\ngenericS3BucketHost=主機\ngenericS3BucketHostDescription=S3 伺服器的主機項目或手動位址\ngenericS3BucketPortDescription=S3 伺服器正在聆聽的連接埠\ngenericS3BucketAccessKeyId=存取金鑰 ID\ngenericS3BucketAccessKeyIdDescription=IAM 使用者存取金鑰 ID\ngenericS3BucketSecretAccessKey=秘密存取金鑰\ngenericS3BucketSecretAccessKeyDescription=相關的秘密存取金鑰\ngenericS3BucketHttps=啟用 HTTPS\ngenericS3BucketHttpsDescription=使用 HTTPS 連線至伺服器。某些供應商可能會要求使用 HTTPS\ntunnelled=隧道式\nawsInstallSync=設定同步\nawsInstallSyncDescription=將 AWS CLI 配置檔案同步至 git 保險庫\nawsInstallLocation=使用者資料位置\nawsInstallLocationDescription=AWS CLI 配置檔案的來源路徑\ninstanceActions=實例動作\nopenSplit=在分割終端開啟\nterminalSplitStrategy=分割視圖方向\nterminalSplitStrategyDescription=控制在批次模式中使用分割視圖功能開啟相鄰的多個終端會話時，如何分割終端標籤。\nterminalSplitStrategyDisabledDescription=控制在批次模式中使用分割視圖功能開啟相鄰的多個終端會話時，如何分割終端標籤。\\n\\n您目前的終端組態不支援分割檢視。\nhorizontal=橫向\nvertical=縱向\nbalanced=平衡\nclose=關閉\nhelpButton=$TOPIC$ 文件連結\nquickAccess=快速存取\ntoggleEnabled=切換狀態\ncurrentPath=目前路徑\ndirectoryContents=目錄內容\ndirectoryOptions=目錄選項\nchooseConnectionType=選擇連線類型\nbatchMode=批次模式\ntoggleButton=切換按鈕\ntailscaleUseSsh=使用 tailscale SSH 授權\ntailscaleUseSshDescription=透過 tailscale SSH 伺服器本身登入，無需任何 SSH 認證\nportDescription=SSH 伺服器執行的連接埠\nloginAs=登入為\nsshGatewayType=閘道類型\nsshGatewayTypeDescription=是否透過隧道或 ProxyJump 選項與目標連線\ngatewayTunnel=閘道隧道\nproxyJump=代理跳轉\ncommandTypeAsyncBackground=在背景中執行脫離\ncommandTypeSyncBackground=在背景執行並等待完成\ncommandTypeTerminalBackground=在終端機中開啟\nasyncBackgroundCommand=背景指令\nsyncBackgroundCommand=封鎖背景指令\nterminalBackgroundCommand=終端命令\ntestingConnection=測試連接 ...\nopenManagementConsole=開放式管理主控台\nopenLxcTerminal=開啟 LXC 終端機\nopenContainerConsole=開啟序列控制台\nkeeper2fa=2FA 方法\nkeeper2faDescription=為您帳戶設定的主要雙因素驗證方法。如果您的 Keeper 帳戶需要雙因素認證才能存取密碼，請啟用此項。\nkeeperTotpDuration=自訂 2FA 碼持續時間\nkeeperTotpDurationDescription=覆寫 2FA 密碼有效期的預設持續時間。僅適用於組織政策允許變更持續時間的情況。\\n\\n可能的值有$VALUES$\nkeeperOtherAuth=其他 (RSA SecurID、Duo Security、Keeper DNA 等)\nextractReusableIdentities=擷取可重複使用的身分\nidentitiesAdded=新增的身分\nsyncMode=同步模式\nsyncModeDescription=控制同步變更的方式。\\n\\n即時模式會儘快推拔變更，啟動和退出模式會一次同步處理會話中的所有變更，而手動模式則只會在您啟動時同步處理。\ntoggleTerminalDock=切換終端機塢座\nscriptDirectory=目錄位置\nscriptDirectoryDescription=包含 shell 指令碼檔案的本機目錄\nscriptSourceUrl=儲存庫 URL\nscriptSourceUrlDescription=包含 shell 腳本檔案的遠端 git 儲存庫的 URL\nscriptCollectionSourceType=來源類型\nscriptCollectionSourceTypeDescription=應從何處載入 shell 腳本的來源類型\nscriptCollectionSourceEntry=來源項目\nscriptCollectionSourceEntryDescription=載入 shell 腳本的來源\ngitRepository=Git 儲存庫\nscriptCollectionSource.displayName=腳本來源\nscriptCollectionSource.displayDescription=自動從現有來源匯入 shell 腳本\ndirectorySource=目錄來源\ngitRepositorySource=Git 儲存庫來源\nrefreshSource=刷新來源\nscriptTextSourceUrl=腳本 URL\nscriptTextSourceUrlDescription=擷取腳本檔案的 URL\nscriptSourceType=腳本來源\nscriptSourceTypeDescription=腳本的來源\nscriptSourceTypeInPlace=就地腳本\nscriptSourceTypeUrl=外部 URL\nscriptSourceTypeSource=現有來源\nimportScripts=匯入腳本\nscriptsContained=$NUMBER$ 腳本\nscriptSourceCollectionImportTitle=從來源匯入腳本 ($SELECTED$/$COUNT$)\nnoScriptsFound=未找到腳本\ntunnel=隧道\nnotInitialized=未初始化\nselectCategory=選擇類別 ...\nscriptSourceName=腳本名稱\nscriptSourceNameDescription=原始碼中腳本的檔案名稱\nworkspaceRestartTitle=工作區就緒\nworkspaceRestartContent=新工作區的捷徑已在$PATH$ 建立。您可以導航到捷徑或立即重新啟動 XPipe，以自動開啟新工作區。\nbrowseShortcut=瀏覽檔案\nsyncModeInstant=即時同步\nsyncModeSession=啟動和退出時同步\nsyncModeManual=手動同步\npushChanges=推送變更\npullChanges=拉動變更\nsourcedFrom=來源於$SOURCE$\ninPlaceScript=就地腳本\ngeneric=通用\nsyncToPlainDirectory=同步至純目錄\nsyncToPlainDirectoryDescription=同步到本地目錄時，您可以將這個目錄當作另一個 git 倉庫，或者只當作一個普通目錄。如果啟用了普通目錄設定，該目錄就不會被初始化為 git 倉庫。\nopenSpiceSession=開啟 SPICE 會話\nterminalBehaviour=終端行為\nnoScanPossible=未找到支援的連線\nnetworkSwitchPorts=網路連接埠\nnswitchGroup.displayName=網路連接埠\nnswitchGroup.displayDescription=列出網路設備上的可用連接埠\nnswitchPort.displayName=網路連接埠\nnswitchPort.displayDescription=控制網路交換器裝置上的個別連接埠\nenablePort=啟用連接埠\nshutdownPort=關閉連接埠\nresetPort=重設連接埠\nuseSystemDefault=使用系統預設值\nportStatus=連接埠狀態\nclearCounters=清除計數器\nshowStatus=顯示狀態\nshowAllPorts=顯示所有連接埠\nactiveLicense=許可證\nactiveLicenseDescription=啟動 XPipe 授權金鑰\nauthenticatorApp=驗證器應用程式\nsecurityKey=安全金鑰\nmcpAdditionalContext=其他 MCP 上下文\nmcpAdditionalContextDescription=傳送給 MCP 用戶端的附加指示。使用此項可控制代理程式的行為，並為您的個別設定提供額外的上下文。\nmcpAdditionalContextSample=- 在未確認之前，請勿自動重新啟動任何服務和 daemons\\n- 設定網路介面時，請務必使用 192.168.1.1/24 作為閘道\nprefsRestartTitle=需要重新啟動\nprefsRestartContent=您變更的某些選項需要重新啟動應用程式才能套用。您現在要重新啟動XPipe嗎？\nbashShell=Bash shell\n"
  },
  {
    "path": "lang/texts/termiusSetup_da.md",
    "content": "# Termius setup\n\nFor at bruge Termius som din terminal kan du forbinde den til XPipe SSH-broen. Det kan ske automatisk, når den lokale ssh-nøgle til broen er blevet tilføjet til Termius.\n\nDet eneste, du skal gøre manuelt, er at tilføje den private nøglefil `%s` til Termius først."
  },
  {
    "path": "lang/texts/termiusSetup_de.md",
    "content": "# Termius setup\n\nUm Termius als Terminal zu verwenden, kannst du ihn mit der XPipe SSH-Bridge verbinden. Das kann automatisch funktionieren, sobald der lokale Bridge-Ssh-Schlüssel zu Termius hinzugefügt wurde.\n\nDas Einzige, was du manuell tun musst, ist, die private Schlüsseldatei `%s` zuerst zu Termius hinzuzufügen."
  },
  {
    "path": "lang/texts/termiusSetup_en.md",
    "content": "# Termius setup\n\nTo use Termius as your terminal, you can connect it to the XPipe SSH bridge. This can work automatically once the local bridge ssh key has been added to Termius.\n\nThe only thing you have to do manually is to add the private key file `%s` to Termius first:\n\n```\n%s\n```"
  },
  {
    "path": "lang/texts/termiusSetup_es.md",
    "content": "# Configuración de Termius\n\nPara utilizar Termius como terminal, puedes conectarlo al puente SSH de XPipe. Esto puede funcionar automáticamente una vez que se haya añadido a Termius la clave ssh del puente local.\n\nLo único que tienes que hacer manualmente es añadir primero el archivo de clave privada `%s` a Termius."
  },
  {
    "path": "lang/texts/termiusSetup_fr.md",
    "content": "# Installation de Termius\n\nPour utiliser Termius comme terminal, tu peux le connecter au pont SSH de XPipe. Cela peut fonctionner automatiquement une fois que la clé ssh du pont local a été ajoutée à Termius.\n\nLa seule chose que tu dois faire manuellement est d'ajouter le fichier de clé privée `%s` à Termius d'abord."
  },
  {
    "path": "lang/texts/termiusSetup_id.md",
    "content": "# Pengaturan Termius\n\nUntuk menggunakan Termius sebagai terminal, Anda dapat menghubungkannya ke jembatan SSH XPipe. Hal ini dapat bekerja secara otomatis setelah kunci ssh bridge lokal ditambahkan ke Termius.\n\nSatu-satunya hal yang harus Anda lakukan secara manual adalah menambahkan file kunci privat `%s` ke Termius terlebih dahulu:\n\n```\n%s\n```"
  },
  {
    "path": "lang/texts/termiusSetup_it.md",
    "content": "# Configurazione di Termius\n\nPer utilizzare Termius come terminale, puoi collegarlo al bridge SSH di XPipe. Questo può funzionare automaticamente una volta che la chiave ssh del bridge locale è stata aggiunta a Termius.\n\nL'unica cosa che devi fare manualmente è aggiungere il file della chiave privata `%s` a Termius."
  },
  {
    "path": "lang/texts/termiusSetup_ja.md",
    "content": "# テルミウスのセットアップ\n\nTermiusをターミナルとして使用するには、XPipe SSHブリッジに接続する。ローカルブリッジのsshキーがTermiusに追加されれば自動的に動作する。\n\n唯一手動で行う必要があるのは、最初に秘密鍵ファイル`%s`をTermiusに追加することだ。"
  },
  {
    "path": "lang/texts/termiusSetup_ko.md",
    "content": "# Termius 설정\n\nTermius를 터미널로 사용하려면 XPipe SSH 브리지에 연결하면 됩니다. 로컬 브리지 ssh 키가 Termius에 추가되면 자동으로 작동합니다.\n\n수동으로 해야 할 일은 개인 키 파일 `%s`를 Termius에 먼저 추가하는 것뿐입니다:\n\n```\n%s\n```"
  },
  {
    "path": "lang/texts/termiusSetup_nl.md",
    "content": "# Termius installatie\n\nOm Termius als terminal te gebruiken, kun je het verbinden met de XPipe SSH bridge. Dit kan automatisch werken zodra de lokale bridge ssh sleutel is toegevoegd aan Termius.\n\nHet enige dat je handmatig moet doen is eerst het private key bestand `%s` toevoegen aan Termius."
  },
  {
    "path": "lang/texts/termiusSetup_pl.md",
    "content": "# Konfiguracja Termius\n\nAby używać Termius jako terminala, możesz podłączyć go do mostka SSH XPipe. Może to działać automatycznie po dodaniu klucza lokalnego mostu ssh do Termius.\n\nJedyne, co musisz zrobić ręcznie, to najpierw dodać plik klucza prywatnego `%s` do Termius:\n\n```\n%s\n```"
  },
  {
    "path": "lang/texts/termiusSetup_pt.md",
    "content": "# Configuração do Termius\n\nPara usar o Termius como terminal, podes ligá-lo à bridge SSH XPipe. Isto pode funcionar automaticamente uma vez que a chave ssh da ponte local tenha sido adicionada ao Termius.\n\nA única coisa que tens de fazer manualmente é adicionar o ficheiro de chave privada `%s` ao Termius primeiro."
  },
  {
    "path": "lang/texts/termiusSetup_ru.md",
    "content": "# Настройка Термиуса\n\nЧтобы использовать Termius в качестве терминала, ты можешь подключить его к SSH-мосту XPipe. Это может работать автоматически, как только локальный ssh-ключ моста будет добавлен в Termius.\n\nЕдинственное, что тебе придется сделать вручную, - это сначала добавить в Termius файл закрытого ключа `%s`."
  },
  {
    "path": "lang/texts/termiusSetup_sv.md",
    "content": "# Termius setup\n\nFör att använda Termius som din terminal kan du ansluta den till XPipe SSH-bryggan. Detta kan fungera automatiskt när den lokala bryggans ssh-nyckel har lagts till i Termius.\n\nDet enda du behöver göra manuellt är att lägga till den privata nyckelfilen `%s` i Termius först:\n\n```\n%s\n```"
  },
  {
    "path": "lang/texts/termiusSetup_tr.md",
    "content": "# Termius kurulumu\n\nTermius'u terminaliniz olarak kullanmak için XPipe SSH köprüsüne bağlayabilirsiniz. Yerel köprü ssh anahtarı Termius'a eklendiğinde bu otomatik olarak çalışabilir.\n\nManuel olarak yapmanız gereken tek şey, önce `%s` özel anahtar dosyasını Termius'a eklemektir."
  },
  {
    "path": "lang/texts/termiusSetup_vi.md",
    "content": "# Cài đặt Termius\n\nĐể sử dụng Termius làm terminal, cậu có thể kết nối nó với XPipe SSH bridge. Quá trình này có thể diễn ra tự động sau khi khóa SSH của cầu nối cục bộ đã được thêm vào Termius.\n\nĐiều duy nhất bạn cần làm thủ công là thêm tệp khóa riêng tư &lt;y&gt;%s&lt;/y&gt; vào Termius trước tiên:\n\n&lt;z&gt;\n%s\n&lt;/z&gt;"
  },
  {
    "path": "lang/texts/termiusSetup_zh-Hans.md",
    "content": "# Termius 设置\n\n要将 Termius 作为终端使用，可以将其连接到 XPipe SSH 网桥。一旦本地网桥的 ssh 密钥添加到 Termius，它就会自动运行。\n\n唯一需要手动操作的是先将私钥文件 `%s` 添加到 Termius。"
  },
  {
    "path": "lang/texts/termiusSetup_zh-Hant.md",
    "content": "# Termius 設定\n\n若要使用 Termius 作為您的終端機，您可以將其連接到 XPipe SSH 橋接器。一旦本機橋接器的 ssh 金鑰已加入 Termius，此功能便會自動運作。\n\n您唯一需要手動做的，就是先將私密金鑰檔案 `%s` 加入 Termius：\n\n```\n%s\n```"
  },
  {
    "path": "lang/texts/xshellSetup_da.md",
    "content": "# Xshell setup\n\nFor at bruge Xshell som din terminal kan du forbinde den til XPipe SSH-broen. Det kan ske automatisk, når den lokale ssh-nøgle til broen er blevet føjet til Xshell med det korrekte navn.\n\nDet eneste, du skal gøre manuelt, er at tilføje den private nøglefil `%s` til Xshell med det faste navn `%s`."
  },
  {
    "path": "lang/texts/xshellSetup_de.md",
    "content": "# Xshell-Einrichtung\n\nUm Xshell als Terminal zu verwenden, kannst du es mit der XPipe SSH-Bridge verbinden. Das kann automatisch funktionieren, sobald der lokale Bridge-Ssh-Schlüssel mit dem richtigen Namen zu Xshell hinzugefügt wurde.\n\nDas Einzige, was du manuell tun musst, ist, die private Schlüsseldatei `%s` mit dem festen Namen `%s` zu Xshell hinzuzufügen."
  },
  {
    "path": "lang/texts/xshellSetup_en.md",
    "content": "# Xshell setup\n\nTo use Xshell as your terminal, you can connect it to the XPipe SSH bridge. This can work automatically once the local bridge ssh key has been added to Xshell with the correct name.\n\nThe only thing you have to do manually is to add the private key file `%s` to Xshell with the fixed name `%s`."
  },
  {
    "path": "lang/texts/xshellSetup_es.md",
    "content": "# Configuración de Xshell\n\nPara utilizar Xshell como terminal, puedes conectarlo al puente SSH de XPipe. Esto puede funcionar automáticamente una vez que la clave ssh del puente local se haya añadido a Xshell con el nombre correcto.\n\nLo único que tienes que hacer manualmente es añadir el archivo de clave privada `%s` a Xshell con el nombre fijo `%s`."
  },
  {
    "path": "lang/texts/xshellSetup_fr.md",
    "content": "# Configuration de Xshell\n\nPour utiliser Xshell comme terminal, tu peux le connecter au pont SSH de XPipe. Cela peut fonctionner automatiquement une fois que la clé ssh du pont local a été ajoutée à Xshell avec le nom correct.\n\nLa seule chose que tu dois faire manuellement est d'ajouter le fichier de clé privée `%s` à Xshell avec le nom fixe `%s`."
  },
  {
    "path": "lang/texts/xshellSetup_id.md",
    "content": "# Pengaturan Xshell\n\nUntuk menggunakan Xshell sebagai terminal, Anda dapat menghubungkannya ke jembatan SSH XPipe. Hal ini dapat bekerja secara otomatis setelah kunci ssh jembatan lokal ditambahkan ke Xshell dengan nama yang benar.\n\nSatu-satunya hal yang harus Anda lakukan secara manual adalah menambahkan berkas kunci pribadi `%s` ke Xshell dengan nama tetap `%s`."
  },
  {
    "path": "lang/texts/xshellSetup_it.md",
    "content": "# Configurazione di Xshell\n\nPer utilizzare Xshell come terminale, puoi collegarlo al bridge SSH di XPipe. Questo può funzionare automaticamente una volta che la chiave ssh del bridge locale è stata aggiunta a Xshell con il nome corretto.\n\nL'unica cosa che devi fare manualmente è aggiungere il file della chiave privata `%s` a Xshell con il nome fisso `%s`."
  },
  {
    "path": "lang/texts/xshellSetup_ja.md",
    "content": "# Xshellのセットアップ\n\nXshellをターミナルとして使用するには、XPipe SSHブリッジに接続する。ローカルブリッジのsshキーが正しい名前でXshellに追加されれば、自動的に動作する。\n\n手動で行う必要があるのは、秘密鍵ファイル`%s`を固定名`%s`でXshellに追加することだけだ。"
  },
  {
    "path": "lang/texts/xshellSetup_ko.md",
    "content": "# Xshell 설정\n\nXshell을 터미널로 사용하려면 XPipe SSH 브리지에 연결하면 됩니다. 로컬 브리지 ssh 키가 올바른 이름으로 Xshell에 추가되면 자동으로 작동합니다.\n\n수동으로 해야 할 일은 개인 키 파일 `%s`를 고정된 이름 `%s`로 Xshell에 추가하는 것뿐입니다."
  },
  {
    "path": "lang/texts/xshellSetup_nl.md",
    "content": "# Xshell instelling\n\nOm Xshell als terminal te gebruiken kun je het verbinden met de XPipe SSH bridge. Dit kan automatisch werken zodra de lokale bridge ssh key is toegevoegd aan Xshell met de juiste naam.\n\nHet enige dat je handmatig moet doen is het private key bestand `%s` toevoegen aan Xshell met de vaste naam `%s`."
  },
  {
    "path": "lang/texts/xshellSetup_pl.md",
    "content": "konfiguracja # Xshell\n\nAby używać Xshell jako terminala, możesz podłączyć go do mostka SSH XPipe. Może to działać automatycznie po dodaniu klucza lokalnego mostu ssh do Xshell z poprawną nazwą.\n\nJedyną rzeczą, którą musisz zrobić ręcznie, jest dodanie pliku klucza prywatnego `%s` do Xshell o stałej nazwie `%s`."
  },
  {
    "path": "lang/texts/xshellSetup_pt.md",
    "content": "# Configuração do Xshell\n\nPara utilizar o Xshell como o teu terminal, podes ligá-lo à ponte SSH XPipe. Isto pode funcionar automaticamente quando a chave ssh da ponte local tiver sido adicionada ao Xshell com o nome correto.\n\nA única coisa que tens de fazer manualmente é adicionar o ficheiro de chave privada `%s` ao Xshell com o nome fixo `%s`."
  },
  {
    "path": "lang/texts/xshellSetup_ru.md",
    "content": "# Xshell setup\n\nЧтобы использовать Xshell в качестве терминала, ты можешь подключить его к SSH-мосту XPipe. Это может работать автоматически, как только локальный ssh-ключ моста будет добавлен в Xshell с правильным именем.\n\nЕдинственное, что тебе придется сделать вручную, - это добавить файл закрытого ключа `%s` в Xshell с фиксированным именем `%s`."
  },
  {
    "path": "lang/texts/xshellSetup_sv.md",
    "content": "# Xshell setup\n\nFör att använda Xshell som din terminal kan du ansluta den till XPipe SSH-bryggan. Detta kan fungera automatiskt när den lokala bryggans ssh-nyckel har lagts till i Xshell med rätt namn.\n\nDet enda du behöver göra manuellt är att lägga till den privata nyckelfilen `%s` i Xshell med det fasta namnet `%s`."
  },
  {
    "path": "lang/texts/xshellSetup_tr.md",
    "content": "# Xshell kurulumu\n\nXshell'i terminaliniz olarak kullanmak için XPipe SSH köprüsüne bağlayabilirsiniz. Yerel köprü ssh anahtarı Xshell'e doğru isimle eklendiğinde bu otomatik olarak çalışabilir.\n\nManuel olarak yapmanız gereken tek şey `%s` özel anahtar dosyasını `%s` sabit adıyla Xshell'e eklemektir."
  },
  {
    "path": "lang/texts/xshellSetup_vi.md",
    "content": "# Cài đặt Xshell\n\nĐể sử dụng Xshell làm terminal, cậu có thể kết nối nó với XPipe SSH bridge. Quá trình này có thể diễn ra tự động sau khi khóa SSH của cầu nối cục bộ đã được thêm vào Xshell với tên chính xác.\n\nĐiều duy nhất bạn cần làm thủ công là thêm tệp khóa riêng tư &lt;y&gt;%s&lt;/y&gt; vào Xshell với tên cố định &lt;y&gt;%s&lt;/y&gt;."
  },
  {
    "path": "lang/texts/xshellSetup_zh-Hans.md",
    "content": "# Xshell 设置\n\n要将 Xshell 用作终端，可以将其连接到 XPipe SSH 网桥。一旦本地桥接器的 ssh 密钥以正确的名称添加到 Xshell，它就会自动运行。\n\n唯一需要手动操作的是将私钥文件 `%s` 添加到 Xshell，并使用固定名称 `%s`。"
  },
  {
    "path": "lang/texts/xshellSetup_zh-Hant.md",
    "content": "# Xshell 設定\n\n若要使用 Xshell 作為您的終端機，您可以將其連接到 XPipe SSH 橋接器。一旦本機橋接器的 ssh 金鑰以正確的名稱新增至 Xshell，即可自動運作。\n\n您唯一需要手動做的，就是將私密金鑰檔案 `%s` 以固定名稱 `%s` 加入 Xshell。"
  },
  {
    "path": "settings.gradle",
    "content": "rootProject.name = 'xpipe'\n\ninclude 'core'\ninclude 'beacon'\n\ninclude \"base\"\nproject(\":base\").projectDir = file(\"ext/base\")\n\nfor (def ext : file(\"ext\").list()) {\n    if (ext == 'base') {\n        continue\n    }\n\n    if (file(\"ext/$ext/build.gradle\").exists()) {\n\n        include \"$ext\"\n        project(\":$ext\").projectDir = file(\"ext/$ext\")\n    }\n}\n\ninclude 'app'\nif (file(\"cli\").exists()) {\n    include 'cli'\n}\ninclude 'dist'\n"
  },
  {
    "path": "version",
    "content": "21.6\n"
  }
]