Compare commits

..

77 commits

Author SHA1 Message Date
Christopher Schnick
81369aca04 Many small fixes 2023-01-20 00:36:42 +01:00
Christopher Schnick
f0b3d06ff4 Small fixes for mac 2023-01-15 11:16:10 +01:00
Christopher Schnick
36f915bdee Fix dependencies 2023-01-14 15:44:12 +01:00
Christopher Schnick
c23d099d53 Add host helper 2023-01-10 08:23:28 +01:00
Christopher Schnick
c4e0932d9e Rework shell choice comp 2023-01-10 08:23:19 +01:00
Christopher Schnick
b647c24821 Introduce API for daemon modes 2023-01-09 06:19:02 +01:00
Christopher Schnick
2f54a79407 Various small improvements 2023-01-07 22:19:54 +01:00
Christopher Schnick
a82717154d Add a few more exchanges and improve secret handling 2023-01-07 04:12:17 +01:00
Christopher Schnick
70568f7a9b Session fixes 2023-01-05 21:41:33 +01:00
Christopher Schnick
f0eccfb17b Implement ability to disable shell history 2023-01-04 20:33:54 +01:00
Christopher Schnick
5dcb994f9b Refactor shells 2023-01-04 05:23:57 +01:00
Christopher Schnick
ac16967efd Various small fixes 2023-01-01 16:38:52 +01:00
Christopher Schnick
23d0fcee57 Small fixes for error handling 2022-12-31 22:13:07 +01:00
Christopher Schnick
b0e9a96fc6 More cleanup 2022-12-30 12:54:53 +01:00
Christopher Schnick
990bea4f59 Cleanup 2022-12-30 12:54:40 +01:00
Christopher Schnick
8b5f2bf44e Implement module installations and rework some exchanges 2022-12-30 12:53:16 +01:00
Christopher Schnick
89da4a6864 Fix daemon external start up command on mac 2022-12-28 05:05:22 +01:00
Christopher Schnick
c17cbef675 More installation fixes 2022-12-28 03:57:56 +01:00
Christopher Schnick
c6c4b9cca9 Add more installation information 2022-12-28 03:30:06 +01:00
Christopher Schnick
699549eeaa Fix installation pass for mac 2022-12-27 14:56:51 +01:00
Christopher Schnick
f8a402a75b Fix NPE 2022-12-27 11:16:53 +01:00
Christopher Schnick
5a8a23222c Fixes for mac 2022-12-27 06:54:42 +01:00
Christopher Schnick
560efbd345 Various small fixes for beacon exchanges 2022-12-25 10:19:18 +01:00
Christopher Schnick
5ce39cf11e Bundle annotation indexer 2022-12-23 11:13:23 +01:00
Christopher Schnick
6527180385 Various small fixes 2022-12-23 10:29:08 +01:00
Christopher Schnick
d9329bdc11 Implement various fixes for sink drains 2022-12-22 03:57:15 +01:00
Christopher Schnick
5799ff9a3e Bump version 2022-12-19 21:48:32 +01:00
Christopher Schnick
0813fd1c92 Fix version typo 2022-12-19 21:48:17 +01:00
Christopher Schnick
6726adabc0 More fixes for beacon start up 2022-12-19 21:47:27 +01:00
Christopher Schnick
63c15d04f0 More beacon server startup fixes 2022-12-19 21:19:44 +01:00
Christopher Schnick
70557711f9 Fix beacon server start up failing 2022-12-19 19:55:51 +01:00
Christopher Schnick
9f347eac48 New release 2022-12-19 17:33:54 +01:00
Christopher Schnick
6024998514 Remove commons codec and commons compress from default commons dependencies 2022-12-19 15:58:01 +01:00
Christopher Schnick
d208710a15 Refactor 2022-12-19 00:31:50 +01:00
Christopher Schnick
9d6ee8e9ac Restructure gradle scripts 2022-12-18 18:04:51 +01:00
Christopher Schnick
5767b49655 Bump versions and rework registry query 2022-12-17 22:29:55 +01:00
Christopher Schnick
a44902edf5 Bump gradle and GraalVM versions 2022-12-17 21:37:13 +01:00
Christopher Schnick
ec10f3dcab Fix bug in beacon server launch (again) 2022-12-16 21:45:07 +01:00
Christopher Schnick
e0d9b7cff2 Fix wrong token variable 2022-12-16 20:00:09 +01:00
Christopher Schnick
9ebbaa5c53 Small fixes relating to shells 2022-12-16 19:49:40 +01:00
Christopher Schnick
4de2ff8a14 Fix beacon server start up on Linux again 2022-12-16 16:45:39 +01:00
Christopher Schnick
d949f661ce More fixes for beacon server start up 2022-12-16 13:53:26 +01:00
Christopher Schnick
5266749c09 Fix custom daemon start up 2022-12-16 13:09:07 +01:00
Christopher Schnick
707f51fb6a Small bug fixes 2022-12-15 21:52:29 +01:00
Christopher Schnick
d69a5face8 Small fixes for proxies 2022-12-15 20:08:51 +01:00
Christopher Schnick
05fa94bbc3 Make bash the default shell 2022-12-14 20:34:01 +01:00
Christopher Schnick
12bb95c0f4 Create ExecScriptHelper.java 2022-12-14 20:20:41 +01:00
Christopher Schnick
1c13402e79 Remove connection hashes and rename machines to shells 2022-12-14 17:40:16 +01:00
Christopher Schnick
eb4ec0ef2c Add utility method for mapped list bindings 2022-12-13 23:52:15 +01:00
Christopher Schnick
0b910aa580 Introduce connection hashes 2022-12-13 23:52:04 +01:00
Christopher Schnick
5ce83ddef2 More shell fixes 2022-12-11 11:19:12 +01:00
Christopher Schnick
ec62a12a20 Small shell fixes and cleanup 2022-12-11 03:45:51 +01:00
Christopher Schnick
f45123470b Fix for file echos on Linux 2022-12-11 00:49:32 +01:00
Christopher Schnick
b235b438dd Remove interactivity switch for sh 2022-12-10 05:20:04 +01:00
Christopher Schnick
2133e2322f More shell fixes 2022-12-10 04:33:40 +01:00
Christopher Schnick
453ccd5d14 Shell fix for sh 2022-12-10 00:32:33 +01:00
Christopher Schnick
bad71d3db6 Small fixes for shells 2022-12-10 00:16:05 +01:00
Christopher Schnick
f9b9808dd2 More fixes for shells and prefs 2022-12-09 01:44:12 +01:00
Christopher Schnick
37db891366 Fixes for shells 2022-12-07 21:55:58 +01:00
Christopher Schnick
c37ab93c13 Fixes for shells 2022-12-07 00:13:16 +01:00
Christopher Schnick
e9c9cd44cd Rework on shells 2022-12-05 00:50:58 +01:00
Christopher Schnick
e43417fa75 Small shell changes and move some comps to extension module 2022-12-03 15:39:11 +01:00
Christopher Schnick
e0339efb78 Small fixes and cleanup 2022-12-02 18:46:46 +01:00
Christopher Schnick
ee0da127e8 Update MessageExchanges.java 2022-12-01 16:28:33 +01:00
Christopher Schnick
42d8870dc7 Deobfuscator fix 2022-12-01 15:40:24 +01:00
Christopher Schnick
ac7b7a3dfc Cleanup 2022-12-01 11:58:06 +01:00
Christopher Schnick
1204b3bcc8 Deobfuscate received server error messages 2022-12-01 11:27:07 +01:00
Christopher Schnick
ad1405d1d8 Update workflow calling condition 2022-11-30 19:58:45 +01:00
Christopher Schnick
895e8bda84 New release 2022-11-30 19:53:56 +01:00
Christopher Schnick
c25119b0de Integrate fxcomps into extension module 2022-11-30 19:50:04 +01:00
Christopher Schnick
2cd847c2f8 Move fxcomps into this repository 2022-11-30 19:23:44 +01:00
Christopher Schnick
f55b733a4f More work on proxies 2022-11-30 19:04:35 +01:00
Christopher Schnick
6a96edcefb More work on proxies 2022-11-27 21:39:41 +01:00
Christopher Schnick
55e254a54c Refactor 2022-11-27 14:59:36 +01:00
Christopher Schnick
a351b92ac7 More fixes for proxies 2022-11-26 16:44:09 +01:00
Christopher Schnick
cc5bee4b8b Basic work for proxies 2022-11-26 12:32:09 +01:00
Christopher Schnick
090b3d311c Cleanup 2022-11-24 09:41:03 +01:00
4525 changed files with 20949 additions and 137584 deletions

8
.gitattributes vendored
View file

@ -1,7 +1,3 @@
* text=auto eol=lf
*.sh text eol=lf
*.bat text eol=crlf
* text=auto
*.png binary
*.xcf binary
*.properties linguist-generated
*.xcf binary

View file

@ -1,10 +0,0 @@
version: 2
updates:
- package-ecosystem: "gradle"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: github-actions
directory: /
schedule:
interval: "daily"

28
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,28 @@
name: Build
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-20.04
steps:
- name: Git checkout
uses: actions/checkout@v2
with:
submodules: 'true'
- name: Set up GraalVM
uses: graalvm/setup-graalvm@v1
with:
version: '22.3.0'
java-version: '19'
github-token: ${{ secrets.XPIPE_GITHUB_TOKEN }}
- name: Verify Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
- name: Execute build
run: ./gradlew clean build

39
.github/workflows/publish.yml vendored Normal file
View file

@ -0,0 +1,39 @@
name: Publish
on:
push:
branches:
- master
jobs:
publish:
runs-on: ubuntu-20.04
steps:
- name: Git checkout
uses: actions/checkout@v2
with:
submodules: 'true'
- name: Set up GraalVM
uses: graalvm/setup-graalvm@v1
with:
version: '22.3.0'
java-version: '19'
github-token: ${{ secrets.XPIPE_GITHUB_TOKEN }}
- name: Verify Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
- name: Publish
run: ./gradlew publish
env:
GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }}
GPG_KEY: ${{ secrets.GPG_KEY }}
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
- name: JReleaser
run: ./gradlew jreleaserRelease
env:
XPIPE_GITHUB_TOKEN: ${{ secrets.XPIPE_GITHUB_TOKEN }}
XPIPE_DISCORD_WEBHOOK: ${{ secrets.XPIPE_DISCORD_WEBHOOK }}

23
.gitignore vendored
View file

@ -1,24 +1,11 @@
.gradle/
build/
.idea/*
!.idea/codeStyles
!.idea/inspectionProfiles
lib/
.idea
local/
local_test/
local_stage/
dev.properties
extensions.txt
dev_storage
local/
local*/
local_*/
.vs
.vscode
obj
out
bin
.DS_Store
ComponentsGenerated.wxs
!dist/javafx/**/lib
!dist/javafx/**/bin
xcuserdata/
*.dylib
project.xcworkspace
bin

View file

@ -1,127 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement via [hello@xpipe.io](mailto:hello@xpipe.io).
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View file

@ -1,117 +0,0 @@
# Development
Any contribution is welcomed!
There are no real formal contribution guidelines right now, they will maybe come later.
## Repository Structure
- [core](core) - Shared core classes of the XPipe Java API, XPipe extensions, and the XPipe daemon implementation.
This mainly concerns API classes not a lot of implementation.
- [beacon](beacon) - The XPipe beacon component is responsible for handling all communications between the XPipe
daemon and the client applications, for example APIs and the CLI
- [app](app) - Contains the XPipe daemon implementation, the XPipe desktop application, and an
API to create all different kinds of extensions for the XPipe platform
- [dist](dist) - Tools to create a distributable package of XPipe
- [ext](ext) - Available XPipe extensions. Essentially every concrete feature implementation is implemented as an extension
## Development Setup
You need to have an up-to-date version of XPipe installed on your local system in order to properly
run XPipe in a development environment.
This is due to the fact that some components are only included in the release version and not in this repository.
XPipe is able to automatically detect your local installation and fetch the required
components from it when it is run in a development environment.
Note 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.
You should therefore always check out the matching version tag for your local repository and local XPipe installation.
You can find the available version tags at https://github.com/xpipe-io/xpipe/tags.
So for example if you currently have XPipe `13.0` installed, you should run `git reset --hard 13.0` first to properly compile against it.
You need to have JDK for Java 22 installed to compile the project.
If you are on Linux or macOS, you can easily accomplish that by running
```bash
curl -s "https://get.sdkman.io" | bash
. "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install java 22.0.2-graalce
sdk default java 22.0.2-graalce
```
.
On Windows, you have to manually install a JDK, e.g. from [Adoptium](https://adoptium.net/temurin/releases/?version=21).
You can configure a few development options in the file `app/dev.properties` which will be automatically generated when gradle is first run.
## Building and Running
You can use the gradle wrapper to build and run the project:
- `gradlew app:run` will run the desktop application. You can set various useful properties in `app/build.gradle`
- `gradlew clean dist` will create a distributable production version in `dist/build/dist/base`.
- `gradlew <project>:test` will run the tests of the specified project.
You are also able to properly debug the built production application through two different methods:
- The `dist/build/dist/base/app/scripts/xpiped_debug` script will launch the application in debug mode and with a console attached to it
- The `dist/build/dist/base/app/scripts/xpiped_debug_attach` script attaches a debugger with the help of [AttachMe](https://plugins.jetbrains.com/plugin/13263-attachme).
Just make sure that the attachme process is running within IntelliJ, and the debugger should launch automatically once you start up the application.
Note that when any unit test is run using a debugger, the XPipe daemon process that is started will also attempt
to connect to that debugger through [AttachMe](https://plugins.jetbrains.com/plugin/13263-attachme) as well.
## Modularity and IDEs
All XPipe components target [Java 22](https://openjdk.java.net/projects/jdk/22/) and make full use of the Java Module System (JPMS).
All components are modularized, including all their dependencies.
In 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).
Further, note that as this is a pretty complicated Java project that fully utilizes modularity,
many IDEs still have problems building this project properly.
For example, you can't build this project in eclipse or vscode as it will complain about missing modules.
The tested and recommended IDE is IntelliJ.
When setting up the project in IntelliJ, make sure that the correct JDK (Java 22)
is selected both for the project and for gradle itself.
## Contributing guide
Especially 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:
### Interacting via the HTTP API
You can create clients that communicate with the XPipe daemon via its HTTP API.
To get started, see the [OpenAPI spec](/openapi.yaml).
### Implementing support for a new editor
All 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.
### Implementing support for a new terminal
All 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.
### Adding more context menu actions in the file browser
In 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.
Once 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).
### Implementing custom actions for the connection hub
All 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).
### Adding more predefined scripts
You 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).
### Adding more system icons for system autodetection
You can register new system types [here](https://github.com/xpipe-io/xpipe/blob/master/app/src/main/java/io/xpipe/app/resources/SystemIcons.java) and add the respective icons [here](https://github.com/xpipe-io/xpipe/tree/master/app/src/main/resources/io/xpipe/app/resources/img/system).
### Adding more file icons for specific types
You 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).
The 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.
### Implementing something else
if 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.
### Adding translations
See the [translation guide](/lang) for details.

7
LICENSE Normal file
View file

@ -0,0 +1,7 @@
Copyright 2022 Christopher Schnick
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, 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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE 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.

View file

@ -1,203 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2023 Christopher Schnick
Copyright 2023 XPipe UG (haftungsbeschränkt)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

213
README.md
View file

@ -1,196 +1,37 @@
<p align="center">
<a href="https://xpipe.io" target="_blank" rel="noopener">
<img src="https://github.com/xpipe-io/.github/raw/main/img/banner.png" alt="XPipe Banner" />
</a>
</p>
[![Build Status](https://github.com/xpipe-io/xpipe_java/actions/workflows/build.yml/badge.svg)](https://github.com/xpipe-io/xpipe_java/actions/workflows/build.yml)
[![Publish Status](https://github.com/xpipe-io/xpipe_java/actions/workflows/publish.yml/badge.svg)](https://github.com/xpipe-io/xpipe_java/actions/workflows/publish.yml)
<h1></h1>
## X-Pipe Java
## About
The fundamental components of the [X-Pipe project](https://xpipe.io).
This repository contains the following four modules:
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 and does not require any setup on your remote systems. So if you normally use CLI tools like `ssh`, `docker`, `kubectl`, etc. to connect to your servers, you can just use XPipe on top of that.
- Core - Shared core classes of the X-Pipe Java API, X-Pipe extensions, and the X-Pipe daemon implementation
- API - The API that can be used to interact with X-Pipe from any JVM-based language10
- Beacon - The X-Pipe beacon component is responsible for handling all communications between the X-Pipe daemon
and the client applications, for example the various programming language APIs and the CLI
- Extension - An API to create all different kinds of extensions for the X-Pipe platform
XPipe fully integrates with your tools such as your favourite text/code editors, terminals, shells, command-line tools and more. 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.
## Installation / Usage
It currently supports:
The *core* and *extension* modules are used in X-Pipe extension development.
For setup instructions, see the [X-Pipe extension development](https://xpipe-io.readthedocs.io/en/latest/dev/extensions/index.html) section.
- [SSH](https://docs.xpipe.io/guide/ssh) connections, config files, and tunnels
- [Docker](https://docs.xpipe.io/guide/docker), [Podman](https://docs.xpipe.io/guide/podman), [LXD](https://docs.xpipe.io/guide/lxc), and [incus](https://docs.xpipe.io/guide/lxc) containers
- [Proxmox PVE](https://docs.xpipe.io/guide/proxmox) virtual machines and containers
- [Hyper-V](https://docs.xpipe.io/guide/hyperv), [KVM](https://docs.xpipe.io/guide/kvm), [VMware Player/Workstation/Fusion](https://docs.xpipe.io/guide/vmware) virtual machines
- [Kubernetes](https://docs.xpipe.io/guide/kubernetes) clusters, pods, and containers
- [Tailscale](https://docs.xpipe.io/guide/tailscale) and [Teleport](https://docs.xpipe.io/guide/teleport) connections
- Windows Subsystem for Linux, Cygwin, and MSYS2 environments
- Powershell Remote Sessions
- RDP and VNC connections
The *beacon* module handles all communication and serves as a
reference when implementing the communication of an API or program that interacts with the X-Pipe daemon.
## Connection hub
The *api* module serves as a reference implementation for other potential X-Pipe APIs
and can also be used to access X-Pipe functionalities from your Java programs.
For setup instructions, see the [X-Pipe Java API Usage](https://xpipe-io.readthedocs.io/en/latest/dev/api/java/index.html) section.
- Easily connect to and access all kinds of remote connections in one place
- Organize all your connections in hierarchical categories so you can keep an overview hundreds of connections
- Create specific login environments on any system to instantly jump into a properly set up environment for every use case
- Quickly perform various commonly used actions like starting/stopping containers, establishing tunnels, and more
- Create desktop shortcuts that automatically open remote connections in your terminal without having to open any GUI
## Development Notes
![Connections](https://github.com/xpipe-io/.github/raw/main/img/hub_shadow.png)
All X-Pipe components target [JDK 17](https://openjdk.java.net/projects/jdk/17/) and make full use of the Java Module System (JPMS).
All components are modularized, including all their dependencies.
In case a dependency is (sadly) not modularized yet, module information is manually added using [moditect](https://github.com/moditect/moditect-gradle-plugin).
These dependency generation rules are accumulated in the [X-Pipe dependencies](https://github.com/xpipe-io/xpipe_java_deps)
repository, which is shared between all components and integrated as a git submodule.
## Powerful file management
- Interact with the file system of any remote system using a workflow optimized for professionals
- Quickly open a terminal session into any directory in your favourite terminal emulator
- Utilize your entire arsenal of locally installed programs to open and edit remote files
- Dynamically elevate sessions with sudo when required without having to restart the session
- Seamlessly transfer files from and to your system desktop environment
- Work and perform transfers on multiple systems at the same time with the built-in tabbed multitasking
![Browser](https://github.com/xpipe-io/.github/raw/main/img/browser_shadow.png)
## Terminal launcher
- Boots you into a shell session in your favourite terminal with one click. Automatically fills password prompts and more
- Comes with support for all commonly used terminal emulators across all operating systems
- Supports opening custom terminal emulators as well via a custom command-line spec
- Works with all command shells such as bash, zsh, cmd, PowerShell, and more, locally and remote
- Connects to a system while the terminal is still starting up, allowing for faster connections than otherwise possible
![Terminal](https://github.com/xpipe-io/.github/raw/main/img/terminal_shadow.png)
<br>
<p align="center">
<img src="https://github.com/xpipe-io/.github/raw/main/img/terminal.gif" alt="Terminal launcher"/>
</p>
<br>
## Versatile scripting system
- Create reusable simple shell scripts, templates, and groups to run on connected remote systems
- Automatically make your scripts available in the PATH on any remote system without any setup
- Setup shell init environments for connections to fully customize your work environment for every purpose
- Open custom shells and custom remote connections by providing your own commands
![scripts](https://github.com/xpipe-io/.github/raw/main/img/scripts_shadow.png)
## Secure vault
- All data is stored exclusively on your local system in a cryptographically secure vault. You can also choose to increase security by using a custom master passphrase for further encryption
- XPipe is able to retrieve secrets automatically from your password manager via it's command-line interface.
- 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.
- Vault changes can be pushed and pulled from your own remote git repository by multiple team members across many systems
# Downloads
Note 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.
## Windows
Installers are the easiest way to get started and come with an optional automatic update functionality:
- [Windows .msi Installer (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-windows-x86_64.msi)
If you don't like installers, you can also use a portable version that is packaged as an archive:
- [Windows .zip Portable (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-windows-x86_64.zip)
Alternatively, you can also use the following package managers:
- [choco](https://community.chocolatey.org/packages/xpipe) to install it with `choco install xpipe`.
- [winget](https://github.com/microsoft/winget-cli) to install it with `winget install xpipe-io.xpipe --source winget`.
## macOS
Installers are the easiest way to get started and come with an optional automatic update functionality:
- [MacOS .pkg Installer (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-macos-x86_64.pkg)
- [MacOS .pkg Installer (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-macos-arm64.pkg)
If you don't like installers, you can also use a portable version that is packaged as an archive:
- [MacOS .dmg Portable (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-macos-x86_64.dmg)
- [MacOS .dmg Portable (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-macos-arm64.dmg)
Alternatively, you can also use [Homebrew](https://github.com/xpipe-io/homebrew-tap) to install XPipe with `brew install --cask xpipe-io/tap/xpipe`.
## Linux
You can install XPipe the fastest by pasting the installation command into your terminal. This will perform the setup automatically.
The script supports installation via `apt`, `dnf`, `yum`, `zypper`, `rpm`, and `pacman` on Linux:
```
bash <(curl -sL https://github.com/xpipe-io/xpipe/raw/master/get-xpipe.sh)
```
Of course, there are also other installation methods available.
### Debian-based distros
The following debian installers are available:
- [Linux .deb Installer (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-linux-x86_64.deb)
- [Linux .deb Installer (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-linux-arm64.deb)
Note that you should use apt to install the package with `sudo apt install <file>` as other package managers, for example dpkg,
are not able to resolve and install any dependency packages.
### RHEL-based distros
The rpm releases are signed with the GPG key https://xpipe.io/signatures/crschnick.asc.
You can import it via `rpm --import https://xpipe.io/signatures/crschnick.asc` to allow your rpm-based package manager to verify the release signature.
The following rpm installers are available:
- [Linux .rpm Installer (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-linux-x86_64.rpm)
- [Linux .rpm Installer (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-linux-arm64.rpm)
The same applies here, you should use a package manager that supports resolving and installing required dependencies if needed.
### Arch
There 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`.
### NixOS
There'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`. This one is however not always up to date.
There is also a custom repository that contains the latest up-to-date releases: https://github.com/xpipe-io/nixpkg.
You can install XPipe by following the instructions in the linked repository.
### Portable
In case you prefer to use an archive version that you can extract anywhere, you can use these:
- [Linux .tar.gz Portable (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-linux-x86_64.tar.gz)
- [Linux .tar.gz Portable (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-linux-arm64.tar.gz)
Alternatively, there are also AppImages available:
- [Linux .AppImage Portable (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-linux-x86_64.AppImage)
- [Linux .AppImage Portable (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-linux-arm64.AppImage)
Note that the portable version assumes that you have some basic packages for graphical systems already installed
as it is not a perfect standalone version. It should however run on most systems.
## Docker container
XPipe 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.
Since 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.
# Further information
## Open source model
XPipe 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.
The distributed XPipe application consists out of two parts:
- The open-source core that you can find this repository. It is licensed under the [Apache License 2.0](/LICENSE.md).
- The closed-source extensions, mostly for homelab/professional plan features, which are not included in this repository
Additional features are available in the homelab/professional plan . For more details see https://xpipe.io/pricing.
If your enterprise puts great emphasis on having access to the full source code, there are also full source-available enterprise options available.
## Documentation
You can find the documentation at https://docs.xpipe.io.
## Discord
[![Discord](https://discordapp.com/api/guilds/979695018782646285/widget.png?style=banner2)](https://discord.gg/8y89vS8cRb)
Some unit tests depend on a connection to an X-Pipe daemon to properly function.
To launch the installed daemon, it is required that you either have X-Pipe
installed or have set the `XPIPE_HOME` environment variable in case you are using a portable version.

View file

@ -1,7 +0,0 @@
# Security
Due to its nature, XPipe has to handle a lot of sensitive information. Therefore, the security, integrity, and privacy of your data has topmost priority.
More information about the security approach of the XPipe application can be found on the documentation website at https://docs.xpipe.io/reference/security.
You 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.

20
api/README.md Normal file
View file

@ -0,0 +1,20 @@
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.xpipe/xpipe-api/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.xpipe/xpipe-api)
[![javadoc](https://javadoc.io/badge2/io.xpipe/xpipe-api/javadoc.svg)](https://javadoc.io/doc/io.xpipe/xpipe-api)
## X-Pipe Java API
The X-Pipe API for Java allows you to use most of the X-Pipe functionality from Java applications:
- Create data stores and data sources
- Query and work with the contents of data sources
- Write data to data sources
## Setup
Either install the [maven dependency](https://maven-badges.herokuapp.com/maven-central/io.xpipe/xpipe-api) from Maven Central
using your favourite build tool or alternatively download the `xpipe-api.jar`, `xpipe-core.jar`, and `xpipe-beacon.jar`
from the [releases page](https://github.com/xpipe-io/xpipe_java/releases/latest) and add them to the classpath.
## Usage
See [the API documentation](https://xpipe-io.readthedocs.io/en/latest/dev/api/java/index.html).

38
api/build.gradle Normal file
View file

@ -0,0 +1,38 @@
plugins {
id 'java-library'
id 'maven-publish'
id 'signing'
id "org.moditect.gradleplugin" version "1.0.0-rc3"
}
apply from: "$projectDir/../gradle_scripts/java.gradle"
apply from: "$projectDir/../gradle_scripts/junit.gradle"
System.setProperty('excludeExtensionLibrary', 'true')
apply from: "$projectDir/../gradle_scripts/extension_test.gradle"
version = file('../misc/version').text
group = 'io.xpipe'
archivesBaseName = 'xpipe-api'
repositories {
mavenCentral()
}
test {
enabled = true
}
dependencies {
api project(':core')
implementation project(':beacon')
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.13.0"
}
configurations {
testImplementation.extendsFrom(dep)
}
apply from: 'publish.gradle'
apply from: "$projectDir/../gradle_scripts/publish-base.gradle"

40
api/publish.gradle Normal file
View file

@ -0,0 +1,40 @@
publishing {
publications {
mavenJava(MavenPublication) {
artifactId = project.archivesBaseName
from components.java
pom.withXml {
def pomNode = asNode()
pomNode.dependencies.'*'.findAll().each() {
it.scope*.value = 'compile'
}
}
pom {
name = 'X-Pipe Java API'
description = 'Contains everything necessary to interact with X-Pipe from Java applications.'
url = 'https://github.com/xpipe-io/xpipe_java/api'
licenses {
license {
name = 'The MIT License (MIT)'
url = 'https://github.com/xpipe-io/xpipe_java/LICENSE.md'
}
}
developers {
developer {
id = 'crschnick'
name = 'Christopher Schnick'
email = 'crschnick@xpipe.io'
}
}
scm {
connection = 'scm:git:git://github.com/xpipe-io/xpipe_java.git'
developerConnection = 'scm:git:ssh://github.com/xpipe-io/xpipe_java.git'
url = 'https://github.com/xpipe-io/xpipe_java'
}
}
}
}
}

View file

@ -0,0 +1,12 @@
package io.xpipe.api;
import java.io.InputStream;
public interface DataRaw extends DataSource {
InputStream open();
byte[] readAll();
byte[] read(int maxBytes);
}

View file

@ -0,0 +1,229 @@
package io.xpipe.api;
import io.xpipe.api.impl.DataSourceImpl;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceReference;
import io.xpipe.core.source.DataSourceType;
import io.xpipe.core.store.DataStore;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* Represents a reference to a data source that is managed by X-Pipe.
* <p>
* The actual data is only queried when required and is not cached.
* Therefore, the queried data is always up-to-date at the point of calling a method that queries the data.
* <p>
* As soon a data source reference is created, the data source is locked
* within X-Pipe to prevent concurrent modification and the problems that can arise from it.
* By default, the lock is held until the calling program terminates and prevents
* other applications from modifying the data source in any way.
* To unlock the data source earlier, you can make use the {@link #unlock()} method.
*/
public interface DataSource {
/**
* NOT YET IMPLEMENTED!
* <p>
* Creates a new supplier data source that will be interpreted as the generated data source.
* In case this program should be a data source generator, this method has to be called at
* least once to register that it actually generates a data source.
* <p>
* All content that is written to this data source until the generator program terminates is
* will be available later on when the data source is used as a supplier later on.
* <p>
* In case this method is called multiple times, the same data source is returned.
*
* @return the generator data source
*/
static DataSource drain() {
return null;
}
/**
* NOT YET IMPLEMENTED!
* <p>
* Creates a data source sink that will block with any read operations
* until an external data producer routes the output into this sink.
*/
static DataSource sink() {
return null;
}
/**
* Wrapper for {@link #get(DataSourceReference)}.
*
* @throws IllegalArgumentException if {@code id} is not a valid data source id
*/
static DataSource getById(String id) {
return get(DataSourceReference.id(id));
}
/**
* Wrapper for {@link #get(DataSourceReference)} using the latest reference.
*/
static DataSource getLatest() {
return get(DataSourceReference.latest());
}
/**
* Wrapper for {@link #get(DataSourceReference)} using a name reference.
*/
static DataSource getByName(String name) {
return get(DataSourceReference.name(name));
}
/**
* Retrieves the data source for a given reference.
*
* @param ref the data source reference
*/
static DataSource get(DataSourceReference ref) {
return DataSourceImpl.get(ref);
}
/**
* Releases the lock held by this program for this data source such
* that other applications can modify the data source again.
*/
static void unlock() {
throw new UnsupportedOperationException();
}
/**
* Wrapper for {@link #create(DataSourceId, String, InputStream)} that creates an anonymous data source.
*/
public static DataSource createAnonymous(String type, Path path) {
return create(null, type, path);
}
/**
* Wrapper for {@link #create(DataSourceId, String, InputStream)}.
*/
public static DataSource create(DataSourceId id, String type, Path path) {
try (var in = Files.newInputStream(path)) {
return create(id, type, in);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Wrapper for {@link #create(DataSourceId, String, InputStream)} that creates an anonymous data source.
*/
public static DataSource createAnonymous(String type, URL url) {
return create(null, type, url);
}
/**
* Wrapper for {@link #create(DataSourceId, String, InputStream)}.
*/
public static DataSource create(DataSourceId id, String type, URL url) {
try (var in = url.openStream()) {
return create(id, type, in);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Wrapper for {@link #create(DataSourceId, String, InputStream)} that creates an anonymous data source.
*/
public static DataSource createAnonymous(String type, InputStream in) {
return create(null, type, in);
}
/**
* Creates a new data source from an input stream.
*
* @param id the data source id
* @param type the data source type
* @param in the input stream to read
* @return a {@link DataSource} instances that can be used to access the underlying data
*/
public static DataSource create(DataSourceId id, String type, InputStream in) {
return DataSourceImpl.create(id, type, in);
}
/**
* Creates a new data source from an input stream.
*
* @param id the data source id
* @return a {@link DataSource} instances that can be used to access the underlying data
*/
public static DataSource create(DataSourceId id, io.xpipe.core.source.DataSource<?> source) {
return DataSourceImpl.create(id, source);
}
/**
* Creates a new data source from an input stream.
* 1
*
* @param id the data source id
* @param type the data source type
* @param in the data store to add
* @return a {@link DataSource} instances that can be used to access the underlying data
*/
public static DataSource create(DataSourceId id, String type, DataStore in) {
return DataSourceImpl.create(id, type, in);
}
void forwardTo(DataSource target);
void appendTo(DataSource target);
public io.xpipe.core.source.DataSource<?> getInternalSource();
/**
* Returns the id of this data source.
*/
DataSourceId getId();
/**
* Returns the type of this data source.
*/
DataSourceType getType();
DataSourceConfig getConfig();
/**
* Attempts to cast this object to a {@link DataTable}.
*
* @throws UnsupportedOperationException if the data source is not a table
*/
default DataTable asTable() {
throw new UnsupportedOperationException("Data source is not a table");
}
/**
* Attempts to cast this object to a {@link DataStructure}.
*
* @throws UnsupportedOperationException if the data source is not a structure
*/
default DataStructure asStructure() {
throw new UnsupportedOperationException("Data source is not a structure");
}
/**
* Attempts to cast this object to a {@link DataText}.
*
* @throws UnsupportedOperationException if the data source is not a text
*/
default DataText asText() {
throw new UnsupportedOperationException("Data source is not a text");
}
/**
* Attempts to cast this object to a {@link DataRaw}.
*
* @throws UnsupportedOperationException if the data source is not raw
*/
default DataRaw asRaw() {
throw new UnsupportedOperationException("Data source is not raw");
}
}

View file

@ -0,0 +1,32 @@
package io.xpipe.api;
import java.util.Map;
/**
* Represents the current configuration of a data source.
*/
public final class DataSourceConfig {
/**
* The data source provider id.
*/
private final String provider;
/**
* The set configuration parameters.
*/
private final Map<String, String> configInstance;
public DataSourceConfig(String provider, Map<String, String> configInstance) {
this.provider = provider;
this.configInstance = configInstance;
}
public String getProvider() {
return provider;
}
public Map<String, String> getConfig() {
return configInstance;
}
}

View file

@ -0,0 +1,23 @@
package io.xpipe.api;
import io.xpipe.api.connector.XPipeApiConnection;
import io.xpipe.beacon.exchange.cli.StoreAddExchange;
import io.xpipe.beacon.util.QuietDialogHandler;
import io.xpipe.core.store.DataStore;
import java.util.Map;
public class DataStores {
public static void addNamedStore(DataStore store, String name) {
XPipeApiConnection.execute(con -> {
var req = StoreAddExchange.Request.builder()
.storeInput(store)
.name(name)
.build();
StoreAddExchange.Response res = con.performSimpleExchange(req);
new QuietDialogHandler(res.getConfig(), con, Map.of()).handle();
});
}
}

View file

@ -0,0 +1,7 @@
package io.xpipe.api;
import io.xpipe.core.data.node.DataStructureNode;
public interface DataStructure extends DataSource {
DataStructureNode read();
}

View file

@ -0,0 +1,26 @@
package io.xpipe.api;
import io.xpipe.core.data.node.ArrayNode;
import io.xpipe.core.data.node.TupleNode;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
public interface DataTable extends Iterable<TupleNode>, DataSource {
Stream<TupleNode> stream();
ArrayNode readAll();
ArrayNode read(int maxRows);
default int countAndDiscard() {
AtomicInteger count = new AtomicInteger();
try (var stream = stream()) {
stream.forEach(dataStructureNodes -> {
count.getAndIncrement();
});
}
return count.get();
}
}

View file

@ -0,0 +1,52 @@
package io.xpipe.api;
import io.xpipe.api.impl.DataTableAccumulatorImpl;
import io.xpipe.core.data.node.DataStructureNode;
import io.xpipe.core.data.node.DataStructureNodeAcceptor;
import io.xpipe.core.data.type.TupleType;
import io.xpipe.core.source.DataSourceId;
/**
* An accumulator for table data.
* <p>
* This class can be used to construct new table data sources by
* accumulating the rows using {@link #add(DataStructureNode)} or {@link #acceptor()} and then calling
* {@link #finish(DataSourceId)} to complete the construction process and create a new data source.
*/
public interface DataTableAccumulator {
public static DataTableAccumulator create(TupleType type) {
return new DataTableAccumulatorImpl(type);
}
/**
* Wrapper for {@link #finish(DataSourceId)}.
*/
default DataTable finish(String id) {
return finish(DataSourceId.fromString(id));
}
/**
* Finishes the construction process and returns the data source reference.
*
* @param id the data source id to assign
*/
DataTable finish(DataSourceId id);
/**
* Adds a row to the table.
*
* @param row the row to add
*/
void add(DataStructureNode row);
/**
* Creates a tuple acceptor that adds all accepted tuples to the table.
*/
DataStructureNodeAcceptor<DataStructureNode> acceptor();
/**
* Returns the current amount of rows added to the table.
*/
int getCurrentRows();
}

View file

@ -0,0 +1,17 @@
package io.xpipe.api;
import java.util.List;
import java.util.stream.Stream;
public interface DataText extends DataSource {
List<String> readAllLines();
List<String> readLines(int maxLines);
Stream<String> lines();
String readAll();
String read(int maxCharacters);
}

View file

@ -0,0 +1,151 @@
package io.xpipe.api.connector;
import io.xpipe.beacon.BeaconClient;
import io.xpipe.beacon.BeaconConnection;
import io.xpipe.beacon.BeaconException;
import io.xpipe.beacon.BeaconServer;
import io.xpipe.beacon.exchange.cli.DialogExchange;
import io.xpipe.core.dialog.DialogReference;
import io.xpipe.core.util.XPipeDaemonMode;
import io.xpipe.core.util.XPipeInstallation;
import java.util.Optional;
public final class XPipeApiConnection extends BeaconConnection {
private XPipeApiConnection() {
}
public static XPipeApiConnection open() {
var con = new XPipeApiConnection();
con.constructSocket();
return con;
}
public static void finishDialog(DialogReference reference) {
try (var con = new XPipeApiConnection()) {
con.constructSocket();
var element = reference.getStart();
while (true) {
if (element != null && element.requiresExplicitUserInput()) {
throw new IllegalStateException();
}
DialogExchange.Response response = con.performSimpleExchange(DialogExchange.Request.builder()
.dialogKey(reference.getDialogId())
.build());
element = response.getElement();
if (response.getElement() == null) {
break;
}
}
} catch (BeaconException e) {
throw e;
} catch (Exception e) {
throw new BeaconException(e);
}
}
public static void execute(Handler handler) {
try (var con = new XPipeApiConnection()) {
con.constructSocket();
handler.handle(con);
} catch (BeaconException e) {
throw e;
} catch (Exception e) {
throw new BeaconException(e);
}
}
public static <T> T execute(Mapper<T> mapper) {
try (var con = new XPipeApiConnection()) {
con.constructSocket();
return mapper.handle(con);
} catch (BeaconException e) {
throw e;
} catch (Exception e) {
throw new BeaconException(e);
}
}
public static Optional<BeaconClient> waitForStartup(Process process) {
for (int i = 0; i < 160; i++) {
if (process != null && !process.isAlive() && process.exitValue() != 0) {
return Optional.empty();
}
try {
Thread.sleep(500);
} catch (InterruptedException ignored) {
}
var s = BeaconClient.tryConnect(BeaconClient.ApiClientInformation.builder()
.version("?")
.language("Java")
.build());
if (s.isPresent()) {
return s;
}
}
return Optional.empty();
}
public static void waitForShutdown() {
for (int i = 0; i < 40; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException ignored) {
}
var r = BeaconServer.isRunning();
if (!r) {
return;
}
}
}
@Override
protected void constructSocket() {
if (!BeaconServer.isRunning()) {
try {
start();
} catch (Exception ex) {
throw new BeaconException("Unable to start xpipe daemon", ex);
}
var r = waitForStartup(null);
if (r.isEmpty()) {
throw new BeaconException("Wait for xpipe daemon timed out");
} else {
beaconClient = r.get();
return;
}
}
try {
beaconClient = BeaconClient.connect(BeaconClient.ApiClientInformation.builder()
.version("?")
.language("Java")
.build());
} catch (Exception ex) {
throw new BeaconException("Unable to connect to running xpipe daemon", ex);
}
}
private void start() throws Exception {
var installation = XPipeInstallation.getLocalDefaultInstallationBasePath(true);
BeaconServer.start(installation, XPipeDaemonMode.BACKGROUND);
}
@FunctionalInterface
public static interface Handler {
void handle(BeaconConnection con) throws Exception;
}
@FunctionalInterface
public static interface Mapper<T> {
T handle(BeaconConnection con) throws Exception;
}
}

View file

@ -0,0 +1,44 @@
package io.xpipe.api.impl;
import io.xpipe.api.DataRaw;
import io.xpipe.api.DataSourceConfig;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceType;
import java.io.InputStream;
public class DataRawImpl extends DataSourceImpl implements DataRaw {
public DataRawImpl(
DataSourceId sourceId,
DataSourceConfig sourceConfig,
io.xpipe.core.source.DataSource<?> internalSource
) {
super(sourceId, sourceConfig, internalSource);
}
@Override
public InputStream open() {
return null;
}
@Override
public byte[] readAll() {
return new byte[0];
}
@Override
public byte[] read(int maxBytes) {
return new byte[0];
}
@Override
public DataSourceType getType() {
return DataSourceType.RAW;
}
@Override
public DataRaw asRaw() {
return this;
}
}

View file

@ -0,0 +1,164 @@
package io.xpipe.api.impl;
import io.xpipe.api.DataSource;
import io.xpipe.api.DataSourceConfig;
import io.xpipe.api.connector.XPipeApiConnection;
import io.xpipe.beacon.exchange.*;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceReference;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.StreamDataStore;
import java.io.InputStream;
public abstract class DataSourceImpl implements DataSource {
private final DataSourceId sourceId;
private final DataSourceConfig config;
private final io.xpipe.core.source.DataSource<?> internalSource;
public DataSourceImpl(
DataSourceId sourceId, DataSourceConfig config, io.xpipe.core.source.DataSource<?> internalSource
) {
this.sourceId = sourceId;
this.config = config;
this.internalSource = internalSource;
}
public static DataSource get(DataSourceReference ds) {
return XPipeApiConnection.execute(con -> {
var req = QueryDataSourceExchange.Request.builder().ref(ds).build();
QueryDataSourceExchange.Response res = con.performSimpleExchange(req);
var config = new DataSourceConfig(res.getProvider(), res.getConfig());
return switch (res.getType()) {
case TABLE -> {
yield new DataTableImpl(res.getId(), config, res.getInternalSource());
}
case STRUCTURE -> {
yield new DataStructureImpl(res.getId(), config, res.getInternalSource());
}
case TEXT -> {
yield new DataTextImpl(res.getId(), config, res.getInternalSource());
}
case RAW -> {
yield new DataRawImpl(res.getId(), config, res.getInternalSource());
}
case COLLECTION -> throw new UnsupportedOperationException("Unimplemented case: " + res.getType());
default -> throw new IllegalArgumentException("Unexpected value: " + res.getType());
};
});
}
public static DataSource create(DataSourceId id, io.xpipe.core.source.DataSource<?> source) {
var startReq =
AddSourceExchange.Request.builder().source(source).target(id).build();
var returnedId = XPipeApiConnection.execute(con -> {
AddSourceExchange.Response r = con.performSimpleExchange(startReq);
return r.getId();
});
var ref = DataSourceReference.id(returnedId);
return get(ref);
}
public static DataSource create(DataSourceId id, String type, DataStore store) {
if (store instanceof StreamDataStore s && s.isContentExclusivelyAccessible()) {
store = XPipeApiConnection.execute(con -> {
var internal = con.createInternalStreamStore();
var req = WriteStreamExchange.Request.builder()
.name(internal.getUuid().toString())
.build();
con.performOutputExchange(req, out -> {
try (InputStream inputStream = s.openInput()) {
inputStream.transferTo(out);
}
});
return internal;
});
}
var startReq = ReadExchange.Request.builder()
.provider(type)
.store(store)
.target(id)
.configureAll(false)
.build();
var startRes = XPipeApiConnection.execute(con -> {
ReadExchange.Response r = con.performSimpleExchange(startReq);
return r;
});
var configInstance = startRes.getConfig();
XPipeApiConnection.finishDialog(configInstance);
var ref = id != null ? DataSourceReference.id(id) : DataSourceReference.latest();
return get(ref);
}
public static DataSource create(DataSourceId id, String type, InputStream in) {
var store = XPipeApiConnection.execute(con -> {
var internal = con.createInternalStreamStore();
var req = WriteStreamExchange.Request.builder()
.name(internal.getUuid().toString())
.build();
con.performOutputExchange(req, out -> {
in.transferTo(out);
});
return internal;
});
var startReq = ReadExchange.Request.builder()
.provider(type)
.store(store)
.target(id)
.configureAll(false)
.build();
var startRes = XPipeApiConnection.execute(con -> {
ReadExchange.Response r = con.performSimpleExchange(startReq);
return r;
});
var configInstance = startRes.getConfig();
XPipeApiConnection.finishDialog(configInstance);
var ref = id != null ? DataSourceReference.id(id) : DataSourceReference.latest();
return get(ref);
}
@Override
public void forwardTo(DataSource target) {
XPipeApiConnection.execute(con -> {
var req = ForwardExchange.Request.builder()
.source(DataSourceReference.id(sourceId))
.target(DataSourceReference.id(target.getId()))
.build();
ForwardExchange.Response res = con.performSimpleExchange(req);
});
}
@Override
public void appendTo(DataSource target) {
XPipeApiConnection.execute(con -> {
var req = ForwardExchange.Request.builder()
.source(DataSourceReference.id(sourceId))
.target(DataSourceReference.id(target.getId()))
.append(true)
.build();
ForwardExchange.Response res = con.performSimpleExchange(req);
});
}
public io.xpipe.core.source.DataSource<?> getInternalSource() {
return internalSource;
}
@Override
public DataSourceId getId() {
return sourceId;
}
@Override
public DataSourceConfig getConfig() {
return config;
}
}

View file

@ -0,0 +1,33 @@
package io.xpipe.api.impl;
import io.xpipe.api.DataSourceConfig;
import io.xpipe.api.DataStructure;
import io.xpipe.core.data.node.DataStructureNode;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceType;
public class DataStructureImpl extends DataSourceImpl implements DataStructure {
DataStructureImpl(
DataSourceId sourceId,
DataSourceConfig sourceConfig,
io.xpipe.core.source.DataSource<?> internalSource
) {
super(sourceId, sourceConfig, internalSource);
}
@Override
public DataSourceType getType() {
return DataSourceType.STRUCTURE;
}
@Override
public DataStructure asStructure() {
return this;
}
@Override
public DataStructureNode read() {
return null;
}
}

View file

@ -0,0 +1,110 @@
package io.xpipe.api.impl;
import io.xpipe.api.DataSource;
import io.xpipe.api.DataTable;
import io.xpipe.api.DataTableAccumulator;
import io.xpipe.api.connector.XPipeApiConnection;
import io.xpipe.api.util.TypeDescriptor;
import io.xpipe.beacon.BeaconException;
import io.xpipe.beacon.exchange.ReadExchange;
import io.xpipe.beacon.exchange.WriteStreamExchange;
import io.xpipe.beacon.exchange.cli.StoreAddExchange;
import io.xpipe.beacon.util.QuietDialogHandler;
import io.xpipe.core.data.node.DataStructureNode;
import io.xpipe.core.data.node.DataStructureNodeAcceptor;
import io.xpipe.core.data.node.TupleNode;
import io.xpipe.core.data.type.TupleType;
import io.xpipe.core.data.typed.TypedDataStreamWriter;
import io.xpipe.core.impl.InternalStreamStore;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceReference;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
public class DataTableAccumulatorImpl implements DataTableAccumulator {
private final XPipeApiConnection connection;
private final TupleType type;
private int rows;
private InternalStreamStore store;
private TupleType writtenDescriptor;
private OutputStream bodyOutput;
public DataTableAccumulatorImpl(TupleType type) {
this.type = type;
connection = XPipeApiConnection.open();
store = new InternalStreamStore();
var addReq = StoreAddExchange.Request.builder().storeInput(store).name(store.getUuid().toString()).build();
StoreAddExchange.Response addRes = connection.performSimpleExchange(addReq);
QuietDialogHandler.handle(addRes.getConfig(), connection);
connection.sendRequest(WriteStreamExchange.Request.builder().name(store.getUuid().toString()).build());
bodyOutput = connection.sendBody();
}
@Override
public synchronized DataTable finish(DataSourceId id) {
try {
bodyOutput.close();
} catch (IOException e) {
throw new BeaconException(e);
}
WriteStreamExchange.Response res = connection.receiveResponse();
connection.close();
var req = ReadExchange.Request.builder()
.target(id)
.store(store)
.provider("xpbt")
.configureAll(false)
.build();
ReadExchange.Response response = XPipeApiConnection.execute(con -> {
return con.performSimpleExchange(req);
});
var configInstance = response.getConfig();
XPipeApiConnection.finishDialog(configInstance);
return DataSource.get(DataSourceReference.id(id)).asTable();
}
private void writeDescriptor() {
if (writtenDescriptor != null) {
return;
}
writtenDescriptor = TupleType.tableType(type.getNames());
connection.withOutputStream(out -> {
out.write((TypeDescriptor.create(type.getNames())).getBytes(StandardCharsets.UTF_8));
});
}
@Override
public synchronized void add(DataStructureNode row) {
TupleNode toUse = type.matches(row)
? row.asTuple()
: type.convert(row).orElseThrow().asTuple();
connection.withOutputStream(out -> {
writeDescriptor();
TypedDataStreamWriter.writeStructure(out, toUse, writtenDescriptor);
rows++;
});
}
@Override
public synchronized DataStructureNodeAcceptor<DataStructureNode> acceptor() {
return node -> {
add(node);
return true;
};
}
@Override
public synchronized int getCurrentRows() {
return rows;
}
}

View file

@ -0,0 +1,128 @@
package io.xpipe.api.impl;
import io.xpipe.api.DataSourceConfig;
import io.xpipe.api.DataTable;
import io.xpipe.api.connector.XPipeApiConnection;
import io.xpipe.beacon.BeaconConnection;
import io.xpipe.beacon.BeaconException;
import io.xpipe.beacon.exchange.api.QueryTableDataExchange;
import io.xpipe.core.data.node.ArrayNode;
import io.xpipe.core.data.node.DataStructureNode;
import io.xpipe.core.data.node.TupleNode;
import io.xpipe.core.data.typed.TypedAbstractReader;
import io.xpipe.core.data.typed.TypedDataStreamParser;
import io.xpipe.core.data.typed.TypedDataStructureNodeReader;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceReference;
import io.xpipe.core.source.DataSourceType;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
public class DataTableImpl extends DataSourceImpl implements DataTable {
DataTableImpl(
DataSourceId id,
DataSourceConfig sourceConfig,
io.xpipe.core.source.DataSource<?> internalSource
) {
super(id, sourceConfig, internalSource);
}
@Override
public DataTable asTable() {
return this;
}
public Stream<TupleNode> stream() {
var iterator = new TableIterator();
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false)
.onClose(iterator::finish);
}
@Override
public DataSourceType getType() {
return DataSourceType.TABLE;
}
@Override
public ArrayNode readAll() {
return read(Integer.MAX_VALUE);
}
@Override
public ArrayNode read(int maxRows) {
List<DataStructureNode> nodes = new ArrayList<>();
XPipeApiConnection.execute(con -> {
var req = QueryTableDataExchange.Request.builder()
.ref(DataSourceReference.id(getId()))
.maxRows(maxRows)
.build();
con.performInputExchange(req, (QueryTableDataExchange.Response res, InputStream in) -> {
var r = new TypedDataStreamParser(res.getDataType());
r.parseStructures(in, TypedDataStructureNodeReader.of(res.getDataType()), nodes::add);
});
});
return ArrayNode.of(nodes);
}
@Override
public Iterator<TupleNode> iterator() {
return new TableIterator();
}
;
private class TableIterator implements Iterator<TupleNode> {
private final BeaconConnection connection;
private final TypedDataStreamParser parser;
private final TypedAbstractReader nodeReader;
private TupleNode node;
{
connection = XPipeApiConnection.open();
var req = QueryTableDataExchange.Request.builder()
.ref(DataSourceReference.id(getId()))
.maxRows(Integer.MAX_VALUE)
.build();
connection.sendRequest(req);
QueryTableDataExchange.Response response = connection.receiveResponse();
nodeReader = TypedDataStructureNodeReader.of(response.getDataType());
parser = new TypedDataStreamParser(response.getDataType());
connection.receiveBody();
}
private void finish() {
connection.close();
}
@Override
public boolean hasNext() {
connection.checkClosed();
try {
node = (TupleNode) parser.parseStructure(connection.getInputStream(), nodeReader);
} catch (IOException e) {
throw new BeaconException(e);
}
if (node == null) {
// finish();
}
return node != null;
}
@Override
public TupleNode next() {
connection.checkClosed();
return node;
}
}
}

View file

@ -0,0 +1,122 @@
package io.xpipe.api.impl;
import io.xpipe.api.DataSourceConfig;
import io.xpipe.api.DataText;
import io.xpipe.api.connector.XPipeApiConnection;
import io.xpipe.beacon.BeaconConnection;
import io.xpipe.beacon.BeaconException;
import io.xpipe.beacon.exchange.api.QueryTextDataExchange;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceReference;
import io.xpipe.core.source.DataSourceType;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
public class DataTextImpl extends DataSourceImpl implements DataText {
DataTextImpl(
DataSourceId sourceId,
DataSourceConfig sourceConfig,
io.xpipe.core.source.DataSource<?> internalSource
) {
super(sourceId, sourceConfig, internalSource);
}
@Override
public DataSourceType getType() {
return DataSourceType.TEXT;
}
@Override
public DataText asText() {
return this;
}
@Override
public List<String> readAllLines() {
return readLines(Integer.MAX_VALUE);
}
@Override
public List<String> readLines(int maxLines) {
try (Stream<String> lines = lines()) {
return lines.limit(maxLines).toList();
}
}
@Override
public Stream<String> lines() {
var iterator = new Iterator<String>() {
private final BeaconConnection connection;
private final BufferedReader reader;
private String nextValue;
{
connection = XPipeApiConnection.open();
var req = QueryTextDataExchange.Request.builder()
.ref(DataSourceReference.id(getId()))
.maxLines(-1)
.build();
connection.sendRequest(req);
connection.receiveResponse();
reader = new BufferedReader(new InputStreamReader(connection.receiveBody(), StandardCharsets.UTF_8));
}
private void close() {
connection.close();
}
@Override
public boolean hasNext() {
connection.checkClosed();
try {
nextValue = reader.readLine();
} catch (IOException e) {
throw new BeaconException(e);
}
return nextValue != null;
}
@Override
public String next() {
return nextValue;
}
};
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false)
.onClose(iterator::close);
}
@Override
public String readAll() {
try (Stream<String> lines = lines()) {
return lines.collect(Collectors.joining("\n"));
}
}
@Override
public String read(int maxCharacters) {
StringBuilder builder = new StringBuilder();
lines().takeWhile(s -> {
if (builder.length() > maxCharacters) {
return false;
}
builder.append(s);
return true;
});
return builder.toString();
}
}

View file

@ -0,0 +1,12 @@
package io.xpipe.api.util;
import java.util.List;
import java.util.stream.Collectors;
public class TypeDescriptor {
public static String create(List<String> names) {
return "[" + names.stream().map(n -> n != null ? "\"" + n + "\"" : null).collect(Collectors.joining(","))
+ "]\n";
}
}

View file

@ -0,0 +1,8 @@
module io.xpipe.api {
exports io.xpipe.api;
exports io.xpipe.api.connector;
exports io.xpipe.api.util;
requires transitive io.xpipe.core;
requires io.xpipe.beacon;
}

View file

@ -0,0 +1,19 @@
package io.xpipe.api.test;
import io.xpipe.beacon.BeaconDaemonController;
import io.xpipe.core.util.XPipeDaemonMode;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
public class ApiTest {
@BeforeAll
public static void setup() throws Exception {
BeaconDaemonController.start(XPipeDaemonMode.TRAY);
}
@AfterAll
public static void teardown() throws Exception {
BeaconDaemonController.stop();
}
}

View file

@ -0,0 +1,29 @@
package io.xpipe.api.test;
import io.xpipe.api.DataTableAccumulator;
import io.xpipe.core.data.node.TupleNode;
import io.xpipe.core.data.node.ValueNode;
import io.xpipe.core.data.type.TupleType;
import io.xpipe.core.data.type.ValueType;
import org.junit.jupiter.api.Test;
import java.util.List;
public class DataTableAccumulatorTest extends ApiTest {
@Test
public void test() {
var type = TupleType.of(List.of("col1", "col2"), List.of(ValueType.of(), ValueType.of()));
var acc = DataTableAccumulator.create(type);
var val = type.convert(TupleNode.of(List.of(ValueNode.of("val1"), ValueNode.of("val2"))))
.orElseThrow();
acc.add(val);
var table = acc.finish(":test");
// Assertions.assertEquals(table.getInfo().getDataType(), TupleType.tableType(List.of("col1", "col2")));
// Assertions.assertEquals(table.getInfo().getRowCountIfPresent(), OptionalInt.empty());
// var read = table.read(1).at(0);
// Assertions.assertEquals(val, read);
}
}

View file

@ -0,0 +1,22 @@
package io.xpipe.api.test;
import io.xpipe.api.DataSource;
import io.xpipe.core.source.DataSourceId;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
public class DataTableTest extends ApiTest {
@BeforeAll
public static void setupStorage() throws Exception {
DataSource.create(
DataSourceId.fromString(":usernames"), "csv", DataTableTest.class.getResource("username.csv"));
}
@Test
public void testGet() {
var table = DataSource.getById(":usernames").asTable();
var r = table.read(2);
var a = 0;
}
}

View file

@ -0,0 +1,9 @@
module io.xpipe.api.test {
requires io.xpipe.api;
requires io.xpipe.beacon;
requires org.junit.jupiter.api;
opens io.xpipe.api.test;
exports io.xpipe.api.test;
}

View file

@ -0,0 +1,6 @@
Username;Identifier ;First name;Last name
booker12;9012;Rachel;Booker
grey07;2070;Laura;Grey
johnson81;4081;Craig;Johnson
jenkins46;9346;Mary;Jenkins
smith79;5079;Jamie;Smith
1 Username Identifier First name Last name
2 booker12 9012 Rachel Booker
3 grey07 2070 Laura Grey
4 johnson81 4081 Craig Johnson
5 jenkins46 9346 Mary Jenkins
6 smith79 5079 Jamie Smith

View file

@ -1,182 +0,0 @@
plugins {
id 'application'
id 'jvm-test-suite'
id 'java-library'
}
repositories {
mavenCentral()
}
apply from: "$rootDir/gradle/gradle_scripts/java.gradle"
apply from: "$rootDir/gradle/gradle_scripts/javafx.gradle"
apply from: "$rootDir/gradle/gradle_scripts/jna.gradle"
apply from: "$rootDir/gradle/gradle_scripts/lombok.gradle"
configurations {
implementation.extendsFrom(javafx)
api.extendsFrom(jna)
}
dependencies {
api project(':core')
api project(':beacon')
compileOnly 'org.hamcrest:hamcrest:3.0'
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.11.4'
compileOnly 'org.junit.jupiter:junit-jupiter-params:5.11.4'
api 'com.vladsch.flexmark:flexmark:0.64.8'
api 'com.vladsch.flexmark:flexmark-util:0.64.8'
api 'com.vladsch.flexmark:flexmark-util-options:0.64.8'
api 'com.vladsch.flexmark:flexmark-util-data:0.64.8'
api 'com.vladsch.flexmark:flexmark-util-ast:0.64.8'
api 'com.vladsch.flexmark:flexmark-util-builder:0.64.8'
api 'com.vladsch.flexmark:flexmark-util-sequence:0.64.8'
api 'com.vladsch.flexmark:flexmark-util-misc:0.64.8'
api 'com.vladsch.flexmark:flexmark-util-dependency:0.64.8'
api 'com.vladsch.flexmark:flexmark-util-collection:0.64.8'
api 'com.vladsch.flexmark:flexmark-util-format:0.64.8'
api 'com.vladsch.flexmark:flexmark-util-html:0.64.8'
api 'com.vladsch.flexmark:flexmark-util-visitor:0.64.8'
api 'com.vladsch.flexmark:flexmark-ext-tables:0.64.8'
api 'com.vladsch.flexmark:flexmark-ext-gfm-strikethrough:0.64.8'
api 'com.vladsch.flexmark:flexmark-ext-gfm-tasklist:0.64.8'
api 'com.vladsch.flexmark:flexmark-ext-footnotes:0.64.8'
api 'com.vladsch.flexmark:flexmark-ext-definition:0.64.8'
api 'com.vladsch.flexmark:flexmark-ext-anchorlink:0.64.8'
api 'com.vladsch.flexmark:flexmark-ext-yaml-front-matter:0.64.8'
api 'com.vladsch.flexmark:flexmark-ext-toc:0.64.8'
api("com.github.weisj:jsvg:1.7.1")
api files("$rootDir/gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar")
api files("$rootDir/gradle/gradle_scripts/vernacular-1.16.jar")
api 'org.bouncycastle:bcprov-jdk18on:1.80'
api 'info.picocli:picocli:4.7.6'
api ('org.kohsuke:github-api:1.326') {
exclude group: 'org.apache.commons', module: 'commons-lang3'
}
api 'org.apache.commons:commons-lang3:3.17.0'
api 'io.sentry:sentry:7.20.0'
api 'commons-io:commons-io:2.18.0'
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.18.2"
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.18.2"
api group: 'org.kordamp.ikonli', name: 'ikonli-material2-pack', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-materialdesign2-pack', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-material-pack', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-feather-pack', version: "12.2.0"
api group: 'org.slf4j', name: 'slf4j-api', version: '2.0.16'
api group: 'org.slf4j', name: 'slf4j-jdk-platform-logging', version: '2.0.16'
api 'io.xpipe:modulefs:0.1.6'
api 'net.synedra:validatorfx:0.4.2'
api files("$rootDir/gradle/gradle_scripts/atlantafx-base-2.0.2.jar")
}
apply from: "$rootDir/gradle/gradle_scripts/local_junit_suite.gradle"
def extensionJarDepList = project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)).toList();
jar {
finalizedBy(extensionJarDepList)
}
application {
mainModule = 'io.xpipe.app'
mainClass = 'io.xpipe.app.Main'
applicationDefaultJvmArgs = jvmRunArgs
}
run {
systemProperty 'io.xpipe.app.useVirtualThreads', 'false'
systemProperty 'io.xpipe.app.mode', 'gui'
systemProperty 'io.xpipe.app.writeLogs', "true"
systemProperty 'io.xpipe.app.writeSysOut', "true"
systemProperty 'io.xpipe.app.developerMode', "true"
systemProperty 'io.xpipe.app.logLevel', "trace"
systemProperty 'io.xpipe.app.fullVersion', rootProject.fullVersion
systemProperty 'io.xpipe.app.staging', isStage
// systemProperty 'io.xpipe.beacon.port', "30000"
// Apply passed xpipe properties
for (final def e in System.getProperties().entrySet()) {
if (e.getKey().toString().contains("xpipe")) {
systemProperty e.getKey().toString(), e.getValue()
}
}
workingDir = rootDir
jvmArgs += ['-XX:+EnableDynamicAgentLoading']
def exts = files(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0].outputs.files.singleFile).toList());
classpath += exts
dependsOn(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0]).toList())
}
task runAttachedDebugger(type: JavaExec) {
workingDir = rootDir
classpath = run.classpath
mainModule = 'io.xpipe.app'
mainClass = 'io.xpipe.app.Main'
modularity.inferModulePath = true
jvmArgs += jvmRunArgs
jvmArgs += List.of(
"-javaagent:${System.getProperty("user.home")}/.attachme/attachme-agent-1.2.9.jar=port:7857,host:localhost".toString(),
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0"
)
jvmArgs += ['-XX:+EnableDynamicAgentLoading']
systemProperties run.systemProperties
def exts = files(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0].outputs.files.singleFile).toList());
classpath += exts
dependsOn(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0]).toList())
}
processResources {
doLast {
def cssFiles = fileTree(dir: "$sourceSets.main.output.resourcesDir/io/xpipe/app/resources/style")
cssFiles.include "**/*.css"
cssFiles.each { css ->
logger.info("converting CSS to BSS ${css}");
javaexec {
workingDir = project.projectDir
jvmArgs += "--module-path=${configurations.javafx.asFileTree.asPath},"
jvmArgs += "--add-modules=javafx.graphics"
main = "com.sun.javafx.css.parser.Css2Bin"
args css
}
delete css
}
}
doLast {
def resourcesDir = new File(sourceSets.main.output.resourcesDir, "io/xpipe/app/resources/third-party")
resourcesDir.mkdirs()
copy {
from "$rootDir/dist/licenses"
into resourcesDir
}
}
doLast {
copy {
from file("$rootDir/openapi.yaml")
into file("${sourceSets.main.output.resourcesDir}/io/xpipe/app/resources/misc");
}
}
}
distTar {
enabled = false;
}
distZip {
enabled = false;
}
assembleDist {
enabled = false;
}

View file

@ -1,4 +0,0 @@
open module io.xpipe.app.localTest {
requires org.junit.jupiter.api;
requires io.xpipe.app;
}

View file

@ -1,13 +0,0 @@
package test;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.test.LocalExtensionTest;
public class Test extends LocalExtensionTest {
@org.junit.jupiter.api.Test
public void test() {
System.out.println("a");
System.out.println(DataStorage.get().getStoreEntries());
}
}

View file

@ -1,29 +0,0 @@
package io.xpipe.app;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.mode.OperationMode;
public class Main {
public static void main(String[] args) {
if (args.length == 1 && args[0].equals("version")) {
AppProperties.init(args);
System.out.println(AppProperties.get().getVersion());
return;
}
// Since this is not marked as a console application, it will not print anything when you run it in a console on
// Windows
if (args.length == 1 && args[0].equals("--help")) {
System.out.println(
"""
The daemon executable xpiped does not accept any command-line arguments.
For a reference on how to use xpipe from the command-line, take a look at https://docs.xpipe.io/cli.
""");
return;
}
OperationMode.init(args);
}
}

View file

@ -1,26 +0,0 @@
package io.xpipe.app.beacon;
import io.xpipe.beacon.BeaconClientException;
import lombok.Value;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
@Value
public class AppBeaconCache {
Set<BeaconShellSession> shellSessions = new HashSet<>();
public BeaconShellSession getShellSession(UUID uuid) throws BeaconClientException {
var found = shellSessions.stream()
.filter(beaconShellSession ->
beaconShellSession.getEntry().getUuid().equals(uuid))
.findFirst();
if (found.isEmpty()) {
throw new BeaconClientException("No active shell session known for id " + uuid);
}
return found.get();
}
}

View file

@ -1,221 +0,0 @@
package io.xpipe.app.beacon;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.resources.AppResources;
import io.xpipe.app.util.MarkdownHelper;
import io.xpipe.beacon.BeaconConfig;
import io.xpipe.beacon.BeaconInterface;
import io.xpipe.core.process.OsType;
import io.xpipe.core.util.XPipeInstallation;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import lombok.Getter;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
public class AppBeaconServer {
private static AppBeaconServer INSTANCE;
@Getter
private final int port;
@Getter
private final boolean propertyPort;
private boolean running;
private ExecutorService executor;
private HttpServer server;
@Getter
private final Set<BeaconSession> sessions = new HashSet<>();
@Getter
private final AppBeaconCache cache = new AppBeaconCache();
@Getter
private String localAuthSecret;
private String notFoundHtml;
private final Map<String, String> resources = new HashMap<>();
public static void setupPort() {
int port;
boolean propertyPort;
if (System.getProperty(BeaconConfig.BEACON_PORT_PROP) != null) {
port = BeaconConfig.getUsedPort();
propertyPort = true;
} else {
port = XPipeInstallation.getDefaultBeaconPort();
propertyPort = false;
}
INSTANCE = new AppBeaconServer(port, propertyPort);
}
private AppBeaconServer(int port, boolean propertyPort) {
this.port = port;
this.propertyPort = propertyPort;
}
public static void init() {
try {
INSTANCE.initAuthSecret();
INSTANCE.start();
TrackEvent.withInfo("Started http server")
.tag("port", INSTANCE.getPort())
.build()
.handle();
} catch (Exception ex) {
// Not terminal!
// We can still continue without the running server
ErrorEvent.fromThrowable("Unable to start local http server on port " + INSTANCE.getPort(), ex)
.build()
.handle();
}
}
public static void reset() {
if (INSTANCE != null) {
INSTANCE.stop();
INSTANCE.deleteAuthSecret();
INSTANCE = null;
}
}
public void addSession(BeaconSession session) {
this.sessions.add(session);
}
public static AppBeaconServer get() {
return INSTANCE;
}
private void stop() {
if (!running) {
return;
}
running = false;
server.stop(0);
executor.shutdown();
try {
executor.awaitTermination(30, TimeUnit.SECONDS);
} catch (InterruptedException ignored) {}
}
private void initAuthSecret() throws IOException {
var file = XPipeInstallation.getLocalBeaconAuthFile();
var id = UUID.randomUUID().toString();
Files.writeString(file, id);
if (OsType.getLocal() != OsType.WINDOWS) {
Files.setPosixFilePermissions(file, PosixFilePermissions.fromString("rw-rw----"));
}
localAuthSecret = id;
}
private void deleteAuthSecret() {
var file = XPipeInstallation.getLocalBeaconAuthFile();
try {
Files.delete(file);
} catch (IOException ignored) {
}
}
private void start() throws IOException {
executor = Executors.newFixedThreadPool(5, r -> {
Thread t = Executors.defaultThreadFactory().newThread(r);
t.setDaemon(true);
t.setName("http handler");
t.setUncaughtExceptionHandler((t1, e) -> {
ErrorEvent.fromThrowable(e).handle();
});
return t;
});
server = HttpServer.create(
new InetSocketAddress(Inet4Address.getByAddress(new byte[] {0x7f, 0x00, 0x00, 0x01}), port), 10);
BeaconInterface.getAll().forEach(beaconInterface -> {
server.createContext(beaconInterface.getPath(), new BeaconRequestHandler<>(beaconInterface));
});
server.setExecutor(executor);
var resourceMap = Map.of(
"openapi.yaml", "misc/openapi.yaml",
"markdown.css", "misc/github-markdown-dark.css",
"highlight.min.js", "misc/highlight.min.js",
"github-dark.min.css", "misc/github-dark.min.css");
resourceMap.forEach((s, s2) -> {
server.createContext("/" + s, exchange -> {
handleResource(exchange, s2);
});
});
server.createContext("/", exchange -> {
handleCatchAll(exchange);
});
server.start();
running = true;
}
private void handleResource(HttpExchange exchange, String resource) throws IOException {
if (!resources.containsKey(resource)) {
AppResources.with(AppResources.XPIPE_MODULE, resource, file -> {
resources.put(resource, Files.readString(file));
});
}
var body = resources.get(resource).getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
try (var out = exchange.getResponseBody()) {
out.write(body);
}
}
private void handleCatchAll(HttpExchange exchange) throws IOException {
if (notFoundHtml == null) {
AppResources.with(AppResources.XPIPE_MODULE, "misc/api.md", file -> {
var md = Files.readString(file);
md = md.replaceAll(
Pattern.quote(
"""
> 400 Response
```json
{
"message": "string"
}
```
"""),
"");
notFoundHtml = MarkdownHelper.toHtml(
md,
head -> {
return head + "\n" + "<link rel=\"stylesheet\" href=\"markdown.css\">"
+ "\n" + "<link rel=\"stylesheet\" href=\"github-dark.min.css\">"
+ "\n" + "<script src=\"highlight.min.js\"></script>"
+ "\n" + "<script>hljs.highlightAll();</script>";
},
s -> {
return "<div style=\"max-width: 800px;margin: auto;\">" + s + "</div>";
},
"standalone");
});
}
var body = notFoundHtml.getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
try (var out = exchange.getResponseBody()) {
out.write(body);
}
}
}

View file

@ -1,210 +0,0 @@
package io.xpipe.app.beacon;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.beacon.*;
import io.xpipe.core.util.JacksonMapper;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import lombok.SneakyThrows;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class BeaconRequestHandler<T> implements HttpHandler {
private final BeaconInterface<T> beaconInterface;
public BeaconRequestHandler(BeaconInterface<T> beaconInterface) {
this.beaconInterface = beaconInterface;
}
@Override
public void handle(HttpExchange exchange) {
if (OperationMode.isInShutdown() && !beaconInterface.acceptInShutdown()) {
writeError(exchange, new BeaconClientErrorResponse("Daemon is currently in shutdown"), 400);
return;
}
if (beaconInterface.requiresCompletedStartup()) {
while (OperationMode.isInStartup()) {
ThreadHelper.sleep(100);
}
}
if (beaconInterface.requiresEnabledApi()
&& !AppPrefs.get().enableHttpApi().get()) {
var ex = new BeaconServerException("HTTP API is not enabled in the settings menu");
writeError(exchange, ex, 403);
return;
}
if (!AppPrefs.get().disableApiAuthentication().get() && beaconInterface.requiresAuthentication()) {
var auth = exchange.getRequestHeaders().getFirst("Authorization");
if (auth == null) {
writeError(exchange, new BeaconClientErrorResponse("Missing Authorization header"), 401);
return;
}
var token = auth.replace("Bearer ", "");
var session = AppBeaconServer.get().getSessions().stream()
.filter(s -> s.getToken().equals(token))
.findFirst()
.orElse(null);
if (session == null) {
writeError(exchange, new BeaconClientErrorResponse("Unknown token"), 403);
return;
}
}
handleAuthenticatedRequest(exchange);
}
private void handleAuthenticatedRequest(HttpExchange exchange) {
T object;
Object response;
try {
if (beaconInterface.readRawRequestBody()) {
object = createDefaultRequest(beaconInterface);
} else {
try (InputStream is = exchange.getRequestBody()) {
var read = is.readAllBytes();
var rawDataRequestClass = beaconInterface.getRequestClass().getDeclaredFields().length == 1
&& beaconInterface
.getRequestClass()
.getDeclaredFields()[0]
.getType()
.equals(byte[].class);
if (!new String(read, StandardCharsets.US_ASCII).trim().startsWith("{") && rawDataRequestClass) {
object = createRawDataRequest(beaconInterface, read);
} else {
var tree = JacksonMapper.getDefault().readTree(read);
TrackEvent.trace("Parsed raw request:\n" + tree.toPrettyString());
var emptyRequestClass = tree.isEmpty()
&& beaconInterface.getRequestClass().getDeclaredFields().length == 0;
object = emptyRequestClass
? createDefaultRequest(beaconInterface)
: JacksonMapper.getDefault().treeToValue(tree, beaconInterface.getRequestClass());
TrackEvent.trace("Parsed request object:\n" + object);
}
}
}
var sync = beaconInterface.getSynchronizationObject();
if (sync != null) {
synchronized (sync) {
response = beaconInterface.handle(exchange, object);
}
} else {
response = beaconInterface.handle(exchange, object);
}
} catch (BeaconClientException clientException) {
ErrorEvent.fromThrowable(clientException).omit().expected().handle();
writeError(exchange, new BeaconClientErrorResponse(clientException.getMessage()), 400);
return;
} catch (BeaconServerException serverException) {
var cause = serverException.getCause() != null ? serverException.getCause() : serverException;
ErrorEvent.fromThrowable(cause).omit().handle();
writeError(exchange, new BeaconServerErrorResponse(cause), 500);
return;
} catch (IOException ex) {
// Handle serialization errors as normal exceptions and other IO exceptions as assuming that the connection
// is broken
if (!ex.getClass().getName().contains("jackson")) {
ErrorEvent.fromThrowable(ex).omit().expected().handle();
} else {
ErrorEvent.fromThrowable(ex).omit().expected().handle();
// Make deserialization error message more readable
var message = ex.getMessage()
.replace("$RequestBuilder", "")
.replace("Exchange$Request", "Request")
.replace("at [Source: UNKNOWN; byte offset: #UNKNOWN]", "")
.replaceAll("(\\w+) is marked non-null but is null", "field $1 is missing from object")
.trim();
writeError(exchange, new BeaconClientErrorResponse(message), 400);
}
return;
} catch (Throwable other) {
ErrorEvent.fromThrowable(other).omit().expected().handle();
writeError(exchange, new BeaconServerErrorResponse(other), 500);
return;
}
try {
var emptyResponseClass = beaconInterface.getResponseClass().getDeclaredFields().length == 0;
if (!emptyResponseClass && response != null) {
TrackEvent.trace("Sending response:\n" + response);
TrackEvent.trace("Sending raw response:\n"
+ JacksonMapper.getCensored().valueToTree(response).toPrettyString());
var bytes = JacksonMapper.getDefault()
.valueToTree(response)
.toPrettyString()
.getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, bytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(bytes);
}
} else {
exchange.sendResponseHeaders(200, -1);
}
} catch (IOException ioException) {
// The exchange implementation might have already sent a response manually
if (!"headers already sent".equals(ioException.getMessage())) {
ErrorEvent.fromThrowable(ioException).omit().expected().handle();
}
} catch (Throwable other) {
ErrorEvent.fromThrowable(other).handle();
writeError(exchange, new BeaconServerErrorResponse(other), 500);
}
}
private void writeError(HttpExchange exchange, Object errorMessage, int code) {
try {
var bytes =
JacksonMapper.getDefault().writeValueAsString(errorMessage).getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(code, bytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(bytes);
}
} catch (IOException ex) {
ErrorEvent.fromThrowable(ex).omit().expected().handle();
}
}
@SneakyThrows
@SuppressWarnings("unchecked")
private <REQ> REQ createDefaultRequest(BeaconInterface<?> beaconInterface) {
var c = beaconInterface.getRequestClass().getDeclaredMethod("builder");
c.setAccessible(true);
var b = c.invoke(null);
var m = b.getClass().getDeclaredMethod("build");
m.setAccessible(true);
return (REQ) beaconInterface.getRequestClass().cast(m.invoke(b));
}
@SneakyThrows
@SuppressWarnings("unchecked")
private <REQ> REQ createRawDataRequest(BeaconInterface<?> beaconInterface, byte[] s) {
var c = beaconInterface.getRequestClass().getDeclaredMethod("builder");
c.setAccessible(true);
var b = c.invoke(null);
var setMethod = Arrays.stream(b.getClass().getDeclaredMethods())
.filter(method -> method.getParameterCount() == 1
&& method.getParameters()[0].getType().equals(byte[].class))
.findFirst()
.orElseThrow();
setMethod.invoke(b, (Object) s);
var m = b.getClass().getDeclaredMethod("build");
m.setAccessible(true);
return (REQ) beaconInterface.getRequestClass().cast(m.invoke(b));
}
}

View file

@ -1,12 +0,0 @@
package io.xpipe.app.beacon;
import io.xpipe.beacon.BeaconClientInformation;
import lombok.Value;
@Value
public class BeaconSession {
BeaconClientInformation clientInformation;
String token;
}

View file

@ -1,13 +0,0 @@
package io.xpipe.app.beacon;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.process.ShellControl;
import lombok.Value;
@Value
public class BeaconShellSession {
DataStoreEntry entry;
ShellControl control;
}

View file

@ -1,82 +0,0 @@
package io.xpipe.app.beacon;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.ShellTemp;
import io.xpipe.beacon.BeaconClientException;
import org.apache.commons.io.FileUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class BlobManager {
private static final Path TEMP = ShellTemp.getLocalTempDataDirectory("blob");
private static BlobManager INSTANCE;
private final Map<UUID, byte[]> memoryBlobs = new ConcurrentHashMap<>();
private final Map<UUID, Path> fileBlobs = new ConcurrentHashMap<>();
public static BlobManager get() {
return INSTANCE;
}
public static void init() {
INSTANCE = new BlobManager();
try {
FileUtils.forceMkdir(TEMP.toFile());
try {
// Remove old files in dir
FileUtils.cleanDirectory(TEMP.toFile());
} catch (IOException ignored) {
}
} catch (IOException e) {
ErrorEvent.fromThrowable(e).handle();
}
}
public static void reset() {
try {
FileUtils.cleanDirectory(TEMP.toFile());
} catch (IOException ignored) {
}
INSTANCE = null;
}
public Path newBlobFile() throws IOException {
var file = TEMP.resolve(UUID.randomUUID().toString());
FileUtils.forceMkdir(file.getParent().toFile());
return file;
}
public void store(UUID uuid, byte[] blob) {
memoryBlobs.put(uuid, blob);
}
public void store(UUID uuid, InputStream blob) throws IOException {
var file = TEMP.resolve(uuid.toString());
try (var fileOut = Files.newOutputStream(file)) {
blob.transferTo(fileOut);
}
fileBlobs.put(uuid, file);
}
public InputStream getBlob(UUID uuid) throws Exception {
var memory = memoryBlobs.get(uuid);
if (memory != null) {
return new ByteArrayInputStream(memory);
}
var found = fileBlobs.get(uuid);
if (found == null) {
throw new BeaconClientException("No saved data known for id " + uuid);
}
return Files.newInputStream(found);
}
}

View file

@ -1,80 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.prefs.ExternalApplicationType;
import io.xpipe.app.terminal.TerminalView;
import io.xpipe.app.util.AskpassAlert;
import io.xpipe.app.util.SecretManager;
import io.xpipe.app.util.SecretQueryState;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.AskpassExchange;
import io.xpipe.core.process.OsType;
import com.sun.net.httpserver.HttpExchange;
public class AskpassExchangeImpl extends AskpassExchange {
@Override
public boolean requiresCompletedStartup() {
return false;
}
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {
if (msg.getRequest() == null) {
var r = AskpassAlert.queryRaw(msg.getPrompt(), null);
return Response.builder().value(r.getSecret()).build();
}
var found = msg.getSecretId() != null
? SecretManager.getProgress(msg.getRequest(), msg.getSecretId())
: SecretManager.getProgress(msg.getRequest());
if (found.isEmpty()) {
throw new BeaconClientException("Unknown askpass request");
}
var p = found.get();
var secret = p.process(msg.getPrompt());
if (p.getState() != SecretQueryState.NORMAL) {
throw new BeaconClientException(SecretQueryState.toErrorMessage(p.getState()));
}
focusTerminalIfNeeded(msg.getPid());
return Response.builder().value(secret.inPlace()).build();
}
private void focusTerminalIfNeeded(long pid) {
if (TerminalView.get() == null) {
return;
}
var found = TerminalView.get().findSession(pid);
if (found.isEmpty()) {
return;
}
var term = TerminalView.get().getTerminalInstances().stream()
.filter(instance -> instance.equals(found.get().getTerminal()))
.findFirst();
if (term.isEmpty()) {
return;
}
var control = term.get().controllable();
if (control.isPresent()) {
control.get().focus();
} else {
if (OsType.getLocal() == OsType.MACOS) {
// Just focus the app, this is correct most of the time
var terminalType = AppPrefs.get().terminalType().getValue();
if (terminalType instanceof ExternalApplicationType.MacApplication m) {
m.focus();
}
}
}
}
@Override
public boolean requiresEnabledApi() {
return false;
}
}

View file

@ -1,35 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.CategoryAddExchange;
import com.sun.net.httpserver.HttpExchange;
public class CategoryAddExchangeImpl extends CategoryAddExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws Throwable {
if (DataStorage.get().getStoreCategoryIfPresent(msg.getParent()).isEmpty()) {
throw new BeaconClientException("Parent category with id " + msg.getParent() + " does not exist");
}
var found = DataStorage.get().getStoreCategories().stream()
.filter(dataStoreCategory -> msg.getParent().equals(dataStoreCategory.getParentCategory())
&& msg.getName().equals(dataStoreCategory.getName()))
.findAny();
if (found.isPresent()) {
return Response.builder().category(found.get().getUuid()).build();
}
var cat = DataStoreCategory.createNew(msg.getParent(), msg.getName());
DataStorage.get().addStoreCategory(cat);
return Response.builder().category(cat.getUuid()).build();
}
@Override
public Object getSynchronizationObject() {
return DataStorage.get();
}
}

View file

@ -1,67 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionAddExchange;
import io.xpipe.core.util.ValidationException;
import com.sun.net.httpserver.HttpExchange;
public class ConnectionAddExchangeImpl extends ConnectionAddExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws Throwable {
var found = DataStorage.get().getStoreEntryIfPresent(msg.getData(), false);
if (found.isPresent()) {
return Response.builder().connection(found.get().getUuid()).build();
}
if (msg.getCategory() != null
&& DataStorage.get()
.getStoreCategoryIfPresent(msg.getCategory())
.isEmpty()) {
throw new BeaconClientException("Category with id " + msg.getCategory() + " does not exist");
}
var entry = DataStoreEntry.createNew(msg.getName(), msg.getData());
if (msg.getCategory() != null) {
entry.setCategoryUuid(msg.getCategory());
}
try {
DataStorage.get().addStoreEntryInProgress(entry);
if (msg.getValidate()) {
entry.validateOrThrow();
}
} catch (Throwable ex) {
if (ex instanceof ValidationException) {
ErrorEvent.expected(ex);
} else if (ex instanceof StackOverflowError) {
// Cycles in connection graphs can fail hard but are expected
ErrorEvent.expected(ex);
}
throw ex;
} finally {
DataStorage.get().removeStoreEntryInProgress(entry);
}
DataStorage.get().addStoreEntryIfNotPresent(entry);
// Explicitly assign category
if (msg.getCategory() != null) {
DataStorage.get()
.moveEntryToCategory(
entry,
DataStorage.get()
.getStoreCategoryIfPresent(msg.getCategory())
.orElseThrow());
}
return Response.builder().connection(entry.getUuid()).build();
}
@Override
public Object getSynchronizationObject() {
return DataStorage.get();
}
}

View file

@ -1,32 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionBrowseExchange;
import io.xpipe.core.store.FileSystemStore;
import com.sun.net.httpserver.HttpExchange;
public class ConnectionBrowseExchangeImpl extends ConnectionBrowseExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws Exception {
var e = DataStorage.get()
.getStoreEntryIfPresent(msg.getConnection())
.orElseThrow(() -> new BeaconClientException("Unknown connection: " + msg.getConnection()));
if (!(e.getStore() instanceof FileSystemStore)) {
throw new BeaconClientException("Not a file system connection");
}
BrowserFullSessionModel.DEFAULT.openFileSystemSync(
e.ref(), msg.getDirectory() != null ? ignored -> msg.getDirectory() : null, null, true);
AppLayoutModel.get().selectBrowser();
return Response.builder().build();
}
@Override
public Object getSynchronizationObject() {
return DataStorage.get();
}
}

View file

@ -1,63 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionInfoExchange;
import io.xpipe.core.store.StorePath;
import com.sun.net.httpserver.HttpExchange;
import org.apache.commons.lang3.ClassUtils;
import java.util.ArrayList;
import java.util.UUID;
import java.util.stream.Collectors;
public class ConnectionInfoExchangeImpl extends ConnectionInfoExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {
var list = new ArrayList<InfoResponse>();
for (UUID uuid : msg.getConnections()) {
var e = DataStorage.get()
.getStoreEntryIfPresent(uuid)
.orElseThrow(() -> new BeaconClientException("Unknown connection: " + uuid));
var names = DataStorage.get()
.getStorePath(DataStorage.get()
.getStoreCategoryIfPresent(e.getCategoryUuid())
.orElseThrow())
.getNames();
var cat = new StorePath(names.subList(1, names.size()));
var cache = e.getStoreCache().entrySet().stream()
.filter(stringObjectEntry -> {
return stringObjectEntry.getValue() != null
&& (ClassUtils.isPrimitiveOrWrapper(
stringObjectEntry.getValue().getClass())
|| stringObjectEntry.getValue() instanceof String);
})
.collect(Collectors.toMap(
stringObjectEntry -> stringObjectEntry.getKey(),
stringObjectEntry -> stringObjectEntry.getValue()));
var apply = InfoResponse.builder()
.lastModified(e.getLastModified())
.lastUsed(e.getLastUsed())
.connection(e.getUuid())
.category(cat)
.name(DataStorage.get().getStorePath(e))
.rawData(e.getStore())
.usageCategory(e.getProvider().getUsageCategory())
.type(e.getProvider().getId())
.state(e.getStorePersistentState() != null ? e.getStorePersistentState() : new Object())
.cache(cache)
.build();
list.add(apply);
}
return Response.builder().infos(list).build();
}
@Override
public Object getSynchronizationObject() {
return DataStorage.get();
}
}

View file

@ -1,28 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStorageQuery;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.beacon.api.ConnectionQueryExchange;
import com.sun.net.httpserver.HttpExchange;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class ConnectionQueryExchangeImpl extends ConnectionQueryExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) {
var found = DataStorageQuery.query(msg.getCategoryFilter(), msg.getConnectionFilter(), msg.getTypeFilter());
return Response.builder()
.found(found.stream().map(entry -> entry.getUuid()).toList())
.build();
}
@Override
public Object getSynchronizationObject() {
return DataStorage.get();
}
}

View file

@ -1,29 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.FixedHierarchyStore;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionRefreshExchange;
import com.sun.net.httpserver.HttpExchange;
public class ConnectionRefreshExchangeImpl extends ConnectionRefreshExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws Throwable {
var e = DataStorage.get()
.getStoreEntryIfPresent(msg.getConnection())
.orElseThrow(() -> new BeaconClientException("Unknown connection: " + msg.getConnection()));
if (e.getStore() instanceof FixedHierarchyStore) {
DataStorage.get().refreshChildren(e, true);
} else {
e.validateOrThrow();
}
return Response.builder().build();
}
@Override
public Object getSynchronizationObject() {
return DataStorage.get();
}
}

View file

@ -1,32 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionRemoveExchange;
import com.sun.net.httpserver.HttpExchange;
import java.util.ArrayList;
import java.util.UUID;
public class ConnectionRemoveExchangeImpl extends ConnectionRemoveExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {
var entries = new ArrayList<DataStoreEntry>();
for (UUID uuid : msg.getConnections()) {
var e = DataStorage.get()
.getStoreEntryIfPresent(uuid)
.orElseThrow(() -> new BeaconClientException("Unknown connection: " + uuid));
entries.add(e);
}
DataStorage.get().deleteWithChildren(entries.toArray(DataStoreEntry[]::new));
return Response.builder().build();
}
@Override
public Object getSynchronizationObject() {
return DataStorage.get();
}
}

View file

@ -1,30 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.terminal.TerminalLauncher;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionTerminalExchange;
import com.sun.net.httpserver.HttpExchange;
public class ConnectionTerminalExchangeImpl extends ConnectionTerminalExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws Exception {
var e = DataStorage.get()
.getStoreEntryIfPresent(msg.getConnection())
.orElseThrow(() -> new BeaconClientException("Unknown connection: " + msg.getConnection()));
if (!(e.getStore() instanceof ShellStore shellStore)) {
throw new BeaconClientException("Not a shell connection");
}
var sc = shellStore.getOrStartSession();
TerminalLauncher.open(e, e.getName(), msg.getDirectory(), sc);
return Response.builder().build();
}
@Override
public Object getSynchronizationObject() {
return DataStorage.get();
}
}

View file

@ -1,32 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionToggleExchange;
import io.xpipe.core.store.SingletonSessionStore;
import com.sun.net.httpserver.HttpExchange;
public class ConnectionToggleExchangeImpl extends ConnectionToggleExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws Exception {
var e = DataStorage.get()
.getStoreEntryIfPresent(msg.getConnection())
.orElseThrow(() -> new BeaconClientException("Unknown connection: " + msg.getConnection()));
if (!(e.getStore() instanceof SingletonSessionStore<?> singletonSessionStore)) {
throw new BeaconClientException("Not a toggleable connection");
}
if (msg.getState()) {
singletonSessionStore.startSessionIfNeeded();
} else {
singletonSessionStore.stopSessionIfNeeded();
}
return Response.builder().build();
}
@Override
public Object getSynchronizationObject() {
return DataStorage.get();
}
}

View file

@ -1,25 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.core.window.AppMainWindow;
import io.xpipe.beacon.api.DaemonFocusExchange;
import com.sun.net.httpserver.HttpExchange;
public class DaemonFocusExchangeImpl extends DaemonFocusExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) {
OperationMode.switchUp(OperationMode.GUI);
var w = AppMainWindow.getInstance();
if (w != null) {
w.focus();
}
return Response.builder().build();
}
@Override
public boolean requiresEnabledApi() {
return false;
}
}

View file

@ -1,33 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.DaemonModeExchange;
import com.sun.net.httpserver.HttpExchange;
public class DaemonModeExchangeImpl extends DaemonModeExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {
var mode = OperationMode.map(msg.getMode());
if (!mode.isSupported()) {
throw new BeaconClientException("Unsupported mode: " + msg.getMode().getDisplayName() + ". Supported: "
+ String.join(
", ",
OperationMode.getAll().stream()
.filter(OperationMode::isSupported)
.map(OperationMode::getId)
.toList()));
}
OperationMode.switchToSyncIfPossible(mode);
return DaemonModeExchange.Response.builder()
.usedMode(OperationMode.map(OperationMode.get()))
.build();
}
@Override
public boolean requiresEnabledApi() {
return false;
}
}

View file

@ -1,49 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.core.AppOpenArguments;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.util.PlatformInit;
import io.xpipe.beacon.BeaconServerException;
import io.xpipe.beacon.api.DaemonOpenExchange;
import io.xpipe.core.process.OsType;
import com.sun.net.httpserver.HttpExchange;
public class DaemonOpenExchangeImpl extends DaemonOpenExchange {
private int openCounter = 0;
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconServerException {
if (msg.getArguments().isEmpty()) {
try {
// At this point we are already loading this on another thread
// so this call will only perform the waiting
PlatformInit.init(true);
} catch (Throwable t) {
throw new BeaconServerException(t);
}
// The open command is used as a default opener on Linux
// We don't want to overwrite the default startup mode
if (OsType.getLocal() == OsType.LINUX && openCounter++ == 0) {
return Response.builder().build();
}
OperationMode.switchToAsync(OperationMode.GUI);
} else {
AppOpenArguments.handle(msg.getArguments());
}
return Response.builder().build();
}
@Override
public boolean requiresEnabledApi() {
return false;
}
@Override
public boolean requiresCompletedStartup() {
return false;
}
}

View file

@ -1,31 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.beacon.api.DaemonStatusExchange;
import com.sun.net.httpserver.HttpExchange;
public class DaemonStatusExchangeImpl extends DaemonStatusExchange {
@Override
public boolean requiresCompletedStartup() {
return false;
}
@Override
public Object handle(HttpExchange exchange, Request body) {
String mode;
if (OperationMode.get() == null) {
mode = "none";
} else {
mode = OperationMode.get().getId();
}
return Response.builder().mode(mode).build();
}
@Override
public boolean requiresEnabledApi() {
return false;
}
}

View file

@ -1,29 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.beacon.api.DaemonStopExchange;
import com.sun.net.httpserver.HttpExchange;
public class DaemonStopExchangeImpl extends DaemonStopExchange {
@Override
public boolean requiresCompletedStartup() {
return false;
}
@Override
public Object handle(HttpExchange exchange, Request msg) {
ThreadHelper.runAsync(() -> {
ThreadHelper.sleep(1000);
OperationMode.close();
});
return Response.builder().success(true).build();
}
@Override
public boolean requiresEnabledApi() {
return false;
}
}

View file

@ -1,39 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.AppVersion;
import io.xpipe.app.util.LicenseProvider;
import io.xpipe.beacon.api.DaemonVersionExchange;
import com.sun.net.httpserver.HttpExchange;
public class DaemonVersionExchangeImpl extends DaemonVersionExchange {
@Override
public boolean requiresCompletedStartup() {
return false;
}
@Override
public Object handle(HttpExchange exchange, Request msg) {
var jvmVersion = System.getProperty("java.vm.vendor") + " "
+ System.getProperty("java.vm.name") + " ("
+ System.getProperty("java.vm.version") + ")";
var version = AppProperties.get().getVersion();
var pro = LicenseProvider.get().hasPaidLicense();
return Response.builder()
.version(version)
.canonicalVersion(AppVersion.parse(version)
.map(appVersion -> appVersion.toString())
.orElse("?"))
.buildVersion(AppProperties.get().getBuild())
.jvmVersion(jvmVersion)
.pro(pro)
.build();
}
@Override
public boolean requiresEnabledApi() {
return false;
}
}

View file

@ -1,26 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.beacon.BlobManager;
import io.xpipe.beacon.api.FsBlobExchange;
import com.sun.net.httpserver.HttpExchange;
import lombok.SneakyThrows;
import java.util.UUID;
public class FsBlobExchangeImpl extends FsBlobExchange {
@Override
@SneakyThrows
public Object handle(HttpExchange exchange, Request msg) {
var id = UUID.randomUUID();
var size = exchange.getRequestBody().available();
if (size > 100_000_000) {
BlobManager.get().store(id, exchange.getRequestBody());
} else {
BlobManager.get().store(id, exchange.getRequestBody().readAllBytes());
}
return Response.builder().blob(id).build();
}
}

View file

@ -1,60 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.beacon.AppBeaconServer;
import io.xpipe.app.beacon.BlobManager;
import io.xpipe.app.ext.ConnectionFileSystem;
import io.xpipe.app.util.FixedSizeInputStream;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.FsReadExchange;
import com.sun.net.httpserver.HttpExchange;
import lombok.SneakyThrows;
import java.io.BufferedInputStream;
import java.io.OutputStream;
import java.nio.file.Files;
public class FsReadExchangeImpl extends FsReadExchange {
@Override
@SneakyThrows
public Object handle(HttpExchange exchange, Request msg) {
var shell = AppBeaconServer.get().getCache().getShellSession(msg.getConnection());
var fs = new ConnectionFileSystem(shell.getControl());
if (!fs.fileExists(msg.getPath().toString())) {
throw new BeaconClientException("File does not exist");
}
var size = fs.getFileSize(msg.getPath().toString());
if (size > 100_000_000) {
var file = BlobManager.get().newBlobFile();
try (var in = fs.openInput(msg.getPath().toString())) {
var fixedIn = new FixedSizeInputStream(new BufferedInputStream(in), size);
try (var fileOut =
Files.newOutputStream(file.resolve(msg.getPath().getFileName()))) {
fixedIn.transferTo(fileOut);
}
in.transferTo(OutputStream.nullOutputStream());
}
exchange.sendResponseHeaders(200, size);
try (var fileIn = Files.newInputStream(file);
var out = exchange.getResponseBody()) {
fileIn.transferTo(out);
}
} else {
byte[] bytes;
try (var in = fs.openInput(msg.getPath().toString())) {
var fixedIn = new FixedSizeInputStream(new BufferedInputStream(in), size);
bytes = fixedIn.readAllBytes();
in.transferTo(OutputStream.nullOutputStream());
}
exchange.sendResponseHeaders(200, bytes.length);
try (var out = exchange.getResponseBody()) {
out.write(bytes);
}
}
return Response.builder().build();
}
}

View file

@ -1,29 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.beacon.AppBeaconServer;
import io.xpipe.app.beacon.BlobManager;
import io.xpipe.app.util.ScriptHelper;
import io.xpipe.beacon.api.FsScriptExchange;
import com.sun.net.httpserver.HttpExchange;
import lombok.SneakyThrows;
import java.nio.charset.StandardCharsets;
public class FsScriptExchangeImpl extends FsScriptExchange {
@Override
@SneakyThrows
public Object handle(HttpExchange exchange, Request msg) {
var shell = AppBeaconServer.get().getCache().getShellSession(msg.getConnection());
String data;
try (var in = BlobManager.get().getBlob(msg.getBlob())) {
data = new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
data = shell.getControl().getShellDialect().prepareScriptContent(data);
var file = ScriptHelper.getExecScriptFile(shell.getControl());
shell.getControl().view().writeScriptFile(file, data);
file = ScriptHelper.fixScriptPermissions(shell.getControl(), file);
return Response.builder().path(file).build();
}
}

View file

@ -1,24 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.beacon.AppBeaconServer;
import io.xpipe.app.beacon.BlobManager;
import io.xpipe.app.ext.ConnectionFileSystem;
import io.xpipe.beacon.api.FsWriteExchange;
import com.sun.net.httpserver.HttpExchange;
import lombok.SneakyThrows;
public class FsWriteExchangeImpl extends FsWriteExchange {
@Override
@SneakyThrows
public Object handle(HttpExchange exchange, Request msg) {
var shell = AppBeaconServer.get().getCache().getShellSession(msg.getConnection());
var fs = new ConnectionFileSystem(shell.getControl());
try (var in = BlobManager.get().getBlob(msg.getBlob());
var os = fs.openOutput(msg.getPath().toString(), in.available())) {
in.transferTo(os);
}
return Response.builder().build();
}
}

View file

@ -1,50 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.beacon.AppBeaconServer;
import io.xpipe.app.beacon.BeaconSession;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.beacon.BeaconAuthMethod;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.HandshakeExchange;
import com.sun.net.httpserver.HttpExchange;
import java.util.UUID;
public class HandshakeExchangeImpl extends HandshakeExchange {
@Override
public boolean requiresCompletedStartup() {
return false;
}
@Override
public Object handle(HttpExchange exchange, Request body) throws BeaconClientException {
if (!checkAuth(body.getAuth())) {
throw new BeaconClientException("Authentication failed");
}
var session = new BeaconSession(body.getClient(), UUID.randomUUID().toString());
AppBeaconServer.get().addSession(session);
return Response.builder().sessionToken(session.getToken()).build();
}
private boolean checkAuth(BeaconAuthMethod authMethod) {
if (authMethod instanceof BeaconAuthMethod.Local local) {
var c = local.getAuthFileContent().trim();
return AppBeaconServer.get().getLocalAuthSecret().equals(c);
}
if (authMethod instanceof BeaconAuthMethod.ApiKey key) {
var c = key.getKey().trim();
return AppPrefs.get().apiKey().get().equals(c);
}
return false;
}
@Override
public boolean requiresEnabledApi() {
return false;
}
}

View file

@ -1,33 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.beacon.AppBeaconServer;
import io.xpipe.beacon.api.ShellExecExchange;
import com.sun.net.httpserver.HttpExchange;
import lombok.SneakyThrows;
import java.util.concurrent.atomic.AtomicReference;
public class ShellExecExchangeImpl extends ShellExecExchange {
@Override
@SneakyThrows
public Object handle(HttpExchange exchange, Request msg) {
var existing = AppBeaconServer.get().getCache().getShellSession(msg.getConnection());
AtomicReference<String> out = new AtomicReference<>();
AtomicReference<String> err = new AtomicReference<>();
long exitCode;
try (var command = existing.getControl().command(msg.getCommand()).start()) {
var r = command.readStdoutAndStderr();
out.set(r[0]);
err.set(r[1]);
command.close();
exitCode = command.getExitCode();
}
return Response.builder()
.stdout(out.get())
.stderr(err.get())
.exitCode(exitCode)
.build();
}
}

View file

@ -1,51 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.beacon.AppBeaconServer;
import io.xpipe.app.beacon.BeaconShellSession;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ShellStartExchange;
import com.sun.net.httpserver.HttpExchange;
import lombok.SneakyThrows;
public class ShellStartExchangeImpl extends ShellStartExchange {
@Override
@SneakyThrows
public Object handle(HttpExchange exchange, Request msg) {
var e = DataStorage.get()
.getStoreEntryIfPresent(msg.getConnection())
.orElseThrow(() -> new BeaconClientException("Unknown connection"));
if (!(e.getStore() instanceof ShellStore s)) {
throw new BeaconClientException("Not a shell connection");
}
var existing = AppBeaconServer.get().getCache().getShellSessions().stream()
.filter(beaconShellSession -> beaconShellSession.getEntry().equals(e))
.findFirst();
var control = (existing.isPresent()
? existing.get().getControl()
: s.standaloneControl().start());
control.setNonInteractive();
control.start();
var d = control.getShellDialect().getDumbMode();
if (!d.supportsAnyPossibleInteraction()) {
control.close();
d.throwIfUnsupported();
}
if (existing.isEmpty()) {
AppBeaconServer.get().getCache().getShellSessions().add(new BeaconShellSession(e, control));
}
return Response.builder()
.shellDialect(control.getShellDialect())
.osType(control.getOsType())
.osName(control.getOsName())
.temp(control.getSystemTemporaryDirectory())
.ttyState(control.getTtyState())
.build();
}
}

View file

@ -1,19 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.beacon.AppBeaconServer;
import io.xpipe.beacon.api.ShellStopExchange;
import com.sun.net.httpserver.HttpExchange;
import lombok.SneakyThrows;
public class ShellStopExchangeImpl extends ShellStopExchange {
@Override
@SneakyThrows
public Object handle(HttpExchange exchange, Request msg) {
var e = AppBeaconServer.get().getCache().getShellSession(msg.getConnection());
e.getControl().close();
AppBeaconServer.get().getCache().getShellSessions().remove(e);
return Response.builder().build();
}
}

View file

@ -1,43 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.terminal.TerminalLauncherManager;
import io.xpipe.beacon.api.SshLaunchExchange;
import io.xpipe.core.process.ShellDialects;
import com.sun.net.httpserver.HttpExchange;
import java.util.List;
public class SshLaunchExchangeImpl extends SshLaunchExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws Exception {
if ("echo $SHELL".equals(msg.getArguments())) {
return Response.builder().command(List.of("echo", "/bin/bash")).build();
}
var usedDialect = ShellDialects.getStartableDialects().stream()
.filter(dialect -> dialect.getExecutableName().equalsIgnoreCase(msg.getArguments()))
.findFirst();
if (msg.getArguments() != null
&& usedDialect.isEmpty()
&& !msg.getArguments().contains("SSH_ORIGINAL_COMMAND")) {
return Response.builder().command(List.of()).build();
}
// There are sometimes multiple requests by a terminal client (e.g. Termius)
// This might fail sometimes, but it is expected
var r = TerminalLauncherManager.sshLaunchExchange();
var c = ProcessControlProvider.get()
.getEffectiveLocalDialect()
.getOpenScriptCommand(r.toString())
.buildBaseParts(null);
return Response.builder().command(c).build();
}
@Override
public boolean requiresEnabledApi() {
return false;
}
}

View file

@ -1,68 +0,0 @@
package io.xpipe.app.beacon.impl;
import atlantafx.base.layout.ModalBox;
import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStorageQuery;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.terminal.TerminalLauncherManager;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.BeaconServerException;
import io.xpipe.beacon.api.TerminalExternalLaunchExchange;
import java.util.List;
public class TerminalExternalLaunchExchangeImpl extends TerminalExternalLaunchExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException, BeaconServerException {
var found = DataStorageQuery.queryUserInput(msg.getConnection());
if (found.isEmpty()) {
throw new BeaconClientException("No connection found for input " + msg.getConnection());
}
if (found.size() > 1) {
throw new BeaconServerException("Multiple connections found: " + found.stream().map(DataStoreEntry::getName).toList());
}
var e = found.getFirst();
var isShell = e.getStore() instanceof ShellStore;
if (!isShell) {
throw new BeaconClientException("Connection " + DataStorage.get().getStorePath(e).toString() + " is not a shell connection");
}
if (!checkPermission()) {
return Response.builder().command(List.of()).build();
}
var r = TerminalLauncherManager.externalExchange(e.ref(), msg.getArguments());
return Response.builder().command(r).build();
}
private boolean checkPermission() {
var cache = AppCache.getBoolean("externalLaunchPermitted", false);
if (cache) {
return true;
}
var r = AppDialog.confirm("externalLaunch");
if (r) {
AppCache.update("externalLaunchPermitted", true);
}
return r;
}
@Override
public boolean requiresEnabledApi() {
return false;
}
@Override
public Object getSynchronizationObject() {
return DataStorage.get();
}
}

View file

@ -1,21 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.terminal.TerminalLauncherManager;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.BeaconServerException;
import io.xpipe.beacon.api.TerminalLaunchExchange;
import com.sun.net.httpserver.HttpExchange;
public class TerminalLaunchExchangeImpl extends TerminalLaunchExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException, BeaconServerException {
var r = TerminalLauncherManager.launchExchange(msg.getRequest());
return Response.builder().targetFile(r).build();
}
@Override
public boolean requiresEnabledApi() {
return false;
}
}

View file

@ -1,30 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.terminal.TerminalLauncherManager;
import io.xpipe.app.terminal.TerminalView;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.TerminalPrepareExchange;
import com.sun.net.httpserver.HttpExchange;
public class TerminalPrepareExchangeImpl extends TerminalPrepareExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {
TerminalView.get().open(msg.getRequest(), msg.getPid());
TerminalLauncherManager.registerPid(msg.getRequest(), msg.getPid());
var term = AppPrefs.get().terminalType().getValue();
var unicode = term.supportsUnicode();
var escapes = term.supportsEscapes();
return Response.builder()
.supportsUnicode(unicode)
.supportsEscapeSequences(escapes)
.build();
}
@Override
public boolean requiresEnabledApi() {
return false;
}
}

View file

@ -1,22 +0,0 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.terminal.TerminalLauncherManager;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.BeaconServerException;
import io.xpipe.beacon.api.TerminalWaitExchange;
import com.sun.net.httpserver.HttpExchange;
public class TerminalWaitExchangeImpl extends TerminalWaitExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException, BeaconServerException {
TerminalLauncherManager.waitExchange(msg.getRequest());
return Response.builder().build();
}
@Override
public boolean requiresEnabledApi() {
return false;
}
}

View file

@ -1,58 +0,0 @@
package io.xpipe.app.browser;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
@Getter
public class BrowserAbstractSessionModel<T extends BrowserSessionTab> {
protected final ObservableList<T> sessionEntries = FXCollections.observableArrayList();
protected final Property<T> selectedEntry = new SimpleObjectProperty<>();
protected final BooleanProperty busy = new SimpleBooleanProperty();
public void closeAsync(BrowserSessionTab e) {
ThreadHelper.runAsync(() -> {
// This is a bit ugly
// If we die on tab init, wait a bit with closing to avoid removal while it is still being inited/added
ThreadHelper.sleep(100);
closeSync(e);
});
}
public void openSync(T e, BooleanProperty externalBusy) throws Exception {
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
e.init();
// Prevent multiple calls from interfering with each other
synchronized (this) {
sessionEntries.add(e);
// The tab pane doesn't automatically select new tabs
selectedEntry.setValue(e);
}
}
}
public void closeSync(BrowserSessionTab e) {
e.close();
synchronized (BrowserAbstractSessionModel.this) {
this.sessionEntries.remove(e);
}
}
public List<T> getSessionEntriesSnapshot() {
synchronized (this) {
return new ArrayList<>(sessionEntries);
}
}
}

View file

@ -1,195 +0,0 @@
package io.xpipe.app.browser;
import io.xpipe.app.browser.file.BrowserConnectionListComp;
import io.xpipe.app.browser.file.BrowserConnectionListFilterComp;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabComp;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.DialogComp;
import io.xpipe.app.comp.base.LeftSplitPaneComp;
import io.xpipe.app.comp.base.StackComp;
import io.xpipe.app.comp.base.VerticalComp;
import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.FileReference;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileSystemStore;
import javafx.beans.property.BooleanProperty;
import javafx.collections.ListChangeListener;
import javafx.geometry.Pos;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.stage.Window;
import javafx.stage.WindowEvent;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class BrowserFileChooserSessionComp extends DialogComp {
private final Stage stage;
private final BrowserFileChooserSessionModel model;
public BrowserFileChooserSessionComp(Stage stage, BrowserFileChooserSessionModel model) {
this.stage = stage;
this.model = model;
}
public static void openSingleFile(
Supplier<DataStoreEntryRef<? extends FileSystemStore>> store, Consumer<FileReference> file, boolean save) {
PlatformThread.runLaterIfNeeded(() -> {
var lastWindow = Window.getWindows().stream()
.filter(window -> window.isFocused())
.findFirst();
var model = new BrowserFileChooserSessionModel(BrowserFileSystemTabModel.SelectionMode.SINGLE_FILE);
DialogComp.showWindow(save ? "saveFileTitle" : "openFileTitle", stage -> {
stage.addEventFilter(WindowEvent.WINDOW_HIDDEN, event -> {
lastWindow.ifPresent(window -> window.requestFocus());
});
var comp = new BrowserFileChooserSessionComp(stage, model);
comp.apply(struc -> struc.get().setPrefSize(1200, 700))
.styleClass("browser")
.styleClass("chooser");
return comp;
});
model.setOnFinish(fileStores -> {
file.accept(fileStores.size() > 0 ? fileStores.getFirst() : null);
});
ThreadHelper.runAsync(() -> {
model.openFileSystemAsync(store.get(), null, null);
});
});
}
@Override
protected String finishKey() {
return "select";
}
@Override
protected Comp<?> pane(Comp<?> content) {
return content;
}
@Override
protected void finish() {
stage.close();
model.finishChooser();
}
@Override
protected void discard() {
model.finishWithoutChoice();
}
@Override
public Comp<?> content() {
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore)
&& storeEntryWrapper.getEntry().getValidity().isUsable();
};
BiConsumer<StoreEntryWrapper, BooleanProperty> action = (w, busy) -> {
ThreadHelper.runFailableAsync(() -> {
var entry = w.getEntry();
if (!entry.getValidity().isUsable()) {
return;
}
// Don't open same system again
var current = model.getSelectedEntry().getValue();
if (current != null && entry.ref().equals(current.getEntry())) {
return;
}
if (entry.getStore() instanceof ShellStore) {
model.openFileSystemAsync(entry.ref(), null, busy);
}
});
};
var bookmarkTopBar = new BrowserConnectionListFilterComp();
var bookmarksList = new BrowserConnectionListComp(
BindingsHelper.map(
model.getSelectedEntry(), v -> v != null ? v.getEntry().get() : null),
applicable,
action,
bookmarkTopBar.getCategory(),
bookmarkTopBar.getFilter());
var bookmarksContainer = new StackComp(List.of(bookmarksList)).styleClass("bookmarks-container");
bookmarksContainer
.apply(struc -> {
var rec = new Rectangle();
rec.widthProperty().bind(struc.get().widthProperty());
rec.heightProperty().bind(struc.get().heightProperty());
rec.setArcHeight(7);
rec.setArcWidth(7);
struc.get().getChildren().getFirst().setClip(rec);
})
.vgrow();
var stack = Comp.of(() -> {
var s = new StackPane();
model.getSelectedEntry().subscribe(selected -> {
PlatformThread.runLaterIfNeeded(() -> {
if (selected != null) {
s.getChildren().setAll(new BrowserFileSystemTabComp(selected, false).createRegion());
} else {
s.getChildren().clear();
}
});
});
return s;
});
var vertical = new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer)).styleClass("left");
var splitPane = new LeftSplitPaneComp(vertical, stack)
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
.withOnDividerChange(AppLayoutModel.get().getSavedState()::setBrowserConnectionsWidth)
.styleClass("background")
.apply(struc -> {
struc.getLeft().setMinWidth(200);
struc.getLeft().setMaxWidth(500);
});
return splitPane;
}
@Override
public Comp<?> bottom() {
return Comp.of(() -> {
var selected = new HBox();
selected.setAlignment(Pos.CENTER_LEFT);
model.getFileSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
PlatformThread.runLaterIfNeeded(() -> {
selected.getChildren()
.setAll(c.getList().stream()
.map(s -> {
var field = new TextField(
s.getRawFileEntry().getPath());
field.setEditable(false);
field.getStyleClass().add("chooser-selection");
HBox.setHgrow(field, Priority.ALWAYS);
return field;
})
.toList());
});
});
var bottomBar = new HBox(selected);
HBox.setHgrow(selected, Priority.ALWAYS);
bottomBar.setAlignment(Pos.CENTER);
return bottomBar;
});
}
}

View file

@ -1,106 +0,0 @@
package io.xpipe.app.browser;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.DerivedObservableList;
import io.xpipe.app.util.FileReference;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystemStore;
import io.xpipe.core.util.FailableFunction;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
@Getter
public class BrowserFileChooserSessionModel extends BrowserAbstractSessionModel<BrowserFileSystemTabModel> {
private final BrowserFileSystemTabModel.SelectionMode selectionMode;
private final ObservableList<BrowserEntry> fileSelection = FXCollections.observableArrayList();
@Setter
private Consumer<List<FileReference>> onFinish;
public BrowserFileChooserSessionModel(BrowserFileSystemTabModel.SelectionMode selectionMode) {
this.selectionMode = selectionMode;
selectedEntry.addListener((observable, oldValue, newValue) -> {
if (newValue == null) {
fileSelection.clear();
return;
}
var l = new DerivedObservableList<>(fileSelection, true);
l.bindContent(newValue.getFileList().getSelection());
});
}
public void finishChooser() {
var chosen = new ArrayList<>(fileSelection);
synchronized (BrowserFileChooserSessionModel.this) {
var open = selectedEntry.getValue();
if (open != null) {
ThreadHelper.runAsync(() -> {
open.close();
});
}
}
var stores = chosen.stream()
.map(entry -> new FileReference(
selectedEntry.getValue().getEntry(),
entry.getRawFileEntry().getPath()))
.toList();
onFinish.accept(stores);
}
public void finishWithoutChoice() {
synchronized (BrowserFileChooserSessionModel.this) {
var open = selectedEntry.getValue();
if (open != null) {
ThreadHelper.runAsync(() -> {
open.close();
});
}
}
}
public void openFileSystemAsync(
DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<BrowserFileSystemTabModel, String, Exception> path,
BooleanProperty externalBusy) {
if (store == null) {
return;
}
ThreadHelper.runFailableAsync(() -> {
BrowserFileSystemTabModel model;
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
model = new BrowserFileSystemTabModel(this, store, selectionMode);
model.init();
// Prevent multiple calls from interfering with each other
synchronized (BrowserFileChooserSessionModel.this) {
selectedEntry.setValue(model);
sessionEntries.add(model);
}
if (path != null) {
model.initWithGivenDirectory(FileNames.toDirectory(path.apply(model)));
} else {
model.initWithDefaultDirectory();
}
}
});
}
}

View file

@ -1,217 +0,0 @@
package io.xpipe.app.browser;
import io.xpipe.app.browser.file.BrowserConnectionListComp;
import io.xpipe.app.browser.file.BrowserConnectionListFilterComp;
import io.xpipe.app.browser.file.BrowserTransferComp;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.AnchorComp;
import io.xpipe.app.comp.base.LeftSplitPaneComp;
import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.comp.base.StackComp;
import io.xpipe.app.comp.base.VerticalComp;
import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Insets;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import java.util.HashMap;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
public class BrowserFullSessionComp extends SimpleComp {
private final BrowserFullSessionModel model;
public BrowserFullSessionComp(BrowserFullSessionModel model) {
this.model = model;
}
@Override
protected Region createSimple() {
var vertical = createLeftSide();
var leftSplit = new SimpleDoubleProperty();
var rightSplit = new SimpleDoubleProperty();
var tabs = new BrowserSessionTabsComp(model, leftSplit, rightSplit);
tabs.apply(struc -> {
struc.get().setViewOrder(1);
struc.get().setPickOnBounds(false);
AnchorPane.setTopAnchor(struc.get(), 0.0);
AnchorPane.setBottomAnchor(struc.get(), 0.0);
AnchorPane.setLeftAnchor(struc.get(), 0.0);
AnchorPane.setRightAnchor(struc.get(), 0.0);
});
vertical.apply(struc -> {
struc.get()
.paddingProperty()
.bind(Bindings.createObjectBinding(
() -> new Insets(tabs.getHeaderHeight().get(), 0, 0, 0), tabs.getHeaderHeight()));
});
var loadingIndicator = LoadingOverlayComp.noProgress(Comp.empty(), model.getBusy())
.apply(struc -> {
AnchorPane.setTopAnchor(struc.get(), 3.0);
AnchorPane.setRightAnchor(struc.get(), 0.0);
})
.styleClass("tab-loading-indicator");
var pinnedStack = createSplitStack(rightSplit, tabs);
var loadingStack = new AnchorComp(List.of(tabs, pinnedStack, loadingIndicator));
loadingStack.apply(struc -> struc.get().setPickOnBounds(false));
var splitPane = new LeftSplitPaneComp(vertical, loadingStack)
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
.withOnDividerChange(d -> {
AppLayoutModel.get().getSavedState().setBrowserConnectionsWidth(d);
leftSplit.set(d);
});
splitPane.apply(struc -> {
struc.getLeft().setMinWidth(200);
struc.getLeft().setMaxWidth(500);
struc.get().setPickOnBounds(false);
});
splitPane.apply(struc -> {
struc.get().skinProperty().subscribe(newValue -> {
if (newValue != null) {
Platform.runLater(() -> {
struc.get().getChildrenUnmodifiable().forEach(node -> {
node.setClip(null);
node.setPickOnBounds(false);
});
struc.get().lookupAll(".split-pane-divider").forEach(node -> node.setViewOrder(-1));
});
}
});
});
splitPane.styleClass("browser");
var r = splitPane.createRegion();
return r;
}
private Comp<CompStructure<VBox>> createLeftSide() {
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
if (!storeEntryWrapper.getEntry().getValidity().isUsable()) {
return false;
}
if (storeEntryWrapper.getEntry().getStore() instanceof ShellStore) {
return true;
}
return storeEntryWrapper.getEntry().getProvider().browserAction(model, storeEntryWrapper.getEntry(), null)
!= null;
};
BiConsumer<StoreEntryWrapper, BooleanProperty> action = (w, busy) -> {
ThreadHelper.runFailableAsync(() -> {
var entry = w.getEntry();
if (!entry.getValidity().isUsable()) {
return;
}
var a = entry.getProvider().browserAction(model, entry, busy);
if (a != null) {
a.execute();
}
});
};
var bookmarkTopBar = new BrowserConnectionListFilterComp();
var bookmarksList = new BrowserConnectionListComp(
BindingsHelper.map(
model.getSelectedEntry(),
v -> v instanceof BrowserStoreSessionTab<?> st
? st.getEntry().get()
: null),
applicable,
action,
bookmarkTopBar.getCategory(),
bookmarkTopBar.getFilter());
var bookmarksContainer = new StackComp(List.of(bookmarksList)).styleClass("bookmarks-container");
bookmarksContainer
.apply(struc -> {
var rec = new Rectangle();
rec.widthProperty().bind(struc.get().widthProperty());
rec.heightProperty().bind(struc.get().heightProperty());
rec.setArcHeight(11);
rec.setArcWidth(11);
struc.get().getChildren().getFirst().setClip(rec);
})
.vgrow();
var localDownloadStage = new BrowserTransferComp(model.getLocalTransfersStage())
.hide(PlatformThread.sync(Bindings.createBooleanBinding(
() -> {
if (model.getSessionEntries().size() == 0) {
return true;
}
return false;
},
model.getSessionEntries(),
model.getSelectedEntry())));
localDownloadStage.prefHeight(200);
localDownloadStage.maxHeight(200);
var vertical =
new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer, localDownloadStage)).styleClass("left");
return vertical;
}
private StackComp createSplitStack(SimpleDoubleProperty rightSplit, BrowserSessionTabsComp tabs) {
var cache = new HashMap<BrowserSessionTab, Region>();
var splitStack = new StackComp(List.of());
splitStack.apply(struc -> struc.get().setPickOnBounds(false));
splitStack.apply(struc -> {
model.getEffectiveRightTab().subscribe((newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
var all = model.getAllTabs();
cache.keySet().removeIf(browserSessionTab -> !all.contains(browserSessionTab));
if (newValue == null) {
struc.get().getChildren().clear();
return;
}
var cached = cache.containsKey(newValue);
if (!cached) {
cache.put(newValue, newValue.comp().createRegion());
}
var r = cache.get(newValue);
struc.get().getChildren().clear();
struc.get().getChildren().add(r);
struc.get().setMinWidth(rightSplit.get());
struc.get().setPrefWidth(rightSplit.get());
struc.get().setMaxWidth(rightSplit.get());
});
});
rightSplit.addListener((observable, oldValue, newValue) -> {
struc.get().setMinWidth(newValue.doubleValue());
struc.get().setPrefWidth(newValue.doubleValue());
struc.get().setMaxWidth(newValue.doubleValue());
});
AnchorPane.setBottomAnchor(struc.get(), 0.0);
AnchorPane.setRightAnchor(struc.get(), 0.0);
tabs.getHeaderHeight().subscribe(number -> {
AnchorPane.setTopAnchor(struc.get(), number.doubleValue());
});
});
return splitStack;
}
}

View file

@ -1,251 +0,0 @@
package io.xpipe.app.browser;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.file.BrowserHistorySavedState;
import io.xpipe.app.browser.file.BrowserHistoryTabModel;
import io.xpipe.app.browser.file.BrowserTransferModel;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystemStore;
import io.xpipe.core.util.FailableFunction;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableMap;
import lombok.Getter;
import lombok.SneakyThrows;
import java.util.*;
@Getter
public class BrowserFullSessionModel extends BrowserAbstractSessionModel<BrowserSessionTab> {
public static final BrowserFullSessionModel DEFAULT = new BrowserFullSessionModel();
@SneakyThrows
public static void init() {
DEFAULT.openSync(new BrowserHistoryTabModel(DEFAULT), null);
if (AppPrefs.get().pinLocalMachineOnStartup().get()) {
var tab = new BrowserFileSystemTabModel(
DEFAULT, DataStorage.get().local().ref(), BrowserFileSystemTabModel.SelectionMode.ALL);
DEFAULT.openSync(tab, null);
DEFAULT.pinTab(tab);
}
}
private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this);
private final Property<Boolean> draggingFiles = new SimpleBooleanProperty();
private final Property<BrowserSessionTab> globalPinnedTab = new SimpleObjectProperty<>();
private final ObservableMap<BrowserSessionTab, BrowserSessionTab> splits = FXCollections.observableHashMap();
private final ObservableValue<BrowserSessionTab> effectiveRightTab = createEffectiveRightTab();
private final SequencedSet<BrowserSessionTab> previousTabs = new LinkedHashSet<>();
private ObservableValue<BrowserSessionTab> createEffectiveRightTab() {
return Bindings.createObjectBinding(
() -> {
var current = selectedEntry.getValue();
if (current == null) {
return null;
}
if (!current.isCloseable()) {
return null;
}
var split = splits.get(current);
if (split != null) {
return split;
}
var global = globalPinnedTab.getValue();
if (global == null) {
return null;
}
if (global == selectedEntry.getValue()) {
return null;
}
return global;
},
globalPinnedTab,
selectedEntry,
splits);
}
public BrowserFullSessionModel() {
sessionEntries.addListener((ListChangeListener<? super BrowserSessionTab>) c -> {
var v = globalPinnedTab.getValue();
if (v != null && !c.getList().contains(v)) {
globalPinnedTab.setValue(null);
}
splits.keySet().removeIf(browserSessionTab -> !c.getList().contains(browserSessionTab));
});
selectedEntry.addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
previousTabs.remove(newValue);
previousTabs.add(newValue);
}
});
}
public Set<BrowserSessionTab> getAllTabs() {
var set = new HashSet<BrowserSessionTab>();
set.addAll(sessionEntries);
set.addAll(splits.values());
if (globalPinnedTab.getValue() != null) {
set.add(globalPinnedTab.getValue());
}
return set;
}
public void splitTab(BrowserSessionTab tab, BrowserSessionTab split) {
if (splits.containsKey(tab)) {
return;
}
splits.put(tab, split);
ThreadHelper.runFailableAsync(() -> {
split.init();
});
}
public void unsplitTab(BrowserSessionTab tab) {
if (splits.values().remove(tab)) {
ThreadHelper.runFailableAsync(() -> {
tab.close();
});
}
}
public void pinTab(BrowserSessionTab tab) {
if (tab.equals(globalPinnedTab.getValue())) {
return;
}
globalPinnedTab.setValue(tab);
var previousOthers = previousTabs.stream()
.filter(browserSessionTab -> browserSessionTab != tab && browserSessionTab.isCloseable())
.toList();
if (previousOthers.size() > 0) {
var prev = previousOthers.getLast();
getSelectedEntry().setValue(prev);
}
}
public void unpinTab(BrowserSessionTab tab) {
ThreadHelper.runFailableAsync(() -> {
globalPinnedTab.setValue(null);
});
}
public void restoreState(BrowserHistorySavedState state) {
ThreadHelper.runAsync(() -> {
var l = new ArrayList<>(state.getEntries());
l.forEach(e -> {
restoreStateAsync(e, null);
// Don't try to run everything in parallel as that can be taxing
ThreadHelper.sleep(1000);
});
});
}
public void restoreStateAsync(BrowserHistorySavedState.Entry e, BooleanProperty busy) {
var storageEntry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
storageEntry.ifPresent(entry -> {
openFileSystemAsync(entry.ref(), model -> e.getPath(), busy);
});
}
public void reset() {
synchronized (BrowserFullSessionModel.this) {
if (globalPinnedTab.getValue() != null) {
globalPinnedTab.setValue(null);
}
var all = new ArrayList<>(sessionEntries);
for (var o : all) {
// Don't close busy connections gracefully
// as we otherwise might lock up
if (!o.canImmediatelyClose()) {
continue;
}
// Prevent blocking of shutdown
closeAsync(o);
}
if (all.size() > 0) {
ThreadHelper.sleep(1000);
}
}
// Delete all files
localTransfersStage.clear(true);
}
public void openFileSystemAsync(
DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<BrowserFileSystemTabModel, String, Exception> path,
BooleanProperty externalBusy) {
if (store == null) {
return;
}
ThreadHelper.runFailableAsync(() -> {
openFileSystemSync(store, path, externalBusy, true);
});
}
public BrowserFileSystemTabModel openFileSystemSync(
DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<BrowserFileSystemTabModel, String, Exception> path,
BooleanProperty externalBusy,
boolean select)
throws Exception {
BrowserFileSystemTabModel model;
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
try (var sessionBusy = new BooleanScope(busy).exclusive().start()) {
model = new BrowserFileSystemTabModel(this, store, BrowserFileSystemTabModel.SelectionMode.ALL);
model.init();
// Prevent multiple calls from interfering with each other
synchronized (BrowserFullSessionModel.this) {
sessionEntries.add(model);
if (select) {
// The tab pane doesn't automatically select new tabs
selectedEntry.setValue(model);
}
}
}
}
if (path != null) {
model.initWithGivenDirectory(FileNames.toDirectory(path.apply(model)));
} else {
model.initWithDefaultDirectory();
}
return model;
}
@Override
public void closeSync(BrowserSessionTab e) {
var split = splits.get(e);
if (split != null) {
split.close();
}
previousTabs.remove(e);
super.closeSync(e);
}
}

View file

@ -1,42 +0,0 @@
package io.xpipe.app.browser;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.storage.DataColor;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import lombok.Getter;
@Getter
public abstract class BrowserSessionTab {
protected final BooleanProperty busy = new SimpleBooleanProperty();
protected final BrowserAbstractSessionModel<?> browserModel;
protected final Property<BrowserSessionTab> splitTab = new SimpleObjectProperty<>();
public BrowserSessionTab(BrowserAbstractSessionModel<?> browserModel) {
this.browserModel = browserModel;
}
public abstract Comp<?> comp();
public abstract boolean canImmediatelyClose();
public abstract void init() throws Exception;
public abstract void close();
public abstract ObservableValue<String> getName();
public abstract String getIcon();
public abstract DataColor getColor();
public boolean isCloseable() {
return true;
}
}

View file

@ -1,505 +0,0 @@
package io.xpipe.app.browser;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ContextMenuHelper;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.app.util.PlatformThread;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ObservableDoubleValue;
import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.control.skin.TabPaneSkin;
import javafx.scene.input.*;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import atlantafx.base.controls.RingProgressIndicator;
import atlantafx.base.theme.Styles;
import lombok.Getter;
import java.util.*;
import static atlantafx.base.theme.Styles.DENSE;
import static atlantafx.base.theme.Styles.toggleStyleClass;
import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS;
public class BrowserSessionTabsComp extends SimpleComp {
private final BrowserFullSessionModel model;
private final ObservableDoubleValue leftPadding;
private final DoubleProperty rightPadding;
@Getter
private final DoubleProperty headerHeight;
public BrowserSessionTabsComp(
BrowserFullSessionModel model, ObservableDoubleValue leftPadding, DoubleProperty rightPadding) {
this.model = model;
this.leftPadding = leftPadding;
this.rightPadding = rightPadding;
this.headerHeight = new SimpleDoubleProperty();
}
public Region createSimple() {
var tabs = createTabPane();
var topBackground = Comp.hspacer().styleClass("top-spacer").createRegion();
leftPadding.subscribe(number -> {
StackPane.setMargin(topBackground, new Insets(0, 0, 0, -number.doubleValue() - 3));
});
var stack = new StackPane(topBackground, tabs);
stack.setAlignment(Pos.TOP_CENTER);
topBackground.prefHeightProperty().bind(headerHeight);
topBackground.maxHeightProperty().bind(topBackground.prefHeightProperty());
topBackground.prefWidthProperty().bind(tabs.widthProperty());
return stack;
}
private TabPane createTabPane() {
var tabs = new TabPane();
tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
tabs.setTabMinWidth(Region.USE_PREF_SIZE);
tabs.setTabMaxWidth(400);
tabs.setTabClosingPolicy(ALL_TABS);
tabs.setSkin(new TabPaneSkin(tabs));
Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING);
toggleStyleClass(tabs, DENSE);
setupCustomStyle(tabs);
// Sync to guarantee that no external changes are made during this
synchronized (model) {
setupTabEntries(tabs);
}
setupKeyEvents(tabs);
return tabs;
}
private void setupTabEntries(TabPane tabs) {
var map = new HashMap<BrowserSessionTab, Tab>();
// Restore state
model.getSessionEntries().forEach(v -> {
var t = createTab(tabs, v);
map.put(v, t);
tabs.getTabs().add(t);
});
tabs.getSelectionModel()
.select(model.getSessionEntries()
.indexOf(model.getSelectedEntry().getValue()));
// Used for ignoring changes by the tabpane when new tabs are added. We want to perform the selections manually!
var addingTab = new SimpleBooleanProperty();
// Handle selection from platform
tabs.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
if (addingTab.get()) {
return;
}
if (newValue == null) {
model.getSelectedEntry().setValue(null);
return;
}
var source = map.entrySet().stream()
.filter(openFileSystemModelTabEntry ->
openFileSystemModelTabEntry.getValue().equals(newValue))
.findAny()
.map(Map.Entry::getKey)
.orElse(null);
model.getSelectedEntry().setValue(source);
});
// Handle selection from model
model.getSelectedEntry().addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
if (newValue == null) {
tabs.getSelectionModel().select(null);
return;
}
var toSelect = map.entrySet().stream()
.filter(openFileSystemModelTabEntry ->
openFileSystemModelTabEntry.getKey().equals(newValue))
.findAny()
.map(Map.Entry::getValue)
.orElse(null);
if (toSelect == null || !tabs.getTabs().contains(toSelect)) {
tabs.getSelectionModel().select(null);
return;
}
tabs.getSelectionModel().select(toSelect);
Platform.runLater(() -> {
toSelect.getContent().requestFocus();
});
});
});
model.getSessionEntries().addListener((ListChangeListener<? super BrowserSessionTab>) c -> {
while (c.next()) {
for (var r : c.getRemoved()) {
PlatformThread.runLaterIfNeeded(() -> {
var t = map.remove(r);
tabs.getTabs().remove(t);
});
}
for (var a : c.getAddedSubList()) {
PlatformThread.runLaterIfNeeded(() -> {
try (var b = new BooleanScope(addingTab).start()) {
var t = createTab(tabs, a);
map.put(a, t);
tabs.getTabs().add(t);
}
});
}
}
});
tabs.getTabs().addListener((ListChangeListener<? super Tab>) c -> {
while (c.next()) {
for (var r : c.getRemoved()) {
var source = map.entrySet().stream()
.filter(openFileSystemModelTabEntry ->
openFileSystemModelTabEntry.getValue().equals(r))
.findAny()
.orElse(null);
// Only handle close events that are triggered from the platform
if (source == null) {
continue;
}
model.closeAsync(source.getKey());
}
}
});
}
private void setupCustomStyle(TabPane tabs) {
tabs.skinProperty().subscribe(newValue -> {
if (newValue != null) {
Platform.runLater(() -> {
tabs.setClip(null);
tabs.setPickOnBounds(false);
tabs.lookupAll(".tab-header-area").forEach(node -> {
node.setClip(null);
node.setPickOnBounds(false);
var r = (Region) node;
r.prefHeightProperty().bind(r.maxHeightProperty());
r.setMinHeight(Region.USE_PREF_SIZE);
});
tabs.lookupAll(".headers-region").forEach(node -> {
node.setClip(null);
node.setPickOnBounds(false);
var r = (Region) node;
r.prefHeightProperty().bind(r.maxHeightProperty());
r.setMinHeight(Region.USE_PREF_SIZE);
});
Region headerArea = (Region) tabs.lookup(".tab-header-area");
headerArea
.paddingProperty()
.bind(Bindings.createObjectBinding(
() -> new Insets(2, 0, 4, -leftPadding.get() + 3), leftPadding));
tabs.setPadding(new Insets(0, 0, 0, -5));
headerHeight.bind(headerArea.heightProperty());
});
}
});
}
private static void setupKeyEvents(TabPane tabs) {
tabs.addEventHandler(KeyEvent.KEY_PRESSED, keyEvent -> {
var current = tabs.getSelectionModel().getSelectedItem();
if (current == null) {
return;
}
if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(keyEvent)) {
tabs.getTabs().remove(current);
keyEvent.consume();
return;
}
if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN)
.match(keyEvent)) {
tabs.getTabs().clear();
keyEvent.consume();
}
if (keyEvent.getCode().isFunctionKey()) {
var start = KeyCode.F1.getCode();
var index = keyEvent.getCode().getCode() - start;
if (index < tabs.getTabs().size()) {
tabs.getSelectionModel().select(index);
keyEvent.consume();
return;
}
}
var forward = new KeyCodeCombination(KeyCode.TAB, KeyCombination.SHORTCUT_DOWN);
if (forward.match(keyEvent)) {
var next = (tabs.getSelectionModel().getSelectedIndex() + 1)
% tabs.getTabs().size();
tabs.getSelectionModel().select(next);
keyEvent.consume();
return;
}
var back = new KeyCodeCombination(KeyCode.TAB, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN);
if (back.match(keyEvent)) {
var previous = (tabs.getTabs().size() + tabs.getSelectionModel().getSelectedIndex() - 1)
% tabs.getTabs().size();
tabs.getSelectionModel().select(previous);
keyEvent.consume();
}
});
}
private ContextMenu createContextMenu(TabPane tabs, Tab tab, BrowserSessionTab tabModel) {
var cm = ContextMenuHelper.create();
if (tabModel.isCloseable()) {
var unpin = ContextMenuHelper.item(LabelGraphic.none(), "unpinTab");
unpin.visibleProperty()
.bind(PlatformThread.sync(Bindings.createBooleanBinding(
() -> {
return model.getGlobalPinnedTab().getValue() != null
&& model.getGlobalPinnedTab().getValue().equals(tabModel);
},
model.getGlobalPinnedTab())));
unpin.setOnAction(event -> {
model.unpinTab(tabModel);
event.consume();
});
cm.getItems().add(unpin);
var pin = ContextMenuHelper.item(LabelGraphic.none(), "pinTab");
pin.visibleProperty()
.bind(PlatformThread.sync(Bindings.createBooleanBinding(
() -> {
return model.getGlobalPinnedTab().getValue() == null;
},
model.getGlobalPinnedTab())));
pin.setOnAction(event -> {
model.pinTab(tabModel);
event.consume();
});
cm.getItems().add(pin);
}
var select = ContextMenuHelper.item(LabelGraphic.none(), "selectTab");
select.acceleratorProperty()
.bind(Bindings.createObjectBinding(
() -> {
var start = KeyCode.F1.getCode();
var index = tabs.getTabs().indexOf(tab);
var keyCode = Arrays.stream(KeyCode.values())
.filter(code -> code.getCode() == start + index)
.findAny()
.orElse(null);
return keyCode != null ? new KeyCodeCombination(keyCode) : null;
},
tabs.getTabs()));
select.setOnAction(event -> {
tabs.getSelectionModel().select(tab);
event.consume();
});
cm.getItems().add(select);
cm.getItems().add(new SeparatorMenuItem());
var close = ContextMenuHelper.item(LabelGraphic.none(), "closeTab");
close.setAccelerator(new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN));
close.setOnAction(event -> {
if (tab.isClosable()) {
tabs.getTabs().remove(tab);
}
event.consume();
});
cm.getItems().add(close);
var closeOthers = ContextMenuHelper.item(LabelGraphic.none(), "closeOtherTabs");
closeOthers.setOnAction(event -> {
tabs.getTabs()
.removeAll(tabs.getTabs().stream()
.filter(t -> t != tab && t.isClosable())
.toList());
event.consume();
});
cm.getItems().add(closeOthers);
var closeLeft = ContextMenuHelper.item(LabelGraphic.none(), "closeLeftTabs");
closeLeft.setOnAction(event -> {
var index = tabs.getTabs().indexOf(tab);
tabs.getTabs()
.removeAll(tabs.getTabs().stream()
.filter(t -> tabs.getTabs().indexOf(t) < index && t.isClosable())
.toList());
event.consume();
});
cm.getItems().add(closeLeft);
var closeRight = ContextMenuHelper.item(LabelGraphic.none(), "closeRightTabs");
closeRight.setOnAction(event -> {
var index = tabs.getTabs().indexOf(tab);
tabs.getTabs()
.removeAll(tabs.getTabs().stream()
.filter(t -> tabs.getTabs().indexOf(t) > index && t.isClosable())
.toList());
event.consume();
});
cm.getItems().add(closeRight);
var closeAll = ContextMenuHelper.item(LabelGraphic.none(), "closeAllTabs");
closeAll.setAccelerator(
new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN));
closeAll.setOnAction(event -> {
tabs.getTabs()
.removeAll(
tabs.getTabs().stream().filter(t -> t.isClosable()).toList());
event.consume();
});
cm.getItems().add(closeAll);
return cm;
}
private Tab createTab(TabPane tabs, BrowserSessionTab tabModel) {
var tab = new Tab();
if (tabModel.isCloseable()) {
tab.setContextMenu(createContextMenu(tabs, tab, tabModel));
}
tab.setClosable(tabModel.isCloseable());
// Prevent closing while busy
tab.setOnCloseRequest(event -> {
if (!tabModel.canImmediatelyClose()) {
event.consume();
}
});
if (tabModel.getIcon() != null) {
var ring = new RingProgressIndicator(0, false);
ring.setMinSize(16, 16);
ring.setPrefSize(16, 16);
ring.setMaxSize(16, 16);
ring.progressProperty()
.bind(Bindings.createDoubleBinding(
() -> tabModel.getBusy().get()
&& !AppPrefs.get().performanceMode().get()
? -1d
: 0,
PlatformThread.sync(tabModel.getBusy()),
AppPrefs.get().performanceMode()));
var image = tabModel.getIcon();
var logo = PrettyImageHelper.ofFixedSizeSquare(image, 16).createRegion();
tab.graphicProperty()
.bind(Bindings.createObjectBinding(
() -> {
return tabModel.getBusy().get() ? ring : logo;
},
PlatformThread.sync(tabModel.getBusy())));
}
if (tabModel.getBrowserModel() instanceof BrowserFullSessionModel sessionModel) {
var global = PlatformThread.sync(sessionModel.getGlobalPinnedTab());
tab.textProperty()
.bind(Bindings.createStringBinding(
() -> {
var n = tabModel.getName().getValue();
return (AppPrefs.get().censorMode().get() ? "*".repeat(n.length()) : n)
+ (global.getValue() == tabModel ? " (" + AppI18n.get("pinned") + ")" : "");
},
tabModel.getName(),
global,
AppI18n.activeLanguage(),
AppPrefs.get().censorMode()));
} else {
tab.textProperty().bind(tabModel.getName());
}
Comp<?> comp = tabModel.comp();
var compRegion = comp.createRegion();
var empty = new StackPane();
empty.setMinWidth(450);
empty.widthProperty().addListener((observable, oldValue, newValue) -> {
if (tabModel.isCloseable() && tabs.getSelectionModel().getSelectedItem() == tab) {
rightPadding.setValue(newValue.doubleValue());
}
});
var split = new SplitPane(compRegion);
if (tabModel.isCloseable()) {
split.getItems().add(empty);
}
tabs.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
if (tabModel.isCloseable() && newValue == tab) {
rightPadding.setValue(empty.getWidth());
}
});
model.getEffectiveRightTab().subscribe(browserSessionTab -> {
PlatformThread.runLaterIfNeeded(() -> {
if (browserSessionTab != null && split.getItems().size() > 1) {
split.getItems().set(1, empty);
} else if (browserSessionTab != null && split.getItems().size() == 1) {
split.getItems().add(empty);
} else if (browserSessionTab == null && split.getItems().size() > 1) {
split.getItems().remove(1);
}
});
});
tab.setContent(split);
var id = UUID.randomUUID().toString();
tab.setId(id);
tabs.skinProperty().subscribe(newValue -> {
if (newValue != null) {
Platform.runLater(() -> {
Label l = (Label) tabs.lookup("#" + id + " .tab-label");
var w = l.maxWidthProperty();
l.minWidthProperty().bind(w);
l.prefWidthProperty().bind(w);
if (!tabModel.isCloseable()) {
l.pseudoClassStateChanged(PseudoClass.getPseudoClass("static"), true);
}
var close = (StackPane) tabs.lookup("#" + id + " .tab-close-button");
close.setPrefWidth(30);
StackPane c = (StackPane) tabs.lookup("#" + id + " .tab-container");
c.getStyleClass().add("color-box");
var color = tabModel.getColor();
if (color != null) {
c.getStyleClass().add(color.getId());
}
c.addEventHandler(
DragEvent.DRAG_ENTERED,
mouseEvent -> Platform.runLater(
() -> tabs.getSelectionModel().select(tab)));
});
}
});
return tab;
}
}

View file

@ -1,48 +0,0 @@
package io.xpipe.app.browser;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.DataStore;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import lombok.Getter;
@Getter
public abstract class BrowserStoreSessionTab<T extends DataStore> extends BrowserSessionTab {
protected final DataStoreEntryRef<? extends T> entry;
private final String name;
public BrowserStoreSessionTab(BrowserAbstractSessionModel<?> browserModel, DataStoreEntryRef<? extends T> entry) {
super(browserModel);
this.entry = entry;
this.name = DataStorage.get().getStoreEntryDisplayName(entry.get());
}
@Override
public ObservableValue<String> getName() {
return new SimpleStringProperty(name);
}
public abstract Comp<?> comp();
public abstract boolean canImmediatelyClose();
public abstract void init() throws Exception;
public abstract void close();
@Override
public String getIcon() {
return entry.get().getEffectiveIconFile();
}
@Override
public DataColor getColor() {
return DataStorage.get().getEffectiveColor(entry.get());
}
}

View file

@ -1,118 +0,0 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.util.ModuleLayerLoader;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import javafx.scene.control.MenuItem;
import javafx.scene.input.KeyCombination;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
public interface BrowserAction {
List<BrowserAction> ALL = new ArrayList<>();
static List<BrowserLeafAction> getFlattened(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return ALL.stream()
.map(browserAction -> getFlattened(browserAction, model, entries))
.flatMap(List::stream)
.toList();
}
static List<BrowserLeafAction> getFlattened(
BrowserAction browserAction, BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return browserAction instanceof BrowserLeafAction
? List.of((BrowserLeafAction) browserAction)
: ((BrowserBranchAction) browserAction)
.getBranchingActions(model, entries).stream()
.map(action -> getFlattened(action, model, entries))
.flatMap(List::stream)
.toList();
}
static BrowserLeafAction byId(String id, BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return getFlattened(model, entries).stream()
.filter(browserAction -> id.equals(browserAction.getId()))
.findAny()
.orElseThrow();
}
default List<BrowserEntry> resolveFilesIfNeeded(List<BrowserEntry> selected) {
return automaticallyResolveLinks()
? selected.stream()
.map(browserEntry ->
new BrowserEntry(browserEntry.getRawFileEntry().resolved(), browserEntry.getModel()))
.toList()
: selected;
}
MenuItem toMenuItem(BrowserFileSystemTabModel model, List<BrowserEntry> selected);
default void init(BrowserFileSystemTabModel model) throws Exception {}
default String getProFeatureId() {
return null;
}
default Node getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return null;
}
default Category getCategory() {
return null;
}
default KeyCombination getShortcut() {
return null;
}
default boolean acceptsEmptySelection() {
return false;
}
ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries);
default boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return true;
}
default boolean automaticallyResolveLinks() {
return true;
}
default boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return true;
}
enum Category {
CUSTOM,
OPEN,
NATIVE,
COPY_PASTE,
MUTATION
}
class Loader implements ModuleLayerLoader {
@Override
public void init(ModuleLayer layer) {
ALL.addAll(ServiceLoader.load(layer, BrowserAction.class).stream()
.map(actionProviderProvider -> actionProviderProvider.get())
.filter(provider -> {
try {
return true;
} catch (Throwable e) {
ErrorEvent.fromThrowable(e).handle();
return false;
}
})
.toList());
}
}
}

View file

@ -1,25 +0,0 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.file.BrowserEntry;
import java.util.List;
public class BrowserActionFormatter {
public static String filesArgument(List<BrowserEntry> entries) {
return entries.size() == 1 ? entries.getFirst().getFileName() : "(" + entries.size() + ")";
}
public static String centerEllipsis(String input, int length) {
if (input == null) {
return "";
}
if (input.length() <= length) {
return input;
}
var half = (length / 2) - 5;
return input.substring(0, half) + " ... " + input.substring(input.length() - half);
}
}

View file

@ -1,22 +0,0 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import java.util.List;
public interface BrowserApplicationPathAction extends BrowserAction {
String getExecutable();
@Override
default void init(BrowserFileSystemTabModel model) {
// Cache result for later calls
model.getCache().isApplicationInPath(getExecutable());
}
@Override
default boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return model.getCache().isApplicationInPath(getExecutable());
}
}

View file

@ -1,41 +0,0 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.util.LicenseProvider;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public interface BrowserBranchAction extends BrowserAction {
default MenuItem toMenuItem(BrowserFileSystemTabModel model, List<BrowserEntry> selected) {
var m = new Menu(getName(model, selected).getValue() + " ...");
for (var sub : getBranchingActions(model, selected)) {
var subselected = resolveFilesIfNeeded(selected);
if (!sub.isApplicable(model, subselected)) {
continue;
}
m.getItems().add(sub.toMenuItem(model, subselected));
}
var graphic = getIcon(model, selected);
if (graphic != null) {
m.setGraphic(graphic);
}
m.setDisable(!isActive(model, selected));
if (getProFeatureId() != null
&& !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) {
m.setDisable(true);
m.setGraphic(new FontIcon("mdi2p-professional-hexagon"));
}
return m;
}
List<? extends BrowserAction> getBranchingActions(BrowserFileSystemTabModel model, List<BrowserEntry> entries);
}

View file

@ -1,113 +0,0 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.comp.base.TooltipAugment;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.LicenseProvider;
import io.xpipe.app.util.ThreadHelper;
import javafx.scene.control.Button;
import javafx.scene.control.MenuItem;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Region;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public interface BrowserLeafAction extends BrowserAction {
void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) throws Exception;
default Button toButton(Region root, BrowserFileSystemTabModel model, List<BrowserEntry> selected) {
var b = new Button();
b.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(model.getBusy(), () -> {
if (model.getFileSystem() == null) {
return;
}
// Start shell in case we exited
model.getFileSystem().getShell().orElseThrow().start();
execute(model, selected);
});
});
event.consume();
});
var name = getName(model, selected);
new TooltipAugment<>(name, getShortcut()).augment(b);
var graphic = getIcon(model, selected);
if (graphic != null) {
b.setGraphic(graphic);
}
b.setMnemonicParsing(false);
b.accessibleTextProperty().bind(name);
root.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (getShortcut() != null && getShortcut().match(event)) {
b.fire();
event.consume();
}
});
b.setDisable(!isActive(model, selected));
model.getCurrentPath().addListener((observable, oldValue, newValue) -> {
b.setDisable(!isActive(model, selected));
});
if (getProFeatureId() != null
&& !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) {
b.setDisable(true);
b.setGraphic(new FontIcon("mdi2p-professional-hexagon"));
}
return b;
}
default MenuItem toMenuItem(BrowserFileSystemTabModel model, List<BrowserEntry> selected) {
var name = getName(model, selected);
var mi = new MenuItem();
mi.textProperty().bind(BindingsHelper.map(name, s -> {
if (getProFeatureId() != null) {
return LicenseProvider.get().getFeature(getProFeatureId()).suffix(s);
}
return s;
}));
mi.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(model.getBusy(), () -> {
if (model.getFileSystem() == null) {
return;
}
// Start shell in case we exited
model.getFileSystem().getShell().orElseThrow().start();
execute(model, selected);
});
});
event.consume();
});
if (getShortcut() != null) {
mi.setAccelerator(getShortcut());
}
var graphic = getIcon(model, selected);
if (graphic != null) {
mi.setGraphic(graphic);
}
mi.setMnemonicParsing(false);
mi.setDisable(!isActive(model, selected));
if (getProFeatureId() != null
&& !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) {
mi.setDisable(true);
}
return mi;
}
default String getId() {
return null;
}
}

View file

@ -1,116 +0,0 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FilePath;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.stream.Collectors;
public class BrowserAlerts {
public static FileConflictChoice showFileConflictAlert(String file, boolean multiple) {
var map = new LinkedHashMap<ButtonType, FileConflictChoice>();
map.put(new ButtonType(AppI18n.get("cancel"), ButtonBar.ButtonData.CANCEL_CLOSE), FileConflictChoice.CANCEL);
if (multiple) {
map.put(new ButtonType(AppI18n.get("skip"), ButtonBar.ButtonData.OTHER), FileConflictChoice.SKIP);
map.put(new ButtonType(AppI18n.get("skipAll"), ButtonBar.ButtonData.OTHER), FileConflictChoice.SKIP_ALL);
}
map.put(new ButtonType(AppI18n.get("replace"), ButtonBar.ButtonData.OTHER), FileConflictChoice.REPLACE);
if (multiple) {
map.put(
new ButtonType(AppI18n.get("replaceAll"), ButtonBar.ButtonData.OTHER),
FileConflictChoice.REPLACE_ALL);
}
map.put(new ButtonType(AppI18n.get("rename"), ButtonBar.ButtonData.OTHER), FileConflictChoice.RENAME);
if (multiple) {
map.put(
new ButtonType(AppI18n.get("renameAll"), ButtonBar.ButtonData.OTHER),
FileConflictChoice.RENAME_ALL);
}
var w = multiple ? 700 : 400;
return AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("fileConflictAlertTitle"));
alert.setHeaderText(AppI18n.get("fileConflictAlertHeader"));
alert.setAlertType(Alert.AlertType.CONFIRMATION);
alert.getButtonTypes().clear();
alert.getDialogPane()
.setContent(AppWindowHelper.alertContentText(
AppI18n.get(
multiple ? "fileConflictAlertContentMultiple" : "fileConflictAlertContent",
file),
w - 50));
alert.getDialogPane().setMinWidth(w);
alert.getDialogPane().setPrefWidth(w);
alert.getDialogPane().setMaxWidth(w);
map.sequencedKeySet()
.forEach(buttonType -> alert.getButtonTypes().add(buttonType));
})
.map(map::get)
.orElse(FileConflictChoice.CANCEL);
}
public static boolean showMoveAlert(List<FileEntry> source, FileEntry target) {
if (source.stream().noneMatch(entry -> entry.getKind() == FileKind.DIRECTORY)) {
return true;
}
return AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("moveAlertTitle"));
alert.setHeaderText(AppI18n.get("moveAlertHeader", source.size(), target.getPath()));
alert.getDialogPane()
.setContent(AppWindowHelper.alertContentText(getSelectedElementsString(source)));
alert.setAlertType(Alert.AlertType.CONFIRMATION);
})
.map(b -> b.getButtonData().isDefaultButton())
.orElse(false);
}
public static boolean showDeleteAlert(List<FileEntry> source) {
if (!AppPrefs.get().confirmDeletions().get()
&& source.stream().noneMatch(entry -> entry.getKind() == FileKind.DIRECTORY)) {
return true;
}
return AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("deleteAlertTitle"));
alert.setHeaderText(AppI18n.get("deleteAlertHeader", source.size()));
alert.getDialogPane()
.setContent(AppWindowHelper.alertContentText(getSelectedElementsString(source)));
alert.setAlertType(Alert.AlertType.CONFIRMATION);
})
.map(b -> b.getButtonData().isDefaultButton())
.orElse(false);
}
private static String getSelectedElementsString(List<FileEntry> source) {
var namesHeader = AppI18n.get("selectedElements");
var names = namesHeader + "\n"
+ source.stream()
.limit(10)
.map(entry -> "- " + new FilePath(entry.getPath()).getFileName())
.collect(Collectors.joining("\n"));
if (source.size() > 10) {
names += "\n+ " + (source.size() - 10) + " ...";
}
return names;
}
public enum FileConflictChoice {
CANCEL,
SKIP,
SKIP_ALL,
REPLACE,
REPLACE_ALL,
RENAME,
RENAME_ALL
}
}

View file

@ -1,90 +0,0 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.store.FileNames;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.util.Callback;
import atlantafx.base.controls.Breadcrumbs;
import java.util.ArrayList;
public class BrowserBreadcrumbBar extends SimpleComp {
private final BrowserFileSystemTabModel model;
public BrowserBreadcrumbBar(BrowserFileSystemTabModel model) {
this.model = model;
}
@Override
protected Region createSimple() {
Callback<Breadcrumbs.BreadCrumbItem<String>, ButtonBase> crumbFactory = crumb -> {
var name = crumb.getValue().equals("/") ? "/" : FileNames.getFileName(crumb.getValue());
var btn = new Button(name, null);
btn.setMnemonicParsing(false);
btn.setFocusTraversable(false);
return btn;
};
return createBreadcrumbs(crumbFactory, null);
}
private Region createBreadcrumbs(
Callback<Breadcrumbs.BreadCrumbItem<String>, ButtonBase> crumbFactory,
Callback<Breadcrumbs.BreadCrumbItem<String>, ? extends Node> dividerFactory) {
var breadcrumbs = new Breadcrumbs<String>();
breadcrumbs.setMinWidth(0);
PlatformThread.sync(model.getCurrentPath()).subscribe(val -> {
if (val == null) {
breadcrumbs.setSelectedCrumb(null);
return;
}
var sc = model.getFileSystem().getShell();
if (sc.isEmpty()) {
breadcrumbs.setDividerFactory(item -> item != null && !item.isLast() ? new Label("/") : null);
} else {
breadcrumbs.setDividerFactory(item -> {
if (item == null) {
return null;
}
if (item.isFirst() && item.getValue().equals("/")) {
return new Label("");
}
return new Label(sc.get().getOsType().getFileSystemSeparator());
});
}
var elements = FileNames.splitHierarchy(val);
var modifiedElements = new ArrayList<>(elements);
if (val.startsWith("/")) {
modifiedElements.addFirst("/");
}
Breadcrumbs.BreadCrumbItem<String> items =
Breadcrumbs.buildTreeModel(modifiedElements.toArray(String[]::new));
breadcrumbs.setSelectedCrumb(items);
});
if (crumbFactory != null) {
breadcrumbs.setCrumbFactory(crumbFactory);
}
if (dividerFactory != null) {
breadcrumbs.setDividerFactory(dividerFactory);
}
breadcrumbs.selectedCrumbProperty().addListener((obs, old, val) -> {
model.cdAsync(val != null ? val.getValue() : null);
});
return breadcrumbs;
}
}

View file

@ -1,136 +0,0 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.util.FailableRunnable;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DataFormat;
import javafx.scene.input.Dragboard;
import lombok.SneakyThrows;
import lombok.Value;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class BrowserClipboard {
public static final Property<Instance> currentCopyClipboard = new SimpleObjectProperty<>();
public static Instance currentDragClipboard;
private static final DataFormat DATA_FORMAT = new DataFormat("application/xpipe-file-list");
static {
Toolkit.getDefaultToolkit()
.getSystemClipboard()
.addFlavorListener(e -> ThreadHelper.runFailableAsync(new FailableRunnable<>() {
@Override
@SuppressWarnings("unchecked")
public void run() {
Clipboard clipboard = (Clipboard) e.getSource();
try {
if (!clipboard.isDataFlavorAvailable(DataFlavor.javaFileListFlavor)) {
return;
}
List<File> data = (List<File>) clipboard.getData(DataFlavor.javaFileListFlavor);
// Sometimes file data can contain invalid chars. Why?
var files = data.stream()
.filter(file ->
file.toString().chars().noneMatch(value -> Character.isISOControl(value)))
.map(f -> f.toPath())
.toList();
if (files.size() == 0) {
return;
}
var entries = new ArrayList<BrowserEntry>();
for (Path file : files) {
entries.add(BrowserLocalFileSystem.getLocalBrowserEntry(file));
}
currentCopyClipboard.setValue(
new Instance(UUID.randomUUID(), null, entries, BrowserFileTransferMode.COPY));
} catch (Exception e) {
ErrorEvent.fromThrowable(e).expected().omit().handle();
}
}
}));
}
@SneakyThrows
public static ClipboardContent startDrag(
FileEntry base, List<BrowserEntry> selected, BrowserFileTransferMode mode) {
if (selected.isEmpty()) {
return null;
}
var content = new ClipboardContent();
var id = UUID.randomUUID();
currentDragClipboard = new Instance(id, base, new ArrayList<>(selected), mode);
content.put(DATA_FORMAT, currentDragClipboard.toClipboardString());
return content;
}
@SneakyThrows
public static void startCopy(FileEntry base, List<BrowserEntry> selected) {
if (selected.isEmpty()) {
currentCopyClipboard.setValue(null);
return;
}
var id = UUID.randomUUID();
currentCopyClipboard.setValue(new Instance(id, base, new ArrayList<>(selected), BrowserFileTransferMode.COPY));
}
public static Instance retrieveCopy() {
return currentCopyClipboard.getValue();
}
public static Instance retrieveDrag(Dragboard dragboard) {
if (currentDragClipboard == null) {
return null;
}
try {
var s = dragboard.getContent(DATA_FORMAT);
if (s != null && s.equals(currentDragClipboard.toClipboardString())) {
var current = currentDragClipboard;
currentDragClipboard = null;
return current;
}
} catch (Exception ex) {
return null;
}
return null;
}
@Value
public static class Instance {
UUID uuid;
FileEntry baseDirectory;
List<BrowserEntry> entries;
BrowserFileTransferMode mode;
public String toClipboardString() {
return entries.stream()
.map(fileEntry -> "\"" + fileEntry.getRawFileEntry().getPath() + "\"")
.collect(Collectors.joining(ProcessControlProvider.get()
.getEffectiveLocalDialect()
.getNewLine()
.getNewLineString()));
}
}
}

View file

@ -1,95 +0,0 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.store.*;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.css.PseudoClass;
import javafx.scene.control.Button;
import javafx.scene.layout.Region;
import java.util.HashSet;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
public final class BrowserConnectionListComp extends SimpleComp {
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private final ObservableValue<DataStoreEntry> selected;
private final Predicate<StoreEntryWrapper> applicable;
private final BiConsumer<StoreEntryWrapper, BooleanProperty> action;
private final Property<StoreCategoryWrapper> category;
private final Property<String> filter;
public BrowserConnectionListComp(
ObservableValue<DataStoreEntry> selected,
Predicate<StoreEntryWrapper> applicable,
BiConsumer<StoreEntryWrapper, BooleanProperty> action,
Property<StoreCategoryWrapper> category,
Property<String> filter) {
this.selected = selected;
this.applicable = applicable;
this.action = action;
this.category = category;
this.filter = filter;
}
@Override
protected Region createSimple() {
var busyEntries = FXCollections.<StoreSection>observableSet(new HashSet<>());
BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment = (s, comp) -> {
comp.disable(Bindings.createBooleanBinding(
() -> {
return busyEntries.contains(s) || !applicable.test(s.getWrapper());
},
busyEntries));
comp.apply(struc -> {
selected.addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
struc.get()
.pseudoClassStateChanged(
SELECTED,
newValue != null
&& newValue.equals(
s.getWrapper().getEntry()));
});
});
});
};
var section = new StoreSectionMiniComp(
StoreSection.createTopLevel(
StoreViewState.get().getAllEntries(),
this::filter,
filter,
category,
StoreViewState.get().getEntriesListUpdateObservable()),
augment,
selectedAction -> {
BooleanProperty busy = new SimpleBooleanProperty(false);
action.accept(selectedAction.getWrapper(), busy);
busy.addListener((observable, oldValue, newValue) -> {
if (newValue) {
busyEntries.add(selectedAction);
} else {
busyEntries.remove(selectedAction);
}
});
});
var r = section.vgrow().createRegion();
r.getStyleClass().add("bookmark-list");
return r;
}
private boolean filter(StoreEntryWrapper w) {
return applicable.test(w);
}
}

View file

@ -1,60 +0,0 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.FilterComp;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.store.StoreCategoryWrapper;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.util.DataStoreCategoryChoiceComp;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.layout.Region;
import atlantafx.base.theme.Styles;
import lombok.Getter;
import java.util.List;
@Getter
public final class BrowserConnectionListFilterComp extends SimpleComp {
private final Property<StoreCategoryWrapper> category =
new SimpleObjectProperty<>(StoreViewState.get().getActiveCategory().getValue());
private final Property<String> filter = new SimpleStringProperty();
@Override
protected Region createSimple() {
var category = new DataStoreCategoryChoiceComp(
StoreViewState.get().getAllConnectionsCategory(),
StoreViewState.get().getActiveCategory(),
this.category)
.styleClass(Styles.LEFT_PILL)
.apply(struc -> {
AppFontSizes.base(struc.get());
});
var filter = new FilterComp(this.filter)
.styleClass(Styles.RIGHT_PILL)
.minWidth(0)
.hgrow()
.apply(struc -> {
AppFontSizes.base(struc.get());
});
var top = new HorizontalComp(List.of(category, filter))
.apply(struc -> struc.get().setFillHeight(true))
.apply(struc -> {
var first = ((Region) struc.get().getChildren().get(0));
var second = ((Region) struc.get().getChildren().get(1));
first.prefHeightProperty().bind(second.heightProperty());
first.minHeightProperty().bind(second.heightProperty());
first.maxHeightProperty().bind(second.heightProperty());
AppFontSizes.xl(struc.get());
})
.styleClass("bookmarks-header")
.createRegion();
return top;
}
}

View file

@ -1,85 +0,0 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.util.InputHelper;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.SeparatorMenuItem;
import java.util.ArrayList;
import java.util.List;
public final class BrowserContextMenu extends ContextMenu {
private final BrowserFileSystemTabModel model;
private final BrowserEntry source;
private final boolean quickAccess;
public BrowserContextMenu(BrowserFileSystemTabModel model, BrowserEntry source, boolean quickAccess) {
this.model = model;
this.source = source;
this.quickAccess = quickAccess;
createMenu();
}
private void createMenu() {
AppFontSizes.lg(getStyleableNode());
InputHelper.onLeft(this, false, e -> {
hide();
e.consume();
});
var empty = source == null;
var selected = new ArrayList<>(
empty
? List.of(new BrowserEntry(model.getCurrentDirectory(), model.getFileList()))
: quickAccess ? List.of() : model.getFileList().getSelection());
if (source != null && !selected.contains(source)) {
selected.add(source);
}
if (model.isClosed()) {
return;
}
for (BrowserAction.Category cat : BrowserAction.Category.values()) {
var all = BrowserAction.ALL.stream()
.filter(browserAction -> browserAction.getCategory() == cat)
.filter(browserAction -> {
if (model.isClosed()) {
return false;
}
var used = browserAction.resolveFilesIfNeeded(selected);
if (!browserAction.isApplicable(model, used)) {
return false;
}
if (!browserAction.acceptsEmptySelection() && empty) {
return false;
}
return true;
})
.toList();
if (all.size() == 0) {
continue;
}
if (getItems().size() > 0) {
getItems().add(new SeparatorMenuItem());
}
for (BrowserAction a : all) {
if (model.isClosed()) {
return;
}
var used = a.resolveFilesIfNeeded(selected);
getItems().add(a.toMenuItem(model, used));
}
}
}
}

View file

@ -1,79 +0,0 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.icon.BrowserIconDirectoryType;
import io.xpipe.app.browser.icon.BrowserIconFileType;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import lombok.Getter;
@Getter
public class BrowserEntry {
private final BrowserFileListModel model;
private final FileEntry rawFileEntry;
private final BrowserIconFileType fileType;
private final BrowserIconDirectoryType directoryType;
public BrowserEntry(FileEntry rawFileEntry, BrowserFileListModel model) {
this.rawFileEntry = rawFileEntry;
this.model = model;
this.fileType = fileType(rawFileEntry);
this.directoryType = directoryType(rawFileEntry);
}
private static BrowserIconFileType fileType(FileEntry rawFileEntry) {
if (rawFileEntry == null) {
return null;
}
rawFileEntry = rawFileEntry.resolved();
if (rawFileEntry.getKind() != FileKind.FILE) {
return null;
}
for (var f : BrowserIconFileType.getAll()) {
if (f.matches(rawFileEntry)) {
return f;
}
}
return null;
}
private static BrowserIconDirectoryType directoryType(FileEntry rawFileEntry) {
if (rawFileEntry == null) {
return null;
}
rawFileEntry = rawFileEntry.resolved();
if (rawFileEntry.getKind() != FileKind.DIRECTORY) {
return null;
}
for (var f : BrowserIconDirectoryType.getAll()) {
if (f.matches(rawFileEntry)) {
return f;
}
}
return null;
}
public String getIcon() {
if (fileType != null) {
return fileType.getIcon();
} else if (directoryType != null) {
return directoryType.getIcon(rawFileEntry);
} else {
return rawFileEntry != null && rawFileEntry.resolved().getKind() == FileKind.DIRECTORY
? "browser/default_folder.svg"
: "browser/default_file.svg";
}
}
public String getFileName() {
return FileNames.getFileName(getRawFileEntry().getPath());
}
}

View file

@ -1,639 +0,0 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.*;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileInfo;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Bounds;
import javafx.scene.control.*;
import javafx.scene.control.skin.TableViewSkin;
import javafx.scene.control.skin.VirtualFlow;
import javafx.scene.input.*;
import javafx.scene.layout.Region;
import atlantafx.base.theme.Styles;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import static io.xpipe.app.util.HumanReadableFormat.byteCount;
import static javafx.scene.control.TableColumn.SortType.ASCENDING;
public final class BrowserFileListComp extends SimpleComp {
private static final PseudoClass HIDDEN = PseudoClass.getPseudoClass("hidden");
private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
private static final PseudoClass FILE = PseudoClass.getPseudoClass("file");
private static final PseudoClass FOLDER = PseudoClass.getPseudoClass("folder");
private static final PseudoClass DRAG = PseudoClass.getPseudoClass("drag");
private static final PseudoClass DRAG_OVER = PseudoClass.getPseudoClass("drag-over");
private static final PseudoClass DRAG_INTO_CURRENT = PseudoClass.getPseudoClass("drag-into-current");
private final BrowserFileListModel fileList;
private final StringProperty typedSelection = new SimpleStringProperty("");
private final DoubleProperty ownerWidth = new SimpleDoubleProperty();
public BrowserFileListComp(BrowserFileListModel fileList) {
this.fileList = fileList;
}
@Override
protected Region createSimple() {
return createTable();
}
@SuppressWarnings("unchecked")
private TableView<BrowserEntry> createTable() {
var filenameCol = new TableColumn<BrowserEntry, String>();
filenameCol.textProperty().bind(AppI18n.observable("name"));
filenameCol.setCellValueFactory(param -> new SimpleStringProperty(
param.getValue() != null
? FileNames.getFileName(
param.getValue().getRawFileEntry().getPath())
: null));
filenameCol.setComparator(Comparator.comparing(String::toLowerCase));
filenameCol.setSortType(ASCENDING);
filenameCol.setCellFactory(col ->
new BrowserFileListNameCell(fileList, typedSelection, fileList.getEditing(), col.getTableView()));
filenameCol.setReorderable(false);
filenameCol.setResizable(false);
var sizeCol = new TableColumn<BrowserEntry, Number>();
sizeCol.textProperty().bind(AppI18n.observable("size"));
sizeCol.setCellValueFactory(param -> new SimpleLongProperty(
param.getValue().getRawFileEntry().resolved().getSize()));
sizeCol.setCellFactory(col -> new FileSizeCell());
sizeCol.setResizable(false);
sizeCol.setPrefWidth(120);
sizeCol.setReorderable(false);
var mtimeCol = new TableColumn<BrowserEntry, Instant>();
mtimeCol.textProperty().bind(AppI18n.observable("modified"));
mtimeCol.setCellValueFactory(param -> new SimpleObjectProperty<>(
param.getValue().getRawFileEntry().resolved().getDate()));
mtimeCol.setCellFactory(col -> new FileTimeCell());
mtimeCol.setResizable(false);
mtimeCol.setPrefWidth(150);
mtimeCol.setReorderable(false);
var modeCol = new TableColumn<BrowserEntry, String>();
modeCol.textProperty().bind(AppI18n.observable("attributes"));
modeCol.setCellValueFactory(param -> new SimpleObjectProperty<>(
param.getValue().getRawFileEntry().resolved().getInfo() instanceof FileInfo.Unix u
? u.getPermissions()
: null));
modeCol.setCellFactory(col -> new FileModeCell());
modeCol.setResizable(false);
modeCol.setPrefWidth(120);
modeCol.setSortable(false);
modeCol.setReorderable(false);
var ownerCol = new TableColumn<BrowserEntry, String>();
ownerCol.textProperty().bind(AppI18n.observable("owner"));
ownerCol.setCellValueFactory(param -> {
return new SimpleObjectProperty<>(formatOwner(param.getValue()));
});
ownerCol.setCellFactory(col -> new FileOwnerCell());
ownerCol.setSortable(false);
ownerCol.setReorderable(false);
ownerCol.setResizable(false);
var table = new TableView<BrowserEntry>();
table.setSkin(new TableViewSkin<>(table));
table.setAccessibleText("Directory contents");
table.setPlaceholder(new Region());
table.getStyleClass().add(Styles.STRIPED);
table.getColumns().setAll(filenameCol, mtimeCol, modeCol, ownerCol, sizeCol);
table.getSortOrder().add(filenameCol);
table.setFocusTraversable(true);
table.setSortPolicy(param -> {
fileList.setComparator(table.getComparator());
return true;
});
table.setFixedCellSize(30.0);
prepareColumnVisibility(table, ownerCol, filenameCol);
prepareTableScrollFix(table);
prepareTableSelectionModel(table);
prepareTableShortcuts(table);
prepareTableEntries(table);
prepareTableChanges(table, filenameCol, mtimeCol, modeCol, ownerCol);
prepareTypedSelectionModel(table);
return table;
}
private static void prepareTableScrollFix(TableView<BrowserEntry> table) {
table.lookupAll(".scroll-bar").stream()
.filter(node -> node.getPseudoClassStates().contains(PseudoClass.getPseudoClass("horizontal")))
.findFirst()
.ifPresent(node -> {
Region region = (Region) node;
region.setMinHeight(0);
region.setPrefHeight(0);
region.setMaxHeight(0);
});
}
private void prepareColumnVisibility(
TableView<BrowserEntry> table,
TableColumn<BrowserEntry, String> ownerCol,
TableColumn<BrowserEntry, String> filenameCol) {
var os = fileList.getFileSystemModel()
.getFileSystem()
.getShell()
.map(shellControl -> shellControl.getOsType())
.orElse(null);
table.widthProperty().subscribe((newValue) -> {
if (os != OsType.WINDOWS && os != OsType.MACOS) {
ownerCol.setVisible(newValue.doubleValue() > 1000);
}
var width = getFilenameWidth(table);
filenameCol.setPrefWidth(width);
});
}
private double getFilenameWidth(TableView<?> tableView) {
var sum = tableView.getColumns().stream()
.filter(tableColumn -> tableColumn.isVisible()
&& tableView.getColumns().indexOf(tableColumn) != 0)
.mapToDouble(value -> value.getPrefWidth())
.sum()
+ 7;
return tableView.getWidth() - sum;
}
private String formatOwner(BrowserEntry param) {
FileInfo.Unix unix = param.getRawFileEntry().resolved().getInfo() instanceof FileInfo.Unix u ? u : null;
if (unix == null) {
return null;
}
var m = fileList.getFileSystemModel();
var user = unix.getUser() != null
? unix.getUser()
: m.getCache().getUsers().getOrDefault(unix.getUid(), "?");
var group = unix.getGroup() != null
? unix.getGroup()
: m.getCache().getGroups().getOrDefault(unix.getGid(), "?");
var uid = String.valueOf(
unix.getUid() != null ? unix.getUid() : m.getCache().getUidForUser(user));
var gid = String.valueOf(
unix.getGid() != null ? unix.getGid() : m.getCache().getGidForGroup(group));
if (uid.equals(gid) && user.equals(group)) {
return user + " [" + uid + "]";
}
return user + " [" + uid + "] / " + group + " [" + gid + "]";
}
private void prepareTypedSelectionModel(TableView<BrowserEntry> table) {
AtomicReference<Instant> lastFail = new AtomicReference<>();
table.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
updateTypedSelection(table, lastFail, event, false);
});
table.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
typedSelection.set("");
lastFail.set(null);
});
fileList.getFileSystemModel().getCurrentPath().addListener((observable, oldValue, newValue) -> {
typedSelection.set("");
lastFail.set(null);
});
table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.ESCAPE) {
typedSelection.set("");
lastFail.set(null);
}
});
}
private void updateTypedSelection(
TableView<BrowserEntry> table, AtomicReference<Instant> lastType, KeyEvent event, boolean recursive) {
var typed = event.getText();
if (typed.isEmpty()) {
return;
}
var updated = typedSelection.get() + typed;
var found = fileList.getShown().getValue().stream()
.filter(browserEntry -> browserEntry.getFileName().toLowerCase().startsWith(updated.toLowerCase()))
.findFirst();
if (found.isEmpty()) {
if (typedSelection.get().isEmpty()) {
return;
}
var inCooldown = lastType.get() != null
&& Duration.between(lastType.get(), Instant.now()).toMillis() < 1000;
if (inCooldown) {
lastType.set(Instant.now());
event.consume();
} else {
lastType.set(null);
typedSelection.set("");
table.getSelectionModel().clearSelection();
if (!recursive) {
updateTypedSelection(table, lastType, event, true);
}
}
return;
}
lastType.set(Instant.now());
typedSelection.set(updated);
table.scrollTo(found.get());
table.getSelectionModel().clearAndSelect(fileList.getShown().getValue().indexOf(found.get()));
event.consume();
}
private void prepareTableSelectionModel(TableView<BrowserEntry> table) {
if (!fileList.getSelectionMode().isMultiple()) {
table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
} else {
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
}
table.getSelectionModel().setCellSelectionEnabled(false);
var updateFromModel = new BooleanScope(new SimpleBooleanProperty());
table.getSelectionModel().getSelectedItems().addListener((ListChangeListener<? super BrowserEntry>) c -> {
if (updateFromModel.get()) {
return;
}
try (var ignored = updateFromModel) {
// Attempt to preserve ordering. Works at least when selecting single entries
var existing = new HashSet<>(fileList.getSelection());
c.getList().forEach(browserEntry -> {
if (!existing.contains(browserEntry)) {
fileList.getSelection().add(browserEntry);
}
});
fileList.getSelection().removeIf(browserEntry -> !c.getList().contains(browserEntry));
}
});
fileList.getSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
var existing = new HashSet<>(table.getSelectionModel().getSelectedItems());
var toApply = new HashSet<>(c.getList());
if (existing.equals(toApply)) {
return;
}
Platform.runLater(() -> {
var tableIndices = table.getSelectionModel().getSelectedItems().stream()
.mapToInt(entry -> table.getItems().indexOf(entry))
.toArray();
var indices = c.getList().stream()
.mapToInt(entry -> table.getItems().indexOf(entry))
.toArray();
if (Arrays.equals(indices, tableIndices)) {
return;
}
if (indices.length == 0) {
table.getSelectionModel().clearSelection();
return;
}
if (indices.length == 1) {
table.getSelectionModel().clearAndSelect(indices[0]);
} else {
table.getSelectionModel().clearSelection();
table.getSelectionModel().selectIndices(indices[0], indices);
}
});
});
}
private void prepareTableShortcuts(TableView<BrowserEntry> table) {
table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
// Prevent post close events
if (fileList.getFileSystemModel().isClosed()) {
return;
}
var selected = fileList.getSelection();
var action = BrowserAction.getFlattened(fileList.getFileSystemModel(), selected).stream()
.filter(browserAction -> browserAction.isApplicable(fileList.getFileSystemModel(), selected)
&& browserAction.isActive(fileList.getFileSystemModel(), selected))
.filter(browserAction -> browserAction.getShortcut() != null)
.filter(browserAction -> browserAction.getShortcut().match(event))
.findAny();
action.ifPresent(browserAction -> {
// Prevent concurrent modification by creating copy on platform thread
var selectionCopy = new ArrayList<>(selected);
ThreadHelper.runFailableAsync(() -> {
browserAction.execute(fileList.getFileSystemModel(), selectionCopy);
});
event.consume();
});
if (action.isPresent()) {
return;
}
if (event.getCode() == KeyCode.ESCAPE) {
table.getSelectionModel().clearSelection();
event.consume();
}
});
}
private void prepareTableEntries(TableView<BrowserEntry> table) {
var emptyEntry = new BrowserFileListCompEntry(table, table, null, fileList);
table.setOnMouseClicked(e -> {
emptyEntry.onMouseClick(e);
});
table.setOnMouseDragEntered(event -> {
emptyEntry.onMouseDragEntered(event);
});
table.setOnDragOver(event -> {
emptyEntry.onDragOver(event);
});
table.setOnDragEntered(event -> {
emptyEntry.onDragEntered(event);
});
table.setOnDragDetected(event -> {
emptyEntry.startDrag(event);
});
table.setOnDragExited(event -> {
emptyEntry.onDragExited(event);
});
table.setOnDragDropped(event -> {
emptyEntry.onDragDrop(event);
});
table.setOnDragDone(event -> {
emptyEntry.onDragDone(event);
});
// Don't let the list view see this event
// otherwise it unselects everything as it doesn't understand shift clicks
table.addEventFilter(MouseEvent.MOUSE_CLICKED, t -> {
if (t.getButton() == MouseButton.PRIMARY && t.isShiftDown() && t.getClickCount() == 1) {
t.consume();
}
});
table.setRowFactory(param -> {
TableRow<BrowserEntry> row = new TableRow<>();
row.accessibleTextProperty()
.bind(Bindings.createStringBinding(
() -> {
return row.getItem() != null ? row.getItem().getFileName() : null;
},
row.itemProperty()));
row.focusTraversableProperty()
.bind(Bindings.createBooleanBinding(
() -> {
return row.getItem() != null;
},
row.itemProperty()));
var listEntry = Bindings.createObjectBinding(
() -> new BrowserFileListCompEntry(table, row, row.getItem(), fileList), row.itemProperty());
// Don't let the list view see this event
// otherwise it unselects everything as it doesn't understand shift clicks
row.addEventFilter(MouseEvent.MOUSE_PRESSED, t -> {
if (t.getButton() == MouseButton.PRIMARY && t.isShiftDown()) {
listEntry.get().onMouseShiftClick(t);
}
});
row.itemProperty().addListener((observable, oldValue, newValue) -> {
row.pseudoClassStateChanged(DRAG, false);
row.pseudoClassStateChanged(DRAG_OVER, false);
});
row.itemProperty().addListener((observable, oldValue, newValue) -> {
row.pseudoClassStateChanged(EMPTY, newValue == null);
row.pseudoClassStateChanged(
FILE, newValue != null && newValue.getRawFileEntry().getKind() != FileKind.DIRECTORY);
row.pseudoClassStateChanged(
FOLDER, newValue != null && newValue.getRawFileEntry().getKind() == FileKind.DIRECTORY);
});
fileList.getDraggedOverDirectory().addListener((observable, oldValue, newValue) -> {
row.pseudoClassStateChanged(DRAG_OVER, newValue != null && newValue == row.getItem());
});
fileList.getDraggedOverEmpty().addListener((observable, oldValue, newValue) -> {
table.pseudoClassStateChanged(DRAG_INTO_CURRENT, newValue);
});
row.setOnMouseClicked(e -> {
listEntry.get().onMouseClick(e);
});
row.setOnMouseDragEntered(event -> {
listEntry.get().onMouseDragEntered(event);
});
row.setOnDragEntered(event -> {
listEntry.get().onDragEntered(event);
});
row.setOnDragOver(event -> {
borderScroll(table, event);
listEntry.get().onDragOver(event);
});
row.setOnDragDetected(event -> {
listEntry.get().startDrag(event);
});
row.setOnDragExited(event -> {
listEntry.get().onDragExited(event);
});
row.setOnDragDropped(event -> {
listEntry.get().onDragDrop(event);
});
row.setOnDragDone(event -> {
listEntry.get().onDragDone(event);
});
return row;
});
}
private void prepareTableChanges(
TableView<BrowserEntry> table,
TableColumn<BrowserEntry, String> filenameCol,
TableColumn<BrowserEntry, Instant> mtimeCol,
TableColumn<BrowserEntry, String> modeCol,
TableColumn<BrowserEntry, String> ownerCol) {
var lastDir = new SimpleObjectProperty<FileEntry>();
Runnable updateHandler = () -> {
PlatformThread.runLaterIfNeeded(() -> {
var newItems = new ArrayList<>(fileList.getShown().getValue());
table.getItems().clear();
var hasModifiedDate = newItems.size() == 0
|| newItems.stream()
.anyMatch(entry -> entry.getRawFileEntry().getDate() != null);
if (!hasModifiedDate) {
mtimeCol.setVisible(false);
} else {
mtimeCol.setVisible(true);
}
ownerWidth.set(fileList.getAll().getValue().stream()
.map(browserEntry -> formatOwner(browserEntry))
.map(s -> s != null ? s.length() * 9 : 0)
.max(Comparator.naturalOrder())
.orElse(150));
ownerCol.setPrefWidth(ownerWidth.get());
if (fileList.getFileSystemModel().getFileSystem() != null) {
var shell = fileList.getFileSystemModel()
.getFileSystem()
.getShell()
.orElseThrow();
if (OsType.WINDOWS.equals(shell.getOsType()) || OsType.MACOS.equals(shell.getOsType())) {
modeCol.setVisible(false);
ownerCol.setVisible(false);
} else {
modeCol.setVisible(true);
if (table.getWidth() > 1000) {
ownerCol.setVisible(true);
}
}
}
// Sort the list ourselves as sorting the table would incur a lot of cell updates
var obs = FXCollections.observableList(newItems);
table.getItems().setAll(obs);
var width = getFilenameWidth(table);
filenameCol.setPrefWidth(width);
TableViewSkin<?> skin = (TableViewSkin<?>) table.getSkin();
var currentDirectory = fileList.getFileSystemModel().getCurrentDirectory();
if (skin != null && !Objects.equals(lastDir.get(), currentDirectory)) {
VirtualFlow<?> flow = (VirtualFlow<?>) skin.getChildren().get(1);
ScrollBar vbar = (ScrollBar) flow.getChildrenUnmodifiable().get(2);
if (vbar.getValue() != 0.0) {
table.scrollTo(0);
}
}
lastDir.setValue(currentDirectory);
});
};
updateHandler.run();
fileList.getShown().addListener((observable, oldValue, newValue) -> {
// Delay to prevent internal tableview exceptions when sorting
Platform.runLater(updateHandler);
});
fileList.getFileSystemModel().getCurrentPath().addListener((observable, oldValue, newValue) -> {
if (oldValue == null) {
updateHandler.run();
}
});
}
private void borderScroll(TableView<?> tableView, DragEvent event) {
TableViewSkin<?> skin = (TableViewSkin<?>) tableView.getSkin();
if (skin == null) {
return;
}
VirtualFlow<?> flow = (VirtualFlow<?>) skin.getChildren().get(1);
ScrollBar vbar = (ScrollBar) flow.getChildrenUnmodifiable().get(2);
if (!vbar.isVisible()) {
return;
}
double proximity = 100;
Bounds tableBounds = tableView.localToScene(tableView.getBoundsInLocal());
double dragY = event.getSceneY();
// Include table header as well in calculations
double topYProximity = tableBounds.getMinY() + proximity + 20;
double bottomYProximity = tableBounds.getMaxY() - proximity;
// clamp new values between 0 and 1 to prevent scrollbar flicking around at the edges
if (dragY < topYProximity) {
var scrollValue = Math.min(topYProximity - dragY, 100) / 10000.0;
vbar.setValue(Math.max(vbar.getValue() - scrollValue, 0));
} else if (dragY > bottomYProximity) {
var scrollValue = Math.min(dragY - bottomYProximity, 100) / 10000.0;
vbar.setValue(Math.min(vbar.getValue() + scrollValue, 1.0));
}
}
private static class FileSizeCell extends TableCell<BrowserEntry, Number> {
@Override
protected void updateItem(Number fileSize, boolean empty) {
super.updateItem(fileSize, empty);
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
setText(null);
} else {
var path = getTableRow().getItem();
if (path.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
setText("");
} else {
setText(byteCount(fileSize.longValue()));
}
}
}
}
private static class FileModeCell extends TableCell<BrowserEntry, String> {
@Override
protected void updateItem(String mode, boolean empty) {
super.updateItem(mode, empty);
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
setText(null);
} else {
setText(mode);
}
}
}
private static class FileOwnerCell extends TableCell<BrowserEntry, String> {
public FileOwnerCell() {
setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
}
@Override
protected void updateItem(String owner, boolean empty) {
super.updateItem(owner, empty);
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
setText(null);
} else {
setText(owner);
}
}
}
private static class FileTimeCell extends TableCell<BrowserEntry, Instant> {
@Override
protected void updateItem(Instant fileTime, boolean empty) {
super.updateItem(fileTime, empty);
if (empty) {
setText(null);
} else {
setText(
fileTime != null
? HumanReadableFormat.date(
fileTime.atZone(ZoneId.systemDefault()).toLocalDateTime())
: "");
}
}
}
}

View file

@ -1,338 +0,0 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileKind;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.TableView;
import javafx.scene.image.Image;
import javafx.scene.input.*;
import lombok.Getter;
import java.io.File;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Timer;
import java.util.TimerTask;
@Getter
public class BrowserFileListCompEntry {
public static final Timer DROP_TIMER = new Timer("dnd", true);
private final TableView<BrowserEntry> tv;
private final Node row;
private final BrowserEntry item;
private final BrowserFileListModel model;
private Point2D lastOver = new Point2D(-1, -1);
private TimerTask activeTask;
private ContextMenu lastContextMenu;
public BrowserFileListCompEntry(
TableView<BrowserEntry> tv, Node row, BrowserEntry item, BrowserFileListModel model) {
this.tv = tv;
this.row = row;
this.item = item;
this.model = model;
}
public void onMouseClick(MouseEvent t) {
if (lastContextMenu != null) {
lastContextMenu.hide();
lastContextMenu = null;
}
if (showContextMenu(t)) {
var cm = new BrowserContextMenu(model.getFileSystemModel(), item, false);
cm.show(row, t.getScreenX(), t.getScreenY());
lastContextMenu = cm;
t.consume();
return;
}
if (t.getButton() == MouseButton.BACK) {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(model.getFileSystemModel().getBusy(), () -> {
model.getFileSystemModel().backSync(1);
});
});
t.consume();
return;
}
if (t.getButton() == MouseButton.FORWARD) {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(model.getFileSystemModel().getBusy(), () -> {
model.getFileSystemModel().forthSync(1);
});
});
t.consume();
return;
}
if (item == null) {
// Only clear for normal clicks
if (t.isStillSincePress()) {
model.getSelection().clear();
if (tv != null) {
tv.requestFocus();
}
}
t.consume();
return;
}
row.requestFocus();
if (t.getClickCount() == 2 && t.getButton() == MouseButton.PRIMARY) {
model.onDoubleClick(item);
t.consume();
}
t.consume();
}
private boolean showContextMenu(MouseEvent event) {
if (item == null) {
return event.getButton() == MouseButton.SECONDARY;
}
if (item.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
return event.getButton() == MouseButton.SECONDARY;
}
if (item.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY) {
return event.getButton() == MouseButton.SECONDARY
|| !AppPrefs.get().editFilesWithDoubleClick().get()
&& event.getButton() == MouseButton.PRIMARY
&& event.getClickCount() == 2;
}
return false;
}
public void onMouseShiftClick(MouseEvent t) {
if (t.getButton() != MouseButton.PRIMARY) {
return;
}
var all = tv.getItems();
var index = item != null ? all.indexOf(item) : all.size() - 1;
var min = Math.min(
index,
tv.getSelectionModel().getSelectedIndices().stream()
.mapToInt(value -> value)
.min()
.orElse(1));
var max = Math.max(
index,
tv.getSelectionModel().getSelectedIndices().stream()
.mapToInt(value -> value)
.max()
.orElse(all.indexOf(item)));
var toSelect = new ArrayList<BrowserEntry>();
for (int i = min; i <= max; i++) {
if (!model.getSelection().contains(model.getShown().getValue().get(i))) {
toSelect.add(model.getShown().getValue().get(i));
}
}
model.getSelection().addAll(toSelect);
t.consume();
}
private boolean acceptsDrop(DragEvent event) {
// Accept drops from outside the app window
if (event.getGestureSource() == null) {
return true;
}
BrowserClipboard.Instance cb = BrowserClipboard.currentDragClipboard;
if (cb == null) {
return false;
}
if (model.getFileSystemModel().getCurrentDirectory() == null) {
return false;
}
if (!Objects.equals(
model.getFileSystemModel().getFileSystem(),
cb.getEntries().getFirst().getRawFileEntry().getFileSystem())) {
return true;
}
// Prevent drag and drops of files into the current directory
if (cb.getBaseDirectory() != null
&& cb.getBaseDirectory()
.getPath()
.equals(model.getFileSystemModel().getCurrentDirectory().getPath())
&& (item == null || item.getRawFileEntry().getKind() != FileKind.DIRECTORY)) {
return false;
}
// Prevent dropping items onto themselves
if (item != null && cb.getEntries().contains(item)) {
return false;
}
return true;
}
public void onDragDrop(DragEvent event) {
model.getDraggedOverEmpty().setValue(false);
model.getDraggedOverDirectory().setValue(null);
// Accept drops from outside the app window
if (event.getGestureSource() == null && event.getDragboard().hasFiles()) {
Dragboard db = event.getDragboard();
var list = db.getFiles().stream().map(File::toPath).toList();
var target = item != null && item.getRawFileEntry().getKind() == FileKind.DIRECTORY
? item.getRawFileEntry()
: model.getFileSystemModel().getCurrentDirectory();
model.getFileSystemModel().dropLocalFilesIntoAsync(target, list);
event.setDropCompleted(true);
event.consume();
}
// Accept drops from inside the app window
if (event.getGestureSource() != null) {
var db = BrowserClipboard.retrieveDrag(event.getDragboard());
if (db == null) {
return;
}
var files = db.getEntries();
var target = item != null && item.getRawFileEntry().getKind() == FileKind.DIRECTORY
? item.getRawFileEntry()
: model.getFileSystemModel().getCurrentDirectory();
model.getFileSystemModel()
.dropFilesIntoAsync(
target,
files.stream()
.map(browserEntry -> browserEntry.getRawFileEntry())
.toList(),
db.getMode());
event.setDropCompleted(true);
event.consume();
}
}
public void onDragExited(DragEvent event) {
if (item != null && item.getRawFileEntry().getKind() == FileKind.DIRECTORY) {
model.getDraggedOverDirectory().setValue(null);
} else {
model.getDraggedOverEmpty().setValue(false);
}
event.consume();
}
public void startDrag(MouseEvent event) {
if (item == null) {
return;
}
if (event.getButton() != MouseButton.PRIMARY) {
return;
}
if (model.getFileSystemModel().getBrowserModel() instanceof BrowserFullSessionModel sessionModel) {
sessionModel.getDraggingFiles().setValue(true);
}
var selected = model.getSelection();
Dragboard db = row.startDragAndDrop(TransferMode.COPY);
db.setContent(BrowserClipboard.startDrag(
model.getFileSystemModel().getCurrentDirectory(),
selected,
event.isAltDown() ? BrowserFileTransferMode.MOVE : BrowserFileTransferMode.NORMAL));
Image image = BrowserFileSelectionListComp.snapshot(selected);
db.setDragView(image, -20, 15);
event.setDragDetect(true);
event.consume();
}
public void onDragDone(DragEvent event) {
if (model.getFileSystemModel().getBrowserModel() instanceof BrowserFullSessionModel sessionModel) {
sessionModel.getDraggingFiles().setValue(false);
event.consume();
}
}
private void acceptDrag(DragEvent event) {
model.getDraggedOverEmpty()
.setValue(item == null || item.getRawFileEntry().getKind() != FileKind.DIRECTORY);
model.getDraggedOverDirectory().setValue(item);
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
}
private void handleHoverTimer(DragEvent event) {
if (item == null || item.getRawFileEntry().getKind() != FileKind.DIRECTORY) {
return;
}
if (lastOver.getX() == event.getX() && lastOver.getY() == event.getY()) {
return;
}
lastOver = (new Point2D(event.getX(), event.getY()));
activeTask = new TimerTask() {
@Override
public void run() {
if (activeTask != this) {
return;
}
if (item != model.getDraggedOverDirectory().getValue()) {
return;
}
model.getFileSystemModel().cdAsync(item.getRawFileEntry().getPath());
}
};
DROP_TIMER.schedule(activeTask, 1200);
}
public void onDragEntered(DragEvent event) {
event.consume();
if (!acceptsDrop(event)) {
return;
}
acceptDrag(event);
}
@SuppressWarnings("unchecked")
public void onMouseDragEntered(MouseDragEvent event) {
event.consume();
if (model.getFileSystemModel().getCurrentDirectory() == null) {
return;
}
if (item == null) {
return;
}
var tv = ((TableView<BrowserEntry>)
row.getParent().getParent().getParent().getParent());
tv.getSelectionModel().select(item);
}
public void onDragOver(DragEvent event) {
event.consume();
if (!acceptsDrop(event)) {
return;
}
acceptDrag(event);
handleHoverTimer(event);
}
}

View file

@ -1,129 +0,0 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.comp.base.TooltipAugment;
import io.xpipe.app.util.InputHelper;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.HBox;
import atlantafx.base.theme.Styles;
import org.kordamp.ikonli.javafx.FontIcon;
public class BrowserFileListFilterComp extends Comp<BrowserFileListFilterComp.Structure> {
private final BrowserFileSystemTabModel model;
private final Property<String> filterString;
public BrowserFileListFilterComp(BrowserFileSystemTabModel model, Property<String> filterString) {
this.model = model;
this.filterString = filterString;
}
@Override
public Structure createBase() {
var expanded = new SimpleBooleanProperty();
var text = new TextFieldComp(filterString, false).createStructure().get();
var button = new Button();
button.minWidthProperty().bind(button.heightProperty());
button.setFocusTraversable(true);
InputHelper.onExactKeyCode(text, KeyCode.ESCAPE, true, keyEvent -> {
if (!expanded.get()) {
return;
}
text.clear();
button.fire();
keyEvent.consume();
});
new TooltipAugment<>("app.search", new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN))
.augment(button);
text.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue && filterString.getValue() == null) {
if (button.isFocused()) {
return;
}
expanded.set(false);
}
});
filterString.addListener((observable, oldValue, newValue) -> {
if (newValue == null && !text.isFocused()) {
expanded.set(false);
}
});
text.setMinWidth(0);
Styles.toggleStyleClass(text, Styles.LEFT_PILL);
filterString.subscribe(val -> {
if (val == null) {
text.getStyleClass().remove(Styles.SUCCESS);
} else {
text.getStyleClass().add(Styles.SUCCESS);
}
});
var fi = new FontIcon("mdi2m-magnify");
button.setGraphic(fi);
button.setOnAction(event -> {
if (model.getCurrentDirectory() == null) {
return;
}
if (expanded.get()) {
if (filterString.getValue() == null) {
expanded.set(false);
}
event.consume();
} else {
expanded.set(true);
text.requestFocus();
event.consume();
}
});
var box = new HBox(text, button);
box.getStyleClass().add("browser-filter");
box.setAlignment(Pos.CENTER);
text.setPrefWidth(0);
text.setFocusTraversable(false);
button.getStyleClass().add(Styles.FLAT);
button.disableProperty().bind(model.getInOverview());
expanded.addListener((observable, oldValue, val) -> {
if (val) {
text.setPrefWidth(250);
text.setFocusTraversable(true);
button.getStyleClass().add(Styles.RIGHT_PILL);
button.getStyleClass().remove(Styles.FLAT);
} else {
text.setPrefWidth(0);
text.setFocusTraversable(false);
button.getStyleClass().remove(Styles.RIGHT_PILL);
button.getStyleClass().add(Styles.FLAT);
}
});
button.minHeightProperty().bind(text.heightProperty());
button.minWidthProperty().bind(text.heightProperty());
button.maxHeightProperty().bind(text.heightProperty());
button.maxWidthProperty().bind(text.heightProperty());
return new Structure(box, text, button);
}
public record Structure(HBox box, TextField textField, Button toggleButton) implements CompStructure<HBox> {
@Override
public HBox get() {
return box;
}
}
}

View file

@ -1,158 +0,0 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
import java.util.*;
import java.util.stream.Stream;
@Getter
public final class BrowserFileListModel {
static final Comparator<BrowserEntry> FILE_TYPE_COMPARATOR =
Comparator.comparing(path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
private final BrowserFileSystemTabModel.SelectionMode selectionMode;
private final BrowserFileSystemTabModel fileSystemModel;
private final Property<Comparator<BrowserEntry>> comparatorProperty =
new SimpleObjectProperty<>(FILE_TYPE_COMPARATOR);
private final Property<List<BrowserEntry>> all = new SimpleObjectProperty<>(new ArrayList<>());
private final Property<List<BrowserEntry>> shown = new SimpleObjectProperty<>(new ArrayList<>());
private final ObservableList<BrowserEntry> selection = FXCollections.observableArrayList();
private final Property<BrowserEntry> draggedOverDirectory = new SimpleObjectProperty<>();
private final Property<Boolean> draggedOverEmpty = new SimpleBooleanProperty();
private final Property<BrowserEntry> editing = new SimpleObjectProperty<>();
public BrowserFileListModel(
BrowserFileSystemTabModel.SelectionMode selectionMode, BrowserFileSystemTabModel fileSystemModel) {
this.selectionMode = selectionMode;
this.fileSystemModel = fileSystemModel;
fileSystemModel.getFilter().addListener((observable, oldValue, newValue) -> {
refreshShown();
});
}
public void setAll(Stream<FileEntry> newFiles) {
try (var s = newFiles) {
var l = s.filter(entry -> entry != null)
.map(entry -> new BrowserEntry(entry, this))
.toList();
all.setValue(l);
refreshShown();
}
}
public void setComparator(Comparator<BrowserEntry> comparator) {
comparatorProperty.setValue(comparator);
refreshShown();
}
private void refreshShown() {
List<BrowserEntry> filtered = fileSystemModel.getFilter().getValue() != null
? all.getValue().stream()
.filter(entry -> {
var name = FileNames.getFileName(
entry.getRawFileEntry().getPath())
.toLowerCase(Locale.ROOT);
var filterString =
fileSystemModel.getFilter().getValue().toLowerCase(Locale.ROOT);
return name.contains(filterString);
})
.toList()
: all.getValue();
var listCopy = new ArrayList<>(filtered);
listCopy.sort(order());
shown.setValue(listCopy);
}
public Comparator<BrowserEntry> order() {
var dirsFirst = Comparator.<BrowserEntry, Boolean>comparing(
path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
var comp = comparatorProperty.getValue();
Comparator<BrowserEntry> us = comp != null ? dirsFirst.thenComparing(comp) : dirsFirst;
return us;
}
public BrowserEntry rename(BrowserEntry old, String newName) {
if (old == null
|| newName == null
|| fileSystemModel == null
|| fileSystemModel.isClosed()
|| fileSystemModel.getCurrentPath().get() == null) {
return old;
}
var fullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), old.getFileName());
var newFullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), newName);
// This check will fail on case-insensitive file systems when changing the case of the file
// So skip it in this case
var skipExistCheck =
fileSystemModel.getFileSystem().getShell().orElseThrow().getOsType() == OsType.WINDOWS
&& old.getFileName().equalsIgnoreCase(newName);
if (!skipExistCheck) {
boolean exists;
try {
exists = fileSystemModel.getFileSystem().fileExists(newFullPath)
|| fileSystemModel.getFileSystem().directoryExists(newFullPath);
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
return old;
}
if (exists) {
ErrorEvent.fromMessage("Target " + newFullPath + " does already exist")
.expected()
.handle();
fileSystemModel.refresh();
return old;
}
}
try {
fileSystemModel.getFileSystem().move(fullPath, newFullPath);
fileSystemModel.refresh();
var b = all.getValue().stream()
.filter(browserEntry ->
browserEntry.getRawFileEntry().getPath().equals(newFullPath))
.findFirst()
.orElse(old);
return b;
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
return old;
}
}
public void onDoubleClick(BrowserEntry entry) {
var r = entry.getRawFileEntry().resolved();
if (r.getKind() == FileKind.DIRECTORY) {
fileSystemModel.cdAsync(r.getPath());
}
if (AppPrefs.get().editFilesWithDoubleClick().get() && r.getKind() == FileKind.FILE) {
var selection = new LinkedHashSet<>(getSelection());
selection.add(entry);
for (BrowserEntry e : selection) {
BrowserFileOpener.openInTextEditor(getFileSystemModel(), e.getRawFileEntry());
}
}
}
}

View file

@ -1,202 +0,0 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.comp.base.LazyTextFieldComp;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ContextMenuHelper;
import io.xpipe.app.util.InputHelper;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileKind;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableStringValue;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.AccessibleRole;
import javafx.scene.Node;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import atlantafx.base.controls.Spacer;
class BrowserFileListNameCell extends TableCell<BrowserEntry, String> {
private final BrowserFileListModel fileList;
private final ObservableStringValue typedSelection;
private final StringProperty img = new SimpleStringProperty();
private final StringProperty text = new SimpleStringProperty();
private final BooleanProperty updating = new SimpleBooleanProperty();
public BrowserFileListNameCell(
BrowserFileListModel fileList,
ObservableStringValue typedSelection,
Property<BrowserEntry> editing,
TableView<BrowserEntry> tableView) {
this.fileList = fileList;
this.typedSelection = typedSelection;
accessibleTextProperty()
.bind(Bindings.createStringBinding(
() -> {
return getItem() != null ? getItem() : null;
},
itemProperty()));
setAccessibleRole(AccessibleRole.TEXT);
var textField = new LazyTextFieldComp(text)
.minWidth(USE_PREF_SIZE)
.createStructure()
.get();
var quickAccess = createQuickAccessButton();
setupShortcuts(tableView, (ButtonBase) quickAccess);
setupRename(fileList, textField, editing);
Node imageView = PrettyImageHelper.ofFixedSize(img, 24, 24).createRegion();
HBox graphic = new HBox(imageView, new Spacer(5), quickAccess, new Spacer(1), textField);
quickAccess.prefHeightProperty().bind(graphic.heightProperty());
graphic.setAlignment(Pos.CENTER_LEFT);
graphic.setPrefHeight(34);
HBox.setHgrow(textField, Priority.ALWAYS);
graphic.setAlignment(Pos.CENTER_LEFT);
setGraphic(graphic);
}
private Region createQuickAccessButton() {
var quickAccess = new BrowserQuickAccessButtonComp(() -> getTableRow().getItem(), fileList.getFileSystemModel())
.hide(Bindings.createBooleanBinding(
() -> {
if (getTableRow() == null) {
return true;
}
var item = getTableRow().getItem();
var notDir = item.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY;
var isParentLink = item.getRawFileEntry()
.equals(fileList.getFileSystemModel().getCurrentParentDirectory());
return notDir || isParentLink;
},
itemProperty()))
.focusTraversable(false)
.createRegion();
return quickAccess;
}
private void setupShortcuts(TableView<BrowserEntry> tableView, ButtonBase quickAccess) {
InputHelper.onExactKeyCode(tableView, KeyCode.RIGHT, false, event -> {
var selected = fileList.getSelection();
if (selected.size() == 1 && selected.getFirst() == getTableRow().getItem()) {
quickAccess.fire();
event.consume();
}
});
InputHelper.onExactKeyCode(tableView, KeyCode.SPACE, true, event -> {
var selection = typedSelection.get() + " ";
var found = fileList.getShown().getValue().stream()
.filter(browserEntry ->
browserEntry.getFileName().toLowerCase().startsWith(selection))
.findFirst();
// Ugly fix to prevent space from showing the menu when there is a file matching
// Due to the table view input map, these events always get sent and consumed, not allowing us to
// differentiate between these cases
if (found.isPresent()) {
return;
}
var selected = fileList.getSelection();
// Only show one menu across all selected entries
if (selected.size() > 0 && selected.getLast() == getTableRow().getItem()) {
var cm = new BrowserContextMenu(
fileList.getFileSystemModel(), getTableRow().getItem(), false);
ContextMenuHelper.toggleShow(cm, this, Side.RIGHT);
event.consume();
}
});
}
private void setupRename(BrowserFileListModel fileList, TextField textField, Property<BrowserEntry> editing) {
ChangeListener<String> listener = (observable, oldValue, newValue) -> {
if (updating.get()) {
return;
}
getTableRow().requestFocus();
var it = getTableRow().getItem();
editing.setValue(null);
ThreadHelper.runAsync(() -> {
if (it == null) {
return;
}
var r = fileList.rename(it, newValue);
Platform.runLater(() -> {
updateItem(getItem(), isEmpty());
fileList.getSelection().setAll(r);
getTableView().scrollTo(r);
});
});
};
text.addListener(listener);
editing.addListener((observable, oldValue, newValue) -> {
if (getTableRow().getItem() != null && getTableRow().getItem().equals(newValue)) {
PlatformThread.runLaterIfNeeded(() -> {
textField.setDisable(false);
textField.requestFocus();
});
}
});
}
@Override
protected void updateItem(String newName, boolean empty) {
if (updating.get()) {
super.updateItem(newName, empty);
return;
}
try (var ignored = new BooleanScope(updating).start()) {
super.updateItem(newName, empty);
if (empty || newName == null || getTableRow().getItem() == null) {
// Don't set image as that would trigger image comp update
// and cells are emptied on each change, leading to unnecessary changes
// img.set(null);
// Visibility seems to be bugged, so use opacity
setOpacity(0.0);
} else {
img.set(getTableRow().getItem().getIcon());
var isDirectory = getTableRow().getItem().getRawFileEntry().getKind() == FileKind.DIRECTORY;
pseudoClassStateChanged(PseudoClass.getPseudoClass("folder"), isDirectory);
var normalName = getTableRow().getItem().getRawFileEntry().getKind() == FileKind.LINK
? getTableRow().getItem().getFileName() + " -> "
+ getTableRow()
.getItem()
.getRawFileEntry()
.resolved()
.getPath()
: getTableRow().getItem().getFileName();
var fileName = normalName;
var hidden = getTableRow().getItem().getRawFileEntry().getInfo().explicitlyHidden()
|| fileName.startsWith(".");
getTableRow().pseudoClassStateChanged(PseudoClass.getPseudoClass("hidden"), hidden);
text.set(fileName);
// Visibility seems to be bugged, so use opacity
setOpacity(1.0);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more