diff --git a/.gitattributes b/.gitattributes index 22b8a97e9..ff4558cd2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,4 +2,4 @@ *.sh text eol=lf *.bat text eol=crlf *.png binary -*.xcf binary \ No newline at end of file +*.xcf binary diff --git a/.gitignore b/.gitignore index 596a2e7c9..65de11c7f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,11 +7,14 @@ lib/ dev.properties extensions.txt dev_storage -local*/ +local/ +local_*/ .vs .vscode obj out bin .DS_Store -ComponentsGenerated.wxs \ No newline at end of file +ComponentsGenerated.wxs +!dist/javafx/**/lib +!dist/javafx/**/bin diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d6aa7ecec..d59365a58 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,7 +18,7 @@ There are no real formal contribution guidelines right now, they will maybe come All XPipe components target [Java 21](https://openjdk.java.net/projects/jdk/20/) 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). +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. diff --git a/README.md b/README.md index a495682ec..e4eab87a9 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ Alternatively, you can also use [Homebrew](https://github.com/xpipe-io/homebrew- XPipe utilizes an open core model, which essentially means that the main application is open source while certain other components are not. Select parts are not open source yet, but may be added to this repository in the future. -This mainly concerns the features only available in the professional tier and the shell handling library implementation. Furthermore, some tests and especially test environments and that run on private servers are also not included in this repository. +This mainly concerns the features only available in the professional tier and the shell handling library implementation. Furthermore, some CI pipelines and tests that run on private servers are also not included in this repository. ## More links diff --git a/api/build.gradle b/api/build.gradle index 9d164d5b0..3dc3ea3dc 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -2,15 +2,11 @@ plugins { id 'java-library' id 'maven-publish' id 'signing' - id "org.moditect.gradleplugin" version "1.0.0-rc3" } apply from: "$rootDir/gradle/gradle_scripts/java.gradle" apply from: "$rootDir/gradle/gradle_scripts/junit.gradle" -System.setProperty('excludeExtensionLibrary', 'true') -apply from: "$rootDir/gradle/gradle_scripts/extension_test.gradle" - version = rootProject.versionString group = 'io.xpipe' archivesBaseName = 'xpipe-api' @@ -19,14 +15,14 @@ repositories { mavenCentral() } -test { - enabled = false +dependencies { + testImplementation project(':api') } dependencies { api project(':core') implementation project(':beacon') - implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.15.2" + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.16.1" } configurations { @@ -38,6 +34,5 @@ task dist(type: Copy) { into "${project(':dist').buildDir}/dist/libraries" } - apply from: 'publish.gradle' apply from: "$rootDir/gradle/gradle_scripts/publish-base.gradle" \ No newline at end of file diff --git a/api/src/test/java/io/xpipe/api/test/DataTableAccumulatorTest.java b/api/src/test/java/io/xpipe/api/test/DataTableAccumulatorTest.java deleted file mode 100644 index 98553916d..000000000 --- a/api/src/test/java/io/xpipe/api/test/DataTableAccumulatorTest.java +++ /dev/null @@ -1,29 +0,0 @@ -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); - } -} diff --git a/api/src/test/java/io/xpipe/api/test/DataTableTest.java b/api/src/test/java/io/xpipe/api/test/DataTableTest.java deleted file mode 100644 index 272c7b252..000000000 --- a/api/src/test/java/io/xpipe/api/test/DataTableTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.xpipe.api.test; - -import io.xpipe.api.DataSource; -import io.xpipe.core.store.DataStoreId; -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( - DataStoreId.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; - } -} diff --git a/api/src/test/java/io/xpipe/api/test/StartupTest.java b/api/src/test/java/io/xpipe/api/test/StartupTest.java new file mode 100644 index 000000000..0920cb2cf --- /dev/null +++ b/api/src/test/java/io/xpipe/api/test/StartupTest.java @@ -0,0 +1,14 @@ +package io.xpipe.api.test; + +import io.xpipe.beacon.BeaconDaemonController; +import io.xpipe.core.util.XPipeDaemonMode; +import org.junit.jupiter.api.Test; + +public class StartupTest { + + @Test + public void test( ) throws Exception { + BeaconDaemonController.start(XPipeDaemonMode.TRAY); + BeaconDaemonController.stop(); + } +} diff --git a/app/build.gradle b/app/build.gradle index a72454003..d660beec5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,122 +1,68 @@ plugins { id 'application' - id "org.moditect.gradleplugin" version "1.0.0-rc3" + id 'jvm-test-suite' + id 'java-library' } repositories { mavenCentral() } -configurations { - dep -} - apply from: "$rootDir/gradle/gradle_scripts/java.gradle" apply from: "$rootDir/gradle/gradle_scripts/javafx.gradle" -apply from: "$projectDir/gradle_scripts/richtextfx.gradle" -apply from: "$rootDir/gradle/gradle_scripts/commons.gradle" -apply from: "$rootDir/gradle/gradle_scripts/prettytime.gradle" -apply from: "$projectDir/gradle_scripts/sentry.gradle" apply from: "$rootDir/gradle/gradle_scripts/lombok.gradle" -apply from: "$projectDir/gradle_scripts/github-api.gradle" -apply from: "$projectDir/gradle_scripts/flexmark.gradle" -apply from: "$rootDir/gradle/gradle_scripts/picocli.gradle" -apply from: "$rootDir/gradle/gradle_scripts/versioncompare.gradle" -apply from: "$rootDir/gradle/gradle_scripts/markdowngenerator.gradle" configurations { - implementation.extendsFrom(dep) + implementation.extendsFrom(javafx) } dependencies { - compileOnly project(':api') - implementation project(':core') - implementation project(':beacon') + api project(':core') + api project(':beacon') compileOnly 'org.hamcrest:hamcrest:2.2' - compileOnly 'org.junit.jupiter:junit-jupiter-api:5.9.3' - compileOnly 'org.junit.jupiter:junit-jupiter-params:5.9.3' + compileOnly 'org.junit.jupiter:junit-jupiter-api:5.10.2' + compileOnly 'org.junit.jupiter:junit-jupiter-params:5.10.2' - implementation 'net.java.dev.jna:jna-jpms:5.13.0' - implementation 'net.java.dev.jna:jna-platform-jpms:5.13.0' - implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.15.2" - implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: "2.15.2" - implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.15.2" - implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jdk8', version: "2.15.2" - implementation group: 'org.kordamp.ikonli', name: 'ikonli-material2-pack', version: "12.2.0" - implementation group: 'org.kordamp.ikonli', name: 'ikonli-materialdesign2-pack', version: "12.2.0" - implementation group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0" - implementation group: 'org.kordamp.ikonli', name: 'ikonli-material-pack', version: "12.2.0" - implementation group: 'org.kordamp.ikonli', name: 'ikonli-feather-pack', version: "12.2.0" - implementation (name: 'preferencesfx-core-11.15.0') - implementation (group: 'com.dlsc.formsfx', name: 'formsfx-core', version: '11.6.0') { - exclude group: 'org.openjfx', module: 'javafx-controls' - exclude group: 'org.openjfx', module: 'javafx-fxml' - } - implementation group: 'org.slf4j', name: 'slf4j-api', version: '2.0.7' - implementation 'io.xpipe:modulefs:0.1.4' - implementation 'com.jfoenix:jfoenix:9.0.10' - implementation 'org.controlsfx:controlsfx:11.1.2' - implementation 'net.synedra:validatorfx:0.4.2' - implementation ('io.github.mkpaz:atlantafx-base:2.0.1') { + api 'com.vladsch.flexmark:flexmark:0.64.0' + api 'com.vladsch.flexmark:flexmark-util-data:0.64.0' + api 'com.vladsch.flexmark:flexmark-util-ast:0.64.0' + api 'com.vladsch.flexmark:flexmark-util-builder:0.64.0' + api 'com.vladsch.flexmark:flexmark-util-sequence:0.64.0' + api 'com.vladsch.flexmark:flexmark-util-misc:0.64.0' + api 'com.vladsch.flexmark:flexmark-util-dependency:0.64.0' + api 'com.vladsch.flexmark:flexmark-util-collection:0.64.0' + api 'com.vladsch.flexmark:flexmark-util-format:0.64.0' + api 'com.vladsch.flexmark:flexmark-util-html:0.64.0' + api 'com.vladsch.flexmark:flexmark-util-visitor:0.64.0' + + api files("$rootDir/gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar") + api 'info.picocli:picocli:4.7.5' + api 'org.kohsuke:github-api:1.318' + api 'io.sentry:sentry:7.3.0' + api 'org.ocpsoft.prettytime:prettytime:5.0.2.Final' + api 'commons-io:commons-io:2.15.1' + api 'net.java.dev.jna:jna-jpms:5.14.0' + api 'net.java.dev.jna:jna-platform-jpms:5.14.0' + api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.16.1" + api group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: "2.16.1" + api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.16.1" + api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jdk8', version: "2.16.1" + 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.11' + api 'io.xpipe:modulefs:0.1.4' + api 'net.synedra:validatorfx:0.4.2' + api ('io.github.mkpaz:atlantafx-base:2.0.1') { exclude group: 'org.openjfx', module: 'javafx-base' exclude group: 'org.openjfx', module: 'javafx-controls' } - implementation name: 'jSystemThemeDetector-3.8' - implementation group: 'com.github.oshi', name: 'oshi-core-java11', version: '6.4.2' - implementation 'org.jetbrains:annotations:24.0.1' - implementation ('de.jangassen:jfa:1.2.0') { - exclude group: 'net.java.dev.jna', module: 'jna' - } } -apply from: "$rootDir/gradle/gradle_scripts/junit.gradle" - -sourceSets { - main { - output.resourcesDir("${project.layout.buildDirectory.get()}/classes/java/main") - } -} - -dependencies { - testImplementation project(':api') - testImplementation project(':core') -} - -project.allExtensions.forEach((Project p) -> { - dependencies { - testCompileOnly p - } -}) - -project.ext { - jvmRunArgs = [ - "--add-exports", "javafx.graphics/com.sun.javafx.scene=com.jfoenix", - "--add-exports", "javafx.graphics/com.sun.javafx.stage=com.jfoenix", - "--add-exports", "javafx.base/com.sun.javafx.binding=com.jfoenix", - "--add-exports", "javafx.base/com.sun.javafx.event=com.jfoenix", - "--add-exports", "javafx.controls/com.sun.javafx.scene.control=com.jfoenix", - "--add-exports", "javafx.controls/com.sun.javafx.scene.control.behavior=com.jfoenix", - "--add-exports", "javafx.graphics/com.sun.javafx.scene.traversal=org.controlsfx.controls", - "--add-exports", "javafx.graphics/com.sun.javafx.scene=org.controlsfx.controls", - "--add-exports", "org.apache.commons.lang3/org.apache.commons.lang3.math=io.xpipe.app", - "--add-opens", "java.base/java.lang=io.xpipe.app", - "--add-opens", "java.base/java.nio.file=io.xpipe.app", - "--add-opens", "java.base/java.lang.reflect=com.jfoenix", - "--add-opens", "java.base/java.lang.reflect=com.jfoenix", - "--add-opens", "java.base/java.lang=io.xpipe.core", - "--add-opens", "java.desktop/java.awt=io.xpipe.app", - "--add-opens", "net.synedra.validatorfx/net.synedra.validatorfx=io.xpipe.app", - "--add-opens", 'com.dlsc.preferencesfx/com.dlsc.preferencesfx.view=io.xpipe.app', - "--add-opens", 'com.dlsc.preferencesfx/com.dlsc.preferencesfx.model=io.xpipe.app', - "-Xmx8g", - "-Dio.xpipe.app.arch=$rootProject.arch", - "-Dfile.encoding=UTF-8", - // Disable this for now as it requires Windows 10+ - // '-XX:+UseZGC', - "-Dvisualvm.display.name=XPipe" - ] -} +apply from: "$rootDir/gradle/gradle_scripts/junit_suite.gradle" import org.gradle.internal.os.OperatingSystem @@ -125,7 +71,6 @@ if (OperatingSystem.current() == OperatingSystem.LINUX) { } def extensionJarDepList = project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)).toList(); - jar { finalizedBy(extensionJarDepList) } @@ -146,14 +91,18 @@ run { systemProperty 'io.xpipe.app.developerMode', "true" systemProperty 'io.xpipe.app.logLevel', "trace" systemProperty 'io.xpipe.app.fullVersion', rootProject.fullVersion - systemProperty 'io.xpipe.app.showcase', 'false' + systemProperty 'io.xpipe.app.showcase', 'true' // systemProperty "io.xpipe.beacon.port", "21724" // systemProperty "io.xpipe.beacon.printMessages", "true" // systemProperty 'io.xpipe.app.debugPlatform', "true" - // systemProperty "io.xpipe.beacon.localProxy", "true" + // Apply passed xpipe properties + for (final def e in System.getProperties().entrySet()) { + if (e.getKey().toString().contains("xpipe")) { + systemProperty e.getKey().toString(), e.getValue() + } + } - systemProperty 'java.library.path', "./lib" workingDir = rootDir } @@ -181,7 +130,7 @@ processResources { javaexec { workingDir = project.projectDir - jvmArgs += "--module-path=$sourceSets.main.runtimeClasspath.asPath," + jvmArgs += "--module-path=${configurations.javafx.asFileTree.asPath}," jvmArgs += "--add-modules=javafx.graphics" main = "com.sun.javafx.css.parser.Css2Bin" args css diff --git a/app/gradle_scripts/flexmark.gradle b/app/gradle_scripts/flexmark.gradle deleted file mode 100644 index 39c8debb4..000000000 --- a/app/gradle_scripts/flexmark.gradle +++ /dev/null @@ -1,159 +0,0 @@ -dependencies { - implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-0.64.0.jar") - implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-data-0.64.0.jar") - implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-ast-0.64.0.jar") - implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-builder-0.64.0.jar") - implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-sequence-0.64.0.jar") - implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-misc-0.64.0.jar") - implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-dependency-0.64.0.jar") - implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-collection-0.64.0.jar") - implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-format-0.64.0.jar") - implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-html-0.64.0.jar") - implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-visitor-0.64.0.jar") -} - -addDependenciesModuleInfo { - overwriteExistingFiles = true - jdepsExtraArgs = ['-q'] - outputDirectory = file("${project.layout.buildDirectory.get()}/generated-modules") - modules { - module { - artifact 'com.vladsch.flexmark:flexmark:0.64.0' - moduleInfoSource = ''' - module com.vladsch.flexmark { - exports com.vladsch.flexmark.html; - exports com.vladsch.flexmark.html.renderer; - exports com.vladsch.flexmark.parser; - exports com.vladsch.flexmark.parser.core; - - requires com.vladsch.flexmark_util_data; - requires com.vladsch.flexmark_util_ast; - requires com.vladsch.flexmark_util_builder; - requires com.vladsch.flexmark_util_sequence; - requires com.vladsch.flexmark_util_misc; - requires com.vladsch.flexmark_util_dependency; - requires com.vladsch.flexmark_util_collection; - requires com.vladsch.flexmark_util_format; - requires com.vladsch.flexmark_util_html; - requires com.vladsch.flexmark_util_visitor; - } - ''' - } - module { - artifact 'com.vladsch.flexmark:flexmark-util-data:0.64.0' - moduleInfoSource = ''' - module com.vladsch.flexmark_util_data { - exports com.vladsch.flexmark.util.data; - - requires com.vladsch.flexmark_util_misc; - } - ''' - } - module { - artifact 'com.vladsch.flexmark:flexmark-util-ast:0.64.0' - moduleInfoSource = ''' - module com.vladsch.flexmark_util_ast { - exports com.vladsch.flexmark.util.ast; - - requires com.vladsch.flexmark_util_data; - requires com.vladsch.flexmark_util_misc; - requires com.vladsch.flexmark_util_collection; - requires com.vladsch.flexmark_util_sequence; - requires com.vladsch.flexmark_util_visitor; - } - ''' - } - module { - artifact 'com.vladsch.flexmark:flexmark-util-builder:0.64.0' - moduleInfoSource = ''' - module com.vladsch.flexmark_util_builder { - exports com.vladsch.flexmark.util.builder; - - requires com.vladsch.flexmark_util_data; - requires com.vladsch.flexmark_util_misc; - } - ''' - } - module { - artifact 'com.vladsch.flexmark:flexmark-util-sequence:0.64.0' - moduleInfoSource = ''' - module com.vladsch.flexmark_util_sequence { - exports com.vladsch.flexmark.util.sequence; - exports com.vladsch.flexmark.util.sequence.mappers; - exports com.vladsch.flexmark.util.sequence.builder; - - opens com.vladsch.flexmark.util.sequence; - - requires com.vladsch.flexmark_util_misc; - requires com.vladsch.flexmark_util_data; - requires com.vladsch.flexmark_util_collection; - } - ''' - } - module { - artifact 'com.vladsch.flexmark:flexmark-util-misc:0.64.0' - moduleInfoSource = ''' - module com.vladsch.flexmark_util_misc { - exports com.vladsch.flexmark.util.misc; - } - ''' - } - module { - artifact 'com.vladsch.flexmark:flexmark-util-dependency:0.64.0' - moduleInfoSource = ''' - module com.vladsch.flexmark_util_dependency { - exports com.vladsch.flexmark.util.dependency; - - requires com.vladsch.flexmark_util_collection; - requires com.vladsch.flexmark_util_misc; - } - ''' - } - module { - artifact 'com.vladsch.flexmark:flexmark-util-collection:0.64.0' - moduleInfoSource = ''' - module com.vladsch.flexmark_util_collection { - exports com.vladsch.flexmark.util.collection; - exports com.vladsch.flexmark.util.collection.iteration; - - requires com.vladsch.flexmark_util_misc; - } - ''' - } - module { - artifact 'com.vladsch.flexmark:flexmark-util-format:0.64.0' - moduleInfoSource = ''' - module com.vladsch.flexmark_util_format { - exports com.vladsch.flexmark.util.format; - - requires com.vladsch.flexmark_util_data; - requires com.vladsch.flexmark_util_sequence; - requires com.vladsch.flexmark_util_misc; - requires com.vladsch.flexmark_util_ast; - requires com.vladsch.flexmark_util_collection; - } - ''' - } - module { - artifact 'com.vladsch.flexmark:flexmark-util-html:0.64.0' - moduleInfoSource = ''' - module com.vladsch.flexmark_util_html { - exports com.vladsch.flexmark.util.html; - - opens com.vladsch.flexmark.util.html; - - requires com.vladsch.flexmark_util_misc; - requires com.vladsch.flexmark_util_sequence; - } - ''' - } - module { - artifact 'com.vladsch.flexmark:flexmark-util-visitor:0.64.0' - moduleInfoSource = ''' - module com.vladsch.flexmark_util_visitor { - exports com.vladsch.flexmark.util.visitor; - } - ''' - } - } -} \ No newline at end of file diff --git a/app/gradle_scripts/github-api.gradle b/app/gradle_scripts/github-api.gradle deleted file mode 100644 index 1d54161b9..000000000 --- a/app/gradle_scripts/github-api.gradle +++ /dev/null @@ -1,30 +0,0 @@ -dependencies { - implementation files("${project.layout.buildDirectory.get()}/generated-modules/github-api-1.301.jar") -} - -addDependenciesModuleInfo { - overwriteExistingFiles = true - jdepsExtraArgs = ['-q'] - outputDirectory = file("${project.layout.buildDirectory.get()}/generated-modules") - modules { - module { - artifact 'org.kohsuke:github-api:1.301' - moduleInfoSource = ''' - module org.kohsuke.github { - exports org.kohsuke.github; - exports org.kohsuke.github.function; - exports org.kohsuke.github.authorization; - exports org.kohsuke.github.extras; - exports org.kohsuke.github.connector; - - requires java.logging; - requires org.apache.commons.io; - requires org.apache.commons.lang3; - requires com.fasterxml.jackson.databind; - - opens org.kohsuke.github; - } - ''' - } - } -} \ No newline at end of file diff --git a/app/gradle_scripts/richtextfx.gradle b/app/gradle_scripts/richtextfx.gradle deleted file mode 100644 index a5dc4a614..000000000 --- a/app/gradle_scripts/richtextfx.gradle +++ /dev/null @@ -1,87 +0,0 @@ -dependencies { - implementation files("${project.layout.buildDirectory.get()}/generated-modules/richtextfx-0.10.6.jar") - implementation files("${project.layout.buildDirectory.get()}/generated-modules/flowless-0.6.6.jar") - implementation files("${project.layout.buildDirectory.get()}/generated-modules/undofx-2.1.1.jar") - implementation files("${project.layout.buildDirectory.get()}/generated-modules/wellbehavedfx-0.3.3.jar") - implementation files("${project.layout.buildDirectory.get()}/generated-modules/reactfx-2.0-M5.jar") -} - -addDependenciesModuleInfo { - overwriteExistingFiles = true - jdepsExtraArgs = ['-q'] - outputDirectory = file("${project.layout.buildDirectory.get()}/generated-modules") - modules { - module { - artifact group: 'org.fxmisc.flowless', name: 'flowless', version: '0.6.6' - moduleInfoSource = ''' - module org.fxmisc.flowless { - exports org.fxmisc.flowless; - requires static javafx.base; - requires static javafx.controls; - requires org.reactfx; - requires org.fxmisc.wellbehavedfx; - } - ''' - } - - module { - artifact group: 'org.fxmisc.undo', name: 'undofx', version: '2.1.1' - moduleInfoSource = ''' - module org.fxmisc.undofx { - exports org.fxmisc.undo; - requires static javafx.base; - requires static javafx.controls; - requires org.reactfx; - requires org.fxmisc.wellbehavedfx; - } - ''' - } - - module { - artifact group: 'org.fxmisc.wellbehaved', name: 'wellbehavedfx', version: '0.3.3' - moduleInfoSource = ''' - module org.fxmisc.wellbehavedfx { - exports org.fxmisc.wellbehaved.event; - exports org.fxmisc.wellbehaved.event.template; - - requires static javafx.base; - requires static javafx.controls; - requires org.reactfx; - } - ''' - } - - module { - artifact group: 'org.fxmisc.richtext', name: 'richtextfx', version: '0.10.6' - moduleInfoSource = ''' - module org.fxmisc.richtext { - exports org.fxmisc.richtext; - exports org.fxmisc.richtext.model; - exports org.fxmisc.richtext.event; - - requires org.fxmisc.flowless; - requires org.fxmisc.undofx; - requires org.fxmisc.wellbehavedfx; - requires static javafx.base; - requires static javafx.controls; - requires org.reactfx; - } - ''' - } - - module { - artifact group: 'org.reactfx', name: 'reactfx', version: '2.0-M5' - moduleInfoSource = ''' - module org.reactfx { - exports org.reactfx; - exports org.reactfx.collection; - exports org.reactfx.value; - exports org.reactfx.util; - - requires static javafx.base; - requires static javafx.controls; - } - ''' - } - } -} diff --git a/app/gradle_scripts/sentry.gradle b/app/gradle_scripts/sentry.gradle deleted file mode 100644 index 0d6470550..000000000 --- a/app/gradle_scripts/sentry.gradle +++ /dev/null @@ -1,41 +0,0 @@ -dependencies { - implementation files("${project.layout.buildDirectory.get()}/generated-modules/sentry-6.29.0.jar") -} - -addDependenciesModuleInfo { - overwriteExistingFiles = true - jdepsExtraArgs = ['-q'] - outputDirectory = file("${project.layout.buildDirectory.get()}/generated-modules") - modules { - module { - artifact 'io.sentry:sentry:6.29.0' - moduleInfoSource = ''' - module io.sentry { - exports io.sentry; - opens io.sentry; - - exports io.sentry.protocol; - opens io.sentry.protocol; - - exports io.sentry.config; - opens io.sentry.config; - - exports io.sentry.transport; - opens io.sentry.transport; - - exports io.sentry.util; - opens io.sentry.util; - - exports io.sentry.cache; - opens io.sentry.cache; - - exports io.sentry.exception; - opens io.sentry.exception; - - exports io.sentry.hints; - opens io.sentry.hints; - } - ''' - } - } -} \ No newline at end of file diff --git a/app/src/localTest/java/module-info.java b/app/src/localTest/java/module-info.java new file mode 100644 index 000000000..9c44005d6 --- /dev/null +++ b/app/src/localTest/java/module-info.java @@ -0,0 +1,4 @@ +open module io.xpipe.app.localTest { + requires org.junit.jupiter.api; + requires io.xpipe.app; +} \ No newline at end of file diff --git a/app/src/localTest/java/test/Test.java b/app/src/localTest/java/test/Test.java new file mode 100644 index 000000000..c11a10b62 --- /dev/null +++ b/app/src/localTest/java/test/Test.java @@ -0,0 +1,14 @@ +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()); + + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserAlerts.java b/app/src/main/java/io/xpipe/app/browser/BrowserAlerts.java index 674d15744..3544685a1 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserAlerts.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserAlerts.java @@ -5,12 +5,46 @@ import io.xpipe.app.core.AppWindowHelper; import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileSystem; 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 enum FileConflictChoice { + CANCEL, + SKIP, + SKIP_ALL, + REPLACE, + REPLACE_ALL + } + + public static FileConflictChoice showFileConflictAlert(String file, boolean multiple) { + var map = new LinkedHashMap(); + map.put(new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE), FileConflictChoice.CANCEL); + if (multiple) { + map.put(new ButtonType("Skip", ButtonBar.ButtonData.OTHER), FileConflictChoice.SKIP); + map.put(new ButtonType("Skip All", ButtonBar.ButtonData.OTHER), FileConflictChoice.SKIP_ALL); + } + map.put(new ButtonType("Replace", ButtonBar.ButtonData.OTHER), FileConflictChoice.REPLACE); + if (multiple) { + map.put(new ButtonType("Replace All", ButtonBar.ButtonData.OTHER), FileConflictChoice.REPLACE_ALL); + } + return AppWindowHelper.showBlockingAlert(alert -> { + alert.setTitle(AppI18n.get("fileConflictAlertTitle")); + alert.setHeaderText(AppI18n.get("fileConflictAlertHeader")); + AppWindowHelper.setContent(alert, AppI18n.get(multiple ? "fileConflictAlertContentMultiple" : "fileConflictAlertContent", file)); + alert.setAlertType(Alert.AlertType.CONFIRMATION); + alert.getButtonTypes().clear(); + map.sequencedKeySet().forEach(buttonType -> alert.getButtonTypes().add(buttonType)); + }) + .map(map::get) + .orElse(FileConflictChoice.CANCEL); + } + public static boolean showMoveAlert(List source, FileSystem.FileEntry target) { if (source.stream().noneMatch(entry -> entry.getKind() == FileKind.DIRECTORY)) { return true; diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserClipboard.java b/app/src/main/java/io/xpipe/app/browser/BrowserClipboard.java index e7cd49da6..01d97b66c 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserClipboard.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserClipboard.java @@ -2,7 +2,7 @@ package io.xpipe.app.browser; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.util.ThreadHelper; -import io.xpipe.core.process.ShellDialects; +import io.xpipe.core.process.ProcessControlProvider; import io.xpipe.core.store.FileSystem; import io.xpipe.core.util.FailableRunnable; import javafx.beans.property.Property; @@ -32,7 +32,7 @@ public class BrowserClipboard { public String toClipboardString() { return entries.stream().map(fileEntry -> "\"" + fileEntry.getPath() + "\"").collect( - Collectors.joining(ShellDialects.getPlatformDefault().getNewLine().getNewLineString())); + Collectors.joining(ProcessControlProvider.get().getEffectiveLocalDialect().getNewLine().getNewLineString())); } } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserComp.java index 150394b31..fd3fd88d3 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserComp.java @@ -69,11 +69,6 @@ public class BrowserComp extends SimpleComp { return true; } - // Also show on local - if (model.getSelected().getValue() != null) { - // return model.getSelected().getValue().isLocal(); - } - return false; }, model.getOpenFileSystems(), model.getSelected()))); localDownloadStage.prefHeight(200); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserGreetingComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserGreetingComp.java index 094c43a24..009382a98 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserGreetingComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserGreetingComp.java @@ -1,6 +1,8 @@ package io.xpipe.app.browser; +import atlantafx.base.theme.Styles; import io.xpipe.app.core.AppFont; +import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.fxcomps.SimpleComp; import javafx.scene.control.Label; import javafx.scene.layout.Region; @@ -11,6 +13,16 @@ public class BrowserGreetingComp extends SimpleComp { @Override protected Region createSimple() { + var r = new Label(getText()); + AppLayoutModel.get().getSelected().addListener((observableValue, entry, t1) -> { + r.setText(getText()); + }); + AppFont.setSize(r, 7); + r.getStyleClass().add(Styles.TEXT_BOLD); + return r; + } + + private String getText() { var ldt = LocalDateTime.now(); var hour = ldt.getHour(); String text; @@ -21,8 +33,6 @@ public class BrowserGreetingComp extends SimpleComp { } else { text = "Good afternoon"; } - var r = new Label(text); - AppFont.setSize(r, 7); - return r; + return text; } } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserModel.java index 5754b7774..52d64ae22 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserModel.java @@ -70,6 +70,12 @@ public class BrowserModel { public void reset() { synchronized (BrowserModel.this) { for (OpenFileSystemModel o : new ArrayList<>(openFileSystems)) { + // Don't close busy connections gracefully + // as we otherwise might lock up + if (o.isBusy()) { + continue; + } + closeFileSystemSync(o); } if (savedState != null) { diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java index ffc6807b6..84657a451 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java @@ -2,11 +2,13 @@ package io.xpipe.app.browser; import atlantafx.base.controls.Spacer; import io.xpipe.app.core.AppFont; +import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.impl.LabelComp; import io.xpipe.app.fxcomps.util.PlatformThread; +import io.xpipe.app.util.HumanReadableFormat; import javafx.beans.binding.Bindings; import javafx.scene.control.ToolBar; import javafx.scene.layout.Region; @@ -21,6 +23,46 @@ public class BrowserStatusBarComp extends SimpleComp { @Override protected Region createSimple() { + var bar = new ToolBar(); + bar.getItems().setAll(createClipboardStatus().createRegion(), createProgressStatus().createRegion(), new Spacer(), createSelectionStatus().createRegion()); + bar.getStyleClass().add("status-bar"); + bar.setOnDragDetected(event -> { + event.consume(); + bar.startFullDrag(); + }); + AppFont.small(bar); + + simulateEmptyCell(bar); + + return bar; + } + + private Comp createProgressStatus() { + var transferredCount = PlatformThread.sync(Bindings.createStringBinding( + () -> { + return HumanReadableFormat.byteCount(model.getProgress().getValue().getTransferred()); + }, + model.getProgress())); + var allCount = PlatformThread.sync(Bindings.createStringBinding( + () -> { + return HumanReadableFormat.byteCount(model.getProgress().getValue().getTotal()); + }, + model.getProgress())); + var progressComp = new LabelComp(Bindings.createStringBinding( + () -> { + if (model.getProgress().getValue() == null || model.getProgress().getValue().done()) { + return null; + } else { + return (model.getProgress().getValue().getName() != null ? model.getProgress().getValue().getName() + " " : "") + transferredCount.getValue() + " / " + allCount.getValue(); + } + }, + transferredCount, + allCount, + model.getProgress())); + return progressComp; + } + + private Comp createClipboardStatus() { var cc = PlatformThread.sync(BrowserClipboard.currentCopyClipboard); var ccCount = Bindings.createStringBinding( () -> { @@ -32,7 +74,10 @@ public class BrowserStatusBarComp extends SimpleComp { } }, cc); + return new LabelComp(ccCount); + } + private Comp createSelectionStatus() { var selectedCount = PlatformThread.sync(Bindings.createIntegerBinding( () -> { return model.getFileList().getSelection().size(); @@ -46,7 +91,6 @@ public class BrowserStatusBarComp extends SimpleComp { .count(); }, model.getFileList().getAll())); - var selectedComp = new LabelComp(Bindings.createStringBinding( () -> { if (selectedCount.getValue().intValue() == 0) { @@ -57,19 +101,7 @@ public class BrowserStatusBarComp extends SimpleComp { }, selectedCount, allCount)); - - var bar = new ToolBar(); - bar.getItems().setAll(new LabelComp(ccCount).createRegion(), new Spacer(), selectedComp.createRegion()); - bar.getStyleClass().add("status-bar"); - bar.setOnDragDetected(event -> { - event.consume(); - bar.startFullDrag(); - }); - AppFont.small(bar); - - simulateEmptyCell(bar); - - return bar; + return selectedComp; } private void simulateEmptyCell(Region r) { diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java index e9db51908..847eb36fe 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java @@ -9,12 +9,12 @@ import io.xpipe.app.fxcomps.impl.*; import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.storage.DataStorage; +import io.xpipe.core.process.OsType; import io.xpipe.core.store.FileNames; import javafx.beans.binding.Bindings; import javafx.collections.FXCollections; import javafx.geometry.Insets; import javafx.scene.image.Image; -import javafx.scene.input.ClipboardContent; import javafx.scene.input.Dragboard; import javafx.scene.input.TransferMode; import javafx.scene.layout.AnchorPane; @@ -29,48 +29,48 @@ import java.util.Optional; public class BrowserTransferComp extends SimpleComp { - private final BrowserTransferModel stage; + private final BrowserTransferModel model; - public BrowserTransferComp(BrowserTransferModel stage) { - this.stage = stage; + public BrowserTransferComp(BrowserTransferModel model) { + this.model = model; } @Override protected Region createSimple() { var background = new LabelComp(AppI18n.observable("transferDescription")) .apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline"))) - .visible(BindingsHelper.persist(Bindings.isEmpty(stage.getItems()))); + .visible(BindingsHelper.persist(Bindings.isEmpty(model.getItems()))); var backgroundStack = new StackComp(List.of(background)).grow(true, true).styleClass("download-background"); - var binding = BindingsHelper.mappedContentBinding(stage.getItems(), item -> item.getFileEntry()); + var binding = BindingsHelper.mappedContentBinding(model.getItems(), item -> item.getFileEntry()); var list = new BrowserSelectionListComp(binding, entry -> Bindings.createStringBinding(() -> { - var sourceItem = stage.getItems().stream().filter(item -> item.getFileEntry() == entry).findAny(); + var sourceItem = model.getItems().stream().filter(item -> item.getFileEntry() == entry).findAny(); if (sourceItem.isEmpty()) { return "?"; } - var name = sourceItem.get().getFinishedDownload().get() ? "Local" : DataStorage.get().getStoreDisplayName(entry.getFileSystem().getStore()).orElse("?"); + var name = sourceItem.get().downloadFinished().get() ? "Local" : DataStorage.get().getStoreDisplayName(entry.getFileSystem().getStore()).orElse("?"); return FileNames.getFileName(entry.getPath()) + " (" + name + ")"; - }, stage.getAllDownloaded())) + }, model.getAllDownloaded())) .apply(struc -> struc.get().setMinHeight(150)) .grow(false, true); - var dragNotice = new LabelComp(stage.getAllDownloaded().flatMap(aBoolean -> aBoolean ? AppI18n.observable("dragLocalFiles") : AppI18n.observable("dragFiles"))) + var dragNotice = new LabelComp(model.getAllDownloaded().flatMap(aBoolean -> aBoolean ? AppI18n.observable("dragLocalFiles") : AppI18n.observable("dragFiles"))) .apply(struc -> struc.get().setGraphic(new FontIcon("mdi2e-export"))) .hide(PlatformThread.sync( - BindingsHelper.persist(Bindings.isEmpty(stage.getItems())))) + BindingsHelper.persist(Bindings.isEmpty(model.getItems())))) .grow(true, false) .apply(struc -> struc.get().setPadding(new Insets(8))); var downloadButton = new IconButtonComp("mdi2d-download", () -> { - stage.download(); + model.download(); }) - .hide(BindingsHelper.persist(Bindings.isEmpty(stage.getItems()))) - .disable(PlatformThread.sync(stage.getAllDownloaded())) + .hide(BindingsHelper.persist(Bindings.isEmpty(model.getItems()))) + .disable(PlatformThread.sync(model.getAllDownloaded())) .apply(new FancyTooltipAugment<>("downloadStageDescription")); var clearButton = new IconButtonComp("mdi2c-close", () -> { - stage.clear(); + model.clear(); }) - .hide(BindingsHelper.persist(Bindings.isEmpty(stage.getItems()))); + .hide(BindingsHelper.persist(Bindings.isEmpty(model.getItems()))); var clearPane = Comp.derive( new HorizontalComp(List.of(downloadButton, clearButton)) .apply(struc -> struc.get().setSpacing(10)), @@ -93,37 +93,45 @@ public class BrowserTransferComp extends SimpleComp { event.acceptTransferModes(TransferMode.ANY); event.consume(); } + + // Accept drops from outside the app window + if (event.getGestureSource() == null && !event.getDragboard().getFiles().isEmpty()) { + event.acceptTransferModes(TransferMode.ANY); + event.consume(); + } }); struc.get().setOnDragDropped(event -> { + // Accept drops from inside the app window if (event.getGestureSource() != null) { var files = BrowserClipboard.retrieveDrag(event.getDragboard()) .getEntries(); - stage.drop(files); + model.drop(model.getBrowserModel().getSelected().getValue(), files); + event.setDropCompleted(true); + event.consume(); + } + + // Accept drops from outside the app window + if (event.getGestureSource() == null) { + model.dropLocal(event.getDragboard().getFiles()); event.setDropCompleted(true); event.consume(); } }); struc.get().setOnDragDetected(event -> { - if (stage.getDownloading().get()) { + if (model.getDownloading().get()) { return; } - // Drag within browser - if (!stage.getAllDownloaded().get()) { - var selected = stage.getItems().stream().map(item -> item.getFileEntry()).toList(); - Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY); - db.setContent(BrowserClipboard.startDrag(null, selected)); + var selected = model.getItems().stream().map(BrowserTransferModel.Item::getFileEntry).toList(); + Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY); - Image image = BrowserSelectionListComp.snapshot(FXCollections.observableList(selected)); - db.setDragView(image, -20, 15); - - event.setDragDetect(true); - event.consume(); + var cc = BrowserClipboard.startDrag(null, selected); + if (cc == null) { return; } - // Drag outside browser - var files = stage.getItems().stream() + var files = model.getItems().stream() + .filter(item -> item.downloadFinished().get()) .map(item -> { try { var file = item.getLocalFile(); @@ -140,31 +148,26 @@ public class BrowserTransferComp extends SimpleComp { }) .flatMap(Optional::stream) .toList(); - Dragboard db = struc.get().startDragAndDrop(TransferMode.MOVE); - var cc = new ClipboardContent(); cc.putFiles(files); db.setContent(cc); - var image = BrowserSelectionListComp.snapshot( - FXCollections.observableList(stage.getItems().stream() - .map(item -> item.getFileEntry()) - .toList())); + Image image = BrowserSelectionListComp.snapshot(FXCollections.observableList(selected)); db.setDragView(image, -20, 15); event.setDragDetect(true); event.consume(); }); struc.get().setOnDragDone(event -> { - // macOS does always report false here - if (!event.isAccepted()) { + // macOS does always report false here, which is unfortunate + if (!event.isAccepted() && !OsType.getLocal().equals(OsType.MACOS)) { return; } - stage.getItems().clear(); + model.clear(); event.consume(); }); }), - PlatformThread.sync(stage.getDownloading())); + PlatformThread.sync(model.getDownloading())); return stack.styleClass("transfer").createRegion(); } } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java index 0204c9fef..e71b1cc33 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java @@ -4,14 +4,20 @@ import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.util.BooleanScope; import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileSystem; +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.ObservableBooleanValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import lombok.Value; import org.apache.commons.io.FileUtils; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; @@ -33,10 +39,25 @@ public class BrowserTransferModel { @Value public static class Item { + OpenFileSystemModel openFileSystemModel; String name; FileSystem.FileEntry fileEntry; Path localFile; - BooleanProperty finishedDownload = new SimpleBooleanProperty(); + Property progress; + + public Item(OpenFileSystemModel openFileSystemModel, String name, FileSystem.FileEntry fileEntry, Path localFile) { + this.openFileSystemModel = openFileSystemModel; + this.name = name; + this.fileEntry = fileEntry; + this.localFile = localFile; + this.progress = new SimpleObjectProperty<>(BrowserTransferProgress.empty(fileEntry.getName(), fileEntry.getSize())); + } + + public ObservableBooleanValue downloadFinished() { + return Bindings.createBooleanBinding(() -> { + return progress.getValue().done(); + }, progress); + } } BrowserModel browserModel; @@ -45,15 +66,18 @@ public class BrowserTransferModel { BooleanProperty allDownloaded = new SimpleBooleanProperty(); public void clear() { - try { - FileUtils.deleteDirectory(TEMP.toFile()); + try (var ls = Files.list(TEMP)) { + var list = ls.toList(); + for (Path path : list) { + Files.delete(path); + } } catch (IOException e) { ErrorEvent.fromThrowable(e).handle(); } items.clear(); } - public void drop(List entries) { + public void drop(OpenFileSystemModel model, List entries) { entries.forEach(entry -> { var name = FileNames.getFileName(entry.getPath()); if (items.stream().anyMatch(item -> item.getName().equals(name))) { @@ -61,12 +85,39 @@ public class BrowserTransferModel { } Path file = TEMP.resolve(name); - var item = new Item(name, entry, file); + var item = new Item(model, name, entry, file); items.add(item); allDownloaded.set(false); }); } + public void dropLocal(List entries) { + if (entries.isEmpty()) { + return; + } + + var empty = items.isEmpty(); + try { + var paths = entries.stream().map(File::toPath).filter(Files::exists).toList(); + for (Path path : paths) { + var entry = FileSystemHelper.getLocal(path); + var name = entry.getName(); + if (items.stream().anyMatch(item -> item.getName().equals(name))) { + return; + } + + var item = new Item(null, name, entry, path); + item.progress.setValue(BrowserTransferProgress.finished(entry.getName(), entry.getSize())); + items.add(item); + } + } catch (Exception ex) { + ErrorEvent.fromThrowable(ex).handle(); + } + if (empty) { + allDownloaded.set(true); + } + } + public void download() { executor.submit(() -> { try { @@ -77,7 +128,11 @@ public class BrowserTransferModel { } for (Item item : new ArrayList<>(items)) { - if (item.getFinishedDownload().get()) { + if (item.downloadFinished().get()) { + continue; + } + + if (item.getOpenFileSystemModel() != null && item.getOpenFileSystemModel().isClosed()) { continue; } @@ -86,9 +141,12 @@ public class BrowserTransferModel { FileSystemHelper.dropFilesInto( FileSystemHelper.getLocal(TEMP), List.of(item.getFileEntry()), - true); + true, + progress -> { + item.getProgress().setValue(progress); + item.getOpenFileSystemModel().getProgress().setValue(progress); + }); } - item.finishedDownload.set(true); } catch (Throwable t) { ErrorEvent.fromThrowable(t).handle(); items.remove(item); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserTransferProgress.java b/app/src/main/java/io/xpipe/app/browser/BrowserTransferProgress.java new file mode 100644 index 000000000..d9907c3ea --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferProgress.java @@ -0,0 +1,27 @@ +package io.xpipe.app.browser; + +import lombok.Value; + +@Value +public class BrowserTransferProgress { + + static BrowserTransferProgress empty() { + return new BrowserTransferProgress(null, 0, 0); + } + + static BrowserTransferProgress empty(String name, long size) { + return new BrowserTransferProgress(name, 0, size); + } + + static BrowserTransferProgress finished(String name, long size) { + return new BrowserTransferProgress(name, size, size); + } + + String name; + long transferred; + long total; + + public boolean done() { + return transferred >= total; + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java index cc8174100..57a51123e 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java @@ -1,10 +1,10 @@ package io.xpipe.app.browser; import atlantafx.base.controls.Spacer; -import atlantafx.base.theme.Styles; import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.comp.base.ListBoxViewComp; import io.xpipe.app.comp.base.TileButtonComp; +import io.xpipe.app.core.AppFont; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.impl.LabelComp; @@ -72,7 +72,7 @@ public class BrowserWelcomeComp extends SimpleComp { return !empty.get() ? "You were recently connected to the following systems:" : "Here you will be able to see where you left off last time."; }, empty)).createRegion(); - header.getStyleClass().add(Styles.TEXT_MUTED); + AppFont.setSize(header, 1); vbox.getChildren().add(header); var storeList = new VBox(); diff --git a/app/src/main/java/io/xpipe/app/browser/FileSystemHelper.java b/app/src/main/java/io/xpipe/app/browser/FileSystemHelper.java index a42e3914d..fc43e013a 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileSystemHelper.java +++ b/app/src/main/java/io/xpipe/app/browser/FileSystemHelper.java @@ -7,10 +7,16 @@ import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.LocalStore; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.LinkedHashMap; import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; public class FileSystemHelper { @@ -144,25 +150,21 @@ public class FileSystemHelper { Files.isDirectory(file) ? FileKind.DIRECTORY : FileKind.FILE); } - public static void dropLocalFilesInto(FileSystem.FileEntry entry, List files) { - try { - var entries = files.stream() - .map(path -> { - try { - return getLocal(path); - } catch (Exception e) { - throw new RuntimeException(e); - } - }) - .toList(); - dropFilesInto(entry, entries, false); - } catch (Exception ex) { - ErrorEvent.fromThrowable(ex).handle(); - } + public static void dropLocalFilesInto(FileSystem.FileEntry entry, List files, Consumer progress) throws Exception { + var entries = files.stream() + .map(path -> { + try { + return getLocal(path); + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .toList(); + dropFilesInto(entry, entries, false, p -> progress.accept(p)); } public static void delete(List files) { - if (files.size() == 0) { + if (files.isEmpty()) { return; } @@ -176,22 +178,33 @@ public class FileSystemHelper { } public static void dropFilesInto( - FileSystem.FileEntry target, List files, boolean explicitCopy) throws Exception { - if (files.size() == 0) { + FileSystem.FileEntry target, List files, boolean explicitCopy, Consumer progress + ) throws Exception { + if (files.isEmpty()) { + progress.accept(BrowserTransferProgress.empty()); return; } + var same = files.getFirst().getFileSystem().equals(target.getFileSystem()); + if (same && !explicitCopy) { + if (!BrowserAlerts.showMoveAlert(files, target)) { + return; + } + } + + AtomicReference lastConflictChoice = new AtomicReference<>(); for (var file : files) { if (file.getFileSystem().equals(target.getFileSystem())) { - dropFileAcrossSameFileSystem(target, file, explicitCopy); + dropFileAcrossSameFileSystem(target, file, explicitCopy, lastConflictChoice, files.size() > 1); + progress.accept(BrowserTransferProgress.finished(file.getName(), file.getSize())); } else { - dropFileAcrossFileSystems(target, file); + dropFileAcrossFileSystems(target, file, progress, lastConflictChoice, files.size() > 1); } } } private static void dropFileAcrossSameFileSystem( - FileSystem.FileEntry target, FileSystem.FileEntry source, boolean explicitCopy) throws Exception { + FileSystem.FileEntry target, FileSystem.FileEntry source, boolean explicitCopy, AtomicReference lastConflictChoice, boolean multiple) throws Exception { // Prevent dropping directory into itself if (source.getPath().equals(target.getPath())) { return; @@ -208,6 +221,10 @@ public class FileSystemHelper { throw ErrorEvent.unreportable(new IllegalArgumentException("Target directory " + targetFile + " does already exist")); } + if (!handleChoice(lastConflictChoice, target.getFileSystem(), targetFile, multiple)) { + return; + } + if (explicitCopy) { target.getFileSystem().copy(sourceFile, targetFile); } else { @@ -215,7 +232,7 @@ public class FileSystemHelper { } } - private static void dropFileAcrossFileSystems(FileSystem.FileEntry target, FileSystem.FileEntry source) + private static void dropFileAcrossFileSystems(FileSystem.FileEntry target, FileSystem.FileEntry source, Consumer progress, AtomicReference lastConflictChoice, boolean multiple) throws Exception { if (target.getKind() != FileKind.DIRECTORY) { throw new IllegalStateException("Target " + target.getPath() + " is not a directory"); @@ -229,6 +246,7 @@ public class FileSystemHelper { return; } + AtomicLong totalSize = new AtomicLong(); if (source.getKind() == FileKind.DIRECTORY) { var directoryName = FileNames.getFileName(source.getPath()); flatFiles.put(source, directoryName); @@ -237,11 +255,14 @@ public class FileSystemHelper { List list = source.getFileSystem().listFilesRecursively(source.getPath()); list.forEach(fileEntry -> { flatFiles.put(fileEntry, FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath()))); + totalSize.addAndGet(fileEntry.getSize()); }); } else { flatFiles.put(source, FileNames.getFileName(source.getPath())); + totalSize.addAndGet(source.getSize()); } + AtomicLong transferred = new AtomicLong(); for (var e : flatFiles.entrySet()) { var sourceFile = e.getKey(); var targetFile = FileNames.join(target.getPath(), e.getValue()); @@ -252,11 +273,106 @@ public class FileSystemHelper { if (sourceFile.getKind() == FileKind.DIRECTORY) { target.getFileSystem().mkdirs(targetFile); } else if (sourceFile.getKind() == FileKind.FILE) { - try (var in = sourceFile.getFileSystem().openInput(sourceFile.getPath()); - var out = target.getFileSystem().openOutput(targetFile)) { - in.transferTo(out); + if (!handleChoice(lastConflictChoice, target.getFileSystem(), targetFile, multiple || flatFiles.size() > 1)) { + continue; + } + + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = sourceFile.getFileSystem().openInput(sourceFile.getPath()); + outputStream = target.getFileSystem().openOutput(targetFile, source.getSize()); + transfer(source,inputStream, outputStream,transferred, totalSize, progress); + inputStream.transferTo(OutputStream.nullOutputStream()); + } catch (Exception ex) { + if (inputStream != null) { + try { + inputStream.close(); + } catch (Exception om) { + ErrorEvent.fromThrowable(om).handle(); + } + } + if (outputStream != null) { + try { + outputStream.close(); + } catch (Exception om) { + ErrorEvent.fromThrowable(om).handle(); + } + } + throw ex; + } + + Exception exception = null; + try { + inputStream.close(); + } catch (Exception om) { + exception = om; + } + try { + outputStream.close(); + } catch (Exception om) { + if (exception != null) { + ErrorEvent.fromThrowable(om).handle(); + } else { + exception = om; + } + } + if (exception != null) { + throw exception; } } } + progress.accept(BrowserTransferProgress.finished(source.getName(), totalSize.get())); + } + + private static boolean handleChoice(AtomicReference previous, FileSystem fileSystem, String target, boolean multiple) throws Exception { + if (previous.get() == BrowserAlerts.FileConflictChoice.CANCEL) { + return false; + } + + if (previous.get() == BrowserAlerts.FileConflictChoice.REPLACE_ALL) { + return true; + } + + if (fileSystem.fileExists(target)) { + if (previous.get() == BrowserAlerts.FileConflictChoice.SKIP_ALL) { + return false; + } + + + var choice = BrowserAlerts.showFileConflictAlert(target, multiple); + if (choice == BrowserAlerts.FileConflictChoice.CANCEL) { + previous.set(BrowserAlerts.FileConflictChoice.CANCEL); + return false; + } + + if (choice == BrowserAlerts.FileConflictChoice.SKIP) { + return false; + } + + if (choice == BrowserAlerts.FileConflictChoice.SKIP_ALL) { + previous.set(BrowserAlerts.FileConflictChoice.SKIP_ALL); + return false; + } + + if (choice == BrowserAlerts.FileConflictChoice.REPLACE_ALL) { + previous.set(BrowserAlerts.FileConflictChoice.REPLACE_ALL); + return true; + } + } + return true; + } + + private static final int DEFAULT_BUFFER_SIZE = 16384; + + private static void transfer(FileSystem.FileEntry source, InputStream inputStream, OutputStream outputStream, AtomicLong transferred, AtomicLong total, Consumer progress) throws IOException { + var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, source.getSize()); + byte[] buffer = new byte[bs]; + int read; + while ((read = inputStream.read(buffer, 0, bs)) >= 0) { + outputStream.write(buffer, 0, read); + transferred.addAndGet(read); + progress.accept(new BrowserTransferProgress(source.getName(), transferred.get(), total.get())); + } } } diff --git a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java index 3d8b347d1..ba9f15c86 100644 --- a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java +++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java @@ -6,7 +6,7 @@ import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.util.BooleanScope; -import io.xpipe.app.util.TerminalHelper; +import io.xpipe.app.util.TerminalLauncher; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.process.*; import io.xpipe.core.store.*; @@ -40,8 +40,8 @@ public final class OpenFileSystemModel { private final BooleanProperty inOverview = new SimpleBooleanProperty(); private final String name; private final String tooltip; - private boolean local; private int customScriptsStartIndex; + private final Property progress = new SimpleObjectProperty<>(BrowserTransferProgress.empty()); public OpenFileSystemModel(BrowserModel browserModel, DataStoreEntryRef entry) { this.browserModel = browserModel; @@ -56,6 +56,19 @@ public final class OpenFileSystemModel { fileList = new BrowserFileListModel(this); } + public boolean isBusy() { + return !progress.getValue().done() || (fileSystem != null && fileSystem.getShell().isPresent() && fileSystem.getShell().get().getLock().isLocked()); + } + + private void startIfNeeded() { + if (fileSystem == null) { + return; + } + + var s = fileSystem.getShell(); + s.ifPresent(ShellControl::start); + } + public void withShell(FailableConsumer c, boolean refresh) { ThreadHelper.runFailableAsync(() -> { if (fileSystem == null) { @@ -132,7 +145,7 @@ public final class OpenFileSystemModel { } // Start shell in case we exited - getFileSystem().getShell().orElseThrow().start(); + startIfNeeded(); // Fix common issues with paths var adjustedPath = FileSystemHelper.adjustPath(this, path); @@ -158,26 +171,21 @@ public final class OpenFileSystemModel { var directory = currentPath.get(); var name = adjustedPath + " - " + entry.get().getName(); ThreadHelper.runFailableAsync(() -> { - if (ShellDialects.getStartableDialects().stream().anyMatch(dialect -> adjustedPath.startsWith(dialect.getOpenCommand()))) { - TerminalHelper.open( + if (ShellDialects.getStartableDialects().stream().anyMatch(dialect -> adjustedPath.startsWith(dialect.getOpenCommand(null)))) { + TerminalLauncher.open( entry.getEntry(), name, + directory, fileSystem .getShell() .get() - .subShell(processControl -> adjustedPath, (sc) -> adjustedPath) - .withInitSnippet(new SimpleScriptSnippet( - fileSystem - .getShell() - .get() - .getShellDialect() - .getCdCommand(currentPath.get()), - ScriptSnippet.ExecutionType.BOTH))); + .singularSubShell(ShellOpenFunction.of(adjustedPath))); } else { - TerminalHelper.open( + TerminalLauncher.open( entry.getEntry(), name, - fileSystem.getShell().get().command(adjustedPath).withWorkingDirectory(directory)); + directory, + fileSystem.getShell().get().command(adjustedPath)); } }); return Optional.ofNullable(currentPath.get()); @@ -227,6 +235,7 @@ public final class OpenFileSystemModel { private boolean loadFilesSync(String dir) { try { if (dir != null) { + startIfNeeded(); var stream = getFileSystem().listFiles(dir); fileList.setAll(stream); } else { @@ -247,7 +256,8 @@ public final class OpenFileSystemModel { return; } - FileSystemHelper.dropLocalFilesInto(entry, files); + startIfNeeded(); + FileSystemHelper.dropLocalFilesInto(entry, files, progress::setValue); refreshSync(); }); }); @@ -266,14 +276,10 @@ public final class OpenFileSystemModel { return; } - var same = files.get(0).getFileSystem().equals(target.getFileSystem()); - if (same && !explicitCopy) { - if (!BrowserAlerts.showMoveAlert(files, target)) { - return; - } - } - - FileSystemHelper.dropFilesInto(target, files, explicitCopy); + startIfNeeded(); + FileSystemHelper.dropFilesInto(target, files, explicitCopy, browserTransferProgress -> { + progress.setValue(browserTransferProgress); + }); refreshSync(); }); }); @@ -294,6 +300,7 @@ public final class OpenFileSystemModel { return; } + startIfNeeded(); var abs = FileNames.join(getCurrentDirectory().getPath(), name); if (fileSystem.directoryExists(abs)) { throw ErrorEvent.unreportable(new IllegalStateException(String.format("Directory %s already exists", abs))); @@ -320,6 +327,7 @@ public final class OpenFileSystemModel { return; } + startIfNeeded(); var abs = FileNames.join(getCurrentDirectory().getPath(), linkName); fileSystem.symbolicLink(abs, targetFile); refreshSync(); @@ -375,9 +383,6 @@ public final class OpenFileSystemModel { } fs.open(); this.fileSystem = fs; - this.local = fs.getShell() - .map(shellControl -> shellControl.hasLocalSystemAccess()) - .orElse(false); this.cache = new OpenFileSystemCache(this); for (BrowserAction b : BrowserAction.ALL) { @@ -408,21 +413,11 @@ public final class OpenFileSystemModel { BooleanScope.execute(busy, () -> { if (fileSystem.getShell().isPresent()) { var connection = fileSystem.getShell().get(); - var snippet = directory != null ? new SimpleScriptSnippet(connection.getShellDialect().getCdCommand(directory), - ScriptSnippet.ExecutionType.BOTH) : null; - if (snippet != null) { - connection.getInitCommands().add(customScriptsStartIndex,snippet); - } + var name = (directory != null ? directory + " - " : "") + entry.get().getName(); + TerminalLauncher.open(entry.getEntry(), name, directory, connection); - try { - var name = (directory != null ? directory + " - " : "") + entry.get().getName(); - TerminalHelper.open(entry.getEntry(), name, connection); - - // Restart connection as we will have to start it anyway, so we speed it up by doing it preemptively - connection.start(); - } finally { - connection.getInitCommands().remove(snippet); - } + // Restart connection as we will have to start it anyway, so we speed it up by doing it preemptively + startIfNeeded(); } }); }); diff --git a/app/src/main/java/io/xpipe/app/browser/action/MultiExecuteAction.java b/app/src/main/java/io/xpipe/app/browser/action/MultiExecuteAction.java index 7e6d40785..d48184884 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/MultiExecuteAction.java +++ b/app/src/main/java/io/xpipe/app/browser/action/MultiExecuteAction.java @@ -4,7 +4,7 @@ import io.xpipe.app.browser.BrowserEntry; import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.ApplicationHelper; -import io.xpipe.app.util.TerminalHelper; +import io.xpipe.app.util.TerminalLauncher; import io.xpipe.core.process.ShellControl; import org.apache.commons.io.FilenameUtils; @@ -24,10 +24,11 @@ public abstract class MultiExecuteAction implements BranchAction { model.withShell( pc -> { for (BrowserEntry entry : entries) { - TerminalHelper.open(model.getEntry().getEntry(), FilenameUtils.getBaseName( - entry.getRawFileEntry().getPath()), pc.command(createCommand(pc, model, entry)) - .withWorkingDirectory(model.getCurrentDirectory() - .getPath())); + TerminalLauncher.open( + model.getEntry().getEntry(), + FilenameUtils.getBaseName(entry.getRawFileEntry().getPath()), + model.getCurrentDirectory() != null ? model.getCurrentDirectory().getPath() : null, + pc.command(createCommand(pc, model, entry))); } }, false); diff --git a/app/src/main/java/io/xpipe/app/comp/base/DialogComp.java b/app/src/main/java/io/xpipe/app/comp/base/DialogComp.java new file mode 100644 index 000000000..59028999f --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/base/DialogComp.java @@ -0,0 +1,89 @@ +package io.xpipe.app.comp.base; + +import atlantafx.base.controls.Spacer; +import atlantafx.base.theme.Styles; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.core.AppWindowHelper; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.CompStructure; +import io.xpipe.app.fxcomps.SimpleCompStructure; +import javafx.application.Platform; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Pos; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; + +import java.util.function.Function; + +public abstract class DialogComp extends Comp> { + + public static void showWindow(String titleKey, Function f) { + var loading = new SimpleBooleanProperty(); + Platform.runLater(() -> { + var stage = AppWindowHelper.sideWindow( + AppI18n.get(titleKey), + window -> { + var c = f.apply(window); + loading.bind(c.busy()); + return c; + }, + false, + loading); + stage.show(); + }); + } + + protected Region createStepNavigation() { + HBox buttons = new HBox(); + buttons.setFillHeight(true); + var customButton = bottom(); + if (customButton != null) { + buttons.getChildren().add(customButton.createRegion()); + } + buttons.getChildren().add(new Spacer()); + buttons.getStyleClass().add("buttons"); + buttons.setSpacing(5); + buttons.setAlignment(Pos.CENTER_RIGHT); + + var nextButton = new ButtonComp(AppI18n.observable("finishStep"), null, this::finish) + .apply(struc -> struc.get().setDefaultButton(true)) + .styleClass(Styles.ACCENT) + .styleClass("next"); + buttons.getChildren().add(nextButton.createRegion()); + return buttons; + } + + @Override + public CompStructure createBase() { + var entryR = content().createRegion(); + entryR.getStyleClass().add("dialog-content"); + + var sp = new ScrollPane(entryR); + sp.setFitToWidth(true); + entryR.minHeightProperty().bind(sp.heightProperty()); + + VBox vbox = new VBox(); + vbox.getChildren().addAll(sp, createStepNavigation()); + vbox.getStyleClass().add("dialog-comp"); + vbox.setFillWidth(true); + VBox.setVgrow(sp, Priority.ALWAYS); + return new SimpleCompStructure<>(vbox); + } + + protected ObservableValue busy() { + return new SimpleBooleanProperty(false); + } + + protected abstract void finish(); + + public abstract Comp content(); + + public Comp bottom() { + return null; + } +} diff --git a/app/src/main/java/io/xpipe/app/comp/base/InstallExtensionComp.java b/app/src/main/java/io/xpipe/app/comp/base/InstallExtensionComp.java deleted file mode 100644 index 774ecd69b..000000000 --- a/app/src/main/java/io/xpipe/app/comp/base/InstallExtensionComp.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.xpipe.app.comp.base; - -import io.xpipe.app.core.AppFont; -import io.xpipe.app.core.AppI18n; -import io.xpipe.app.core.AppResources; -import io.xpipe.app.ext.DownloadModuleInstall; -import io.xpipe.app.fxcomps.Comp; -import io.xpipe.app.fxcomps.SimpleComp; -import io.xpipe.app.fxcomps.impl.LabelComp; -import io.xpipe.app.util.Hyperlinks; -import io.xpipe.app.util.OptionsBuilder; -import javafx.scene.control.Hyperlink; -import javafx.scene.control.TextArea; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; -import javafx.scene.layout.VBox; -import lombok.EqualsAndHashCode; -import lombok.Value; - -import java.nio.file.Files; - -@Value -@EqualsAndHashCode(callSuper = true) -public class InstallExtensionComp extends SimpleComp { - - DownloadModuleInstall install; - - @Override - protected Region createSimple() { - var builder = new OptionsBuilder(); - builder.addTitle("installRequired"); - var header = new LabelComp(AppI18n.observable("extensionInstallDescription")) - .apply(struc -> struc.get().setWrapText(true)); - builder.addComp(header); - - if (install.getVendorURL() != null) { - var vendorLink = Comp.of(() -> { - var hl = new Hyperlink(install.getVendorURL()); - hl.setOnAction(e -> Hyperlinks.open(install.getVendorURL())); - return hl; - }); - builder.addComp(vendorLink); - } - - if (install.getLicenseFile() != null) { - builder.addTitle("license"); - - var changeNotice = new LabelComp(AppI18n.observable("extensionInstallLicenseNote")) - .apply(struc -> struc.get().setWrapText(true)); - builder.addComp(changeNotice); - - var license = Comp.of(() -> { - var text = new TextArea(); - text.setEditable(false); - AppResources.with(install.getModule(), install.getLicenseFile(), file -> { - var s = Files.readString(file); - text.setText(s); - }); - text.setWrapText(true); - VBox.setVgrow(text, Priority.ALWAYS); - AppFont.verySmall(text); - return text; - }); - builder.addComp(license); - } - - return builder.build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java b/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java index 55a970496..7ffaba463 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java @@ -1,6 +1,5 @@ package io.xpipe.app.comp.base; -import com.jfoenix.controls.JFXTextField; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; @@ -28,7 +27,7 @@ public class LazyTextFieldComp extends Comp { @Override public LazyTextFieldComp.Structure createBase() { var sp = new StackPane(); - var r = new JFXTextField(); + var r = new TextField(); r.setOnKeyPressed(ke -> { if (ke.getCode().equals(KeyCode.ESCAPE)) { diff --git a/app/src/main/java/io/xpipe/app/comp/base/LoadingOverlayComp.java b/app/src/main/java/io/xpipe/app/comp/base/LoadingOverlayComp.java index 47aa0c775..679ea1197 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/LoadingOverlayComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/LoadingOverlayComp.java @@ -16,6 +16,9 @@ import javafx.scene.layout.StackPane; public class LoadingOverlayComp extends Comp> { + private static final double FPS = 30.0; + private static final double cycleDurationSeconds = 4.0; + public static LoadingOverlayComp noProgress(Comp comp, ObservableValue loading) { return new LoadingOverlayComp(comp, loading, new SimpleDoubleProperty(-1)); } @@ -39,6 +42,11 @@ public class LoadingOverlayComp extends Comp> { loading.progressProperty().bind(progress); loading.visibleProperty().bind(Bindings.not(AppPrefs.get().performanceMode())); +// var pane = new StackPane(); +// Parent node = new Indicator((int) (FPS * cycleDurationSeconds), 2.0).getNode(); +// pane.getChildren().add(node); +// pane.setAlignment(Pos.CENTER); + var loadingOverlay = new StackPane(loading); loadingOverlay.getStyleClass().add("loading-comp"); loadingOverlay.setVisible(showLoading.getValue()); diff --git a/app/src/main/java/io/xpipe/app/comp/base/MultiStepComp.java b/app/src/main/java/io/xpipe/app/comp/base/MultiStepComp.java deleted file mode 100644 index 353accec8..000000000 --- a/app/src/main/java/io/xpipe/app/comp/base/MultiStepComp.java +++ /dev/null @@ -1,285 +0,0 @@ -package io.xpipe.app.comp.base; - -import atlantafx.base.controls.Spacer; -import atlantafx.base.theme.Styles; -import com.jfoenix.controls.JFXTabPane; -import io.xpipe.app.core.AppI18n; -import io.xpipe.app.fxcomps.Comp; -import io.xpipe.app.fxcomps.CompStructure; -import io.xpipe.app.fxcomps.SimpleCompStructure; -import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; -import javafx.beans.binding.Bindings; -import javafx.beans.property.Property; -import javafx.beans.property.ReadOnlyProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ObservableValue; -import javafx.css.PseudoClass; -import javafx.geometry.Pos; -import javafx.scene.control.Label; -import javafx.scene.control.Tab; -import javafx.scene.layout.*; -import lombok.Getter; - -import java.util.List; - -public abstract class MultiStepComp extends Comp> { - private static final PseudoClass COMPLETED = PseudoClass.getPseudoClass("completed"); - private static final PseudoClass CURRENT = PseudoClass.getPseudoClass("current"); - private static final PseudoClass NEXT = PseudoClass.getPseudoClass("next"); - private final Property completed = new SimpleBooleanProperty(); - private final Property> currentStep = new SimpleObjectProperty<>(); - @Getter - private List entries; - @Getter - private int currentIndex = 0; - - private Step getValue() { - return currentStep.getValue(); - } - - private void set(Step step) { - currentStep.setValue(step); - } - - public void next() { - PlatformThread.runLaterIfNeeded(() -> { - if (isFinished()) { - return; - } - - if (!getValue().canContinue()) { - return; - } - - if (isLastPage()) { - getValue().onContinue(); - finish(); - currentIndex++; - completed.setValue(true); - return; - } - - int index = Math.min(getCurrentIndex() + 1, entries.size() - 1); - if (currentIndex == index) { - return; - } - - getValue().onContinue(); - entries.get(index).step().onInit(); - currentIndex = index; - set(entries.get(index).step()); - }); - } - - public void previous() { - PlatformThread.runLaterIfNeeded(() -> { - int index = Math.max(currentIndex - 1, 0); - if (currentIndex == index) { - return; - } - - getValue().onBack(); - currentIndex = index; - set(entries.get(index).step()); - }); - } - - public boolean isCompleted(Entry e) { - return entries.indexOf(e) < currentIndex; - } - - public boolean isNext(Entry e) { - return entries.indexOf(e) > currentIndex; - } - - public boolean isCurrent(Entry e) { - return entries.indexOf(e) == currentIndex; - } - - public boolean isFirstPage() { - return currentIndex == 0; - } - - public boolean isLastPage() { - return currentIndex == entries.size() - 1; - } - - public boolean isFinished() { - return currentIndex == entries.size(); - } - - protected Region createStepOverview(Region content) { - if (entries.size() == 1) { - return new Region(); - } - - HBox box = new HBox(); - box.setFillHeight(true); - box.getStyleClass().add("top"); - box.setAlignment(Pos.CENTER); - - var comp = this; - int number = 1; - for (var entry : comp.getEntries()) { - VBox element = new VBox(); - element.setFillWidth(true); - element.setAlignment(Pos.CENTER); - var label = new Label(); - label.textProperty().bind(entry.name); - label.getStyleClass().add("name"); - element.getChildren().add(label); - element.getStyleClass().add("entry"); - - var line = new Region(); - boolean first = number == 1; - boolean last = number == comp.getEntries().size(); - line.prefWidthProperty() - .bind(Bindings.createDoubleBinding( - () -> element.getWidth() / ((first || last) ? 2 : 1), element.widthProperty())); - line.setMinWidth(0); - line.getStyleClass().add("line"); - var lineBox = new HBox(line); - lineBox.setFillHeight(true); - if (first) { - lineBox.setAlignment(Pos.CENTER_RIGHT); - } else if (last) { - lineBox.setAlignment(Pos.CENTER_LEFT); - } else { - lineBox.setAlignment(Pos.CENTER); - } - - var circle = new Region(); - circle.getStyleClass().add("circle"); - var numberLabel = new Label("" + number); - numberLabel.getStyleClass().add("number"); - var stack = new StackPane(); - stack.getChildren().add(lineBox); - stack.getChildren().add(circle); - stack.getChildren().add(numberLabel); - stack.setAlignment(Pos.CENTER); - element.getChildren().add(stack); - - Runnable updatePseudoClasses = () -> { - element.pseudoClassStateChanged(CURRENT, comp.isCurrent(entry)); - element.pseudoClassStateChanged(NEXT, comp.isNext(entry)); - element.pseudoClassStateChanged(COMPLETED, comp.isCompleted(entry)); - }; - updatePseudoClasses.run(); - comp.currentStep.addListener((c, o, n) -> { - updatePseudoClasses.run(); - }); - - box.getChildren().add(element); - - element.prefWidthProperty() - .bind(Bindings.createDoubleBinding( - () -> content.getWidth() / comp.getEntries().size(), content.widthProperty())); - - number++; - } - - return box; - } - - protected Region createStepNavigation() { - MultiStepComp comp = this; - - HBox buttons = new HBox(); - buttons.setFillHeight(true); - buttons.getChildren().add(new Region()); - buttons.getChildren().add(new Spacer()); - buttons.getStyleClass().add("buttons"); - buttons.setSpacing(5); - - SimpleChangeListener.apply(currentStep, val -> { - buttons.getChildren().set(0, val.bottom() != null ? val.bottom().createRegion() : new Region()); - }); - - buttons.setAlignment(Pos.CENTER_RIGHT); - var nextText = Bindings.createStringBinding( - () -> isLastPage() ? AppI18n.get("finishStep") : AppI18n.get("nextStep"), currentStep); - var nextButton = new ButtonComp(nextText, null, comp::next) - .styleClass(Styles.ACCENT) - .styleClass("next"); - - var previousButton = new ButtonComp(AppI18n.observable("previousStep"), null, comp::previous) - .styleClass("next") - .apply(struc -> struc.get() - .disableProperty() - .bind(Bindings.createBooleanBinding(this::isFirstPage, currentStep))); - - previousButton.apply( - s -> s.get().visibleProperty().bind(Bindings.createBooleanBinding(() -> !isFirstPage(), currentStep))); - - buttons.getChildren().add(previousButton.createRegion()); - buttons.getChildren().add(nextButton.createRegion()); - - return buttons; - } - - @Override - public CompStructure createBase() { - this.entries = setup(); - this.set(entries.get(currentIndex).step); - - VBox content = new VBox(); - var comp = this; - Region box = createStepOverview(content); - - var compContent = new JFXTabPane(); - compContent.getStyleClass().add("content"); - for (var ignored : comp.getEntries()) { - compContent.getTabs().add(new Tab(null, null)); - } - var entryR = comp.getValue().createRegion(); - entryR.getStyleClass().add("step"); - compContent.getTabs().set(currentIndex, new Tab(null, entryR)); - compContent.getSelectionModel().select(currentIndex); - - content.getChildren().addAll(box, compContent, createStepNavigation()); - content.getStyleClass().add("multi-step-comp"); - content.setFillWidth(true); - VBox.setVgrow(compContent, Priority.ALWAYS); - currentStep.addListener((c, o, n) -> { - var nextTab = compContent - .getTabs() - .get(entries.stream().map(e -> e.step).toList().indexOf(n)); - if (nextTab.getContent() == null) { - var createdRegion = n.createRegion(); - createdRegion.getStyleClass().add("step"); - nextTab.setContent(createdRegion); - } - compContent.getSelectionModel().select(comp.getCurrentIndex()); - }); - return new SimpleCompStructure<>(content); - } - - protected abstract List setup(); - - protected abstract void finish(); - - public ReadOnlyProperty completedProperty() { - return completed; - } - - public abstract static class Step> extends Comp { - - public Comp bottom() { - return null; - } - - public void onInit() {} - - public void onBack() {} - - public void onContinue() {} - - public boolean canContinue() { - return true; - } - } - - public record Entry(ObservableValue name, Step step) {} -} diff --git a/app/src/main/java/io/xpipe/app/comp/base/OsLogoComp.java b/app/src/main/java/io/xpipe/app/comp/base/OsLogoComp.java index 3c3f8ffb0..68ea67975 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/OsLogoComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/OsLogoComp.java @@ -57,6 +57,7 @@ public class OsLogoComp extends SimpleComp { private static final Map ICONS = new HashMap<>(); private static final String LINUX_DEFAULT = "linux-24.png"; + private static final String LINUX_DEFAULT_SVG = "linux.svg"; private String getImage(String name) { if (name == null) { @@ -66,7 +67,7 @@ public class OsLogoComp extends SimpleComp { if (ICONS.isEmpty()) { AppResources.with(AppResources.XPIPE_MODULE, "img/os", file -> { try (var list = Files.list(file)) { - list.filter(path -> path.toString().endsWith(".svg") && !path.toString().endsWith(LINUX_DEFAULT)) + list.filter(path -> path.toString().endsWith(".svg") && !path.toString().endsWith(LINUX_DEFAULT_SVG)) .map(path -> FileNames.getFileName(path.toString())).forEach(path -> { var base = FileNames.getBaseName(path).replace("-dark", "") + "-24.png"; ICONS.put(FileNames.getBaseName(base).split("-")[0], "os/" + base); diff --git a/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java b/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java index b0a30f5dc..353b5fbde 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java @@ -6,6 +6,7 @@ import io.xpipe.app.core.AppLogs; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; +import io.xpipe.app.fxcomps.augment.Augment; import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.util.PlatformThread; @@ -14,13 +15,13 @@ import io.xpipe.app.issue.UserReportComp; import io.xpipe.app.update.UpdateAvailableAlert; import io.xpipe.app.update.XPipeDistributionType; import io.xpipe.app.util.Hyperlinks; +import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.Property; import javafx.css.PseudoClass; import javafx.scene.control.Button; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; -import javafx.scene.layout.VBox; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; import java.util.List; @@ -39,6 +40,23 @@ public class SideMenuBarComp extends Comp> { var vbox = new VBox(); vbox.setFillWidth(true); + var selectedBorder = Bindings.createObjectBinding(() -> { + var c = Platform.getPreferences().getAccentColor(); + return new Border(new BorderStroke(c, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, + new BorderWidths(0, 3, 0, 0))); + }, Platform.getPreferences().accentColorProperty()); + + var hoverBorder = Bindings.createObjectBinding(() -> { + var c = Platform.getPreferences().getAccentColor().darker(); + return new Border(new BorderStroke(c, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, + new BorderWidths(0, 3, 0, 0))); + }, Platform.getPreferences().accentColorProperty()); + + var noneBorder = Bindings.createObjectBinding(() -> { + return new Border(new BorderStroke(Color.TRANSPARENT, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, + new BorderWidths(0, 3, 0, 0))); + }, Platform.getPreferences().accentColorProperty()); + var selected = PseudoClass.getPseudoClass("selected"); entries.forEach(e -> { var b = new IconButtonComp(e.icon(), () -> value.setValue(e)).apply(new FancyTooltipAugment<>(e.name())); @@ -50,11 +68,32 @@ public class SideMenuBarComp extends Comp> { struc.get().pseudoClassStateChanged(selected, n.equals(e)); }); }); + struc.get().borderProperty().bind(Bindings.createObjectBinding(() -> { + if (value.getValue().equals(e)) { + return selectedBorder.get(); + } + + if (struc.get().isHover()) { + return hoverBorder.get(); + } + + return noneBorder.get(); + }, struc.get().hoverProperty(), value, hoverBorder, selectedBorder, noneBorder)); }); b.accessibleText(e.name()); vbox.getChildren().add(b.createRegion()); }); + Augment> simpleBorders = struc -> { + struc.get().borderProperty().bind(Bindings.createObjectBinding(() -> { + if (struc.get().isHover()) { + return hoverBorder.get(); + } + + return noneBorder.get(); + }, struc.get().hoverProperty(), value, hoverBorder, selectedBorder, noneBorder)); + }; + { var b = new IconButtonComp( "mdal-bug_report", @@ -65,7 +104,7 @@ public class SideMenuBarComp extends Comp> { } UserReportComp.show(event.build()); }) - .apply(new FancyTooltipAugment<>("reportIssue")).accessibleTextKey("reportIssue"); + .apply(new FancyTooltipAugment<>("reportIssue")).apply(simpleBorders).accessibleTextKey("reportIssue"); b.apply(struc -> { AppFont.setSize(struc.get(), 2); }); @@ -74,26 +113,16 @@ public class SideMenuBarComp extends Comp> { { var b = new IconButtonComp("mdi2g-github", () -> Hyperlinks.open(Hyperlinks.GITHUB)) - .apply(new FancyTooltipAugment<>("visitGithubRepository")).accessibleTextKey("visitGithubRepository"); + .apply(new FancyTooltipAugment<>("visitGithubRepository")).apply(simpleBorders).accessibleTextKey("visitGithubRepository"); b.apply(struc -> { AppFont.setSize(struc.get(), 2); }); vbox.getChildren().add(b.createRegion()); } -// { -// var b = new IconButtonComp("mdi2c-comment-processing-outline", () -> Hyperlinks.open(Hyperlinks.ROADMAP)) -// .apply(new FancyTooltipAugment<>("roadmap")); -// b.apply(struc -> { -// AppFont.setSize(struc.get(), 2); -// }); -// vbox.getChildren().add(b.createRegion()); -// } - - { var b = new IconButtonComp("mdi2d-discord", () -> Hyperlinks.open(Hyperlinks.DISCORD)) - .apply(new FancyTooltipAugment<>("discord")).accessibleTextKey("discord"); + .apply(new FancyTooltipAugment<>("discord")).apply(simpleBorders).accessibleTextKey("discord"); b.apply(struc -> { AppFont.setSize(struc.get(), 2); }); diff --git a/app/src/main/java/io/xpipe/app/comp/base/StoreToggleComp.java b/app/src/main/java/io/xpipe/app/comp/base/StoreToggleComp.java index b29d2fa53..1d2e52ead 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/StoreToggleComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/StoreToggleComp.java @@ -39,7 +39,7 @@ public class StoreToggleComp extends SimpleComp { }, section.getWrapper().getValidity(), section.getShowDetails())); - var t = new NamedToggleComp(value, AppI18n.observable(nameKey)) + var t = new ToggleSwitchComp(value, AppI18n.observable(nameKey)) .visible(visible) .disable(disable); value.addListener((observable, oldValue, newValue) -> { diff --git a/app/src/main/java/io/xpipe/app/comp/base/NamedToggleComp.java b/app/src/main/java/io/xpipe/app/comp/base/ToggleSwitchComp.java similarity index 77% rename from app/src/main/java/io/xpipe/app/comp/base/NamedToggleComp.java rename to app/src/main/java/io/xpipe/app/comp/base/ToggleSwitchComp.java index b237eacdd..07b9c1ba8 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/NamedToggleComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ToggleSwitchComp.java @@ -3,16 +3,16 @@ package io.xpipe.app.comp.base; import atlantafx.base.controls.ToggleSwitch; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.util.PlatformThread; -import javafx.beans.property.BooleanProperty; +import javafx.beans.property.Property; import javafx.beans.value.ObservableValue; import javafx.scene.layout.Region; -public class NamedToggleComp extends SimpleComp { +public class ToggleSwitchComp extends SimpleComp { - private final BooleanProperty selected; + private final Property selected; private final ObservableValue name; - public NamedToggleComp(BooleanProperty selected, ObservableValue name) { + public ToggleSwitchComp(Property selected, ObservableValue name) { this.selected = selected; this.name = name; } @@ -22,7 +22,7 @@ public class NamedToggleComp extends SimpleComp { var s = new ToggleSwitch(); s.setSelected(selected.getValue()); s.selectedProperty().addListener((observable, oldValue, newValue) -> { - selected.set(newValue); + selected.setValue(newValue); }); selected.addListener((observable, oldValue, newValue) -> { PlatformThread.runLaterIfNeeded(() -> { diff --git a/app/src/main/java/io/xpipe/app/comp/store/DenseStoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/store/DenseStoreEntryComp.java index 8635ee8ea..78dfb8cdd 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/DenseStoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/DenseStoreEntryComp.java @@ -4,7 +4,6 @@ import io.xpipe.app.core.AppFont; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.binding.Bindings; import javafx.geometry.HPos; import javafx.geometry.Insets; @@ -32,16 +31,18 @@ public class DenseStoreEntryComp extends StoreEntryComp { : Comp.empty(); information.setGraphic(state.createRegion()); - var summary = wrapper.getSummary(); var info = wrapper.getEntry().getProvider().informationString(wrapper); - SimpleChangeListener.apply(grid.hoverProperty(), val -> { - if (val && summary.getValue() != null && wrapper.getEntry().getProvider().alwaysShowSummary()) { - information.textProperty().bind(PlatformThread.sync(summary)); - } else { - information.textProperty().bind(PlatformThread.sync(info)); - - } - }); + var summary = wrapper.getSummary(); + if (wrapper.getEntry().getProvider() != null) { + information.textProperty().bind(PlatformThread.sync(Bindings.createStringBinding(() -> { + var val = summary.getValue(); + if (val != null && grid.isHover() && wrapper.getEntry().getProvider().alwaysShowSummary()) { + return val; + } else { + return info.getValue(); + } + }, grid.hoverProperty(), info, summary))); + } return information; } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java index e33003bbd..7adf38d68 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java @@ -1,13 +1,13 @@ package io.xpipe.app.comp.store; +import atlantafx.base.controls.Spacer; +import io.xpipe.app.comp.base.DialogComp; import io.xpipe.app.comp.base.ErrorOverlayComp; -import io.xpipe.app.comp.base.MultiStepComp; import io.xpipe.app.comp.base.PopupMenuButtonComp; -import io.xpipe.app.core.*; -import io.xpipe.app.core.mode.OperationMode; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.core.AppWindowHelper; import io.xpipe.app.ext.DataStoreProvider; import io.xpipe.app.fxcomps.Comp; -import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.SimpleChangeListener; @@ -25,12 +25,14 @@ import javafx.beans.binding.Bindings; import javafx.beans.property.*; import javafx.beans.value.ObservableValue; import javafx.geometry.Insets; +import javafx.geometry.Orientation; import javafx.scene.control.Alert; import javafx.scene.control.ScrollPane; import javafx.scene.control.Separator; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; +import javafx.stage.Stage; import lombok.AccessLevel; import lombok.experimental.FieldDefaults; @@ -41,9 +43,10 @@ import java.util.function.Consumer; import java.util.function.Predicate; @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) -public class StoreCreationComp extends MultiStepComp.Step> { +public class StoreCreationComp extends DialogComp { - MultiStepComp parent; + Stage window; + Consumer consumer; Property provider; Property store; Predicate filter; @@ -58,14 +61,15 @@ public class StoreCreationComp extends MultiStepComp.Step> { boolean staticDisplay; public StoreCreationComp( - MultiStepComp parent, + Stage window, Consumer consumer, Property provider, Property store, Predicate filter, String initialName, DataStoreEntry existingEntry, boolean staticDisplay) { - this.parent = parent; + this.window = window; + this.consumer = consumer; this.provider = provider; this.store = store; this.filter = filter; @@ -178,36 +182,13 @@ public class StoreCreationComp extends MultiStepComp.Step> { DataStoreEntry existingEntry) { var prop = new SimpleObjectProperty<>(provider); var store = new SimpleObjectProperty<>(s); - var loading = new SimpleBooleanProperty(); - var name = "addConnection"; - Platform.runLater(() -> { - var stage = AppWindowHelper.sideWindow( - AppI18n.get(name), - window -> { - return new MultiStepComp() { + DialogComp.showWindow("addConnection", stage -> new StoreCreationComp( + stage, con, prop, store, filter, initialName, existingEntry, staticDisplay)); + } - private final StoreCreationComp creator = new StoreCreationComp( - this, prop, store, filter, initialName, existingEntry, staticDisplay); - - @Override - protected List setup() { - loading.bind(creator.busy); - return List.of(new Entry(AppI18n.observable("a"), creator)); - } - - @Override - protected void finish() { - window.close(); - if (creator.entry.getValue() != null) { - con.accept(creator.entry.getValue()); - } - } - }; - }, - false, - loading); - stage.show(); - }); + @Override + protected ObservableValue busy() { + return busy; } @Override @@ -259,18 +240,97 @@ public class StoreCreationComp extends MultiStepComp.Step> { .build(); } + private void commit() { + if (finished.get()) { + return; + } + finished.setValue(true); + + if (entry.getValue() != null) { + consumer.accept(entry.getValue()); + } + + PlatformThread.runLaterIfNeeded(() -> { + window.close(); + }); + } + @Override - public CompStructure createBase() { + protected void finish() { + if (finished.get()) { + return; + } + + if (store.getValue() == null) { + return; + } + + // We didn't change anything + if (existingEntry != null && existingEntry.getStore().equals(store.getValue())) { + commit(); + return; + } + + if (messageProp.getValue() != null && !changedSinceError.get()) { + if (AppPrefs.get().developerMode().getValue() && showInvalidConfirmAlert()) { + commit(); + return; + } + } + + if (!validator.getValue().validate()) { + var msg = validator + .getValue() + .getValidationResult() + .getMessages() + .getFirst() + .getText(); + TrackEvent.info(msg); + var newMessage = msg; + // Temporary fix for equal error message not showing up again + if (Objects.equals(newMessage, messageProp.getValue())) { + newMessage = newMessage + " "; + } + messageProp.setValue(newMessage); + changedSinceError.setValue(false); + return; + } + + ThreadHelper.runAsync(() -> { + try (var b = new BooleanScope(busy).start()) { + DataStorage.get().addStoreEntryInProgress(entry.getValue()); + entry.getValue().validateOrThrow(); + commit(); + } catch (Throwable ex) { + var newMessage = ExceptionConverter.convertMessage(ex); + // Temporary fix for equal error message not showing up again + if (Objects.equals(newMessage, messageProp.getValue())) { + newMessage = newMessage + " "; + } + messageProp.setValue(newMessage); + changedSinceError.setValue(false); + if (ex instanceof ValidationException) { + ErrorEvent.unreportable(ex); + } + ErrorEvent.fromThrowable(ex).omit().handle(); + } finally { + DataStorage.get().removeStoreEntryInProgress(entry.getValue()); + } + }); + } + + @Override + public Comp content() { var back = Comp.of(this::createLayout); var message = new ErrorOverlayComp(back, messageProp); - return message.createStructure(); + return message; } private Region createLayout() { var layout = new BorderPane(); layout.getStyleClass().add("store-creator"); layout.setPadding(new Insets(20)); - var providerChoice = new DataStoreProviderChoiceComp(filter, provider, staticDisplay); + var providerChoice = new StoreProviderChoiceComp(filter, provider, staticDisplay); if (staticDisplay) { providerChoice.apply(struc -> struc.get().setDisable(true)); } @@ -297,93 +357,9 @@ public class StoreCreationComp extends MultiStepComp.Step> { var sep = new Separator(); sep.getStyleClass().add("spacer"); - var top = new VBox(providerChoice.createRegion(), sep); + var top = new VBox(providerChoice.createRegion(), new Spacer(7, Orientation.VERTICAL), sep); top.getStyleClass().add("top"); layout.setTop(top); return layout; } - - @Override - public boolean canContinue() { - if (provider.getValue() != null) { - var install = provider.getValue().getRequiredAdditionalInstallation(); - if (install != null && !AppExtensionManager.getInstance().isInstalled(install)) { - ThreadHelper.runAsync(() -> { - try (var ignored = new BooleanScope(busy).start()) { - AppExtensionManager.getInstance().installIfNeeded(install); - /* - TODO: Use reload - */ - finished.setValue(true); - OperationMode.shutdown(false, false); - PlatformThread.runLaterIfNeeded(parent::next); - } catch (Exception ex) { - ErrorEvent.fromThrowable(ex).handle(); - } - }); - return false; - } - } - - if (finished.get()) { - return true; - } - - if (store.getValue() == null) { - return false; - } - - // We didn't change anything - if (existingEntry != null && existingEntry.getStore().equals(store.getValue())) { - return true; - } - - if (messageProp.getValue() != null && !changedSinceError.get()) { - if (AppPrefs.get().developerMode().getValue() && showInvalidConfirmAlert()) { - return true; - } - } - - if (!validator.getValue().validate()) { - var msg = validator - .getValue() - .getValidationResult() - .getMessages() - .getFirst() - .getText(); - TrackEvent.info(msg); - var newMessage = msg; - // Temporary fix for equal error message not showing up again - if (Objects.equals(newMessage, messageProp.getValue())) { - newMessage = newMessage + " "; - } - messageProp.setValue(newMessage); - changedSinceError.setValue(false); - return false; - } - - ThreadHelper.runAsync(() -> { - try (var b = new BooleanScope(busy).start()) { - DataStorage.get().addStoreEntryInProgress(entry.getValue()); - entry.getValue().validateOrThrow(); - finished.setValue(true); - PlatformThread.runLaterIfNeeded(parent::next); - } catch (Throwable ex) { - var newMessage = ExceptionConverter.convertMessage(ex); - // Temporary fix for equal error message not showing up again - if (Objects.equals(newMessage, messageProp.getValue())) { - newMessage = newMessage + " "; - } - messageProp.setValue(newMessage); - changedSinceError.setValue(false); - if (ex instanceof ValidationException) { - ErrorEvent.unreportable(ex); - } - ErrorEvent.fromThrowable(ex).omit().handle(); - } finally { - DataStorage.get().removeStoreEntryInProgress(entry.getValue()); - } - }); - return false; - } } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java index 6ffc72965..24707c1e7 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java @@ -42,7 +42,8 @@ public abstract class StoreEntryComp extends SimpleComp { public static StoreEntryComp create( StoreEntryWrapper entry, Comp content, boolean preferLarge) { - if (!preferLarge) { + var forceCondensed = AppPrefs.get() != null && AppPrefs.get().condenseConnectionDisplay().get(); + if (!preferLarge || forceCondensed) { return new DenseStoreEntryComp(entry, true, content); } else { return new StandardStoreEntryComp(entry, content); @@ -54,7 +55,10 @@ public abstract class StoreEntryComp extends SimpleComp { if (prov != null) { return prov.customEntryComp(e, topLevel); } else { - return new StandardStoreEntryComp(e.getWrapper(), null); + var forceCondensed = AppPrefs.get() != null && AppPrefs.get().condenseConnectionDisplay().get(); + return forceCondensed ? + new DenseStoreEntryComp(e.getWrapper(), true, null) : + new StandardStoreEntryComp(e.getWrapper(), null); } } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreIntroComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreIntroComp.java index b85d8c91c..4cb258c1d 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreIntroComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreIntroComp.java @@ -1,5 +1,6 @@ package io.xpipe.app.comp.store; +import atlantafx.base.theme.Styles; import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.SimpleComp; @@ -7,11 +8,9 @@ import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.ScanAlert; import javafx.beans.property.SimpleStringProperty; -import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; -import javafx.scene.control.Separator; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; @@ -23,23 +22,22 @@ public class StoreIntroComp extends SimpleComp { @Override public Region createSimple() { var title = new Label(AppI18n.get("storeIntroTitle")); + title.getStyleClass().add(Styles.TEXT_BOLD); AppFont.setSize(title, 7); var introDesc = new Label(AppI18n.get("storeIntroDescription")); - - var mfi = new FontIcon("mdi2p-playlist-plus"); - var machine = new Label(AppI18n.get("storeMachineDescription")); - machine.heightProperty().addListener((c, o, n) -> { - mfi.iconSizeProperty().set(n.intValue()); - }); + introDesc.setWrapText(true); + introDesc.setMaxWidth(470); var scanButton = new Button(AppI18n.get("detectConnections"), new FontIcon("mdi2m-magnify")); scanButton.setOnAction(event -> ScanAlert.showAsync(DataStorage.get().local())); + scanButton.setDefaultButton(true); var scanPane = new StackPane(scanButton); scanPane.setAlignment(Pos.CENTER); var img = PrettyImageHelper.ofSvg(new SimpleStringProperty("Wave.svg"), 80, 150).createRegion(); - var text = new VBox(title, introDesc, new Separator(Orientation.HORIZONTAL), machine); + var text = new VBox(title, introDesc); + text.setSpacing(5); text.setAlignment(Pos.CENTER_LEFT); var hbox = new HBox(img, text); hbox.setSpacing(35); diff --git a/app/src/main/java/io/xpipe/app/comp/store/DataStoreProviderChoiceComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreProviderChoiceComp.java similarity index 91% rename from app/src/main/java/io/xpipe/app/comp/store/DataStoreProviderChoiceComp.java rename to app/src/main/java/io/xpipe/app/comp/store/StoreProviderChoiceComp.java index c8d6b92ec..c3bee0dff 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/DataStoreProviderChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreProviderChoiceComp.java @@ -5,7 +5,6 @@ import io.xpipe.app.ext.DataStoreProviders; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; -import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.JfxHelper; import javafx.beans.property.Property; import javafx.scene.control.ComboBox; @@ -22,7 +21,7 @@ import java.util.function.Supplier; @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @AllArgsConstructor -public class DataStoreProviderChoiceComp extends Comp>> { +public class StoreProviderChoiceComp extends Comp>> { Predicate filter; Property provider; @@ -58,7 +57,7 @@ public class DataStoreProviderChoiceComp extends Comp AppPrefs.get().developerShowHiddenProviders().get() || p.getCreationCategory() != null || staticDisplay) + .filter(p -> p.getCreationCategory() != null || staticDisplay) .toList(); l.forEach(dataStoreProvider -> cb.getItems().add(dataStoreProvider)); if (provider.getValue() == null) { diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java b/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java index 15c95390a..12cbbcf58 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java @@ -70,7 +70,7 @@ public class StoreSection { var current = mappedSortMode.getValue(); if (current != null) { return c.thenComparing(current.comparator()) - .compare(o1, o2); + .compare(current.representative(o1), current.representative(o2)); } else { return c.compare(o1, o2); } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionComp.java index ac4ae2ae7..6ed041466 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionComp.java @@ -38,7 +38,7 @@ public class StoreSectionComp extends Comp> { @Override public CompStructure createBase() { - var root = StandardStoreEntryComp.customSection(section, topLevel) + var root = StoreEntryComp.customSection(section, topLevel) .apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS)); var button = new IconButtonComp( Bindings.createStringBinding( diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreSortMode.java b/app/src/main/java/io/xpipe/app/comp/store/StoreSortMode.java index 4c43d2b3f..0a9bb031e 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreSortMode.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreSortMode.java @@ -11,7 +11,14 @@ import java.util.stream.Stream; public interface StoreSortMode { + StoreSection representative(StoreSection s); + StoreSortMode ALPHABETICAL_DESC = new StoreSortMode() { + @Override + public StoreSection representative(StoreSection s) { + return s; + } + @Override public String getId() { return "alphabetical-desc"; @@ -25,6 +32,11 @@ public interface StoreSortMode { }; StoreSortMode ALPHABETICAL_ASC = new StoreSortMode() { + @Override + public StoreSection representative(StoreSection s) { + return s; + } + @Override public String getId() { return "alphabetical-asc"; @@ -39,6 +51,14 @@ public interface StoreSortMode { }; StoreSortMode DATE_DESC = new StoreSortMode() { + @Override + public StoreSection representative(StoreSection s) { + var c = comparator(); + return Stream.of(s.getShownChildren().stream().max((o1, o2) -> { + return c.compare(representative(o1), representative(o2)); + }).orElse(s), s).max(c).orElseThrow(); + } + @Override public String getId() { return "date-desc"; @@ -56,6 +76,14 @@ public interface StoreSortMode { }; StoreSortMode DATE_ASC = new StoreSortMode() { + @Override + public StoreSection representative(StoreSection s) { + var c = comparator(); + return Stream.of(s.getShownChildren().stream().min((o1, o2) -> { + return c.compare(representative(o1), representative(o2)); + }).orElse(s), s).min(c).orElseThrow(); + } + @Override public String getId() { return "date-asc"; diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreViewState.java b/app/src/main/java/io/xpipe/app/comp/store/StoreViewState.java index 690d112d1..a4b2b5579 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreViewState.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreViewState.java @@ -3,6 +3,7 @@ package io.xpipe.app.comp.store; import io.xpipe.app.core.AppCache; import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreCategory; import io.xpipe.app.storage.DataStoreEntry; @@ -70,7 +71,7 @@ public class StoreViewState { private StoreViewState() { initContent(); - addStorageListeners(); + addListeners(); } private void updateContent() { @@ -112,7 +113,20 @@ public class StoreViewState { .orElseThrow())); } - private void addStorageListeners() { + private void addListeners() { + if (AppPrefs.get() != null) { + AppPrefs.get().condenseConnectionDisplay().addListener((observable, oldValue, newValue) -> { + Platform.runLater(() -> { + synchronized (this) { + var l = new ArrayList<>(allEntries); + allEntries.clear(); + allEntries.setAll(l); + } + }); + }); + } + + // Watch out for synchronizing all calls to the entries and categories list! DataStorage.get().addListener(new StorageListener() { @Override diff --git a/app/src/main/java/io/xpipe/app/core/AppCharsets.java b/app/src/main/java/io/xpipe/app/core/AppCharsets.java deleted file mode 100644 index 27317e659..000000000 --- a/app/src/main/java/io/xpipe/app/core/AppCharsets.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.xpipe.app.core; - -import io.xpipe.app.prefs.AppPrefs; -import io.xpipe.core.charsetter.Charsetter; -import io.xpipe.core.charsetter.CharsetterContext; -import io.xpipe.core.charsetter.StreamCharset; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -public class AppCharsets { - - private static final List observedCharsets = new ArrayList<>(); - - public static void init() { - var system = System.getProperty("file.encoding"); - var systemLocale = Locale.getDefault(); - var appLocale = AppPrefs.get().language.getValue().getLocale(); - var used = AppCache.get("observedCharsets", List.class, () -> new ArrayList()); - var ctx = new CharsetterContext(system, systemLocale, appLocale, used); - Charsetter.init(ctx); - } - - public static void observe(StreamCharset c) { - if (c == null) { - return; - } - - var used = AppCache.get("observedCharsets", List.class, () -> new ArrayList()); - used.add(c.getCharset().name()); - AppCache.update("observedCharsets", used); - - init(); - } -} diff --git a/app/src/main/java/io/xpipe/app/core/AppCharsetter.java b/app/src/main/java/io/xpipe/app/core/AppCharsetter.java deleted file mode 100644 index 5ce783f60..000000000 --- a/app/src/main/java/io/xpipe/app/core/AppCharsetter.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.xpipe.app.core; - -import io.xpipe.core.charsetter.Charsetter; -import io.xpipe.core.charsetter.StreamCharset; -import io.xpipe.core.util.FailableConsumer; -import io.xpipe.core.util.FailableSupplier; -import org.apache.commons.io.ByteOrderMark; -import org.apache.commons.io.input.BOMInputStream; - -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.Charset; - -public class AppCharsetter extends Charsetter { - - private static final int MAX_BYTES = 8192; - - public static void init() { - Charsetter.INSTANCE = new AppCharsetter(); - } - - public Result read(FailableSupplier in, FailableConsumer con) - throws Exception { - checkInit(); - - try (var is = in.get(); - var bin = new BOMInputStream(is)) { - ByteOrderMark bom = bin.getBOM(); - String charsetName = bom == null ? null : bom.getCharsetName(); - var charset = charsetName != null - ? StreamCharset.get(Charset.forName(charsetName), bom.getCharsetName() != null) - : null; - - bin.mark(MAX_BYTES); - var bytes = bin.readNBytes(MAX_BYTES); - bin.reset(); - if (charset == null) { - charset = StreamCharset.get(inferCharset(bytes), false); - } - var nl = inferNewLine(bytes); - - if (con != null) { - con.accept(new InputStreamReader(bin, charset.getCharset())); - } - return new Result(charset, nl); - } - } -} diff --git a/app/src/main/java/io/xpipe/app/core/AppExtensionManager.java b/app/src/main/java/io/xpipe/app/core/AppExtensionManager.java index db6d615aa..95c445268 100644 --- a/app/src/main/java/io/xpipe/app/core/AppExtensionManager.java +++ b/app/src/main/java/io/xpipe/app/core/AppExtensionManager.java @@ -2,7 +2,6 @@ package io.xpipe.app.core; import io.xpipe.app.exchange.MessageExchangeImpls; import io.xpipe.app.ext.ExtensionException; -import io.xpipe.app.ext.ModuleInstall; import io.xpipe.app.ext.XPipeServiceProviders; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.TrackEvent; @@ -110,36 +109,11 @@ public class AppExtensionManager { .collect(Collectors.toSet()); } - public boolean isInstalled(ModuleInstall install) { - var target = - AppExtensionManager.getInstance().getGeneratedModulesDirectory(install.getModule(), install.getId()); - return Files.exists(target) && Files.isRegularFile(target.resolve("finished")); - } - - public void installIfNeeded(ModuleInstall install) throws Exception { - var target = - AppExtensionManager.getInstance().getGeneratedModulesDirectory(install.getModule(), install.getId()); - if (Files.exists(target) && Files.isRegularFile(target.resolve("finished"))) { - return; - } - - Files.createDirectories(target); - install.installInternal(target); - Files.createFile(target.resolve("finished")); - } - - public Path getGeneratedModulesDirectory(String module, String ext) { - var base = AppProperties.get() - .getDataDir() - .resolve("generated_extensions") - .resolve(AppProperties.get().getVersion()) - .resolve(module); - return ext != null ? base.resolve(ext) : base; - } - private void loadAllExtensions() { - for (Path extensionBaseDirectory : extensionBaseDirectories) { - loadExtensionRootDirectory(extensionBaseDirectory); + for (var ext : List.of("jdbc", "proc", "uacc")) { + var extension = findAndParseExtension(ext,baseLayer).orElseThrow(() -> ExtensionException.corrupt("Missing module " + ext)); + loadedExtensions.add(extension); + leafModuleLayers.add(extension.getModule().getLayer()); } if (leafModuleLayers.size() > 0) { @@ -154,43 +128,12 @@ public class AppExtensionManager { } } - private void loadExtensionRootDirectory(Path dir) { - if (!Files.exists(dir)) { - return; - } - - // Order results as on unix systems the file list order is not deterministic - try (var s = Files.list(dir).sorted(Comparator.comparing(path -> path.toString()))) { - s.forEach(sub -> { - if (Files.isDirectory(sub)) { - // TODO: Better detection for x modules - if (sub.toString().endsWith("x")) { - return; - } - - var extension = parseExtensionDirectory(sub, baseLayer); - if (extension.isEmpty()) { - return; - } - - loadedExtensions.add(extension.get()); - var xModule = findAndParseExtension( - extension.get().getId() + "x", - extension.get().getModule().getLayer()); - if (xModule.isPresent()) { - loadedExtensions.add(xModule.get()); - leafModuleLayers.add(xModule.get().getModule().getLayer()); - } else { - leafModuleLayers.add(extension.get().getModule().getLayer()); - } - } - }); - } catch (IOException ex) { - ErrorEvent.fromThrowable(ex).handle(); - } - } - private Optional findAndParseExtension(String name, ModuleLayer parent) { + var inModulePath = ModuleLayer.boot().findModule("io.xpipe.ext." + name); + if (inModulePath.isPresent()) { + return Optional.of(new Extension(null, inModulePath.get().getName(), name, inModulePath.get(), 0)); + } + for (Path extensionBaseDirectory : extensionBaseDirectories) { var found = parseExtensionDirectory(extensionBaseDirectory.resolve(name), parent); if (found.isPresent()) { @@ -206,7 +149,7 @@ public class AppExtensionManager { return Optional.empty(); } - if (loadedExtensions.stream().anyMatch(extension -> extension.dir.equals(dir)) + if (loadedExtensions.stream().anyMatch(extension -> dir.equals(extension.dir)) || loadedExtensions.stream() .anyMatch(extension -> extension.id.equals(dir.getFileName().toString()))) { diff --git a/app/src/main/java/io/xpipe/app/core/AppFileWatcher.java b/app/src/main/java/io/xpipe/app/core/AppFileWatcher.java index c9f8e835c..b3bac3a79 100644 --- a/app/src/main/java/io/xpipe/app/core/AppFileWatcher.java +++ b/app/src/main/java/io/xpipe/app/core/AppFileWatcher.java @@ -114,7 +114,7 @@ public class AppFileWatcher { this.baseDir = dir; this.listener = listener; createRecursiveWatchers(dir); - TrackEvent.withTrace("watcher", "Added watched directory") + TrackEvent.withTrace("Added watched directory") .tag("location", dir) .handle(); } @@ -177,7 +177,7 @@ public class AppFileWatcher { } // Handle event - TrackEvent.withTrace("watcher", "Watch event") + TrackEvent.withTrace("Watch event") .tag("baseDir", baseDir) .tag("file", baseDir.relativize(file)) .tag("kind", event.kind().name()) diff --git a/app/src/main/java/io/xpipe/app/core/AppGreetings.java b/app/src/main/java/io/xpipe/app/core/AppGreetings.java index 6fe40cedd..08be1218e 100644 --- a/app/src/main/java/io/xpipe/app/core/AppGreetings.java +++ b/app/src/main/java/io/xpipe/app/core/AppGreetings.java @@ -1,6 +1,5 @@ package io.xpipe.app.core; -import com.jfoenix.controls.JFXCheckBox; import io.xpipe.app.comp.base.MarkdownComp; import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.fxcomps.Comp; @@ -52,7 +51,7 @@ public class AppGreetings { public static void showIfNeeded() { boolean set = AppCache.get("legalAccepted", Boolean.class, () -> false); - if (set || !AppState.get().isInitialLaunch()) { + if (set || AppProperties.get().isDevelopmentEnvironment()) { return; } var read = new SimpleBooleanProperty(); @@ -72,20 +71,21 @@ public class AppGreetings { }); var acceptanceBox = Comp.of(() -> { - var cb = new JFXCheckBox(); + var cb = new CheckBox(); cb.selectedProperty().bindBidirectional(accepted); var label = new Label(AppI18n.get("legalAccept")); label.setGraphic(cb); AppFont.medium(label); - label.setPadding(new Insets(40, 0, 10, 0)); + label.setPadding(new Insets(20, 0, 10, 0)); label.setOnMouseClicked(event -> accepted.set(!accepted.get())); + label.setGraphicTextGap(10); return label; }) .createRegion(); var layout = new BorderPane(); - layout.getStyleClass().add("window-content"); + layout.setPadding(new Insets(20)); layout.setCenter(accordion); layout.setBottom(acceptanceBox); layout.setPrefWidth(700); diff --git a/app/src/main/java/io/xpipe/app/core/AppI18n.java b/app/src/main/java/io/xpipe/app/core/AppI18n.java index a7b5599ec..dd82de39c 100644 --- a/app/src/main/java/io/xpipe/app/core/AppI18n.java +++ b/app/src/main/java/io/xpipe/app/core/AppI18n.java @@ -51,7 +51,7 @@ public class AppI18n { i.load(); if (AppPrefs.get() != null) { - AppPrefs.get().language.addListener((c, o, n) -> { + AppPrefs.get().language().addListener((c, o, n) -> { i.clear(); i.load(); }); @@ -210,7 +210,7 @@ public class AppI18n { private boolean matchesLocale(Path f) { var l = AppPrefs.get() != null - ? AppPrefs.get().language.getValue().getLocale() + ? AppPrefs.get().language().getValue().getLocale() : SupportedLocale.ENGLISH.getLocale(); var name = FilenameUtils.getBaseName(f.getFileName().toString()); var ending = "_" + l.toLanguageTag(); @@ -311,7 +311,7 @@ public class AppI18n { this.prettyTime = new PrettyTime( AppPrefs.get() != null - ? AppPrefs.get().language.getValue().getLocale() + ? AppPrefs.get().language().getValue().getLocale() : SupportedLocale.ENGLISH.getLocale()); } } diff --git a/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java b/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java index 4175b2ac1..996c46f72 100644 --- a/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java +++ b/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java @@ -5,7 +5,7 @@ import io.xpipe.app.browser.BrowserModel; import io.xpipe.app.comp.DeveloperTabComp; import io.xpipe.app.comp.store.StoreLayoutComp; import io.xpipe.app.fxcomps.Comp; -import io.xpipe.app.prefs.PrefsComp; +import io.xpipe.app.prefs.AppPrefsComp; import io.xpipe.app.util.LicenseProvider; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; @@ -79,7 +79,7 @@ public class AppLayoutModel { AppI18n.observable("browser"), "mdi2f-file-cabinet", new BrowserComp(BrowserModel.DEFAULT)), new Entry(AppI18n.observable("connections"), "mdi2c-connection", new StoreLayoutComp()), new Entry( - AppI18n.observable("settings"), "mdsmz-miscellaneous_services", new PrefsComp(this)))); + AppI18n.observable("settings"), "mdsmz-miscellaneous_services", new AppPrefsComp()))); // new SideMenuBarComp.Entry(AppI18n.observable("help"), "mdi2b-book-open-variant", new // StorageLayoutComp()), // new SideMenuBarComp.Entry(AppI18n.observable("account"), "mdi2a-account", new StorageLayoutComp()) diff --git a/app/src/main/java/io/xpipe/app/core/AppLogs.java b/app/src/main/java/io/xpipe/app/core/AppLogs.java index a4ba4d8b1..797c2f6be 100644 --- a/app/src/main/java/io/xpipe/app/core/AppLogs.java +++ b/app/src/main/java/io/xpipe/app/core/AppLogs.java @@ -24,7 +24,6 @@ import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoField; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -32,14 +31,14 @@ import java.util.concurrent.ConcurrentHashMap; public class AppLogs { - public static final List DEFAULT_LEVELS = List.of("error", "warn", "info", "debug", "trace"); + public static final List LOG_LEVELS = List.of("error", "warn", "info", "debug", "trace"); private static final String WRITE_SYSOUT_PROP = "io.xpipe.app.writeSysOut"; private static final String WRITE_LOGS_PROP = "io.xpipe.app.writeLogs"; private static final String DEBUG_PLATFORM_PROP = "io.xpipe.app.debugPlatform"; private static final String LOG_LEVEL_PROP = "io.xpipe.app.logLevel"; private static final String DEFAULT_LOG_LEVEL = "info"; - private static final DateTimeFormatter FORMATTER = + private static final DateTimeFormatter NAME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss").withZone(ZoneId.systemDefault()); private static final DateTimeFormatter MESSAGE_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss:SSS").withZone(ZoneId.systemDefault()); @@ -60,16 +59,14 @@ public class AppLogs { @Getter private final String logLevel; - private final PrintStream outStream; - private final Map categoryWriters; + private final PrintStream outFileStream; - public AppLogs(Path logDir, boolean writeToSysout, boolean writeToFile, String logLevel) { + public AppLogs(Path logDir, boolean writeToSysout, boolean writeToFile, String logLevel, PrintStream outFileStream) { this.logDir = logDir; this.writeToSysout = writeToSysout; this.writeToFile = writeToFile; this.logLevel = logLevel; - this.outStream = System.out; - this.categoryWriters = new HashMap<>(); + this.outFileStream = outFileStream; this.originalSysOut = System.out; this.originalSysErr = System.err; @@ -96,20 +93,34 @@ public class AppLogs { } public static void init() { + if (INSTANCE != null) { + return; + } + var logDir = AppProperties.get().getDataDir().resolve("logs"); + // Regularly clean logs dir if (XPipeSession.get().isNewBuildSession() && Files.exists(logDir)) { try { - FileUtils.cleanDirectory(logDir.toFile()); + List all; + try (var s = Files.list(logDir)) { + all = s.toList(); + } + for (Path path : all) { + // Don't delete installer logs + if (path.getFileName().toString().contains("installer")) { + continue; + } + + FileUtils.forceDelete(path.toFile()); + } } catch (Exception ex) { ErrorEvent.fromThrowable(ex).handle(); } } - var shouldLogToFile = shouldWriteLogs(); - var now = Instant.now(); - var name = FORMATTER.format(now); + var name = NAME_FORMATTER.format(now); Path usedLogsDir = logDir.resolve(name); // When two instances are being launched within the same second, add milliseconds @@ -117,23 +128,34 @@ public class AppLogs { usedLogsDir = logDir.resolve(name + "_" + now.get(ChronoField.MILLI_OF_SECOND)); } + PrintStream outFileStream = null; + var shouldLogToFile = shouldWriteLogs(); if (shouldLogToFile) { try { Files.createDirectories(usedLogsDir); + var file = usedLogsDir.resolve("xpipe.log"); + var fos = new FileOutputStream(file.toFile(), true); + var buf = new BufferedOutputStream(fos); + outFileStream = new PrintStream(buf, false); } catch (Exception ex) { ErrorEvent.fromThrowable(ex).build().handle(); - shouldLogToFile = false; } } var shouldLogToSysout = shouldWriteSysout(); + if (shouldLogToFile && outFileStream == null) { + TrackEvent.info("Log file initialization failed. Writing to standard out"); + shouldLogToSysout = true; + shouldLogToFile = false; + } + if (shouldLogToFile && !shouldLogToSysout) { TrackEvent.info("Writing log output to " + usedLogsDir + " from now on"); } var level = determineLogLevel(); - INSTANCE = new AppLogs(usedLogsDir, shouldLogToSysout, shouldLogToFile, level); + INSTANCE = new AppLogs(usedLogsDir, shouldLogToSysout, shouldLogToFile, level, outFileStream); } public static void teardown() { @@ -150,44 +172,9 @@ public class AppLogs { } private void close() { - outStream.close(); - categoryWriters.forEach((k, s) -> { - s.close(); - }); - } - - private String getCategory(TrackEvent event) { - if (event.getCategory() != null) { - return event.getCategory(); + if (outFileStream != null) { + outFileStream.close(); } - - return "misc"; - } - - private synchronized PrintStream getLogStream(TrackEvent e) { - return categoryWriters.computeIfAbsent(getCategory(e), (cat) -> { - var file = logDir.resolve(cat + ".log"); - FileOutputStream fos; - try { - fos = new FileOutputStream(file.toFile(), true); - } catch (IOException ex) { - return outStream; - } - return new PrintStream(fos, false); - }); - } - - public synchronized PrintStream getCatchAllLogStream() { - return categoryWriters.computeIfAbsent("xpipe", (cat) -> { - var file = logDir.resolve(cat + ".log"); - FileOutputStream fos; - try { - fos = new FileOutputStream(file.toFile(), true); - } catch (IOException ex) { - return outStream; - } - return new PrintStream(fos, false); - }); } private boolean shouldDebugPlatform() { @@ -212,7 +199,6 @@ public class AppLogs { TrackEvent.builder() .type("info") - .category("sysout") .message(line) .build() .handle(); @@ -248,7 +234,7 @@ public class AppLogs { private static String determineLogLevel() { if (System.getProperty(LOG_LEVEL_PROP) != null) { String p = System.getProperty(LOG_LEVEL_PROP); - return DEFAULT_LEVELS.contains(p) ? p : "info"; + return LOG_LEVELS.contains(p) ? p : "trace"; } return DEFAULT_LOG_LEVEL; @@ -264,9 +250,9 @@ public class AppLogs { } public synchronized void logEvent(TrackEvent event) { - var li = DEFAULT_LEVELS.indexOf(determineLogLevel()); + var li = LOG_LEVELS.indexOf(determineLogLevel()); int i = li == -1 ? 5 : li; - int current = DEFAULT_LEVELS.indexOf(event.getType()); + int current = LOG_LEVELS.indexOf(event.getType()); if (current <= i) { if (writeToSysout) { logSysOut(event); @@ -281,12 +267,9 @@ public class AppLogs { var time = MESSAGE_FORMATTER.format(event.getInstant()); var string = new StringBuilder(time).append(" - ").append(event.getType()).append(": "); - if (event.getCategory() != null) { - string.append("[").append(event.getCategory()).append("] "); - } string.append(event); var toLog = string.toString(); - outStream.println(toLog); + this.originalSysOut.println(toLog); } private void logToFile(TrackEvent event) { @@ -295,8 +278,7 @@ public class AppLogs { new StringBuilder(time).append(" - ").append(event.getType()).append(": "); string.append(event); var toLog = string.toString(); - getLogStream(event).println(toLog); - getCatchAllLogStream().println(toLog); + outFileStream.println(toLog); } private void setLogLevels() { @@ -312,10 +294,6 @@ public class AppLogs { } } - public Path getLogsDirectory() { - return logDir.getParent(); - } - public Path getSessionLogsDirectory() { return logDir; } @@ -390,7 +368,6 @@ public class AppLogs { } } TrackEvent.builder() - .category(name) .type(level.toString().toLowerCase()) .message(msg) .build() @@ -399,62 +376,62 @@ public class AppLogs { @Override public boolean isTraceEnabled() { - return DEFAULT_LEVELS.indexOf("trace") - <= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel()); + return LOG_LEVELS.indexOf("trace") + <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel()); } @Override public boolean isTraceEnabled(Marker marker) { - return DEFAULT_LEVELS.indexOf("trace") - <= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel()); + return LOG_LEVELS.indexOf("trace") + <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel()); } @Override public boolean isDebugEnabled() { - return DEFAULT_LEVELS.indexOf("debug") - <= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel()); + return LOG_LEVELS.indexOf("debug") + <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel()); } @Override public boolean isDebugEnabled(Marker marker) { - return DEFAULT_LEVELS.indexOf("debug") - <= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel()); + return LOG_LEVELS.indexOf("debug") + <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel()); } @Override public boolean isInfoEnabled() { - return DEFAULT_LEVELS.indexOf("info") - <= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel()); + return LOG_LEVELS.indexOf("info") + <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel()); } @Override public boolean isInfoEnabled(Marker marker) { - return DEFAULT_LEVELS.indexOf("info") - <= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel()); + return LOG_LEVELS.indexOf("info") + <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel()); } @Override public boolean isWarnEnabled() { - return DEFAULT_LEVELS.indexOf("warn") - <= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel()); + return LOG_LEVELS.indexOf("warn") + <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel()); } @Override public boolean isWarnEnabled(Marker marker) { - return DEFAULT_LEVELS.indexOf("warn") - <= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel()); + return LOG_LEVELS.indexOf("warn") + <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel()); } @Override public boolean isErrorEnabled() { - return DEFAULT_LEVELS.indexOf("error") - <= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel()); + return LOG_LEVELS.indexOf("error") + <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel()); } @Override public boolean isErrorEnabled(Marker marker) { - return DEFAULT_LEVELS.indexOf("error") - <= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel()); + return LOG_LEVELS.indexOf("error") + <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel()); } } } diff --git a/app/src/main/java/io/xpipe/app/core/AppMainWindow.java b/app/src/main/java/io/xpipe/app/core/AppMainWindow.java index 6ce9273c3..5c4c60da1 100644 --- a/app/src/main/java/io/xpipe/app/core/AppMainWindow.java +++ b/app/src/main/java/io/xpipe/app/core/AppMainWindow.java @@ -83,7 +83,6 @@ public class AppMainWindow { private void logChange() { TrackEvent.withDebug("Window resize") - .windowCategory() .tag("x", stage.getX()) .tag("y", stage.getY()) .tag("width", stage.getWidth()) @@ -98,7 +97,6 @@ public class AppMainWindow { applyState(state); TrackEvent.withDebug("Window initialized") - .windowCategory() .tag("x", stage.getX()) .tag("y", stage.getY()) .tag("width", stage.getWidth()) diff --git a/app/src/main/java/io/xpipe/app/core/AppProperties.java b/app/src/main/java/io/xpipe/app/core/AppProperties.java index d498da878..0506ad7ba 100644 --- a/app/src/main/java/io/xpipe/app/core/AppProperties.java +++ b/app/src/main/java/io/xpipe/app/core/AppProperties.java @@ -101,6 +101,11 @@ public class AppProperties { return INSTANCE; } + public boolean isDevelopmentEnvironment() { + return !AppProperties.get().isImage() + && AppProperties.get().isDeveloperMode(); + } + public boolean isDeveloperMode() { if (AppPrefs.get() == null) { return false; diff --git a/app/src/main/java/io/xpipe/app/core/AppSocketServer.java b/app/src/main/java/io/xpipe/app/core/AppSocketServer.java index 6088d5301..1b11a3c5f 100644 --- a/app/src/main/java/io/xpipe/app/core/AppSocketServer.java +++ b/app/src/main/java/io/xpipe/app/core/AppSocketServer.java @@ -112,7 +112,7 @@ public class AppSocketServer { private boolean performExchange(Socket clientSocket, int id) throws Exception { if (clientSocket.isClosed()) { - TrackEvent.trace("beacon", "Socket closed"); + TrackEvent.trace("Socket closed"); return false; } @@ -121,14 +121,14 @@ public class AppSocketServer { node = JacksonMapper.getDefault().readTree(blockIn); } if (node.isMissingNode()) { - TrackEvent.trace("beacon", "Received EOF"); + TrackEvent.trace("Received EOF"); return false; } - TrackEvent.trace("beacon", "Received raw request: \n" + node.toPrettyString()); + TrackEvent.trace("Received raw request: \n" + node.toPrettyString()); var req = parseRequest(node); - TrackEvent.trace("beacon", "Parsed request: \n" + req.toString()); + TrackEvent.trace("Parsed request: \n" + req.toString()); var prov = MessageExchangeImpls.byRequest(req); if (prov.isEmpty()) { @@ -145,19 +145,19 @@ public class AppSocketServer { @Override public OutputStream sendBody() throws IOException { - TrackEvent.trace("beacon", "Starting writing body for #" + id); + TrackEvent.trace("Starting writing body for #" + id); return AppSocketServer.this.sendBody(clientSocket); } @Override public InputStream receiveBody() throws IOException { - TrackEvent.trace("beacon", "Starting to read body for #" + id); + TrackEvent.trace("Starting to read body for #" + id); return AppSocketServer.this.receiveBody(clientSocket); } }, req); - TrackEvent.trace("beacon", "Sending response to #" + id + ": \n" + res.toString()); + TrackEvent.trace("Sending response to #" + id + ": \n" + res.toString()); AppSocketServer.this.sendResponse(clientSocket, res); try { @@ -170,7 +170,6 @@ public class AppSocketServer { } TrackEvent.builder() - .category("beacon") .type("trace") .message("Socket connection #" + id + " performed exchange " + req.getClass().getSimpleName()) @@ -187,7 +186,7 @@ public class AppSocketServer { informationNode = JacksonMapper.getDefault().readTree(blockIn); } if (informationNode.isMissingNode()) { - TrackEvent.trace("beacon", "Received EOF"); + TrackEvent.trace("Received EOF"); return; } var information = @@ -197,7 +196,6 @@ public class AppSocketServer { } TrackEvent.builder() - .category("beacon") .type("trace") .message("Created new socket connection #" + id) .tag("client", information != null ? information.toDisplayString() : "Unknown") @@ -211,29 +209,28 @@ public class AppSocketServer { } } TrackEvent.builder() - .category("beacon") .type("trace") .message("Socket connection #" + id + " finished successfully") .build() .handle(); } catch (ClientException ce) { - TrackEvent.trace("beacon", "Sending client error to #" + id + ": " + ce.getMessage()); + TrackEvent.trace("Sending client error to #" + id + ": " + ce.getMessage()); sendClientErrorResponse(clientSocket, ce.getMessage()); } catch (ServerException se) { - TrackEvent.trace("beacon", "Sending server error to #" + id + ": " + se.getMessage()); - ErrorEvent.fromThrowable(se).build().handle(); + TrackEvent.trace("Sending server error to #" + id + ": " + se.getMessage()); Deobfuscator.deobfuscate(se); sendServerErrorResponse(clientSocket, se); + ErrorEvent.fromThrowable(se).build().handle(); } catch (SocketException ex) { // Do not send error and omit it, as this might happen often // We do not send the error as the socket connection might be broken ErrorEvent.fromThrowable(ex).omitted(true).build().handle(); } catch (Throwable ex) { - TrackEvent.trace("beacon", "Sending internal server error to #" + id + ": " + ex.getMessage()); - ErrorEvent.fromThrowable(ex).build().handle(); + TrackEvent.trace("Sending internal server error to #" + id + ": " + ex.getMessage()); Deobfuscator.deobfuscate(ex); sendServerErrorResponse(clientSocket, ex); + ErrorEvent.fromThrowable(ex).build().handle(); } } catch (SocketException ex) { // Omit it, as this might happen often @@ -243,14 +240,13 @@ public class AppSocketServer { } finally { try { clientSocket.close(); - TrackEvent.trace("beacon", "Closed socket #" + id); + TrackEvent.trace("Closed socket #" + id); } catch (IOException e) { ErrorEvent.fromThrowable(e).build().handle(); } } TrackEvent.builder() - .category("beacon") .type("trace") .message("Socket connection #" + id + " finished unsuccessfully"); } @@ -296,7 +292,7 @@ public class AppSocketServer { } var content = writer.toString(); - TrackEvent.trace("beacon", "Sending raw response:\n" + content); + TrackEvent.trace("Sending raw response:\n" + content); try (OutputStream blockOut = BeaconFormat.writeBlocks(outSocket.getOutputStream())) { blockOut.write(content.getBytes(StandardCharsets.UTF_8)); } @@ -336,7 +332,7 @@ public class AppSocketServer { private T parseRequest(JsonNode header) throws Exception { ObjectNode content = (ObjectNode) header.required("xPipeMessage"); - TrackEvent.trace("beacon", "Parsed raw request:\n" + content.toPrettyString()); + TrackEvent.trace("Parsed raw request:\n" + content.toPrettyString()); var type = content.required("messageType").textValue(); var phase = content.required("messagePhase").textValue(); diff --git a/app/src/main/java/io/xpipe/app/core/AppStyle.java b/app/src/main/java/io/xpipe/app/core/AppStyle.java index 44301de2a..f926bd055 100644 --- a/app/src/main/java/io/xpipe/app/core/AppStyle.java +++ b/app/src/main/java/io/xpipe/app/core/AppStyle.java @@ -28,7 +28,7 @@ public class AppStyle { loadStylesheets(); if (AppPrefs.get() != null) { - AppPrefs.get().useSystemFont.addListener((c, o, n) -> { + AppPrefs.get().useSystemFont().addListener((c, o, n) -> { changeFontUsage(n); }); } @@ -48,7 +48,7 @@ public class AppStyle { return; } - TrackEvent.trace("core", "Loading styles for module " + module.getName()); + TrackEvent.trace("Loading styles for module " + module.getName()); Files.walkFileTree(path, new SimpleFileVisitor<>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { @@ -93,7 +93,7 @@ public class AppStyle { } public static void addStylesheets(Scene scene) { - if (AppPrefs.get() != null && !AppPrefs.get().useSystemFont.get()) { + if (AppPrefs.get() != null && !AppPrefs.get().useSystemFont().getValue()) { scene.getStylesheets().add(FONT_CONTENTS); } diff --git a/app/src/main/java/io/xpipe/app/core/AppTheme.java b/app/src/main/java/io/xpipe/app/core/AppTheme.java index dc8e9726b..6fa98a4dd 100644 --- a/app/src/main/java/io/xpipe/app/core/AppTheme.java +++ b/app/src/main/java/io/xpipe/app/core/AppTheme.java @@ -1,7 +1,6 @@ package io.xpipe.app.core; import atlantafx.base.theme.*; -import com.jthemedetecor.OsThemeDetector; import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.SimpleChangeListener; @@ -14,7 +13,10 @@ import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.application.Application; +import javafx.application.ColorScheme; import javafx.application.Platform; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ObservableValue; import javafx.css.PseudoClass; import javafx.scene.image.Image; import javafx.scene.image.ImageView; @@ -71,30 +73,21 @@ public class AppTheme { } try { - OsThemeDetector detector = OsThemeDetector.getDetector(); if (AppPrefs.get().theme.getValue() == null) { - try { - setDefault(detector.isDark()); - } catch (Throwable ex) { - ErrorEvent.fromThrowable(ex).omit().handle(); - setDefault(false); - } + setDefault(Platform.getPreferences().getColorScheme()); } - // The gnome detector sometimes runs into issues, also it's not that important - if (!OsType.getLocal().equals(OsType.LINUX)) { - detector.registerListener(dark -> { - PlatformThread.runLaterIfNeeded(() -> { - if (dark && !AppPrefs.get().theme.getValue().isDark()) { - AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme()); - } + Platform.getPreferences().colorSchemeProperty().addListener((observableValue, colorScheme, t1) -> { + Platform.runLater(() -> { + if (t1 == ColorScheme.DARK && !AppPrefs.get().theme.getValue().isDark()) { + AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme()); + } - if (!dark && AppPrefs.get().theme.getValue().isDark()) { - AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme()); - } - }); + if (t1 != ColorScheme.DARK && AppPrefs.get().theme.getValue().isDark()) { + AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme()); + } }); - } + }); } catch (Throwable t) { ErrorEvent.fromThrowable(t).omit().handle(); } @@ -110,8 +103,8 @@ public class AppTheme { init = true; } - private static void setDefault(boolean dark) { - if (dark) { + private static void setDefault(ColorScheme colorScheme) { + if (colorScheme == ColorScheme.DARK) { AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme()); } else { AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme()); @@ -189,8 +182,8 @@ public class AppTheme { } @Override - public String toTranslatedString() { - return name; + public ObservableValue toTranslatedString() { + return new SimpleStringProperty(name); } } @@ -244,8 +237,8 @@ public class AppTheme { } @Override - public String toTranslatedString() { - return theme.getName(); + public ObservableValue toTranslatedString() { + return new SimpleStringProperty(theme.getName()); } @Override diff --git a/app/src/main/java/io/xpipe/app/core/AppWindowHelper.java b/app/src/main/java/io/xpipe/app/core/AppWindowHelper.java index da1765f0e..74d38d8c9 100644 --- a/app/src/main/java/io/xpipe/app/core/AppWindowHelper.java +++ b/app/src/main/java/io/xpipe/app/core/AppWindowHelper.java @@ -3,6 +3,7 @@ package io.xpipe.app.core; import io.xpipe.app.comp.base.LoadingOverlayComp; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.issue.TrackEvent; +import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.process.OsType; import javafx.application.Platform; @@ -19,6 +20,7 @@ import javafx.scene.layout.Pane; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.text.Text; +import javafx.stage.Modality; import javafx.stage.Screen; import javafx.stage.Stage; import javafx.stage.Window; @@ -63,6 +65,9 @@ public class AppWindowHelper { public static Stage sideWindow( String title, Function> contentFunc, boolean bindSize, ObservableValue loading) { var stage = new Stage(); + if (AppMainWindow.getInstance() != null) { + stage.initOwner(AppMainWindow.getInstance().getStage()); + } stage.setTitle(title); if (AppMainWindow.getInstance() != null) { stage.initOwner(AppMainWindow.getInstance().getStage()); @@ -72,6 +77,10 @@ public class AppWindowHelper { setupContent(stage, contentFunc, bindSize, loading); setupStylesheets(stage.getScene()); + if (AppPrefs.get() != null && AppPrefs.get().enforceWindowModality().get()) { + stage.initModality(Modality.WINDOW_MODAL); + } + stage.setOnShown(e -> { // If we set the theme pseudo classes earlier when the window is not shown // they do not apply. Is this a bug in JavaFX? @@ -110,6 +119,24 @@ public class AppWindowHelper { }); } + public static void setContent(Alert alert, String s) { + alert.getDialogPane().setMinWidth(505); + alert.getDialogPane().setPrefWidth(505); + alert.getDialogPane().setMaxWidth(505); + alert.getDialogPane().setContent(AppWindowHelper.alertContentText(s)); + } + + public static boolean showConfirmationAlert(ObservableValue title, ObservableValue header, ObservableValue content) { + return AppWindowHelper.showBlockingAlert(alert -> { + alert.titleProperty().bind(title); + alert.headerTextProperty().bind(header); + setContent(alert, content.getValue()); + alert.setAlertType(Alert.AlertType.CONFIRMATION); + }) + .map(b -> b.getButtonData().isDefaultButton()) + .orElse(false); + } + public static Optional showBlockingAlert(Consumer c) { Supplier supplier = () -> { Alert a = AppWindowHelper.createEmptyAlert(); diff --git a/app/src/main/java/io/xpipe/app/core/check/AppCertutilCheck.java b/app/src/main/java/io/xpipe/app/core/check/AppCertutilCheck.java new file mode 100644 index 000000000..6f4937a75 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/core/check/AppCertutilCheck.java @@ -0,0 +1,35 @@ +package io.xpipe.app.core.check; + +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.core.process.OsType; + +import java.util.concurrent.TimeUnit; + +public class AppCertutilCheck { + + private static boolean getResult() { + var fc = new ProcessBuilder(System.getenv("WINDIR") + "\\Windows32\\certutil").redirectError(ProcessBuilder.Redirect.DISCARD); + try { + var proc = fc.start(); + var out = new String(proc.getInputStream().readAllBytes()); + proc.waitFor(1, TimeUnit.SECONDS); + return proc.exitValue() == 0 && !out.contains("The system cannot execute the specified program"); + } catch (Exception e) { + return false; + } + } + + public static void check() { + if (AppPrefs.get().disableCertutilUse().get()) { + return; + } + + if (!OsType.getLocal().equals(OsType.WINDOWS)) { + return; + } + + if (!getResult()) { + AppPrefs.get().disableCertutilUse.set(true); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/core/check/AppShellCheck.java b/app/src/main/java/io/xpipe/app/core/check/AppShellCheck.java index ce5283b14..f7b8fa66c 100644 --- a/app/src/main/java/io/xpipe/app/core/check/AppShellCheck.java +++ b/app/src/main/java/io/xpipe/app/core/check/AppShellCheck.java @@ -2,8 +2,8 @@ package io.xpipe.app.core.check; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.util.LocalShell; +import io.xpipe.core.process.ProcessControlProvider; import io.xpipe.core.process.ProcessOutputException; -import io.xpipe.core.process.ShellDialects; import java.util.Optional; @@ -24,7 +24,7 @@ public class AppShellCheck { - The operating system is not supported You can reach out to us if you want to properly diagnose the cause individually and hopefully fix it. - """.formatted(ShellDialects.getPlatformDefault().getDisplayName(), err.get()); + """.formatted(ProcessControlProvider.get().getEffectiveLocalDialect().getDisplayName(), err.get()); ErrorEvent.fromThrowable(new IllegalStateException(msg)).handle(); } } diff --git a/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java b/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java index 704cca618..dcb381864 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java @@ -4,16 +4,14 @@ import io.xpipe.app.browser.BrowserModel; import io.xpipe.app.comp.store.StoreViewState; import io.xpipe.app.core.*; import io.xpipe.app.core.check.AppAvCheck; +import io.xpipe.app.core.check.AppCertutilCheck; import io.xpipe.app.core.check.AppShellCheck; import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.update.XPipeDistributionType; -import io.xpipe.app.util.FileBridge; -import io.xpipe.app.util.LicenseProvider; -import io.xpipe.app.util.LocalShell; -import io.xpipe.app.util.LockedSecretValue; +import io.xpipe.app.util.*; import io.xpipe.core.util.JacksonMapper; public class BaseMode extends OperationMode { @@ -39,30 +37,33 @@ public class BaseMode extends OperationMode { // For debugging // if (true) throw new IllegalStateException(); - TrackEvent.info("mode", "Initializing base mode components ..."); + TrackEvent.info("Initializing base mode components ..."); AppExtensionManager.init(true); JacksonMapper.initModularized(AppExtensionManager.getInstance().getExtendedLayer()); - JacksonMapper.configure(objectMapper -> { - objectMapper.registerSubtypes(LockedSecretValue.class); - }); // Load translations before storage initialization to localize store error messages // Also loaded before antivirus alert to localize that AppI18n.init(); LicenseProvider.get().init(); + AppPrefs.init(); + AppCertutilCheck.check(); AppAvCheck.check(); LocalShell.init(); - AppShellCheck.check(); XPipeDistributionType.init(); - AppPrefs.init(); - AppCharsets.init(); - AppCharsetter.init(); + AppShellCheck.check(); + AppPrefs.setDefaults(); + // Initialize socket server before storage + // as we should be prepared for git askpass commands AppSocketServer.init(); + UnlockAlert.showIfNeeded(); DataStorage.init(); AppFileWatcher.init(); FileBridge.init(); ActionProvider.initProviders(); - TrackEvent.info("mode", "Finished base components initialization"); + TrackEvent.info("Finished base components initialization"); initialized = true; + + var sec = new VaultKeySecretValue(new char[] {1, 2, 3}); + sec.getSecret(); } @Override @@ -70,7 +71,7 @@ public class BaseMode extends OperationMode { @Override public void finalTeardown() { - TrackEvent.info("mode", "Background mode shutdown started"); + TrackEvent.info("Background mode shutdown started"); BrowserModel.DEFAULT.reset(); StoreViewState.reset(); DataStorage.reset(); @@ -80,6 +81,6 @@ public class BaseMode extends OperationMode { AppDataLock.unlock(); // Shut down socket server last to keep a non-daemon thread running AppSocketServer.reset(); - TrackEvent.info("mode", "Background mode shutdown finished"); + TrackEvent.info("Background mode shutdown finished"); } } diff --git a/app/src/main/java/io/xpipe/app/core/mode/GuiMode.java b/app/src/main/java/io/xpipe/app/core/mode/GuiMode.java index e4fa7978a..49504b4de 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/GuiMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/GuiMode.java @@ -7,7 +7,6 @@ import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.update.UpdateChangelogAlert; -import io.xpipe.app.util.UnlockAlert; import javafx.stage.Stage; public class GuiMode extends PlatformMode { @@ -21,10 +20,9 @@ public class GuiMode extends PlatformMode { public void onSwitchTo() throws Throwable { super.onSwitchTo(); - UnlockAlert.showIfNeeded(); AppGreetings.showIfNeeded(); - TrackEvent.info("mode", "Waiting for window setup completion ..."); + TrackEvent.info("Waiting for window setup completion ..."); PlatformThread.runLaterIfNeededBlocking(() -> { if (AppMainWindow.getInstance() == null) { try { @@ -35,7 +33,7 @@ public class GuiMode extends PlatformMode { } AppMainWindow.getInstance().show(); }); - TrackEvent.info("mode", "Window setup complete"); + TrackEvent.info("Window setup complete"); UpdateChangelogAlert.showIfNeeded(); } @@ -43,7 +41,7 @@ public class GuiMode extends PlatformMode { @Override public void onSwitchFrom() { PlatformThread.runLaterIfNeededBlocking(() -> { - TrackEvent.info("mode", "Closing windows"); + TrackEvent.info("Closing windows"); Stage.getWindows().stream().toList().forEach(w -> { w.hide(); }); diff --git a/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java b/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java index 7ce664095..7b308c80a 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java @@ -13,7 +13,6 @@ import io.xpipe.app.util.XPipeSession; import io.xpipe.core.util.FailableRunnable; import io.xpipe.core.util.XPipeDaemonMode; import io.xpipe.core.util.XPipeInstallation; -import io.xpipe.core.util.XPipeSystemId; import javafx.application.Platform; import lombok.Getter; @@ -93,7 +92,7 @@ public abstract class OperationMode { // throw new OutOfMemoryError(); // } - TrackEvent.info("mode", "Initial setup"); + TrackEvent.info("Initial setup"); AppProperties.init(); AppState.init(); XPipeSession.init(AppProperties.get().getBuildUuid()); @@ -104,8 +103,7 @@ public abstract class OperationMode { AppProperties.logArguments(args); AppProperties.logSystemProperties(); AppProperties.logPassedProperties(); - XPipeSystemId.init(); - TrackEvent.info("mode", "Finished initial setup"); + TrackEvent.info("Finished initial setup"); } catch (Throwable ex) { ErrorEvent.fromThrowable(ex).term().handle(); } @@ -211,11 +209,6 @@ public abstract class OperationMode { OperationMode.halt(1); } - // In case we perform any operations such as opening a terminal - // give it some time to open while this process is still alive - // Otherwise it might quit because the parent process is dead already - ThreadHelper.sleep(1000); - OperationMode.halt(0); }; @@ -250,7 +243,7 @@ public abstract class OperationMode { } // Run a timer to always exit after some time in case we get stuck - if (!hasError) { + if (!hasError && !AppProperties.get().isDevelopmentEnvironment()) { ThreadHelper.runAsync(() -> { ThreadHelper.sleep(25000); TrackEvent.info("Shutdown took too long. Halting ..."); diff --git a/app/src/main/java/io/xpipe/app/core/mode/PlatformMode.java b/app/src/main/java/io/xpipe/app/core/mode/PlatformMode.java index 12b1db6e6..160ccb085 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/PlatformMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/PlatformMode.java @@ -23,27 +23,27 @@ public abstract class PlatformMode extends OperationMode { return; } - TrackEvent.info("mode", "Platform mode initial setup"); + TrackEvent.info("Platform mode initial setup"); PlatformState.initPlatformOrThrow(); AppFont.init(); AppTheme.init(); AppStyle.init(); AppImages.init(); AppLayoutModel.init(); - TrackEvent.info("mode", "Finished essential component initialization before platform"); + TrackEvent.info("Finished essential component initialization before platform"); - TrackEvent.info("mode", "Launching application ..."); + TrackEvent.info("Launching application ..."); ThreadHelper.createPlatformThread("app", false, () -> { - TrackEvent.info("mode", "Application thread started"); + TrackEvent.info("Application thread started"); Application.launch(App.class); }) .start(); - TrackEvent.info("mode", "Waiting for platform application startup ..."); + TrackEvent.info("Waiting for platform application startup ..."); while (App.getApp() == null) { ThreadHelper.sleep(100); } - TrackEvent.info("mode", "Application startup finished ..."); + TrackEvent.info("Application startup finished ..."); // If we downloaded an update, and decided to no longer automatically update, don't remind us! // You can still update manually in the about tab @@ -56,12 +56,12 @@ public abstract class PlatformMode extends OperationMode { @Override public void finalTeardown() throws Throwable { - TrackEvent.info("mode", "Shutting down platform components"); + TrackEvent.info("Shutting down platform components"); onSwitchFrom(); StoreViewState.reset(); AppLayoutModel.reset(); PlatformState.teardown(); - TrackEvent.info("mode", "Platform shutdown finished"); + TrackEvent.info("Platform shutdown finished"); BACKGROUND.finalTeardown(); } } diff --git a/app/src/main/java/io/xpipe/app/core/mode/TrayMode.java b/app/src/main/java/io/xpipe/app/core/mode/TrayMode.java index 6358b9b70..e74743130 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/TrayMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/TrayMode.java @@ -23,19 +23,19 @@ public class TrayMode extends PlatformMode { super.onSwitchTo(); PlatformThread.runLaterIfNeededBlocking(() -> { if (AppTray.get() == null) { - TrackEvent.info("mode", "Initializing tray"); + TrackEvent.info("Initializing tray"); AppTray.init(); } AppTray.get().show(); - TrackEvent.info("mode", "Finished tray initialization"); + TrackEvent.info("Finished tray initialization"); }); } @Override public void onSwitchFrom() { if (AppTray.get() != null) { - TrackEvent.info("mode", "Closing tray"); + TrackEvent.info("Closing tray"); PlatformThread.runLaterIfNeededBlocking(() -> AppTray.get().hide()); } } diff --git a/app/src/main/java/io/xpipe/app/exchange/AskpassExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/AskpassExchangeImpl.java index 47b0c6129..95a9dacc2 100644 --- a/app/src/main/java/io/xpipe/app/exchange/AskpassExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/exchange/AskpassExchangeImpl.java @@ -2,7 +2,7 @@ package io.xpipe.app.exchange; import io.xpipe.app.core.AppStyle; import io.xpipe.app.core.AppTheme; -import io.xpipe.app.util.AskpassAlert; +import io.xpipe.app.util.SecretManager; import io.xpipe.beacon.BeaconHandler; import io.xpipe.beacon.exchange.AskpassExchange; @@ -11,9 +11,16 @@ public class AskpassExchangeImpl extends AskpassExchange @Override public Response handleRequest(BeaconHandler handler, Request msg) { + var found = msg.getSecretId() != null ? SecretManager.getProgress(msg.getRequest(), msg.getSecretId()) : SecretManager.getProgress(msg.getRequest()); + if (found.isEmpty()) { + return Response.builder().build(); + } + AppStyle.init(); AppTheme.init(); - var r = AskpassAlert.query(msg.getPrompt(), msg.getRequest(), msg.getStoreId(), msg.getSubId()); - return Response.builder().value(r != null ? r.inPlace() : null).build(); + + var p = found.get(); + var secret = p.process(msg.getPrompt()); + return Response.builder().value(secret != null ? secret.inPlace() : null).build(); } } diff --git a/app/src/main/java/io/xpipe/app/exchange/DialogExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/DialogExchangeImpl.java index a65be5bc3..b6d46c203 100644 --- a/app/src/main/java/io/xpipe/app/exchange/DialogExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/exchange/DialogExchangeImpl.java @@ -31,7 +31,7 @@ public class DialogExchangeImpl extends DialogExchange @Override public DialogExchange.Response handleRequest(BeaconHandler handler, Request msg) throws Exception { if (msg.isCancel()) { - TrackEvent.withTrace("beacon", "Received cancel dialog request") + TrackEvent.withTrace("Received cancel dialog request") .tag("key", msg.getDialogKey()) .handle(); openDialogs.remove(msg.getDialogKey()); @@ -42,7 +42,7 @@ public class DialogExchangeImpl extends DialogExchange var dialog = openDialogs.get(msg.getDialogKey()); var e = dialog.receive(msg.getValue()); - TrackEvent.withTrace("beacon", "Received normal dialog request") + TrackEvent.withTrace("Received normal dialog request") .tag("key", msg.getDialogKey()) .tag("value", msg.getValue()) .tag("newElement", e) diff --git a/app/src/main/java/io/xpipe/app/exchange/LaunchExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/LaunchExchangeImpl.java index f3886a5a1..fd719aae6 100644 --- a/app/src/main/java/io/xpipe/app/exchange/LaunchExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/exchange/LaunchExchangeImpl.java @@ -16,7 +16,9 @@ public class LaunchExchangeImpl extends LaunchExchange public Response handleRequest(BeaconHandler handler, Request msg) throws Exception { var store = getStoreEntryById(msg.getId(), false); if (store.getStore() instanceof LaunchableStore s) { - var command = s.prepareLaunchCommand().prepareTerminalOpen(TerminalInitScriptConfig.ofName(store.getName())); + var command = s.prepareLaunchCommand().prepareTerminalOpen( + TerminalInitScriptConfig.ofName(store.getName()), + null); return Response.builder().command(split(command)).build(); } diff --git a/app/src/main/java/io/xpipe/app/exchange/TerminalLaunchExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/TerminalLaunchExchangeImpl.java new file mode 100644 index 000000000..44312fb7c --- /dev/null +++ b/app/src/main/java/io/xpipe/app/exchange/TerminalLaunchExchangeImpl.java @@ -0,0 +1,17 @@ +package io.xpipe.app.exchange; + +import io.xpipe.app.util.TerminalLauncherManager; +import io.xpipe.beacon.BeaconHandler; +import io.xpipe.beacon.ClientException; +import io.xpipe.beacon.ServerException; +import io.xpipe.beacon.exchange.TerminalLaunchExchange; + +public class TerminalLaunchExchangeImpl extends TerminalLaunchExchange + implements MessageExchangeImpl { + + @Override + public Response handleRequest(BeaconHandler handler, Request msg) throws ServerException, ClientException { + var r = TerminalLauncherManager.performLaunch(msg.getRequest()); + return Response.builder().targetFile(r).build(); + } +} diff --git a/app/src/main/java/io/xpipe/app/exchange/TerminalWaitExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/TerminalWaitExchangeImpl.java new file mode 100644 index 000000000..bac98a9e2 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/exchange/TerminalWaitExchangeImpl.java @@ -0,0 +1,17 @@ +package io.xpipe.app.exchange; + +import io.xpipe.app.util.TerminalLauncherManager; +import io.xpipe.beacon.BeaconHandler; +import io.xpipe.beacon.ClientException; +import io.xpipe.beacon.ServerException; +import io.xpipe.beacon.exchange.TerminalWaitExchange; + +public class TerminalWaitExchangeImpl extends TerminalWaitExchange + implements MessageExchangeImpl { + + @Override + public Response handleRequest(BeaconHandler handler, Request msg) throws ServerException, ClientException { + TerminalLauncherManager.waitForCompletion(msg.getRequest()); + return Response.builder().build(); + } +} diff --git a/app/src/main/java/io/xpipe/app/exchange/cli/ModeExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/cli/ModeExchangeImpl.java index 1b91e049f..d08a88c6e 100644 --- a/app/src/main/java/io/xpipe/app/exchange/cli/ModeExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/exchange/cli/ModeExchangeImpl.java @@ -2,6 +2,7 @@ package io.xpipe.app.exchange.cli; import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.exchange.MessageExchangeImpl; +import io.xpipe.app.util.ThreadHelper; import io.xpipe.beacon.BeaconHandler; import io.xpipe.beacon.ClientException; import io.xpipe.beacon.exchange.cli.ModeExchange; @@ -11,8 +12,9 @@ public class ModeExchangeImpl extends ModeExchange @Override public Response handleRequest(BeaconHandler handler, Request msg) throws Exception { - if (OperationMode.get() == null) { - throw new ClientException("Mode switch already in progress"); + // Wait for startup + while (OperationMode.get() == null) { + ThreadHelper.sleep(100); } var mode = OperationMode.map(msg.getMode()); diff --git a/app/src/main/java/io/xpipe/app/exchange/cli/SinkExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/cli/SinkExchangeImpl.java index edd3187dc..79ddef7f0 100644 --- a/app/src/main/java/io/xpipe/app/exchange/cli/SinkExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/exchange/cli/SinkExchangeImpl.java @@ -20,7 +20,7 @@ public class SinkExchangeImpl extends SinkExchange ShellStore store = ds.getStore().asNeeded(); try (var fs = store.createFileSystem(); var inputStream = handler.receiveBody(); - var output = fs.openOutput(msg.getPath())) { + var output = fs.openOutput(msg.getPath(), -1)) { inputStream.transferTo(output); } diff --git a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java index 29938be93..e79be028d 100644 --- a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java @@ -35,10 +35,6 @@ public interface DataStoreProvider { return false; } - default ModuleInstall getRequiredAdditionalInstallation() { - return null; - } - default void validate() { for (Class storeClass : getStoreClasses()) { if (!JacksonizedValue.class.isAssignableFrom(storeClass)) { diff --git a/app/src/main/java/io/xpipe/app/ext/DownloadModuleInstall.java b/app/src/main/java/io/xpipe/app/ext/DownloadModuleInstall.java deleted file mode 100644 index 43dfdb7cd..000000000 --- a/app/src/main/java/io/xpipe/app/ext/DownloadModuleInstall.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.xpipe.app.ext; - -import lombok.Getter; - -import java.util.List; - -public abstract class DownloadModuleInstall extends ModuleInstall { - - private final String licenseFile; - private final String vendorURL; - - @Getter - private final List assets; - - public DownloadModuleInstall(String id, String module, String licenseFile, String vendorURL, List assets) { - super(id, module); - this.licenseFile = licenseFile; - this.vendorURL = vendorURL; - this.assets = assets; - } - - @Override - public String getLicenseFile() { - return licenseFile; - } - - @Override - public String getVendorURL() { - return vendorURL; - } -} diff --git a/app/src/main/java/io/xpipe/app/ext/ModuleInstall.java b/app/src/main/java/io/xpipe/app/ext/ModuleInstall.java deleted file mode 100644 index a83de8d4b..000000000 --- a/app/src/main/java/io/xpipe/app/ext/ModuleInstall.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.xpipe.app.ext; - -import lombok.Getter; - -import java.nio.file.Path; - -@Getter -public abstract class ModuleInstall { - - private final String id; - - private final String module; - - protected ModuleInstall(String id, String module) { - this.id = id; - this.module = module; - } - - public abstract String getLicenseFile(); - - public abstract String getVendorURL(); - - public abstract void installInternal(Path directory) throws Exception; -} diff --git a/app/src/main/java/io/xpipe/app/ext/PrefsChoiceValue.java b/app/src/main/java/io/xpipe/app/ext/PrefsChoiceValue.java index 20d02e645..8e3f87436 100644 --- a/app/src/main/java/io/xpipe/app/ext/PrefsChoiceValue.java +++ b/app/src/main/java/io/xpipe/app/ext/PrefsChoiceValue.java @@ -2,6 +2,7 @@ package io.xpipe.app.ext; import io.xpipe.app.core.AppI18n; import io.xpipe.app.util.Translatable; +import javafx.beans.value.ObservableValue; import lombok.SneakyThrows; import java.util.Arrays; @@ -54,8 +55,8 @@ public interface PrefsChoiceValue extends Translatable { } @Override - default String toTranslatedString() { - return AppI18n.get(getId()); + default ObservableValue toTranslatedString() { + return AppI18n.observable(getId()); } String getId(); diff --git a/app/src/main/java/io/xpipe/app/ext/PrefsHandler.java b/app/src/main/java/io/xpipe/app/ext/PrefsHandler.java index d11a82f3a..1c634f30e 100644 --- a/app/src/main/java/io/xpipe/app/ext/PrefsHandler.java +++ b/app/src/main/java/io/xpipe/app/ext/PrefsHandler.java @@ -1,10 +1,9 @@ package io.xpipe.app.ext; -import com.dlsc.preferencesfx.model.Setting; - -import java.util.List; +import io.xpipe.app.fxcomps.Comp; +import javafx.beans.property.Property; public interface PrefsHandler { - void addSetting(List category, String group, Setting setting, Class c); + void addSetting(String id, Class c, Property property, Comp comp); } diff --git a/app/src/main/java/io/xpipe/app/ext/PrefsProvider.java b/app/src/main/java/io/xpipe/app/ext/PrefsProvider.java index b540dc590..c1f0b954a 100644 --- a/app/src/main/java/io/xpipe/app/ext/PrefsProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/PrefsProvider.java @@ -1,8 +1,6 @@ package io.xpipe.app.ext; -import com.dlsc.formsfx.model.structure.Field; import io.xpipe.core.util.ModuleLayerLoader; -import javafx.beans.value.ObservableBooleanValue; import java.util.List; import java.util.ServiceLoader; @@ -44,12 +42,7 @@ public abstract class PrefsProvider { .orElseThrow(); } - protected > T editable(T o, ObservableBooleanValue v) { - o.editableProperty().bind(v); - return o; - } - public abstract void addPrefs(PrefsHandler handler); - public abstract void init(); + public abstract void initDefaultValues(); } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoiceComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoiceComp.java index 431b83a57..3ce1cda07 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoiceComp.java @@ -7,6 +7,7 @@ import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.SimpleChangeListener; +import io.xpipe.app.util.Translatable; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; @@ -16,11 +17,19 @@ import javafx.util.StringConverter; import lombok.AccessLevel; import lombok.experimental.FieldDefaults; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) public class ChoiceComp extends Comp>> { + public static ChoiceComp ofTranslatable(Property value, List range, boolean includeNone) { + var map = range.stream().collect(Collectors.toMap(o -> o, Translatable::toTranslatedString, (v1, v2) -> v2, LinkedHashMap::new)); + return new ChoiceComp<>(value, map,includeNone); + } + Property value; ObservableValue>> range; boolean includeNone; diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/CodeSnippetComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/CodeSnippetComp.java index 2b490e0bc..07a9cc377 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/CodeSnippetComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/CodeSnippetComp.java @@ -9,10 +9,10 @@ import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ObservableValue; import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.control.TextArea; import javafx.scene.input.ScrollEvent; import javafx.scene.layout.*; import javafx.scene.paint.Color; -import org.fxmisc.richtext.InlineCssTextArea; import org.kordamp.ikonli.javafx.FontIcon; public class CodeSnippetComp extends Comp> { @@ -36,7 +36,7 @@ public class CodeSnippetComp extends Comp> { (int) (color.getRed() * 255), (int) (color.getGreen() * 255), (int) (color.getBlue() * 255)); } - private void fillArea(VBox lineNumbers, InlineCssTextArea s) { + private void fillArea(VBox lineNumbers, TextArea s) { lineNumbers.getChildren().clear(); s.clear(); @@ -47,8 +47,7 @@ public class CodeSnippetComp extends Comp> { lineNumbers.getChildren().add(numberLabel); for (var el : line.elements()) { - String hex = toRGBCode(el.color()); - s.append(el.text(), "-fx-fill: " + hex + ";"); + s.appendText(el.text()); } boolean last = number == value.getValue().lines().size(); @@ -74,7 +73,7 @@ public class CodeSnippetComp extends Comp> { @Override public CompStructure createBase() { - var s = new InlineCssTextArea(); + var s = new javafx.scene.control.TextArea(); s.setEditable(false); s.setBackground(null); s.getStyleClass().add("code-snippet"); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreChoiceComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreChoiceComp.java index 5334b23b4..6debd15c1 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreChoiceComp.java @@ -12,6 +12,7 @@ import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.util.DataStoreCategoryChoiceComp; import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.LocalStore; import io.xpipe.core.store.ShellStore; import javafx.beans.binding.Bindings; import javafx.beans.property.Property; @@ -154,9 +155,7 @@ public class DataStoreChoiceComp extends SimpleComp { return null; } - if (mode == Mode.PROXY - && entry.getStore() instanceof ShellStore - && ShellStore.isLocal(entry.getStore().asNeeded())) { + if (mode == Mode.PROXY && entry.getStore() instanceof LocalStore) { return AppI18n.get("none"); } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/FancyTooltipAugment.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/FancyTooltipAugment.java index 34cdbef4e..c6c1705dd 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/FancyTooltipAugment.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/FancyTooltipAugment.java @@ -1,27 +1,15 @@ package io.xpipe.app.fxcomps.impl; -import com.jfoenix.controls.JFXTooltip; import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.augment.Augment; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.Shortcuts; -import javafx.animation.KeyFrame; -import javafx.animation.Timeline; import javafx.beans.value.ObservableValue; -import javafx.event.EventHandler; -import javafx.event.WeakEventHandler; -import javafx.geometry.NodeOrientation; -import javafx.scene.Node; -import javafx.scene.Scene; -import javafx.scene.input.MouseEvent; -import javafx.stage.Window; -import javafx.util.Duration; +import javafx.scene.control.Tooltip; public class FancyTooltipAugment> implements Augment { - private static final TooltipBehavior BEHAVIOR = - new TooltipBehavior(Duration.millis(400), Duration.INDEFINITE, Duration.millis(100)); private final ObservableValue text; public FancyTooltipAugment(ObservableValue text) { @@ -35,7 +23,7 @@ public class FancyTooltipAugment> implements Augment< @Override public void augment(S struc) { var region = struc.get(); - var tt = new JFXTooltip(); + var tt = new Tooltip(); var toDisplay = text.getValue(); if (Shortcuts.getShortcut(region) != null) { toDisplay = toDisplay + " (" + Shortcuts.getShortcut(region).getDisplayText() + ")"; @@ -46,188 +34,6 @@ public class FancyTooltipAugment> implements Augment< tt.setMaxWidth(400); tt.getStyleClass().add("fancy-tooltip"); - BEHAVIOR.install(region, tt); - } - - private static class TooltipBehavior { - - private static final String TOOLTIP_PROP = "jfoenix-tooltip"; - private final Timeline hoverTimer = new Timeline(); - private final Timeline visibleTimer = new Timeline(); - private final Timeline leftTimer = new Timeline(); - /** - * the currently hovered node - */ - private Node hoveredNode; - /** - * the next tooltip to be shown - */ - private JFXTooltip nextTooltip; - - private final EventHandler exitHandler = (MouseEvent event) -> { - // stop running hover timer as the mouse exited the node - if (hoverTimer.getStatus() == Timeline.Status.RUNNING) { - hoverTimer.stop(); - } else if (visibleTimer.getStatus() == Timeline.Status.RUNNING) { - // if tool tip was already showing, stop the visible timer - // and start the left timer to hide the current tooltip - visibleTimer.stop(); - leftTimer.playFromStart(); - } - hoveredNode = null; - nextTooltip = null; - }; - private final WeakEventHandler weakExitHandler = new WeakEventHandler<>(exitHandler); - /** - * the current showing tooltip - */ - private JFXTooltip currentTooltip; - // if mouse is pressed then stop all timers / clear all fields - private final EventHandler pressedHandler = (MouseEvent event) -> { - // stop timers - hoverTimer.stop(); - visibleTimer.stop(); - leftTimer.stop(); - // hide current tooltip - if (currentTooltip != null) { - currentTooltip.hide(); - } - // clear fields - hoveredNode = null; - currentTooltip = null; - nextTooltip = null; - }; - private final WeakEventHandler weakPressedHandler = new WeakEventHandler<>(pressedHandler); - - private TooltipBehavior(Duration hoverDelay, Duration visibleDuration, Duration leftDelay) { - setHoverDelay(hoverDelay); - hoverTimer.setOnFinished(event -> { - ensureHoveredNodeIsVisible(() -> { - // set tooltip orientation - NodeOrientation nodeOrientation = hoveredNode.getEffectiveNodeOrientation(); - nextTooltip.getScene().setNodeOrientation(nodeOrientation); - // show tooltip - showTooltip(nextTooltip); - currentTooltip = nextTooltip; - hoveredNode = null; - // start visible timer - visibleTimer.playFromStart(); - }); - // clear next tooltip - nextTooltip = null; - }); - setVisibleDuration(visibleDuration); - visibleTimer.setOnFinished(event -> hideCurrentTooltip()); - setLeftDelay(leftDelay); - leftTimer.setOnFinished(event -> hideCurrentTooltip()); - } - - private void setHoverDelay(Duration duration) { - hoverTimer.getKeyFrames().setAll(new KeyFrame(duration)); - } - - private void setVisibleDuration(Duration duration) { - visibleTimer.getKeyFrames().setAll(new KeyFrame(duration)); - } - - private final EventHandler moveHandler = (MouseEvent event) -> { - // if tool tip is already showing, do nothing - if (visibleTimer.getStatus() == Timeline.Status.RUNNING) { - return; - } - hoveredNode = (Node) event.getSource(); - Object property = hoveredNode.getProperties().get(TOOLTIP_PROP); - if (property instanceof JFXTooltip tooltip) { - ensureHoveredNodeIsVisible(() -> { - // if a tooltip is already showing then show this tooltip immediately - if (leftTimer.getStatus() == Timeline.Status.RUNNING) { - if (currentTooltip != null) { - currentTooltip.hide(); - } - currentTooltip = tooltip; - // show the tooltip - showTooltip(tooltip); - // stop left timer and start the visible timer to hide the tooltip - // once finished - leftTimer.stop(); - visibleTimer.playFromStart(); - } else { - // else mark the tooltip as the next tooltip to be shown once the hover - // timer is finished (restart the timer) - // t.setActivated(true); - nextTooltip = tooltip; - hoverTimer.stop(); - hoverTimer.playFromStart(); - } - }); - } else { - uninstall(hoveredNode); - } - }; - - private void setLeftDelay(Duration duration) { - leftTimer.getKeyFrames().setAll(new KeyFrame(duration)); - } - - private final WeakEventHandler weakMoveHandler = new WeakEventHandler<>(moveHandler); - - private void hideCurrentTooltip() { - currentTooltip.hide(); - currentTooltip = null; - hoveredNode = null; - } - - private void showTooltip(JFXTooltip tooltip) { - // anchors are computed differently for each tooltip - tooltip.show(hoveredNode, -1, -1); - } - - private void install(Node node, JFXTooltip tooltip) { - if (node == null) { - return; - } - if (tooltip == null) { - uninstall(node); - return; - } - node.removeEventHandler(MouseEvent.MOUSE_MOVED, weakMoveHandler); - node.removeEventHandler(MouseEvent.MOUSE_EXITED, weakExitHandler); - node.removeEventHandler(MouseEvent.MOUSE_PRESSED, weakPressedHandler); - node.addEventHandler(MouseEvent.MOUSE_MOVED, weakMoveHandler); - node.addEventHandler(MouseEvent.MOUSE_EXITED, weakExitHandler); - node.addEventHandler(MouseEvent.MOUSE_PRESSED, weakPressedHandler); - node.getProperties().put(TOOLTIP_PROP, tooltip); - } - - private void uninstall(Node node) { - if (node == null) { - return; - } - node.removeEventHandler(MouseEvent.MOUSE_MOVED, weakMoveHandler); - node.removeEventHandler(MouseEvent.MOUSE_EXITED, weakExitHandler); - node.removeEventHandler(MouseEvent.MOUSE_PRESSED, weakPressedHandler); - Object tooltip = node.getProperties().get(TOOLTIP_PROP); - if (tooltip != null) { - node.getProperties().remove(TOOLTIP_PROP); - if (tooltip.equals(currentTooltip) || tooltip.equals(nextTooltip)) { - weakPressedHandler.handle(null); - } - } - } - - private void ensureHoveredNodeIsVisible(Runnable visibleRunnable) { - final Window owner = getWindow(hoveredNode); - if (owner != null && owner.isShowing()) { - final boolean treeVisible = true; // NodeHelper.isTreeVisible(hoveredNode); - if (treeVisible && owner.isFocused()) { - visibleRunnable.run(); - } - } - } - - private Window getWindow(final Node node) { - final Scene scene = node == null ? null : node.getScene(); - return scene == null ? null : scene.getWindow(); - } + Tooltip.install(struc.get(), tt); } } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/OptionsComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/OptionsComp.java index 2e626aef1..da7457bdd 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/OptionsComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/OptionsComp.java @@ -50,13 +50,13 @@ public class OptionsComp extends Comp> { var nameRegions = new ArrayList(); Region firstComp = null; - for (var entry : getEntries()) { Region compRegion = null; if (entry.comp() != null) { compRegion = entry.comp().createRegion(); } if (firstComp == null) { + compRegion.getStyleClass().add("first"); firstComp = compRegion; } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/SecretFieldComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/SecretFieldComp.java index 29e57e5b4..13e334676 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/SecretFieldComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/SecretFieldComp.java @@ -1,12 +1,13 @@ package io.xpipe.app.fxcomps.impl; +import io.xpipe.app.core.AppFont; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.util.SecretHelper; -import io.xpipe.core.util.SecretValue; +import io.xpipe.core.util.InPlaceSecretValue; import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; import javafx.scene.control.PasswordField; import javafx.scene.control.TextField; @@ -14,14 +15,25 @@ import java.util.Objects; public class SecretFieldComp extends Comp> { - private final Property value; + public static SecretFieldComp ofString(Property s) { + var prop = new SimpleObjectProperty(s.getValue() != null ? InPlaceSecretValue.of(s.getValue()) : null); + prop.addListener((observable, oldValue, newValue) -> { + s.setValue(newValue != null ? new String(newValue.getSecret()) : null); + }); + s.addListener((observableValue, s1, t1) -> { + prop.set(t1 != null ? InPlaceSecretValue.of(t1) : null); + }); + return new SecretFieldComp(prop); + } - public SecretFieldComp(Property value) { + private final Property value; + + public SecretFieldComp(Property value) { this.value = value; } - protected SecretValue encrypt(char[] c) { - return SecretHelper.encrypt(c); + protected InPlaceSecretValue encrypt(char[] c) { + return InPlaceSecretValue.of(c); } @Override @@ -42,6 +54,7 @@ public class SecretFieldComp extends Comp> { text.setText(n != null ? n.getSecretValue() : null); }); }); + AppFont.small(text); return new SimpleCompStructure<>(text); } } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/TabPaneComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/TabPaneComp.java deleted file mode 100644 index 53bc439a7..000000000 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/TabPaneComp.java +++ /dev/null @@ -1,64 +0,0 @@ -package io.xpipe.app.fxcomps.impl; - -import com.jfoenix.controls.JFXTabPane; -import io.xpipe.app.fxcomps.Comp; -import io.xpipe.app.fxcomps.CompStructure; -import io.xpipe.app.fxcomps.SimpleCompStructure; -import io.xpipe.app.fxcomps.util.PlatformThread; -import javafx.beans.property.Property; -import javafx.beans.value.ObservableValue; -import javafx.geometry.Pos; -import javafx.scene.control.Label; -import javafx.scene.control.Tab; -import lombok.Getter; -import org.kordamp.ikonli.javafx.FontIcon; - -import java.util.List; - -@Getter -public class TabPaneComp extends Comp> { - - private final Property selected; - private final List entries; - - public TabPaneComp(Property selected, List entries) { - this.selected = selected; - this.entries = entries; - } - - @Override - public CompStructure createBase() { - JFXTabPane tabPane = new JFXTabPane(); - tabPane.getStyleClass().add("tab-pane-comp"); - - for (var e : entries) { - Tab tab = new Tab(); - var ll = new Label(null); - if (e.graphic != null) { - ll.setGraphic(new FontIcon(e.graphic())); - } - ll.textProperty().bind(e.name()); - ll.getStyleClass().add("name"); - ll.setAlignment(Pos.CENTER); - tab.setGraphic(ll); - var content = e.comp().createRegion(); - tab.setContent(content); - tabPane.getTabs().add(tab); - content.prefWidthProperty().bind(tabPane.widthProperty()); - } - - tabPane.getSelectionModel().select(entries.indexOf(selected.getValue())); - tabPane.getSelectionModel().selectedIndexProperty().addListener((c, o, n) -> { - selected.setValue(entries.get(n.intValue())); - }); - selected.addListener((c, o, n) -> { - PlatformThread.runLaterIfNeeded(() -> { - tabPane.getSelectionModel().select(entries.indexOf(n)); - }); - }); - - return new SimpleCompStructure<>(tabPane); - } - - public record Entry(ObservableValue name, String graphic, Comp comp) {} -} diff --git a/app/src/main/java/io/xpipe/app/issue/ErrorDetailsComp.java b/app/src/main/java/io/xpipe/app/issue/ErrorDetailsComp.java index 3b7a114b7..9fc1a9d51 100644 --- a/app/src/main/java/io/xpipe/app/issue/ErrorDetailsComp.java +++ b/app/src/main/java/io/xpipe/app/issue/ErrorDetailsComp.java @@ -1,19 +1,14 @@ package io.xpipe.app.issue; import io.xpipe.app.core.AppFont; -import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; -import io.xpipe.app.fxcomps.impl.TabPaneComp; import io.xpipe.core.util.Deobfuscator; -import javafx.beans.property.SimpleObjectProperty; import javafx.geometry.Insets; import javafx.scene.control.TextArea; import javafx.scene.layout.Region; import lombok.AllArgsConstructor; -import java.util.ArrayList; - @AllArgsConstructor public class ErrorDetailsComp extends SimpleComp { @@ -31,18 +26,12 @@ public class ErrorDetailsComp extends SimpleComp { return tf; } - return null; + return new Region(); } @Override protected Region createSimple() { - var items = new ArrayList(); - if (event.getThrowable() != null) { - items.add(new TabPaneComp.Entry( - AppI18n.observable("stackTrace"), "mdoal-code", Comp.of(this::createStrackTraceContent))); - } - - var tb = new TabPaneComp(new SimpleObjectProperty<>(items.size() > 0 ? items.get(0) : null), items); + var tb = Comp.of(this::createStrackTraceContent); tb.apply(r -> AppFont.small(r.get())); return tb.createRegion(); } diff --git a/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java b/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java index a53e98caf..ee3bb93e8 100644 --- a/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java +++ b/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java @@ -8,6 +8,7 @@ import lombok.Singular; import java.nio.file.Path; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; @Builder @Getter @@ -61,12 +62,31 @@ public class ErrorEvent { return builder().description(msg); } - public void handle() { - EventHandler.get().modify(this); - EventHandler.get().handle(this); + public List getThrowableChain() { + var list = new ArrayList(); + Throwable t = getThrowable(); + while (t != null) { + list.addFirst(t); + t = t.getCause(); + } + return list; } + private boolean shouldIgnore(Throwable throwable) { + return (throwable != null && HANDLED.stream().anyMatch(t -> t == throwable) && !terminal) || + (throwable != null && throwable.getCause() != throwable && shouldIgnore(throwable.getCause())); + } + public void handle() { + // Check object identity to allow for multiple exceptions with same trace + if (shouldIgnore(throwable)) { + return; + } + + EventHandler.get().modify(this); + EventHandler.get().handle(this); + HANDLED.add(throwable); + } public void addAttachment(Path file) { attachments = new ArrayList<>(attachments); @@ -101,6 +121,7 @@ public class ErrorEvent { } private static final Map EVENT_BASES = new ConcurrentHashMap<>(); + private static final Set HANDLED = new CopyOnWriteArraySet<>(); public static T unreportableIfEndsWith(T t, String... s) { return unreportableIf( diff --git a/app/src/main/java/io/xpipe/app/issue/EventHandlerImpl.java b/app/src/main/java/io/xpipe/app/issue/EventHandlerImpl.java index ebec28693..55c6813a5 100644 --- a/app/src/main/java/io/xpipe/app/issue/EventHandlerImpl.java +++ b/app/src/main/java/io/xpipe/app/issue/EventHandlerImpl.java @@ -14,7 +14,6 @@ public class EventHandlerImpl extends EventHandler { var suffix = ee.getThrowable() != null ? Deobfuscator.deobfuscateToString(ee.getThrowable()) : ""; te.message(prefix + suffix); te.type("error"); - te.category("exception"); te.tag("omitted", ee.isOmitted()); te.tag("terminal", ee.isTerminal()); te.elements(ee.getAttachments().stream().map(Path::toString).toList()); @@ -31,15 +30,6 @@ public class EventHandlerImpl extends EventHandler { } } - private void handleBasic(ErrorEvent ee) { - if (ee.getDescription() != null) { - System.err.println(ee.getDescription()); - } - if (ee.getThrowable() != null) { - Deobfuscator.printStackTrace(ee.getThrowable()); - } - } - @Override public void handle(ErrorEvent ee) { if (ee.isTerminal()) { diff --git a/app/src/main/java/io/xpipe/app/issue/GuiErrorHandler.java b/app/src/main/java/io/xpipe/app/issue/GuiErrorHandler.java index 2751c06b8..a72849e11 100644 --- a/app/src/main/java/io/xpipe/app/issue/GuiErrorHandler.java +++ b/app/src/main/java/io/xpipe/app/issue/GuiErrorHandler.java @@ -3,6 +3,8 @@ package io.xpipe.app.issue; import io.xpipe.app.util.LicenseProvider; import io.xpipe.app.util.LicenseRequiredException; +import java.util.stream.Stream; + public class GuiErrorHandler extends GuiErrorHandlerBase implements ErrorHandler { private final ErrorHandler log = new LogErrorHandler(); @@ -28,8 +30,10 @@ public class GuiErrorHandler extends GuiErrorHandlerBase implements ErrorHandler } private void handleGui(ErrorEvent event) { - if (event.getThrowable() instanceof LicenseRequiredException lex) { - LicenseProvider.get().showLicenseAlert(lex); + var lex = event.getThrowableChain().stream().flatMap(throwable -> throwable instanceof LicenseRequiredException le ? + Stream.of(le) : Stream.of()).findFirst(); + if (lex.isPresent()) { + LicenseProvider.get().showLicenseAlert(lex.get()); event.setShouldSendDiagnostics(true); ErrorAction.ignore().handle(event); } else { diff --git a/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java b/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java index 98163bf9f..876b3d61a 100644 --- a/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java +++ b/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java @@ -48,8 +48,10 @@ public class SentryErrorHandler implements ErrorHandler { options.setTag("arch", System.getProperty("os.arch")); options.setDist(XPipeDistributionType.get().getId()); options.setTag("staging", String.valueOf(AppProperties.get().isStaging())); - options.setCacheDirPath(AppProperties.get().getDataDir().resolve("cache").toString()); + options.setSendModules(false); options.setAttachThreads(false); + options.setEnableDeduplication(false); + options.setCacheDirPath(AppProperties.get().getDataDir().resolve("cache").toString()); }); } init = true; @@ -147,7 +149,7 @@ public class SentryErrorHandler implements ErrorHandler { return Sentry.captureEvent(event, sc -> fillScope(ee, sc)); } - private static void fillScope(ErrorEvent ee, Scope s) { + private static void fillScope(ErrorEvent ee, IScope s) { if (ee.isShouldSendDiagnostics()) { var atts = ee.getAttachments().stream() .map(d -> { diff --git a/app/src/main/java/io/xpipe/app/issue/TerminalErrorHandler.java b/app/src/main/java/io/xpipe/app/issue/TerminalErrorHandler.java index f751db70b..78f2649e3 100644 --- a/app/src/main/java/io/xpipe/app/issue/TerminalErrorHandler.java +++ b/app/src/main/java/io/xpipe/app/issue/TerminalErrorHandler.java @@ -49,7 +49,7 @@ public class TerminalErrorHandler extends GuiErrorHandlerBase implements ErrorHa return; } - if (OperationMode.isInStartup()) { + if (OperationMode.isInStartup() && !AppProperties.get().isDevelopmentEnvironment()) { handleProbableUpdate(); } diff --git a/app/src/main/java/io/xpipe/app/issue/TrackEvent.java b/app/src/main/java/io/xpipe/app/issue/TrackEvent.java index 75bfaac2f..29f5f1cae 100644 --- a/app/src/main/java/io/xpipe/app/issue/TrackEvent.java +++ b/app/src/main/java/io/xpipe/app/issue/TrackEvent.java @@ -18,7 +18,6 @@ public class TrackEvent { private final Instant instant = Instant.now(); private String type; private String message; - private String category; @Singular private Map tags; @@ -26,30 +25,14 @@ public class TrackEvent { @Singular private List elements; - public static TrackEventBuilder storage() { - return TrackEvent.builder().category("storage"); - } - public static TrackEventBuilder fromMessage(String type, String message) { return builder().type(type).message(message); } - public static void simple(String type, String message) { - builder().type(type).message(message).build().handle(); - } - public static TrackEventBuilder withInfo(String message) { return builder().type("info").message(message); } - public static TrackEventBuilder withInfo(String category, String message) { - return builder().category(category).type("info").message(message); - } - - public static TrackEventBuilder withWarn(String category, String message) { - return builder().category(category).type("warn").message(message); - } - public static TrackEventBuilder withWarn(String message) { return builder().type("warn").message(message); } @@ -58,10 +41,6 @@ public class TrackEvent { return builder().type("trace").message(message); } - public static TrackEventBuilder withTrace(String cat, String message) { - return builder().category(cat).type("trace").message(message); - } - public static void info(String message) { builder().type("info").message(message).build().handle(); } @@ -74,14 +53,6 @@ public class TrackEvent { return builder().type("debug").message(message); } - public static TrackEventBuilder withDebug(String cat, String message) { - return builder().category(cat).type("debug").message(message); - } - - public static void debug(String cat, String message) { - builder().category(cat).type("debug").message(message).build().handle(); - } - public static void debug(String message) { builder().type("debug").message(message).build().handle(); } @@ -90,14 +61,6 @@ public class TrackEvent { builder().type("trace").message(message).build().handle(); } - public static void info(String cat, String message) { - builder().category(cat).type("info").message(message).build().handle(); - } - - public static void trace(String cat, String message) { - builder().category(cat).type("trace").message(message).build().handle(); - } - public static TrackEventBuilder withError(String message) { return builder().type("error").message(message); } @@ -142,19 +105,8 @@ public class TrackEvent { public static class TrackEventBuilder { - public TrackEventBuilder trace() { - this.type("trace"); - return this; - } - - public TrackEventBuilder windowCategory() { - this.category("window"); - return this; - } - public TrackEventBuilder copy() { var copy = builder(); - copy.category = category; copy.message = message; copy.tags$key = new ArrayList<>(tags$key); copy.tags$value = new ArrayList<>(tags$value); diff --git a/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java b/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java index e3b32b569..9e35209fe 100644 --- a/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java +++ b/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java @@ -43,7 +43,7 @@ public class LauncherCommand implements Callable { final List inputs = List.of(); public static void runLauncher(String[] args) { - TrackEvent.builder().category("launcher").type("debug").message("Launcher received commands: " + Arrays.asList(args)).handle(); + TrackEvent.builder().type("debug").message("Launcher received commands: " + Arrays.asList(args)).handle(); var cmd = new CommandLine(new LauncherCommand()); cmd.setExecutionExceptionHandler((ex, commandLine, parseResult) -> { diff --git a/app/src/main/java/io/xpipe/app/launcher/LauncherInput.java b/app/src/main/java/io/xpipe/app/launcher/LauncherInput.java index 7fb8b2dde..dae6444d6 100644 --- a/app/src/main/java/io/xpipe/app/launcher/LauncherInput.java +++ b/app/src/main/java/io/xpipe/app/launcher/LauncherInput.java @@ -24,7 +24,7 @@ public abstract class LauncherInput { return; } - TrackEvent.withDebug("launcher", "Handling arguments") + TrackEvent.withDebug("Handling arguments") .elements(arguments) .handle(); diff --git a/app/src/main/java/io/xpipe/app/prefs/AboutComp.java b/app/src/main/java/io/xpipe/app/prefs/AboutCategory.java similarity index 70% rename from app/src/main/java/io/xpipe/app/prefs/AboutComp.java rename to app/src/main/java/io/xpipe/app/prefs/AboutCategory.java index 710992dd2..e9bb6843e 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AboutComp.java +++ b/app/src/main/java/io/xpipe/app/prefs/AboutCategory.java @@ -1,12 +1,15 @@ package io.xpipe.app.prefs; +import atlantafx.base.theme.Styles; import io.xpipe.app.comp.base.TileButtonComp; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.core.AppProperties; import io.xpipe.app.core.AppWindowHelper; import io.xpipe.app.fxcomps.Comp; -import io.xpipe.app.fxcomps.CompStructure; +import io.xpipe.app.fxcomps.impl.LabelComp; import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.util.Hyperlinks; +import io.xpipe.app.util.JfxHelper; import io.xpipe.app.util.OptionsBuilder; import javafx.geometry.Insets; import javafx.scene.control.ScrollPane; @@ -14,7 +17,7 @@ import javafx.scene.layout.Region; import java.util.List; -public class AboutComp extends Comp> { +public class AboutCategory extends AppPrefsCategory { private Comp createLinks() { return new OptionsBuilder() @@ -79,15 +82,44 @@ public class AboutComp extends Comp> { } @Override - public CompStructure createBase() { - var props = new PropertiesComp().padding(new Insets(0, 0, 0, 15)); + protected String getId() { + return "about"; + } + + private Comp createProperties() { + var title = Comp.of(() -> { + return JfxHelper.createNamedEntry(AppI18n.get("xPipeClient"), "Version " + AppProperties.get().getVersion() + " (" + + AppProperties.get().getArch() + ")", "logo/logo_48x48.png"); + }).styleClass(Styles.TEXT_BOLD); + + var section = new OptionsBuilder() + .addComp(title, null) + .addComp(Comp.vspacer(10)) + .name("build") + .addComp( + new LabelComp(AppProperties.get().getBuild()), + null) + .name("runtimeVersion") + .addComp( + new LabelComp(System.getProperty("java.vm.version")), + null) + .name("virtualMachine") + .addComp( + new LabelComp(System.getProperty("java.vm.vendor") + " " + System.getProperty("java.vm.name")), + null) + .buildComp(); + return section.styleClass("properties-comp"); + } + + @Override + protected Comp create() { + var props = createProperties().padding(new Insets(0, 0, 0, 15)); var update = new UpdateCheckComp().grow(true, false); - var box = new VerticalComp(List.of(props, Comp.separator(), update, Comp.separator(), createLinks())) + return new VerticalComp(List.of(props, Comp.separator(), update, Comp.separator(), createLinks())) .apply(s -> s.get().setFillWidth(true)) .apply(struc -> struc.get().setSpacing(15)) .styleClass("information") .styleClass("about-tab") .apply(struc -> struc.get().setPrefWidth(600)); - return box.createStructure(); } } diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPreferencesFx.java b/app/src/main/java/io/xpipe/app/prefs/AppPreferencesFx.java deleted file mode 100644 index 7856313b0..000000000 --- a/app/src/main/java/io/xpipe/app/prefs/AppPreferencesFx.java +++ /dev/null @@ -1,177 +0,0 @@ -package io.xpipe.app.prefs; - -import com.dlsc.formsfx.model.structure.Form; -import com.dlsc.formsfx.model.util.TranslationService; -import com.dlsc.preferencesfx.PreferencesFxEvent; -import com.dlsc.preferencesfx.history.History; -import com.dlsc.preferencesfx.model.Category; -import com.dlsc.preferencesfx.model.PreferencesFxModel; -import com.dlsc.preferencesfx.util.SearchHandler; -import com.dlsc.preferencesfx.util.StorageHandler; -import com.dlsc.preferencesfx.view.*; -import javafx.beans.property.ObjectProperty; -import javafx.event.EventHandler; -import javafx.event.EventType; -import javafx.scene.Node; -import javafx.scene.control.ScrollPane; -import lombok.Getter; -import lombok.SneakyThrows; - -import java.util.List; - -@Getter -public class AppPreferencesFx { - - private final PreferencesFxModel preferencesFxModel; - - private NavigationView navigationView; - private NavigationPresenter navigationPresenter; - - private UndoRedoBox undoRedoBox; - - private BreadCrumbView breadCrumbView; - private BreadCrumbPresenter breadCrumbPresenter; - - private CategoryController categoryController; - - private PreferencesFxView preferencesFxView; - private PreferencesFxPresenter preferencesFxPresenter; - - private AppPreferencesFx(StorageHandler storageHandler, Category... categories) { - preferencesFxModel = new PreferencesFxModel(storageHandler, new SearchHandler(), new History(), categories); - - configure(); - } - - public static AppPreferencesFx of(Category... categories) { - return new AppPreferencesFx(new JsonStorageHandler(), categories); - } - - private void configure() { - preferencesFxModel.setSaveSettings(true); - preferencesFxModel.setHistoryDebugState(true); - preferencesFxModel.setInstantPersistent(true); - preferencesFxModel.setButtonsVisible(false); - } - - public void loadSettings() { - // setting values are only loaded if they are present already - preferencesFxModel.loadSettingValues(); - } - - @SneakyThrows - public void setupControls() { - undoRedoBox = new UndoRedoBox(preferencesFxModel.getHistory()); - - breadCrumbView = new BreadCrumbView(preferencesFxModel, undoRedoBox) { - @Override - public void initializeParts() {} - - @Override - public void layoutParts() {} - }; - breadCrumbPresenter = new BreadCrumbPresenter(preferencesFxModel, breadCrumbView); - - categoryController = new CategoryController(); - categoryController.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - categoryController.setFitToWidth(true); - initializeCategoryViews(); - - // display initial category - categoryController.setView(preferencesFxModel.getDisplayedCategory()); - - navigationView = new NavigationView(preferencesFxModel); - var searchField = navigationView.getClass().getDeclaredField("searchFld"); - searchField.setAccessible(true); - Node search = (Node) searchField.get(navigationView); - search.setManaged(false); - search.setVisible(false); - navigationPresenter = new NavigationPresenter(preferencesFxModel, navigationView); - - preferencesFxView = - new PreferencesFxView(preferencesFxModel, navigationView, breadCrumbView, categoryController); - preferencesFxPresenter = new PreferencesFxPresenter(preferencesFxModel, preferencesFxView) { - @Override - public void setupEventHandlers() { - // Ignore window close - } - }; - } - - public ObjectProperty translationServiceProperty() { - return preferencesFxModel.translationServiceProperty(); - } - - /** - * Prepares the CategoryController by creating CategoryView / CategoryPresenter pairs from all - * Categories and loading them into the CategoryController. - */ - private void initializeCategoryViews() { - preferencesFxModel.getFlatCategoriesLst().forEach(category -> { - var categoryView = new CustomCategoryView(preferencesFxModel, category); - CategoryPresenter categoryPresenter = - new CategoryPresenter(preferencesFxModel, category, categoryView, breadCrumbPresenter) { - @Override - @SneakyThrows - public void initializeViewParts() { - var formMethod = CategoryPresenter.class.getDeclaredMethod("createForm"); - formMethod.setAccessible(true); - - var formField = CategoryPresenter.class.getDeclaredField("form"); - formField.setAccessible(true); - formField.set(this, formMethod.invoke(this)); - categoryView.initializeFormRenderer((Form) formField.get(this)); - - this.addI18nListener(); - this.addInstantPersistenceListener(); - } - }; - categoryController.addView(category, categoryView, categoryPresenter); - }); - } - - /** - * Call this method to manually save the changed settings when showing the preferences by using - * {@link #getView()}. - */ - public void saveSettings() { - preferencesFxModel.saveSettings(); - ((JsonStorageHandler) preferencesFxModel.getStorageHandler()).save(); - } - - /** - * Call this method to undo all changes made in the settings when showing the preferences by using - * {@link #getView()}. - */ - public void discardChanges() { - preferencesFxModel.discardChanges(); - } - - /** - * Registers an event handler with the model. The handler is called when the model receives an - * {@code Event} of the specified type during the bubbling phase of event delivery. - * - * @param eventType the type of the events to receive by the handler - * @param eventHandler the handler to register - * @return PreferencesFx to allow for chaining. - * @throws NullPointerException if either event type or handler are {@code null}. - */ - public AppPreferencesFx addEventHandler( - EventType eventType, EventHandler eventHandler) { - preferencesFxModel.addEventHandler(eventType, eventHandler); - return this; - } - - /** - * Returns a PreferencesFxView, so that it can be used as a Node. - * - * @return a PreferencesFxView, so that it can be used as a Node. - */ - public PreferencesFxView getView() { - return preferencesFxView; - } - - public List getCategories() { - return preferencesFxModel.getCategories(); - } -} diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java index ca45226a1..7cc9bf5d9 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java @@ -1,45 +1,56 @@ package io.xpipe.app.prefs; -import com.dlsc.formsfx.model.structure.*; -import com.dlsc.preferencesfx.formsfx.view.controls.DoubleSliderControl; -import com.dlsc.preferencesfx.formsfx.view.controls.SimpleTextControl; -import com.dlsc.preferencesfx.model.Category; -import com.dlsc.preferencesfx.model.Group; -import com.dlsc.preferencesfx.model.Setting; -import com.dlsc.preferencesfx.util.VisibilityProperty; -import io.xpipe.app.comp.base.ButtonComp; -import io.xpipe.app.core.AppI18n; +import io.xpipe.app.core.AppCache; import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.core.AppProperties; import io.xpipe.app.core.AppTheme; -import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.ext.PrefsHandler; import io.xpipe.app.ext.PrefsProvider; -import io.xpipe.app.fxcomps.impl.StackComp; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; -import io.xpipe.app.issue.ErrorEvent; -import io.xpipe.app.util.*; -import io.xpipe.core.store.LocalStore; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.util.PlatformThread; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.util.ApplicationHelper; +import io.xpipe.app.util.ElevationAccess; +import io.xpipe.app.util.PasswordLockSecretValue; +import io.xpipe.core.util.InPlaceSecretValue; import io.xpipe.core.util.ModuleHelper; -import io.xpipe.core.util.SecretValue; import javafx.beans.binding.Bindings; import javafx.beans.property.*; import javafx.beans.value.ObservableBooleanValue; import javafx.beans.value.ObservableDoubleValue; import javafx.beans.value.ObservableStringValue; import javafx.beans.value.ObservableValue; -import javafx.collections.FXCollections; -import javafx.geometry.Insets; -import javafx.geometry.Pos; import lombok.Getter; -import lombok.SneakyThrows; -import org.kordamp.ikonli.javafx.FontIcon; +import lombok.Value; import java.nio.file.Path; import java.util.*; public class AppPrefs { + @Value + public static class Mapping { + + String key; + Property property; + Class valueClass; + boolean vaultSpecific; + + public Mapping(String key, Property property, Class valueClass) { + this.key = key; + this.property = property; + this.valueClass = valueClass; + this.vaultSpecific = false; + } + + public Mapping(String key, Property property, Class valueClass, boolean vaultSpecific) { + this.key = key; + this.property = property; + this.valueClass = valueClass; + this.vaultSpecific = vaultSpecific; + } + } + public boolean isDevelopmentEnvironment() { return developerMode().getValue() && !ModuleHelper.isImage(); } @@ -62,219 +73,189 @@ public class AppPrefs { developerMode()); } - private static final int tooltipDelayMin = 0; - private static final int tooltipDelayMax = 1500; - private static final int editorReloadTimeoutMin = 0; - private static final int editorReloadTimeoutMax = 1500; + private final List> mapping = new ArrayList<>(); + public static final Path DEFAULT_STORAGE_DIR = AppProperties.get().getDataDir().resolve("storage"); private static final String DEVELOPER_MODE_PROP = "io.xpipe.app.developerMode"; private static AppPrefs INSTANCE; - private final SimpleListProperty languageList = - new SimpleListProperty<>(FXCollections.observableArrayList(Arrays.asList(SupportedLocale.values()))); - private final SimpleListProperty themeList = - new SimpleListProperty<>(FXCollections.observableArrayList(AppTheme.Theme.ALL)); - private final SimpleListProperty closeBehaviourList = new SimpleListProperty<>( - FXCollections.observableArrayList(PrefsChoiceValue.getSupported(CloseBehaviour.class))); - private final SimpleListProperty externalEditorList = new SimpleListProperty<>( - FXCollections.observableArrayList(PrefsChoiceValue.getSupported(ExternalEditorType.class))); - private final SimpleListProperty logLevelList = - new SimpleListProperty<>(FXCollections.observableArrayList("trace", "debug", "info", "warn", "error")); - private final Map> classMap = new HashMap<>(); // Languages // ========= - private final ObjectProperty languageInternal = - typed(new SimpleObjectProperty<>(SupportedLocale.ENGLISH), SupportedLocale.class); - public final Property language = new SimpleObjectProperty<>(SupportedLocale.ENGLISH); - private final SingleSelectionField languageControl = Field.ofSingleSelectionType( - languageList, languageInternal) - .render(() -> new TranslatableComboBoxControl<>()); + private final ObjectProperty language = + map(new SimpleObjectProperty<>(SupportedLocale.ENGLISH), "language", SupportedLocale.class); + public ObservableValue language() { + return language; + } + final BooleanProperty dontAcceptNewHostKeys = map(new SimpleBooleanProperty(false), "dontAcceptNewHostKeys", Boolean.class); + public ObservableBooleanValue dontAcceptNewHostKeys() { + return dontAcceptNewHostKeys; + } - - final BooleanProperty performanceMode = typed(new SimpleBooleanProperty(false), Boolean.class); - + final BooleanProperty performanceMode = map(new SimpleBooleanProperty(false), "performanceMode", Boolean.class); public ObservableBooleanValue performanceMode() { return performanceMode; } - public final ObjectProperty theme = typed(new SimpleObjectProperty<>(), AppTheme.Theme.class); - private final SingleSelectionField themeControl = - Field.ofSingleSelectionType(themeList, theme).render(() -> new TranslatableComboBoxControl<>()); - private final BooleanProperty useSystemFontInternal = typed(new SimpleBooleanProperty(true), Boolean.class); - public final ReadOnlyBooleanProperty useSystemFont = useSystemFontInternal; - private final IntegerProperty tooltipDelayInternal = typed(new SimpleIntegerProperty(1000), Integer.class); - private final IntegerProperty connectionTimeOut = typed(new SimpleIntegerProperty(10), Integer.class); + final BooleanProperty useBundledTools = map(new SimpleBooleanProperty(false), "useBundledTools", Boolean.class); + public ObservableBooleanValue useBundledTools() { + return useBundledTools; + } - public ReadOnlyIntegerProperty connectionTimeout() { + public final ObjectProperty theme = map(new SimpleObjectProperty<>(), "theme", AppTheme.Theme.class); + final BooleanProperty useSystemFont = map(new SimpleBooleanProperty(true), "useSystemFont", Boolean.class); + public ObservableValue useSystemFont() { + return useSystemFont; + } + + final Property uiScale = map(new SimpleObjectProperty<>(null), "uiScale", Integer.class); + public ReadOnlyProperty uiScale() { + return uiScale; + } + + final Property connectionTimeOut = map(new SimpleObjectProperty<>(10), "connectionTimeout", Integer.class); + public ReadOnlyProperty connectionTimeOut() { return connectionTimeOut; } - private final BooleanProperty saveWindowLocation = typed(new SimpleBooleanProperty(true), Boolean.class); + final BooleanProperty saveWindowLocation = map(new SimpleBooleanProperty(true), "saveWindowLocation", Boolean.class); // External terminal // ================= - private final ObjectProperty terminalType = - typed(new SimpleObjectProperty<>(), ExternalTerminalType.class); - private final SimpleListProperty terminalTypeList = new SimpleListProperty<>( - FXCollections.observableArrayList(PrefsChoiceValue.getSupported(ExternalTerminalType.class))); - private final SingleSelectionField terminalTypeControl = Field.ofSingleSelectionType( - terminalTypeList, terminalType) - .render(() -> new TranslatableComboBoxControl<>()); + final ObjectProperty terminalType = + map(new SimpleObjectProperty<>(), "terminalType", ExternalTerminalType.class); // Lock // ==== @Getter - private final Property lockPassword = new SimpleObjectProperty<>(); + private final Property lockPassword = new SimpleObjectProperty<>(); @Getter - private final StringProperty lockCrypt = typed(new SimpleStringProperty(""), String.class); + private final StringProperty lockCrypt = mapVaultSpecific(new SimpleStringProperty(), "workspaceLock", String.class); // Window opacity // ============== - private final DoubleProperty windowOpacity = typed(new SimpleDoubleProperty(1.0), Double.class); - private final DoubleField windowOpacityField = - Field.ofDoubleType(windowOpacity).render(() -> { - var r = new DoubleSliderControl(0.3, 1.0, 2); - r.setMinWidth(200); - return r; - }); - + final DoubleProperty windowOpacity = map(new SimpleDoubleProperty(1.0), "windowOpacity", Double.class); // Custom terminal // =============== - private final StringProperty customTerminalCommand = typed(new SimpleStringProperty(""), String.class); - private final StringField customTerminalCommandControl = editable( - StringField.ofStringType(customTerminalCommand).placeholder("customTerminalPlaceholder").render(() -> new SimpleTextControl()), - terminalType.isEqualTo(ExternalTerminalType.CUSTOM)); + final StringProperty customTerminalCommand = map(new SimpleStringProperty(""), "customTerminalCommand", String.class); - private final BooleanProperty preferTerminalTabs = typed(new SimpleBooleanProperty(true), Boolean.class); - private final BooleanField preferTerminalTabsField = - BooleanField.ofBooleanType(preferTerminalTabs).render(() -> new CustomToggleControl()); + final BooleanProperty preferTerminalTabs = map(new SimpleBooleanProperty(true), "preferTerminalTabs", Boolean.class); - - // Fast terminal - // =========== - public final BooleanProperty enableFastTerminalStartup = typed(new SimpleBooleanProperty(false), Boolean.class); - public ObservableBooleanValue enableFastTerminalStartup() { - return enableFastTerminalStartup; + final BooleanProperty clearTerminalOnInit = map(new SimpleBooleanProperty(true), "clearTerminalOnInit", Boolean.class); + public ReadOnlyBooleanProperty clearTerminalOnInit() { + return clearTerminalOnInit; + } + + public final BooleanProperty disableCertutilUse = map(new SimpleBooleanProperty(false), "disableCertutilUse", Boolean.class); + public ObservableBooleanValue disableCertutilUse() { + return disableCertutilUse; + } + + public final BooleanProperty useLocalFallbackShell = map(new SimpleBooleanProperty(false), "useLocalFallbackShell", Boolean.class); + public ObservableBooleanValue useLocalFallbackShell() { + return useLocalFallbackShell; + } + + public final BooleanProperty disableTerminalRemotePasswordPreparation = map(new SimpleBooleanProperty(false), "disableTerminalRemotePasswordPreparation", Boolean.class); + public ObservableBooleanValue disableTerminalRemotePasswordPreparation() { + return disableTerminalRemotePasswordPreparation; + } + + public final Property elevationPolicy = map(new SimpleObjectProperty<>(ElevationAccess.ALLOW), "elevationPolicy", ElevationAccess.class); + public ObservableValue elevationPolicy() { + return elevationPolicy; + } + + public final BooleanProperty dontCachePasswords = map(new SimpleBooleanProperty(false), "dontCachePasswords", Boolean.class); + public ObservableBooleanValue dontCachePasswords() { + return dontCachePasswords; + } + + public final BooleanProperty denyTempScriptCreation = map(new SimpleBooleanProperty(false), "denyTempScriptCreation", Boolean.class); + public ObservableBooleanValue denyTempScriptCreation() { + return denyTempScriptCreation; } - private final BooleanField enableFastTerminalStartupField = - BooleanField.ofBooleanType(enableFastTerminalStartup).render(() -> new CustomToggleControl()); // Password manager // ================ - final StringProperty passwordManagerCommand = typed(new SimpleStringProperty(""), String.class); + final StringProperty passwordManagerCommand = map(new SimpleStringProperty(""), "passwordManagerCommand", String.class); // Start behaviour // =============== - private final SimpleListProperty startupBehaviourList = new SimpleListProperty<>( - FXCollections.observableArrayList(PrefsChoiceValue.getSupported(StartupBehaviour.class))); - private final ObjectProperty startupBehaviour = - typed(new SimpleObjectProperty<>(StartupBehaviour.GUI), StartupBehaviour.class); + final ObjectProperty startupBehaviour = + map(new SimpleObjectProperty<>(StartupBehaviour.GUI), "startupBehaviour", StartupBehaviour.class); - private final SingleSelectionField startupBehaviourControl = Field.ofSingleSelectionType( - startupBehaviourList, startupBehaviour) - .render(() -> new TranslatableComboBoxControl<>()); // Git storage // =========== - public final BooleanProperty enableGitStorage = typed(new SimpleBooleanProperty(false), Boolean.class); + public final BooleanProperty enableGitStorage = map(new SimpleBooleanProperty(false), "enableGitStorage", Boolean.class); public ObservableBooleanValue enableGitStorage() { return enableGitStorage; } - final StringProperty storageGitRemote = typed(new SimpleStringProperty(""), String.class); + final StringProperty storageGitRemote = map(new SimpleStringProperty(""), "storageGitRemote", String.class); public ObservableStringValue storageGitRemote() { return storageGitRemote; } // Close behaviour // =============== - private final ObjectProperty closeBehaviour = - typed(new SimpleObjectProperty<>(CloseBehaviour.QUIT), CloseBehaviour.class); - private final SingleSelectionField closeBehaviourControl = Field.ofSingleSelectionType( - closeBehaviourList, closeBehaviour) - .render(() -> new TranslatableComboBoxControl<>()); + final ObjectProperty closeBehaviour = + map(new SimpleObjectProperty<>(CloseBehaviour.QUIT), "closeBehaviour", CloseBehaviour.class); // External editor // =============== final ObjectProperty externalEditor = - typed(new SimpleObjectProperty<>(), ExternalEditorType.class); - private final SingleSelectionField externalEditorControl = Field.ofSingleSelectionType( - externalEditorList, externalEditor) - .render(() -> new TranslatableComboBoxControl<>()); + map(new SimpleObjectProperty<>(), "externalEditor", ExternalEditorType.class); - final StringProperty customEditorCommand = typed(new SimpleStringProperty(""), String.class); - private final StringField customEditorCommandControl = editable( - StringField.ofStringType(customEditorCommand).placeholder("customEditorPlaceholder").render(() -> new SimpleTextControl()), - externalEditor.isEqualTo(ExternalEditorType.CUSTOM)); - private final IntegerProperty editorReloadTimeout = typed(new SimpleIntegerProperty(1000), Integer.class); + final StringProperty customEditorCommand = map(new SimpleStringProperty(""), "customEditorCommand", String.class); + private final IntegerProperty editorReloadTimeout = map(new SimpleIntegerProperty(1000), "editorReloadTimeout", Integer.class); - private final BooleanProperty preferEditorTabs = typed(new SimpleBooleanProperty(true), Boolean.class); - private final BooleanField preferEditorTabsField = - BooleanField.ofBooleanType(preferEditorTabs).render(() -> new CustomToggleControl()); + final BooleanProperty preferEditorTabs = map(new SimpleBooleanProperty(true), "preferEditorTabs", Boolean.class); // Automatically update // ==================== - private final BooleanProperty automaticallyCheckForUpdates = typed(new SimpleBooleanProperty(true), Boolean.class); - private final BooleanField automaticallyCheckForUpdatesField = - BooleanField.ofBooleanType(automaticallyCheckForUpdates).render(() -> new CustomToggleControl()); + final BooleanProperty automaticallyCheckForUpdates = map(new SimpleBooleanProperty(true), "automaticallyCheckForUpdates", Boolean.class); + private final BooleanProperty confirmDeletions = map(new SimpleBooleanProperty(true), "confirmDeletions", Boolean.class); - private final BooleanProperty confirmDeletions = typed(new SimpleBooleanProperty(true), Boolean.class); + + final BooleanProperty encryptAllVaultData = mapVaultSpecific(new SimpleBooleanProperty(false), "encryptAllVaultData", Boolean.class); + public ObservableBooleanValue encryptAllVaultData() { + return encryptAllVaultData; + } + + + final BooleanProperty enforceWindowModality = map(new SimpleBooleanProperty(false), "enforceWindowModality", Boolean.class); + public ObservableBooleanValue enforceWindowModality() { + return enforceWindowModality; + } + + + final BooleanProperty condenseConnectionDisplay = map(new SimpleBooleanProperty(false), "condenseConnectionDisplay", Boolean.class); + public ObservableBooleanValue condenseConnectionDisplay() { + return condenseConnectionDisplay; + } // Storage // ======= final ObjectProperty storageDirectory = - typed(new SimpleObjectProperty<>(DEFAULT_STORAGE_DIR), Path.class); - final StringField storageDirectoryControl = - PrefFields.ofPath(storageDirectory).validate(CustomValidators.absolutePath(), CustomValidators.directory()); + map(new SimpleObjectProperty<>(DEFAULT_STORAGE_DIR), "storageDirectory", Path.class); - // Developer mode - // ============== - private final BooleanProperty internalDeveloperMode = typed(new SimpleBooleanProperty(false), Boolean.class); - private final BooleanProperty effectiveDeveloperMode = System.getProperty(DEVELOPER_MODE_PROP) != null - ? new SimpleBooleanProperty(Boolean.parseBoolean(System.getProperty(DEVELOPER_MODE_PROP))) - : internalDeveloperMode; - private final BooleanField developerModeField = Field.ofBooleanType(effectiveDeveloperMode) - .editable(System.getProperty(DEVELOPER_MODE_PROP) == null) - .render(() -> new CustomToggleControl()); + final BooleanProperty developerMode = map(new SimpleBooleanProperty(false), "developerMode", Boolean.class); final BooleanProperty developerDisableUpdateVersionCheck = - typed(new SimpleBooleanProperty(false), Boolean.class); - final BooleanField developerDisableUpdateVersionCheckField = - BooleanField.ofBooleanType(developerDisableUpdateVersionCheck).render(() -> new CustomToggleControl()); + map(new SimpleBooleanProperty(false), "developerDisableUpdateVersionCheck", Boolean.class); private final ObservableBooleanValue developerDisableUpdateVersionCheckEffective = bindDeveloperTrue(developerDisableUpdateVersionCheck); final BooleanProperty developerDisableGuiRestrictions = - typed(new SimpleBooleanProperty(false), Boolean.class); - final BooleanField developerDisableGuiRestrictionsField = - BooleanField.ofBooleanType(developerDisableGuiRestrictions).render(() -> new CustomToggleControl()); + map(new SimpleBooleanProperty(false), "developerDisableGuiRestrictions", Boolean.class); private final ObservableBooleanValue developerDisableGuiRestrictionsEffective = bindDeveloperTrue(developerDisableGuiRestrictions); - final BooleanProperty developerShowHiddenProviders = typed(new SimpleBooleanProperty(false), Boolean.class); - final BooleanField developerShowHiddenProvidersField = - BooleanField.ofBooleanType(developerShowHiddenProviders).render(() -> new CustomToggleControl()); - private final ObservableBooleanValue developerShowHiddenProvidersEffective = - bindDeveloperTrue(developerShowHiddenProviders); - - final BooleanProperty developerShowHiddenEntries = typed(new SimpleBooleanProperty(false), Boolean.class); - final BooleanField developerShowHiddenEntriesField = - BooleanField.ofBooleanType(developerShowHiddenEntries).render(() -> new CustomToggleControl()); - private final ObservableBooleanValue developerShowHiddenEntriesEffective = - bindDeveloperTrue(developerShowHiddenEntries); - - final BooleanProperty developerDisableConnectorInstallationVersionCheck = - typed(new SimpleBooleanProperty(false), Boolean.class); - final BooleanField developerDisableConnectorInstallationVersionCheckField = BooleanField.ofBooleanType( - developerDisableConnectorInstallationVersionCheck) - .render(() -> new CustomToggleControl()); - private final ObservableBooleanValue developerDisableConnectorInstallationVersionCheckEffective = - bindDeveloperTrue(developerDisableConnectorInstallationVersionCheck); - public ReadOnlyProperty closeBehaviour() { return closeBehaviour; } @@ -287,21 +268,27 @@ public class AppPrefs { return customEditorCommand; } - public void changeLock(SecretValue newLockPw) { + public void changeLock(InPlaceSecretValue newLockPw) { if (newLockPw == null) { - lockCrypt.setValue(""); lockPassword.setValue(null); + lockCrypt.setValue(null); + if (DataStorage.get() != null) { + DataStorage.get().forceRewrite(); + } return; } lockPassword.setValue(newLockPw); - lockCrypt.setValue(new LockedSecretValue("xpipe".toCharArray()).getEncryptedValue()); + lockCrypt.setValue(new PasswordLockSecretValue("xpipe".toCharArray()).getEncryptedValue()); + if (DataStorage.get() != null) { + DataStorage.get().forceRewrite(); + } } - public boolean unlock(SecretValue lockPw) { + public boolean unlock(InPlaceSecretValue lockPw) { lockPassword.setValue(lockPw); - var check = new LockedSecretValue("xpipe".toCharArray()).getEncryptedValue(); - if (!check.equals(lockCrypt.get())) { + var check = PasswordLockSecretValue.builder().encryptedValue(lockCrypt.get()).build().getSecret(); + if (!Arrays.equals(check, new char[] {'x', 'p', 'i', 'p', 'e'})) { lockPassword.setValue(null); return false; } else { @@ -338,7 +325,9 @@ public class AppPrefs { } public ObservableValue developerMode() { - return effectiveDeveloperMode; + return System.getProperty(DEVELOPER_MODE_PROP) != null + ? new SimpleBooleanProperty(Boolean.parseBoolean(System.getProperty(DEVELOPER_MODE_PROP))) + : developerMode; } public ObservableDoubleValue windowOpacity() { @@ -357,41 +346,43 @@ public class AppPrefs { return developerDisableGuiRestrictionsEffective; } - public ObservableBooleanValue developerDisableConnectorInstallationVersionCheck() { - return developerDisableConnectorInstallationVersionCheckEffective; - } - - public ObservableBooleanValue developerShowHiddenProviders() { - return developerShowHiddenProvidersEffective; - } - - public ObservableBooleanValue developerShowHiddenEntries() { - return developerShowHiddenEntriesEffective; - } - - private AppPreferencesFx preferencesFx; - private boolean controlsSetup; - @Getter - private final Set> proRequiredSettings = new HashSet<>(); + private final List categories; + private final AppPrefsStorageHandler globalStorageHandler = new AppPrefsStorageHandler( + AppProperties.get().getDataDir().resolve("settings").resolve("preferences.json")); + private final AppPrefsStorageHandler vaultStorageHandler = new AppPrefsStorageHandler( + storageDirectory().getValue().resolve("preferences.json")); + private final Map, Comp> customEntries = new LinkedHashMap<>(); + @Getter + private final Property selectedCategory; + private final PrefsHandler extensionHandler = new PrefsHandlerImpl(); private AppPrefs() { - try { - preferencesFx = createPreferences(); - } catch (Exception e) { - ErrorEvent.fromThrowable(e).terminal(true).build().handle(); + this.categories = List.of(new AboutCategory(), new SystemCategory(), new AppearanceCategory(), + new SyncCategory(), new VaultCategory(), new TerminalCategory(), new EditorCategory(), new ConnectionsCategory(), new SecurityCategory(), + new PasswordManagerCategory(), new TroubleshootCategory(), new DeveloperCategory()); + var selected = AppCache.get("selectedPrefsCategory", Integer.class, () -> 0); + if (selected == null) { + selected = 0; } - - SimpleChangeListener.apply(languageInternal, val -> { - language.setValue(val); - }); + this.selectedCategory = new SimpleObjectProperty<>(categories.get(selected >= 0 && selected < categories.size() ? selected : 0)); } public static void init() { INSTANCE = new AppPrefs(); - INSTANCE.preferencesFx.loadSettings(); - INSTANCE.initValues(); - PrefsProvider.getAll().forEach(prov -> prov.init()); + PrefsProvider.getAll().forEach(prov -> prov.addPrefs(INSTANCE.extensionHandler)); + INSTANCE.load(); + + INSTANCE.encryptAllVaultData.addListener((observableValue, aBoolean, t1) -> { + if (DataStorage.get() != null) { + DataStorage.get().forceRewrite(); + } + }); + } + + public static void setDefaults() { + INSTANCE.initDefaultValues(); + PrefsProvider.getAll().forEach(prov -> prov.initDefaultValues()); } public static void reset() { @@ -405,121 +396,73 @@ public class AppPrefs { return INSTANCE; } - // Storage directory - // ================= - - private T typed(T o, Class clazz) { - classMap.put(o, clazz); + @SuppressWarnings("unchecked") + private T map(T o, String name, Class clazz) { + mapping.add(new Mapping(name, (Property) o, (Class) clazz)); return o; } - private > T editable(T o, ObservableBooleanValue v) { - o.editableProperty().bind(v); + @SuppressWarnings("unchecked") + private T mapVaultSpecific(T o, String name, Class clazz) { + mapping.add(new Mapping(name, (Property) o, (Class) clazz, true)); return o; } - public AppPreferencesFx createControls() { - if (!controlsSetup) { - preferencesFx.setupControls(); - SimpleChangeListener.apply(languageInternal, val -> { - preferencesFx.translationServiceProperty().set(new QuietResourceBundleService()); - }); - controlsSetup = true; - } - - return preferencesFx; - } - - public void setFromExternal(ReadOnlyProperty prop, T newValue) { + public void setFromExternal(ObservableValue prop, T newValue) { var writable = (Property) prop; - writable.setValue(newValue); - save(); + PlatformThread.runLaterIfNeededBlocking(() -> { + writable.setValue(newValue); + save(); + }); } - public void setFromText(ReadOnlyProperty prop, String newValue) { - var field = getFieldForEntry(prop); - if (field == null || !field.isEditable()) { - return; - } - - field.userInputProperty().set(newValue); - if (!field.validate()) { - return; - } - - field.persist(); - save(); - } - - public void initValues() { + public void initDefaultValues() { if (externalEditor.get() == null) { ExternalEditorType.detectDefault(); } if (terminalType.get() == null) { - terminalType.set(ExternalTerminalType.getDefault()); + terminalType.set(ExternalTerminalType.determineDefault()); } } + public Comp getCustomComp(String id) { + return customEntries.entrySet().stream().filter(e -> e.getKey().getKey().equals(id)).findFirst().map(Map.Entry::getValue).orElseThrow(); + } + + public void load() { + for (Mapping value : mapping) { + var def = value.getProperty().getValue(); + AppPrefsStorageHandler handler = value.isVaultSpecific() ? vaultStorageHandler : globalStorageHandler; + var r = loadValue(handler, value); + + // This can be used to facilitate backwards compatibility + // Overdose is not really needed as many moved properties have changed anyways + var isDefault = Objects.equals(r, def); + if (isDefault && value.isVaultSpecific()) { + loadValue(globalStorageHandler,value); + } + } + } + + private T loadValue(AppPrefsStorageHandler handler, Mapping value) { + var val = handler.loadObject(value.getKey(), value.getValueClass(), value.getProperty().getValue()); + value.getProperty().setValue(val); + return val; + } + public void save() { - preferencesFx.saveSettings(); - } - - public void cancel() { - preferencesFx.discardChanges(); - } - - public Class getSettingType(String breadcrumb) { - var s = getSetting(breadcrumb); - if (s == null) { - throw new IllegalStateException("Unknown breadcrumb " + breadcrumb); + for (Mapping m : mapping) { + AppPrefsStorageHandler handler = m.isVaultSpecific() ? vaultStorageHandler : globalStorageHandler; + handler.updateObject(m.getKey(), m.getProperty().getValue()); } - - var found = classMap.get(s.valueProperty()); - if (found == null) { - throw new IllegalStateException("Unassigned type for " + breadcrumb); - } - return found; + vaultStorageHandler.save(); + globalStorageHandler.save(); } - private Setting getSetting(String breadcrumb) { - for (var c : preferencesFx.getCategories()) { - if (c.getGroups() == null) { - continue; - } - - for (var g : c.getGroups()) { - for (var s : g.getSettings()) { - if (s.getBreadcrumb().equals(breadcrumb)) { - return s; - } - } - } - } - return null; - } - - private DataField getFieldForEntry(ReadOnlyProperty property) { - for (var c : preferencesFx.getCategories()) { - if (c.getGroups() == null) { - continue; - } - - for (var g : c.getGroups()) { - for (var s : g.getSettings()) { - if (s.valueProperty().equals(property)) { - return (DataField) s.getElement(); - } - } - } - } - return null; - } - - public void selectCategory(int index) { + public void selectCategory(int selected) { AppLayoutModel.get().selectSettings(); - preferencesFx - .getNavigationPresenter() - .setSelectedCategory(preferencesFx.getCategories().get(index)); + var index = selected >= 0 && selected < categories.size() ? selected : 0; + selectedCategory.setValue(categories.get(index)); } public String passwordManagerString(String key) { @@ -533,175 +476,14 @@ public class AppPrefs { return ApplicationHelper.replaceFileArgument(passwordManagerCommand.get(), "KEY", key); } - @SneakyThrows - private AppPreferencesFx createPreferences() { - var ctr = Setting.class.getDeclaredConstructor(String.class, Element.class, Property.class); - ctr.setAccessible(true); - var terminalTest = ctr.newInstance( - null, - new LazyNodeElement<>(() -> new StackComp( - List.of(new ButtonComp(AppI18n.observable("test"), new FontIcon("mdi2p-play"), () -> { - save(); - ThreadHelper.runFailableAsync(() -> { - var term = AppPrefs.get().terminalType().getValue(); - if (term != null) { - TerminalHelper.open( - "Test", - new LocalStore().control().command("echo Test")); - } - }); - }))) - .padding(new Insets(15, 0, 0, 0)) - .apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)) - .createRegion()), - null); - var editorTest = ctr.newInstance( - null, - new LazyNodeElement<>(() -> new StackComp( - List.of(new ButtonComp(AppI18n.observable("test"), new FontIcon("mdi2p-play"), () -> { - save(); - ThreadHelper.runFailableAsync(() -> { - var editor = - AppPrefs.get().externalEditor().getValue(); - if (editor != null) { - FileOpener.openReadOnlyString("Test"); - } - }); - }))) - .padding(new Insets(15, 0, 0, 0)) - .apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)) - .createRegion()), - null); - var about = ctr.newInstance(null, new LazyNodeElement<>(() -> new AboutComp().createRegion()), null); - var troubleshoot = - ctr.newInstance(null, new LazyNodeElement<>(() -> new TroubleshootComp().createRegion()), null); - - var categories = new ArrayList<>(List.of( - Category.of("about", Group.of(about)), - Category.of( - "system", - Group.of( - "appBehaviour", - Setting.of("startupBehaviour", startupBehaviourControl, startupBehaviour), - Setting.of("closeBehaviour", closeBehaviourControl, closeBehaviour)), - Group.of( - "advanced", - Setting.of("developerMode", developerModeField, internalDeveloperMode)), - Group.of( - "updates", - Setting.of( - "automaticallyUpdate", - automaticallyCheckForUpdatesField, - automaticallyCheckForUpdates))), - new VaultCategory(this).create(), - Category.of( - "appearance", - Group.of( - "uiOptions", - Setting.of("theme", themeControl, theme), - Setting.of("performanceMode", BooleanField.ofBooleanType(performanceMode).render(() -> new CustomToggleControl()), performanceMode), - Setting.of("windowOpacity", windowOpacityField, windowOpacity), - Setting.of("useSystemFont", BooleanField.ofBooleanType(useSystemFontInternal).render(() -> new CustomToggleControl()), useSystemFontInternal), - Setting.of("tooltipDelay", tooltipDelayInternal, tooltipDelayMin, tooltipDelayMax), - Setting.of("language", languageControl, languageInternal)), - Group.of("windowOptions", Setting.of("saveWindowLocation", BooleanField.ofBooleanType(saveWindowLocation).render(() -> new CustomToggleControl()), saveWindowLocation))), - Category.of( - "connections", - Group.of( - Setting.of( - "connectionTimeout", - connectionTimeOut, - 5, - 50))), - new PasswordCategory(this).create(), - Category.of( - "editor", - Group.of( - Setting.of("editorProgram", externalEditorControl, externalEditor), - editorTest, - Setting.of("customEditorCommand", customEditorCommandControl, customEditorCommand) - .applyVisibility(VisibilityProperty.of( - externalEditor.isEqualTo(ExternalEditorType.CUSTOM))), - Setting.of( - "editorReloadTimeout", - editorReloadTimeout, - editorReloadTimeoutMin, - editorReloadTimeoutMax), - Setting.of("preferEditorTabs", preferEditorTabsField, preferEditorTabs))), - Category.of( - "terminal", - Group.of( - Setting.of("terminalProgram", terminalTypeControl, terminalType), - terminalTest, - Setting.of("customTerminalCommand", customTerminalCommandControl, customTerminalCommand) - .applyVisibility(VisibilityProperty.of( - terminalType.isEqualTo(ExternalTerminalType.CUSTOM))), - Setting.of("preferTerminalTabs", preferTerminalTabsField, preferTerminalTabs) - //Setting.of("enableFastTerminalStartup", enableFastTerminalStartupField, enableFastTerminalStartup) - )), - new DeveloperCategory(this).create(), - Category.of("troubleshoot", Group.of(troubleshoot)))); - - categories.get(categories.size() - 2).setVisibilityProperty(VisibilityProperty.of(developerMode())); - - var handler = new PrefsHandlerImpl(categories); - PrefsProvider.getAll().forEach(prov -> prov.addPrefs(handler)); - - var cats = handler.getCategories().toArray(Category[]::new); - return AppPreferencesFx.of(cats); - } - - static Group group(String name, Setting... settings) { - return Group.of( - name, Arrays.stream(settings).filter(setting -> setting != null).toArray(Setting[]::new)); - } - @Getter private class PrefsHandlerImpl implements PrefsHandler { - private final List categories; - - private PrefsHandlerImpl(List categories) { - this.categories = categories; - } - @Override - public void addSetting(List category, String group, Setting setting, Class cl) { - classMap.put(setting.valueProperty(), cl); - var foundCat = categories.stream() - .filter(c -> c.getDescription().equals(category.get(0))) - .findAny(); - var usedCat = foundCat.orElse(null); - var index = categories.indexOf(usedCat); - if (foundCat.isEmpty()) { - usedCat = Category.of(category.get(0), Group.of()); - categories.add(usedCat); - } - - var foundGroup = usedCat.getGroups().stream() - .filter(g -> - g.getDescription() != null && g.getDescription().equals(group)) - .findAny(); - var usedGroup = foundGroup.orElse(null); - if (foundGroup.isEmpty()) { - categories.remove(usedCat); - usedGroup = Group.of(group); - var modCatGroups = new ArrayList<>(usedCat.getGroups()); - modCatGroups.add(usedGroup); - usedCat = Category.of(usedCat.getDescription(), modCatGroups.toArray(Group[]::new)); - } - - var modGroupSettings = new ArrayList<>(usedGroup.getSettings()); - modGroupSettings.add(setting); - var newGroup = Group.of(usedGroup.getDescription(), modGroupSettings.toArray(Setting[]::new)); - var modCatGroups = new ArrayList<>(usedCat.getGroups()); - modCatGroups.removeIf( - g -> g.getDescription() != null && g.getDescription().equals(group)); - modCatGroups.add(newGroup); - var newCategory = Category.of(usedCat.getDescription(), modCatGroups.toArray(Group[]::new)); - categories.remove(usedCat); - categories.add(index, newCategory); + public void addSetting(String id, Class c, Property property, Comp comp) { + var m = new Mapping(id, property, c); + customEntries.put(m,comp); + mapping.add(m); } - } } diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefsCategory.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefsCategory.java index 524768ffb..03696a991 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AppPrefsCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefsCategory.java @@ -1,26 +1,10 @@ package io.xpipe.app.prefs; -import com.dlsc.formsfx.model.structure.Element; -import com.dlsc.preferencesfx.model.Category; -import com.dlsc.preferencesfx.model.Setting; import io.xpipe.app.fxcomps.Comp; -import javafx.beans.property.Property; -import lombok.SneakyThrows; public abstract class AppPrefsCategory { - @SneakyThrows - public static Setting lazyNode(String name, Comp comp, Property property) { - var ctr = Setting.class.getDeclaredConstructor(String.class, Element.class, Property.class); - ctr.setAccessible(true); - return ctr.newInstance(name, new LazyNodeElement<>(() -> comp.createRegion()), property); - } + protected abstract String getId(); - protected final AppPrefs prefs; - - public AppPrefsCategory(AppPrefs prefs) { - this.prefs = prefs; - } - - protected abstract Category create(); + protected abstract Comp create(); } diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java new file mode 100644 index 000000000..8fb06b585 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java @@ -0,0 +1,52 @@ +package io.xpipe.app.prefs; + +import io.xpipe.app.core.AppFont; +import io.xpipe.app.fxcomps.SimpleComp; +import io.xpipe.app.fxcomps.util.PlatformThread; +import io.xpipe.app.fxcomps.util.SimpleChangeListener; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; + +import java.util.stream.Collectors; + +public class AppPrefsComp extends SimpleComp { + + @Override + protected Region createSimple() { + var map = AppPrefs.get().getCategories().stream() + .collect(Collectors.toMap(appPrefsCategory -> appPrefsCategory, appPrefsCategory -> { + return appPrefsCategory.create().maxWidth(700).padding(new Insets(40, 40, 20, 40)).styleClass("prefs-container").createRegion(); + })); + var pfxSp = new ScrollPane(); + SimpleChangeListener.apply(AppPrefs.get().getSelectedCategory(), val -> { + PlatformThread.runLaterIfNeeded(() -> { + pfxSp.setContent(map.get(val)); + }); + }); + AppPrefs.get().getSelectedCategory().addListener((observable, oldValue, newValue) -> { + pfxSp.setVvalue(0); + }); + pfxSp.setFitToWidth(true); + var pfxLimit = new StackPane(pfxSp); + pfxLimit.setAlignment(Pos.TOP_LEFT); + + var sidebar = new AppPrefsSidebarComp().createRegion(); + sidebar.setMinWidth(350); + sidebar.setPrefWidth(350); + sidebar.setMaxWidth(350); + + var split = new HBox(sidebar, pfxLimit); + HBox.setHgrow(pfxLimit, Priority.ALWAYS); + split.setFillHeight(true); + split.getStyleClass().add("prefs"); + var stack = new StackPane(split); + stack.setPickOnBounds(false); + AppFont.medium(stack); + return stack; + } +} diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefsSidebarComp.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefsSidebarComp.java new file mode 100644 index 000000000..8dd521c98 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefsSidebarComp.java @@ -0,0 +1,46 @@ +package io.xpipe.app.prefs; + +import io.xpipe.app.comp.base.ButtonComp; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.SimpleComp; +import io.xpipe.app.fxcomps.impl.VerticalComp; +import io.xpipe.app.fxcomps.util.PlatformThread; +import io.xpipe.app.fxcomps.util.SimpleChangeListener; +import javafx.css.PseudoClass; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.layout.Region; +import javafx.scene.text.TextAlignment; + +public class AppPrefsSidebarComp extends SimpleComp { + + private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected"); + + @Override + protected Region createSimple() { + var buttons = AppPrefs.get().getCategories().stream().>map(appPrefsCategory -> { + return new ButtonComp(AppI18n.observable(appPrefsCategory.getId()), () -> { + AppPrefs.get().getSelectedCategory().setValue(appPrefsCategory); + }).apply(struc -> { + struc.get().setTextAlignment(TextAlignment.LEFT); + struc.get().setAlignment(Pos.CENTER_LEFT); + SimpleChangeListener.apply(AppPrefs.get().getSelectedCategory(), val -> { + struc.get().pseudoClassStateChanged(SELECTED,appPrefsCategory.equals(val)); + }); + }).grow(true, false); + }).toList(); + var vbox = new VerticalComp(buttons).styleClass("sidebar"); + vbox.apply(struc -> { + SimpleChangeListener.apply(PlatformThread.sync(AppPrefs.get().getSelectedCategory()), val -> { + var index = val != null ? AppPrefs.get().getCategories().indexOf(val) : 0; + if (index >= struc.get().getChildren().size()) { + return; + } + + ((Button) struc.get().getChildren().get(index)).fire(); + }); + }); + return vbox.createRegion(); + } +} diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefsStorageHandler.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefsStorageHandler.java new file mode 100644 index 000000000..1e6f06c71 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefsStorageHandler.java @@ -0,0 +1,90 @@ +package io.xpipe.app.prefs; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import io.xpipe.app.ext.PrefsChoiceValue; +import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.issue.TrackEvent; +import io.xpipe.app.util.JsonConfigHelper; +import io.xpipe.core.util.JacksonMapper; +import org.apache.commons.io.FileUtils; + +import java.nio.file.Path; + +import static io.xpipe.app.ext.PrefsChoiceValue.getAll; +import static io.xpipe.app.ext.PrefsChoiceValue.getSupported; + +public class AppPrefsStorageHandler { + + private final Path file; + private ObjectNode content; + + public AppPrefsStorageHandler(Path file) {this.file = file;} + + private JsonNode getContent(String key) { + if (content == null) { + content = JsonConfigHelper.readConfigObject(file); + } + return content.get(key); + } + + private void setContent(String key, JsonNode value) { + content.set(key, value); + } + + void save() { + JsonConfigHelper.writeConfig(file, content); + } + + public void updateObject(String key, Object object) { + var tree = object instanceof PrefsChoiceValue prefsChoiceValue + ? new TextNode(prefsChoiceValue.getId()) + : (object != null ? JacksonMapper.getDefault().valueToTree(object) : NullNode.getInstance()); + setContent(key, tree); + } + + @SuppressWarnings("unchecked") + public T loadObject(String id, Class type, T defaultObject) { + var tree = getContent(id); + if (tree == null) { + TrackEvent.debug("Preferences value not found for key: " + id); + return defaultObject; + } + + if (PrefsChoiceValue.class.isAssignableFrom(type)) { + var all = getAll(type); + if (all != null) { + Class cast = (Class) type; + var in = tree.asText(); + var found = all.stream().filter(t -> ((PrefsChoiceValue) t).getId().equalsIgnoreCase(in)).findAny(); + if (found.isEmpty()) { + TrackEvent.withWarn("Invalid prefs value found").tag("key", id).tag("value", in).handle(); + return defaultObject; + } + + var supported = getSupported(cast); + if (!supported.contains(found.get())) { + TrackEvent.withWarn("Unsupported prefs value found").tag("key", id).tag("value", in).handle(); + return defaultObject; + } + + TrackEvent.debug("Loading preferences value for key " + id + " from value " + found.get()); + return found.get(); + } + } + + try { + TrackEvent.debug("Loading preferences value for key " + id + " from value " + tree); + return JacksonMapper.getDefault().treeToValue(tree, type); + } catch (Exception ex) { + ErrorEvent.fromThrowable(ex).omit().handle(); + return defaultObject; + } + } + + public boolean clear() { + return FileUtils.deleteQuietly(file.toFile()); + } +} diff --git a/app/src/main/java/io/xpipe/app/prefs/AppearanceCategory.java b/app/src/main/java/io/xpipe/app/prefs/AppearanceCategory.java new file mode 100644 index 000000000..0c5f6e065 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/AppearanceCategory.java @@ -0,0 +1,51 @@ +package io.xpipe.app.prefs; + +import atlantafx.base.controls.ProgressSliderSkin; +import atlantafx.base.theme.Styles; +import io.xpipe.app.core.AppTheme; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.impl.ChoiceComp; +import io.xpipe.app.fxcomps.impl.IntFieldComp; +import io.xpipe.app.util.OptionsBuilder; +import javafx.scene.control.Slider; + +public class AppearanceCategory extends AppPrefsCategory { + + @Override + protected String getId() { + return "appearance"; + } + + @Override + protected Comp create() { + var prefs = AppPrefs.get(); + return new OptionsBuilder() + .addTitle("uiOptions") + .sub(new OptionsBuilder() + .nameAndDescription("theme") + .addComp(ChoiceComp.ofTranslatable(prefs.theme, AppTheme.Theme.ALL, false), prefs.theme) + .nameAndDescription("performanceMode") + .addToggle(prefs.performanceMode) + .nameAndDescription("uiScale") + .addComp(new IntFieldComp(prefs.uiScale).maxWidth(100), prefs.uiScale) + .nameAndDescription("useSystemFont") + .addToggle(prefs.useSystemFont) + .nameAndDescription("condenseConnectionDisplay") + .addToggle(prefs.condenseConnectionDisplay) + ).addTitle("windowOptions").sub(new OptionsBuilder() + .nameAndDescription("windowOpacity") + .addComp(Comp.of(() -> { + var s = new Slider(0.3, 1.0, prefs.windowOpacity.get()); + s.getStyleClass().add(Styles.SMALL); + prefs.windowOpacity.bind(s.valueProperty()); + s.setSkin(new ProgressSliderSkin(s)); + return s; + }), prefs.windowOpacity) + .nameAndDescription("saveWindowLocation") + .addToggle(prefs.saveWindowLocation) + .nameAndDescription("enforceWindowModality") + .addToggle(prefs.enforceWindowModality) + ) + .buildComp(); + } +} diff --git a/app/src/main/java/io/xpipe/app/prefs/CloseBehaviourAlert.java b/app/src/main/java/io/xpipe/app/prefs/CloseBehaviourAlert.java index a5d16375a..645cc1073 100644 --- a/app/src/main/java/io/xpipe/app/prefs/CloseBehaviourAlert.java +++ b/app/src/main/java/io/xpipe/app/prefs/CloseBehaviourAlert.java @@ -35,7 +35,7 @@ public class CloseBehaviourAlert { var vb = new VBox(); vb.setSpacing(7); for (var cb : PrefsChoiceValue.getSupported(CloseBehaviour.class)) { - RadioButton rb = new RadioButton(cb.toTranslatedString()); + RadioButton rb = new RadioButton(cb.toTranslatedString().getValue()); rb.setToggleGroup(group); rb.selectedProperty().addListener((c, o, n) -> { if (n) { diff --git a/app/src/main/java/io/xpipe/app/prefs/ConnectionsCategory.java b/app/src/main/java/io/xpipe/app/prefs/ConnectionsCategory.java new file mode 100644 index 000000000..121c94e55 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/ConnectionsCategory.java @@ -0,0 +1,32 @@ +package io.xpipe.app.prefs; + +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.impl.IntFieldComp; +import io.xpipe.app.util.OptionsBuilder; +import io.xpipe.core.process.OsType; +import javafx.beans.property.SimpleBooleanProperty; + +public class ConnectionsCategory extends AppPrefsCategory { + + @Override + protected String getId() { + return "connections"; + } + + @Override + protected Comp create() { + var prefs = AppPrefs.get(); + return new OptionsBuilder() + .addTitle("connections") + .sub(new OptionsBuilder() + .nameAndDescription("useBundledTools") + .addToggle(prefs.useBundledTools) + .hide(new SimpleBooleanProperty(!OsType.getLocal().equals(OsType.WINDOWS))) + .nameAndDescription("connectionTimeout") + .addComp(new IntFieldComp(prefs.connectionTimeOut).maxWidth(100), prefs.connectionTimeOut) + .nameAndDescription("useLocalFallbackShell") + .addToggle(prefs.useLocalFallbackShell) + ) + .buildComp(); + } +} diff --git a/app/src/main/java/io/xpipe/app/prefs/CustomCategoryView.java b/app/src/main/java/io/xpipe/app/prefs/CustomCategoryView.java deleted file mode 100644 index f7f404669..000000000 --- a/app/src/main/java/io/xpipe/app/prefs/CustomCategoryView.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.xpipe.app.prefs; - -import com.dlsc.formsfx.model.structure.Form; -import com.dlsc.preferencesfx.model.Category; -import com.dlsc.preferencesfx.model.PreferencesFxModel; -import com.dlsc.preferencesfx.view.CategoryView; - -public class CustomCategoryView extends CategoryView { - - public CustomCategoryView(PreferencesFxModel model, Category categoryModel) { - super(model, categoryModel); - } - - public void initializeFormRenderer(Form form) { - getChildren().clear(); - var preferencesFormRenderer = new CustomFormRenderer(form); - getChildren().add(preferencesFormRenderer); - } -} diff --git a/app/src/main/java/io/xpipe/app/prefs/CustomFormRenderer.java b/app/src/main/java/io/xpipe/app/prefs/CustomFormRenderer.java deleted file mode 100644 index 8532c2875..000000000 --- a/app/src/main/java/io/xpipe/app/prefs/CustomFormRenderer.java +++ /dev/null @@ -1,155 +0,0 @@ -package io.xpipe.app.prefs; - -import com.dlsc.formsfx.model.structure.Element; -import com.dlsc.formsfx.model.structure.Field; -import com.dlsc.formsfx.model.structure.Form; -import com.dlsc.preferencesfx.formsfx.view.controls.SimpleControl; -import com.dlsc.preferencesfx.formsfx.view.renderer.PreferencesFxFormRenderer; -import com.dlsc.preferencesfx.formsfx.view.renderer.PreferencesFxGroup; -import com.dlsc.preferencesfx.formsfx.view.renderer.PreferencesFxGroupRenderer; -import com.dlsc.preferencesfx.util.PreferencesFxUtils; -import io.xpipe.app.core.AppFont; -import io.xpipe.app.core.AppI18n; -import io.xpipe.app.fxcomps.util.BindingsHelper; -import javafx.beans.binding.Bindings; -import javafx.geometry.Insets; -import javafx.scene.control.Label; -import javafx.scene.layout.GridPane; -import javafx.scene.layout.Region; - -import java.util.List; -import java.util.stream.Collectors; - -public class CustomFormRenderer extends PreferencesFxFormRenderer { - - public static final double SPACING = 8.0; - - public CustomFormRenderer(Form form) { - super(form); - } - - @Override - public void initializeParts() { - groups = form.getGroups().stream() - .map(group -> new PreferencesFxGroupRenderer((PreferencesFxGroup) group, this) { - - @Override - public void initializeParts() { - super.initializeParts(); - grid.getStyleClass().add("grid"); - } - - @Override - @SuppressWarnings({"rawtypes", "unchecked"}) - public void layoutParts() { - StringBuilder styleClass = new StringBuilder("group"); - - // if there are no rows yet, getRowCount returns -1, in this case the next row is 0 - int nextRow = PreferencesFxUtils.getRowCount(grid) + 1; - - List elements = preferencesGroup.getElements().stream() - .map(Element.class::cast) - .toList(); - - // Only when the preferencesGroup has a title - if (preferencesGroup.getTitle() != null && elements.size() > 0) { - titleLabel.setPrefWidth(USE_COMPUTED_SIZE); - grid.add(titleLabel, 0, nextRow++); - titleLabel.getStyleClass().add("group-title"); - AppFont.setSize(titleLabel, 2); - // Set margin for all but first group titles to visually separate groups - if (nextRow > 1) { - GridPane.setMargin(titleLabel, new Insets(SPACING * 5, 0, SPACING, 0)); - } else { - GridPane.setMargin(titleLabel, new Insets(SPACING, 0, SPACING, 0)); - } - } - - styleClass.append("-setting"); - - int rowAmount = nextRow; - for (int i = 0; i < elements.size(); i++) { - // add to GridPane - Element element = elements.get(i); - var offset = preferencesGroup.getTitle() != null ? 15 : 0; - if (element instanceof Field f) { - SimpleControl c = (SimpleControl) f.getRenderer(); - c.setField(f); - AppFont.header(c.getFieldLabel()); - c.getFieldLabel().textProperty().unbind(); - c.getFieldLabel().textProperty().bind(Bindings.createStringBinding(() -> { - return f.labelProperty().get() + (AppPrefs.get().getProRequiredSettings().contains(f) ? " (Pro)" : ""); - }, f.labelProperty())); - grid.add(c.getFieldLabel(), 0, i + rowAmount); - - var canFocus = BindingsHelper.persist( - c.getNode().disabledProperty().not()); - - var descriptionLabel = new Label(); - descriptionLabel.setMaxWidth(600); - AppFont.medium(descriptionLabel); - descriptionLabel.setWrapText(true); - descriptionLabel - .disableProperty() - .bind(c.getFieldLabel().disabledProperty()); - descriptionLabel - .opacityProperty() - .bind(c.getFieldLabel() - .opacityProperty() - .multiply(0.65)); - descriptionLabel - .managedProperty() - .bind(c.getFieldLabel().managedProperty()); - descriptionLabel - .visibleProperty() - .bind(c.getFieldLabel().visibleProperty()); - - var descriptionKey = f.getLabel() != null ? f.getLabel() + "Description" : null; - if (AppI18n.getInstance().containsKey(descriptionKey)) { - rowAmount++; - descriptionLabel.textProperty().bind(AppI18n.observable(descriptionKey)); - descriptionLabel.focusTraversableProperty().bind(canFocus); - grid.add(descriptionLabel, 0, i + rowAmount); - } - - rowAmount++; - - var node = c.getNode(); - ((Region) node).setMaxWidth(250); - ((Region) node).setMinWidth(250); - AppFont.medium(c.getNode()); - c.getFieldLabel().focusTraversableProperty().bind(canFocus); - grid.add(node, 0, i + rowAmount); - - if (i == elements.size() - 1) { - // additional styling for the last setting - styleClass.append("-last"); - } - - GridPane.setMargin(descriptionLabel, new Insets(SPACING, 0, 0, offset)); - GridPane.setMargin(node, new Insets(SPACING, 0, 0, offset)); - - if (!((i == 0) && (nextRow > 0))) { - GridPane.setMargin(c.getFieldLabel(), new Insets(SPACING * 6, 0, 0, offset)); - } else { - GridPane.setMargin(c.getFieldLabel(), new Insets(SPACING, 0, 0, offset)); - } - - c.getFieldLabel().getStyleClass().add(styleClass + "-label"); - node.getStyleClass().add(styleClass + "-node"); - } - - if (element instanceof LazyNodeElement nodeElement) { - var node = nodeElement.getNode(); - if (node instanceof Region r) { - r.setMaxWidth(600); - } - grid.add(node, 0, i + rowAmount); - GridPane.setMargin(node, new Insets(SPACING, 0, 0, offset)); - } - } - } - }) - .collect(Collectors.toList()); - } -} diff --git a/app/src/main/java/io/xpipe/app/prefs/CustomToggleControl.java b/app/src/main/java/io/xpipe/app/prefs/CustomToggleControl.java deleted file mode 100644 index 1998a1503..000000000 --- a/app/src/main/java/io/xpipe/app/prefs/CustomToggleControl.java +++ /dev/null @@ -1,79 +0,0 @@ -package io.xpipe.app.prefs; - -import atlantafx.base.controls.ToggleSwitch; -import com.dlsc.formsfx.model.structure.BooleanField; -import com.dlsc.preferencesfx.formsfx.view.controls.SimpleControl; -import com.dlsc.preferencesfx.formsfx.view.controls.ToggleControl; -import com.dlsc.preferencesfx.util.VisibilityProperty; -import javafx.scene.control.Label; - -/** - * Displays a control for boolean values with a toggle from ControlsFX. - * - * @author François Martin - * @author Marco Sanfratello - */ -public class CustomToggleControl extends SimpleControl { - - /** - * Constructs a ToggleControl of {@link ToggleControl} type, with visibility condition. - * - * @param visibilityProperty property for control visibility of this element - * - * @return the constructed ToggleControl - */ - public static ToggleControl of(VisibilityProperty visibilityProperty) { - ToggleControl toggleControl = new ToggleControl(); - - toggleControl.setVisibilityProperty(visibilityProperty); - - return toggleControl; - } - - /** - * {@inheritDoc} - */ - @Override - public void initializeParts() { - super.initializeParts(); - fieldLabel = new Label(field.labelProperty().getValue()); - node = new atlantafx.base.controls.ToggleSwitch(); - node.getStyleClass().add("toggle-control"); - node.setSelected(field.getValue()); - } - - /** - * {@inheritDoc} - */ - @Override - public void layoutParts() {} - - /** - * {@inheritDoc} - */ - @Override - public void setupBindings() { - super.setupBindings(); - } - - /** - * {@inheritDoc} - */ - @Override - public void setupValueChangedListeners() { - super.setupValueChangedListeners(); - field.userInputProperty().addListener((observable, oldValue, newValue) -> { - node.setSelected(Boolean.parseBoolean(field.getUserInput())); - }); - } - - /** - * {@inheritDoc} - */ - @Override - public void setupEventHandlers() { - node.selectedProperty().addListener((observable, oldValue, newValue) -> { - field.userInputProperty().setValue(String.valueOf(newValue)); - }); - } -} diff --git a/app/src/main/java/io/xpipe/app/prefs/CustomValidators.java b/app/src/main/java/io/xpipe/app/prefs/CustomValidators.java deleted file mode 100644 index 1c920a46f..000000000 --- a/app/src/main/java/io/xpipe/app/prefs/CustomValidators.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.xpipe.app.prefs; - -import com.dlsc.formsfx.model.validators.CustomValidator; -import com.dlsc.formsfx.model.validators.Validator; -import io.xpipe.app.core.AppI18n; - -import java.nio.file.Files; -import java.nio.file.Path; - -public class CustomValidators { - - public static Validator absolutePath() { - return CustomValidator.forPredicate( - (String s) -> { - try { - var p = Path.of(s); - return p.isAbsolute(); - } catch (Exception ex) { - return false; - } - }, - AppI18n.get("notAnAbsolutePath")); - } - - public static Validator directory() { - return CustomValidator.forPredicate( - (String s) -> { - var p = Path.of(s); - return Files.exists(p) && Files.isDirectory(p); - }, - AppI18n.get("notADirectory")); - } -} diff --git a/app/src/main/java/io/xpipe/app/prefs/DeveloperCategory.java b/app/src/main/java/io/xpipe/app/prefs/DeveloperCategory.java index 69f795d24..1b7def71f 100644 --- a/app/src/main/java/io/xpipe/app/prefs/DeveloperCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/DeveloperCategory.java @@ -1,32 +1,32 @@ package io.xpipe.app.prefs; import atlantafx.base.theme.Styles; -import com.dlsc.preferencesfx.model.Category; -import com.dlsc.preferencesfx.model.Group; -import com.dlsc.preferencesfx.model.Setting; import io.xpipe.app.comp.base.ButtonComp; +import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.TextFieldComp; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.util.LocalShell; +import io.xpipe.app.util.OptionsBuilder; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.process.ProcessOutputException; import javafx.beans.property.SimpleStringProperty; import javafx.geometry.Insets; import javafx.geometry.Pos; -import lombok.SneakyThrows; import org.kordamp.ikonli.javafx.FontIcon; import java.util.List; public class DeveloperCategory extends AppPrefsCategory { - public DeveloperCategory(AppPrefs prefs) { - super(prefs); + @Override + protected String getId() { + return "developer"; } - @SneakyThrows - public Category create() { + @Override + protected Comp create() { + var prefs = AppPrefs.get(); var localCommand = new SimpleStringProperty(); Runnable test = () -> { prefs.save(); @@ -44,9 +44,7 @@ public class DeveloperCategory extends AppPrefsCategory { }); }; - var runLocalCommand = lazyNode( - "shellCommandTest", - new HorizontalComp(List.of( + var runLocalCommand = new HorizontalComp(List.of( new TextFieldComp(localCommand) .apply(struc -> struc.get().setPromptText("Local command")) .styleClass(Styles.LEFT_PILL) @@ -56,32 +54,17 @@ public class DeveloperCategory extends AppPrefsCategory { .grow(false, true))) .padding(new Insets(15, 0, 0, 0)) .apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)) - .apply(struc -> struc.get().setFillHeight(true)), - null); - return Category.of( - "developer", Group.of( - Setting.of( - "developerDisableUpdateVersionCheck", - prefs.developerDisableUpdateVersionCheckField, - prefs.developerDisableUpdateVersionCheck), - Setting.of( - "developerDisableGuiRestrictions", - prefs.developerDisableGuiRestrictionsField, - prefs.developerDisableGuiRestrictions), - Setting.of( - "developerDisableConnectorInstallationVersionCheck", - prefs.developerDisableConnectorInstallationVersionCheckField, - prefs.developerDisableConnectorInstallationVersionCheck), - Setting.of( - "developerShowHiddenEntries", - prefs.developerShowHiddenEntriesField, - prefs.developerShowHiddenEntries), - Setting.of( - "developerShowHiddenProviders", - prefs.developerShowHiddenProvidersField, - prefs.developerShowHiddenProviders)), - Group.of("shellCommandTest", - runLocalCommand) - ); + .apply(struc -> struc.get().setFillHeight(true)); + return new OptionsBuilder() + .addTitle("developer") + .sub(new OptionsBuilder() + .nameAndDescription("developerDisableUpdateVersionCheck") + .addToggle(prefs.developerDisableUpdateVersionCheck) + .nameAndDescription("developerDisableGuiRestrictions") + .addToggle(prefs.developerDisableGuiRestrictions) + .nameAndDescription("shellCommandTest") + .addComp(runLocalCommand) + ) + .buildComp(); } } diff --git a/app/src/main/java/io/xpipe/app/prefs/EditorCategory.java b/app/src/main/java/io/xpipe/app/prefs/EditorCategory.java new file mode 100644 index 000000000..920b14113 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/EditorCategory.java @@ -0,0 +1,46 @@ +package io.xpipe.app.prefs; + +import io.xpipe.app.comp.base.ButtonComp; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.ext.PrefsChoiceValue; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.impl.ChoiceComp; +import io.xpipe.app.fxcomps.impl.StackComp; +import io.xpipe.app.fxcomps.impl.TextFieldComp; +import io.xpipe.app.util.FileOpener; +import io.xpipe.app.util.OptionsBuilder; +import io.xpipe.app.util.ThreadHelper; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.util.List; + +public class EditorCategory extends AppPrefsCategory { + + @Override + protected String getId() { + return "editor"; + } + + @Override + protected Comp create() { + var prefs = AppPrefs.get(); + var terminalTest = new StackComp( + List.of(new ButtonComp(AppI18n.observable("test"), new FontIcon("mdi2p-play"), () -> { + prefs.save(); + ThreadHelper.runFailableAsync(() -> { + var editor = + AppPrefs.get().externalEditor().getValue(); + if (editor != null) { + FileOpener.openReadOnlyString("Test"); + } + }); + }) )).padding(new Insets(15, 0, 0, 0)).apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)); + return new OptionsBuilder().addTitle("editorConfiguration").sub(new OptionsBuilder().nameAndDescription("editorProgram").addComp( + ChoiceComp.ofTranslatable(prefs.externalEditor, PrefsChoiceValue.getSupported(ExternalEditorType.class), false)).nameAndDescription( + "customEditorCommand").addComp(new TextFieldComp(prefs.customEditorCommand, true).apply( + struc -> struc.get().setPromptText("myeditor $FILE")).hide(prefs.externalEditor.isNotEqualTo(ExternalEditorType.CUSTOM))).addComp( + terminalTest).nameAndDescription("preferEditorTabs").addToggle(prefs.preferEditorTabs)).buildComp(); + } +} diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java index a5155b6df..5db743563 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java @@ -2,11 +2,14 @@ package io.xpipe.app.prefs; import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.util.ApplicationHelper; import io.xpipe.app.util.LocalShell; +import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.OsType; import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellDialects; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Comparator; @@ -103,6 +106,32 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue { return false; } } + + protected void launch(String title, String args) throws Exception { + try (ShellControl pc = LocalShell.getShell()) { + if (!ApplicationHelper.isInPath(pc, executable)) { + throw ErrorEvent.unreportable( + new IOException( + "Executable " + executable + + " not found in PATH. Either add it to the PATH and refresh the environment by restarting XPipe, or specify an absolute executable path using the custom terminal setting.")); + } + + if (ShellDialects.isPowershell(pc)) { + var cmd = CommandBuilder.of().add("Start-Process", "-FilePath").addFile(executable).add("-ArgumentList") + .add(pc.getShellDialect().literalArgument(args)); + pc.executeSimpleCommand(cmd); + return; + } + + var toExecute = executable + " " + args; + if (pc.getOsType().equals(OsType.WINDOWS)) { + toExecute = "start \"" + title + "\" " + toExecute; + } else { + toExecute = "nohup " + toExecute + " /dev/null & disown"; + } + pc.executeSimpleCommand(toExecute); + } + } } public abstract static class WindowsType extends ExternalApplicationType { @@ -119,7 +148,7 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue { protected Optional determineFromPath() { // Try to locate if it is in the Path try (var cc = LocalShell.getShell() - .command(ShellDialects.getPlatformDefault().getWhichCommand(executable)) + .command(CommandBuilder.ofFunction(var1 -> var1.getShellDialect().getWhichCommand(executable))) .start()) { var out = cc.readStdoutDiscardErr(); var exit = cc.getExitCode(); diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java index 8ae784eee..dd86d463a 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java @@ -3,7 +3,6 @@ package io.xpipe.app.prefs; import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.util.ApplicationHelper; -import io.xpipe.app.util.LocalShell; import io.xpipe.app.util.WindowsRegistry; import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.OsType; @@ -18,14 +17,14 @@ import java.util.function.Supplier; public interface ExternalEditorType extends PrefsChoiceValue { - ExternalEditorType NOTEPAD = new WindowsType("app.notepad", "notepad") { + ExternalEditorType NOTEPAD = new WindowsType("app.notepad", "notepad", false) { @Override protected Optional determineInstallation() { return Optional.of(Path.of(System.getenv("SystemRoot") + "\\System32\\notepad.exe")); } }; - ExternalEditorType VSCODIUM_WINDOWS = new WindowsType("app.vscodium", "codium.cmd") { + ExternalEditorType VSCODIUM_WINDOWS = new WindowsType("app.vscodium", "codium.cmd", false) { @Override protected Optional determineInstallation() { @@ -35,14 +34,9 @@ public interface ExternalEditorType extends PrefsChoiceValue { .resolve("bin") .resolve("codium.cmd")); } - - @Override - public boolean detach() { - return false; - } }; - ExternalEditorType VSCODE_WINDOWS = new WindowsType("app.vscode", "code.cmd") { + ExternalEditorType VSCODE_WINDOWS = new WindowsType("app.vscode", "code.cmd", false) { @Override protected Optional determineInstallation() { @@ -52,14 +46,9 @@ public interface ExternalEditorType extends PrefsChoiceValue { .resolve("bin") .resolve("code.cmd")); } - - @Override - public boolean detach() { - return false; - } }; - ExternalEditorType VSCODE_INSIDERS_WINDOWS = new WindowsType("app.vscodeInsiders", "code-insiders.cmd") { + ExternalEditorType VSCODE_INSIDERS_WINDOWS = new WindowsType("app.vscodeInsiders", "code-insiders.cmd", false) { @Override protected Optional determineInstallation() { @@ -69,14 +58,9 @@ public interface ExternalEditorType extends PrefsChoiceValue { .resolve("bin") .resolve("code-insiders.cmd")); } - - @Override - public boolean detach() { - return false; - } }; - ExternalEditorType NOTEPADPLUSPLUS_WINDOWS = new WindowsType("app.notepad++", "notepad++") { + ExternalEditorType NOTEPADPLUSPLUS = new WindowsType("app.notepad++", "notepad++", false) { @Override protected Optional determineInstallation() { @@ -91,7 +75,7 @@ public interface ExternalEditorType extends PrefsChoiceValue { }; LinuxPathType VSCODE_LINUX = new LinuxPathType("app.vscode", "code"); - + LinuxPathType VSCODIUM_LINUX = new LinuxPathType("app.vscodium", "codium"); LinuxPathType GNOME = new LinuxPathType("app.gnomeTextEditor", "gnome-text-editor"); @@ -120,12 +104,7 @@ public interface ExternalEditorType extends PrefsChoiceValue { } ApplicationHelper.executeLocalApplication( - shellControl -> String.format( - "open -a %s %s", - shellControl - .getShellDialect() - .fileArgument(execFile.orElseThrow().toString()), - shellControl.getShellDialect().fileArgument(file.toString())), + CommandBuilder.of().add("open", "-a").addFile(execFile.orElseThrow().toString()).addFile(file.toString()), false); } } @@ -139,7 +118,7 @@ public interface ExternalEditorType extends PrefsChoiceValue { ExternalEditorType VSCODE_MACOS = new MacOsEditor("app.vscode", "Visual Studio Code"); ExternalEditorType VSCODIUM_MACOS = new MacOsEditor("app.vscodium", "VSCodium"); - + ExternalEditorType CUSTOM = new ExternalEditorType() { @Override @@ -150,7 +129,8 @@ public interface ExternalEditorType extends PrefsChoiceValue { } var format = customCommand.toLowerCase(Locale.ROOT).contains("$file") ? customCommand : customCommand + " $FILE"; - ApplicationHelper.executeLocalApplication(sc -> ApplicationHelper.replaceFileArgument(format, "FILE", file.toString()), true); + ApplicationHelper.executeLocalApplication( + CommandBuilder.of().add(ApplicationHelper.replaceFileArgument(format, "FILE", file.toString())), true); } @Override @@ -163,13 +143,16 @@ public interface ExternalEditorType extends PrefsChoiceValue { class GenericPathType extends ExternalApplicationType.PathApplication implements ExternalEditorType { - public GenericPathType(String id, String command) { + private final boolean detach; + + public GenericPathType(String id, String command, boolean detach) { super(id, command); + this.detach = detach; } @Override public void launch(Path file) throws Exception { - LocalShell.getShell().executeSimpleCommand(CommandBuilder.of().add(executable).addFile(file.toString())); + ApplicationHelper.executeLocalApplication(CommandBuilder.of().add(executable).addFile(file.toString()), detach); } @Override @@ -181,7 +164,7 @@ public interface ExternalEditorType extends PrefsChoiceValue { class LinuxPathType extends GenericPathType { public LinuxPathType(String id, String command) { - super(id, command); + super(id, command, true); } @Override @@ -193,15 +176,11 @@ public interface ExternalEditorType extends PrefsChoiceValue { abstract class WindowsType extends ExternalApplicationType.WindowsType implements ExternalEditorType { - private final String executable; + private final boolean detach; - public WindowsType(String id, String executable) { + public WindowsType(String id, String executable, boolean detach) { super(id, executable); - this.executable = executable; - } - - public boolean detach() { - return true; + this.detach = detach; } @Override @@ -216,22 +195,19 @@ public interface ExternalEditorType extends PrefsChoiceValue { Optional finalLocation = location; ApplicationHelper.executeLocalApplication( - sc -> String.format( - "%s %s", - sc.getShellDialect().fileArgument(finalLocation.get().toString()), - sc.getShellDialect().fileArgument(file.toString())), - detach()); + CommandBuilder.of().addFile(finalLocation.get().toString()).addFile(file.toString()), + detach); } } - ExternalEditorType FLEET = new GenericPathType("app.fleet", "fleet"); - ExternalEditorType INTELLIJ = new GenericPathType("app.intellij", "idea"); - ExternalEditorType PYCHARM = new GenericPathType("app.pycharm", "pycharm"); - ExternalEditorType WEBSTORM = new GenericPathType("app.webstorm", "webstorm"); - ExternalEditorType CLION = new GenericPathType("app.clion", "clion"); + ExternalEditorType FLEET = new GenericPathType("app.fleet", "fleet", false); + ExternalEditorType INTELLIJ = new GenericPathType("app.intellij", "idea", false); + ExternalEditorType PYCHARM = new GenericPathType("app.pycharm", "pycharm", false); + ExternalEditorType WEBSTORM = new GenericPathType("app.webstorm", "webstorm", false); + ExternalEditorType CLION = new GenericPathType("app.clion", "clion", false); - List WINDOWS_EDITORS = List.of(VSCODIUM_WINDOWS, VSCODE_INSIDERS_WINDOWS, VSCODE_WINDOWS, NOTEPADPLUSPLUS_WINDOWS, NOTEPAD); - List LINUX_EDITORS = List.of(ExternalEditorType.VSCODIUM_LINUX, VSCODE_LINUX, KATE, GEDIT, PLUMA, LEAFPAD, MOUSEPAD, GNOME); + List WINDOWS_EDITORS = List.of(VSCODIUM_WINDOWS, VSCODE_INSIDERS_WINDOWS, VSCODE_WINDOWS, NOTEPADPLUSPLUS, NOTEPAD); + List LINUX_EDITORS = List.of(VSCODIUM_LINUX, VSCODE_LINUX, KATE, GEDIT, PLUMA, LEAFPAD, MOUSEPAD, GNOME); List MACOS_EDITORS = List.of(BBEDIT, VSCODIUM_MACOS, VSCODE_MACOS, SUBLIME_MACOS, TEXT_EDIT); List CROSS_PLATFORM_EDITORS = List.of(FLEET, INTELLIJ, PYCHARM, WEBSTORM, CLION); @@ -258,7 +234,7 @@ public interface ExternalEditorType extends PrefsChoiceValue { var customProperty = AppPrefs.get().customEditorCommand; if (OsType.getLocal().equals(OsType.WINDOWS)) { typeProperty.set(WINDOWS_EDITORS.stream() - .filter(externalEditorType -> externalEditorType.isAvailable()) + .filter(PrefsChoiceValue::isAvailable) .findFirst() .orElse(null)); } @@ -278,7 +254,7 @@ public interface ExternalEditorType extends PrefsChoiceValue { } } else { typeProperty.set(LINUX_EDITORS.stream() - .filter(externalEditorType -> externalEditorType.isAvailable()) + .filter(ExternalApplicationType.PathApplication::isAvailable) .findFirst() .orElse(null)); } @@ -286,7 +262,7 @@ public interface ExternalEditorType extends PrefsChoiceValue { if (OsType.getLocal().equals(OsType.MACOS)) { typeProperty.set(MACOS_EDITORS.stream() - .filter(externalEditorType -> externalEditorType.isAvailable()) + .filter(PrefsChoiceValue::isAvailable) .findFirst() .orElse(null)); } diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java index 444309631..de31da2ae 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java @@ -4,67 +4,52 @@ import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.util.*; -import io.xpipe.core.process.CommandBuilder; -import io.xpipe.core.process.OsType; -import io.xpipe.core.process.ShellControl; +import io.xpipe.core.process.*; import io.xpipe.core.store.FileNames; -import io.xpipe.core.store.LocalStore; import lombok.Getter; import lombok.Value; +import lombok.With; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.List; -import java.util.Locale; -import java.util.Optional; -import java.util.stream.Stream; +import java.util.*; +import java.util.function.Supplier; public interface ExternalTerminalType extends PrefsChoiceValue { - ExternalTerminalType CMD = new PathType("app.cmd", "cmd.exe") { + ExternalTerminalType CMD = new SimplePathType("app.cmd", "cmd.exe") { @Override - public void launch(LaunchConfiguration configuration) throws Exception { - LocalShell.getShell() - .executeSimpleCommand(CommandBuilder.of() - .add("start") - .addQuoted(configuration.getTitle()) - .add("cmd", "/c") - .addFile(configuration.getScriptFile())); - } - - @Override - public boolean supportsColoredTitle() { + public boolean supportsTabs() { return false; } @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.WINDOWS); - } - }; + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + if (configuration.getScriptDialect().equals(CMD)) { + return CommandBuilder.of() + .add("/c") + .add(configuration.getScriptFile()); + } - ExternalTerminalType POWERSHELL_WINDOWS = new SimplePathType("app.powershell", "powershell") { - - @Override - public boolean supportsColoredTitle() { - return false; - } - - @Override - protected CommandBuilder toCommand(String name, String file) { return CommandBuilder.of() - .add("-ExecutionPolicy", "RemoteSigned", "-NoProfile", "-Command", "cmd", "/C", "'" + file + "'"); + .add("/c") + .add(configuration.getDialectLaunchCommand()); } @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.WINDOWS); + public boolean supportsColoredTitle() { + return false; } }; - ExternalTerminalType PWSH_WINDOWS = new SimplePathType("app.pwsh", "pwsh") { + ExternalTerminalType POWERSHELL = new SimplePathType("app.powershell", "powershell") { + + @Override + public boolean supportsTabs() { + return false; + } @Override public boolean supportsColoredTitle() { @@ -72,31 +57,62 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } @Override - protected CommandBuilder toCommand(String name, String file) { + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + if (configuration.getScriptDialect().equals(ShellDialects.POWERSHELL)) { + return CommandBuilder.of() + .add("-ExecutionPolicy", "Bypass") + .add("-File") + .add(configuration.getScriptFile()); + } + + return CommandBuilder.of() + .add("-Command") + .add(configuration.getDialectLaunchCommand()); + } + }; + + ExternalTerminalType PWSH = new SimplePathType("app.pwsh", "pwsh") { + + @Override + public boolean supportsTabs() { + return false; + } + + @Override + public boolean supportsColoredTitle() { + return false; + } + + @Override + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + if (configuration.getScriptDialect().equals(ShellDialects.POWERSHELL_CORE)) { + return CommandBuilder.of() + .add("-ExecutionPolicy", "Bypass") + .add("-File") + .add(configuration.getScriptFile()); + } + // Fix for https://github.com/PowerShell/PowerShell/issues/18530#issuecomment-1325691850 + var script = ScriptHelper.createLocalExecScript("set \"PSModulePath=\"\r\n& \"" + configuration.getScriptFile() + "\""); return CommandBuilder.of() - .add("-ExecutionPolicy", "RemoteSigned", "-NoProfile", "-Command", "cmd", "/C") - .add(sc -> { - var script = - ScriptHelper.createLocalExecScript("set \"PSModulePath=\"\r\n\"" + file + "\"\npause"); - return "'" + script + "'"; - }); - } - - @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.WINDOWS); + .add("-Command") + .add(configuration.withScriptFile(script).getDialectLaunchCommand()); } }; ExternalTerminalType WINDOWS_TERMINAL_PREVIEW = new ExternalTerminalType() { + @Override + public boolean supportsTabs() { + return true; + } + @Override public void launch(LaunchConfiguration configuration) throws Exception { // A weird behavior in Windows Terminal causes the trailing // backslash of a filepath to escape the closing quote in the title argument // So just remove that slash - var fixedName = FileNames.removeTrailingSlash(configuration.getTitle()); + var fixedName = FileNames.removeTrailingSlash(configuration.getColoredTitle()); LocalShell.getShell() .executeSimpleCommand(CommandBuilder.of() .addFile(getPath().toString()) @@ -115,39 +131,41 @@ public interface ExternalTerminalType extends PrefsChoiceValue { return Files.exists(getPath()); } - @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.WINDOWS); - } - @Override public String getId() { return "app.windowsTerminalPreview"; } }; - ExternalTerminalType WINDOWS_TERMINAL = new PathType("app.windowsTerminal", "wt.exe") { + ExternalTerminalType WINDOWS_TERMINAL = new SimplePathType("app.windowsTerminal", "wt.exe") { @Override - public void launch(LaunchConfiguration configuration) throws Exception { + public boolean supportsTabs() { + return true; + } + + @Override + protected CommandBuilder toCommand(LaunchConfiguration configuration) { // A weird behavior in Windows Terminal causes the trailing // backslash of a filepath to escape the closing quote in the title argument // So just remove that slash - var fixedName = FileNames.removeTrailingSlash(configuration.getTitle()); - LocalShell.getShell() - .executeSimpleCommand(CommandBuilder.of() - .add("wt", "-w", "1", "nt", "--title") + var fixedName = FileNames.removeTrailingSlash(configuration.getColoredTitle()); + var toExec = !ShellDialects.isPowershell(LocalShell.getShell()) ? + CommandBuilder.of().addFile(configuration.getScriptFile()) : + CommandBuilder.of().add("powershell", "-ExecutionPolicy", "Bypass", "-File").addQuoted(configuration.getScriptFile()); + return CommandBuilder.of() + .add("-w", "1", "nt", "--title") .addQuoted(fixedName) - .addFile(configuration.getScriptFile())); - } - - @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.WINDOWS); + .add(toExec); } }; - ExternalTerminalType ALACRITTY_WINDOWS = new PathType("app.alacrittyWindows", "alacritty") { + ExternalTerminalType ALACRITTY_WINDOWS = new SimplePathType("app.alacritty", "alacritty") { + + @Override + public boolean supportsTabs() { + return false; + } @Override public boolean supportsColoredTitle() { @@ -155,25 +173,18 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } @Override - public void launch(LaunchConfiguration configuration) throws Exception { - var b = CommandBuilder.of().add("alacritty"); + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + var b = CommandBuilder.of(); if (configuration.getColor() != null) { b.add("-o") .addQuoted("colors.primary.background='%s'" .formatted(configuration.getColor().toHexString())); } - LocalShell.getShell() - .executeSimpleCommand(b.add("-t") - .addQuoted(configuration.getTitle()) + return b.add("-t").addQuoted(configuration.getCleanTitle()) .add("-e") .add("cmd") .add("/c") - .addQuoted(configuration.getScriptFile().replaceAll(" ", "^$0"))); - } - - @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.WINDOWS); + .addQuoted(configuration.getScriptFile().replaceAll(" ", "^$0")); } }; @@ -199,13 +210,17 @@ public interface ExternalTerminalType extends PrefsChoiceValue { protected abstract void execute(Path file, LaunchConfiguration configuration) throws Exception; } - ExternalTerminalType TABBY_WINDOWS = new WindowsType("app.tabbyWindows", "Tabby") { + ExternalTerminalType TABBY_WINDOWS = new WindowsType("app.tabby", "Tabby") { + + @Override + public boolean supportsTabs() { + return true; + } @Override protected void execute(Path file, LaunchConfiguration configuration) throws Exception { ApplicationHelper.executeLocalApplication( - shellControl -> shellControl.getShellDialect().fileArgument(file.toString()) + " run " - + shellControl.getShellDialect().fileArgument(configuration.getScriptFile()), + CommandBuilder.of().addFile(file.toString()).add("run").addFile(configuration.getScriptFile()), true); } @@ -229,12 +244,18 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } }; - ExternalTerminalType WEZ_WINDOWS = new WindowsType("app.wezWindows", "wezterm-gui") { + ExternalTerminalType WEZ_WINDOWS = new WindowsType("app.wezterm", "wezterm-gui") { + + @Override + public boolean supportsTabs() { + return false; + } @Override protected void execute(Path file, LaunchConfiguration configuration) throws Exception { - new LocalStore().control().command(CommandBuilder.of().addFile(file.toString()).add("start") - .addFile(configuration.getScriptFile())).execute(); + ApplicationHelper.executeLocalApplication( + CommandBuilder.of().addFile(file.toString()).add("start").addFile(configuration.getScriptFile()), + true); } @Override @@ -249,49 +270,34 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } }; - ExternalTerminalType WEZ_LINUX = new SimplePathType("app.wezLinux", "wezterm-gui") { + ExternalTerminalType WEZ_LINUX = new SimplePathType("app.wezterm", "wezterm-gui") { @Override - protected CommandBuilder toCommand(String name, String file) { - return CommandBuilder.of().add("start").addFile(file); + public boolean supportsTabs() { + return false; } @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.LINUX); + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + return CommandBuilder.of().add("start").addFile(configuration.getScriptFile()); } }; - // ExternalTerminalType HYPER_WINDOWS = new WindowsFullPathType("app.hyperWindows") { - // - // @Override - // protected String createCommand(ShellControl shellControl, String name, String path, String file) { - // return shellControl.getShellDialect().fileArgument(path) + " " - // + shellControl.getShellDialect().fileArgument(file); - // } - // - // @Override - // protected Optional determinePath() { - // Optional launcherDir; - // launcherDir = WindowsRegistry.readString( - // WindowsRegistry.HKEY_CURRENT_USER, - // "SOFTWARE\\ac619139-e2f9-5cb9-915f-69b22e7bff50", - // "InstallLocation") - // .map(p -> p + "\\Hyper.exe"); - // return launcherDir.map(Path::of); - // } - // }; + ExternalTerminalType GNOME_TERMINAL = new PathCheckType("app.gnomeTerminal", "gnome-terminal") { - ExternalTerminalType GNOME_TERMINAL = new PathType("app.gnomeTerminal", "gnome-terminal") { + @Override + public boolean supportsTabs() { + return false; + } @Override public void launch(LaunchConfiguration configuration) throws Exception { try (ShellControl pc = LocalShell.getShell()) { - ApplicationHelper.checkIsInPath(pc, executable, toTranslatedString(), null); + ApplicationHelper.checkIsInPath(pc, executable, toTranslatedString().getValue(), null); var toExecute = CommandBuilder.of() .add(executable, "-v", "--title") - .addQuoted(configuration.getTitle()) + .addQuoted(configuration.getColoredTitle()) .add("--") .addFile(configuration.getScriptFile()) .buildString(pc); @@ -301,206 +307,206 @@ public interface ExternalTerminalType extends PrefsChoiceValue { pc.executeSimpleCommand(toExecute); } } - - @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.LINUX); - } }; ExternalTerminalType KONSOLE = new SimplePathType("app.konsole", "konsole") { + @Override + public boolean supportsTabs() { + return true; + } + @Override public boolean supportsColoredTitle() { return false; } @Override - protected CommandBuilder toCommand(String name, String file) { + protected CommandBuilder toCommand(LaunchConfiguration configuration) { // Note for later: When debugging konsole launches, it will always open as a child process of // IntelliJ/XPipe even though we try to detach it. // This is not the case for production where it works as expected - return CommandBuilder.of().add("--new-tab", "-e").addFile(file); - } - - @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.LINUX); + return CommandBuilder.of().add("--new-tab", "-e").addFile(configuration.getScriptFile()); } }; ExternalTerminalType XFCE = new SimplePathType("app.xfce", "xfce4-terminal") { @Override - protected CommandBuilder toCommand(String name, String file) { - return CommandBuilder.of() - .add("--tab", "--title") - .addQuoted(name) - .add("--command") - .addFile(file); + public boolean supportsTabs() { + return true; } @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.LINUX); + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + return CommandBuilder.of() + .add("--tab", "--title") + .addQuoted(configuration.getColoredTitle()) + .add("--command") + .addFile(configuration.getScriptFile()); } }; ExternalTerminalType ELEMENTARY = new SimplePathType("app.elementaryTerminal", "io.elementary.terminal") { @Override - protected CommandBuilder toCommand(String name, String file) { - return CommandBuilder.of().add("--new-tab").add("-e").addFile(file); + public boolean supportsTabs() { + return true; } @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.LINUX); + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + return CommandBuilder.of().add("--new-tab").add("-e").addFile(configuration.getColoredTitle()); } }; ExternalTerminalType TILIX = new SimplePathType("app.tilix", "tilix") { @Override - protected CommandBuilder toCommand(String name, String file) { - return CommandBuilder.of().add("-t").addQuoted(name).add("-e").addFile(file); + public boolean supportsTabs() { + return false; } @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.LINUX); + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + return CommandBuilder.of().add("-t").addQuoted(configuration.getColoredTitle()).add("-e").addFile(configuration.getScriptFile()); } }; ExternalTerminalType TERMINATOR = new SimplePathType("app.terminator", "terminator") { @Override - protected CommandBuilder toCommand(String name, String file) { - return CommandBuilder.of() - .add("-e") - .addQuoted(file) - .add("-T") - .addQuoted(name) - .add("--new-tab"); + public boolean supportsTabs() { + return true; } @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.LINUX); + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + return CommandBuilder.of() + .add("-e") + .addQuoted(configuration.getScriptFile()) + .add("-T") + .addQuoted(configuration.getColoredTitle()) + .add("--new-tab"); } }; - ExternalTerminalType KITTY = new SimplePathType("app.kitty", "kitty") { + ExternalTerminalType KITTY_LINUX = new SimplePathType("app.kitty", "kitty") { @Override - protected CommandBuilder toCommand(String name, String file) { - return CommandBuilder.of().add("-1").add("-T").addQuoted(name).addQuoted(file); + public boolean supportsTabs() { + return false; } @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.LINUX); + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + return CommandBuilder.of().add("-1").add("-T").addQuoted(configuration.getColoredTitle()).addQuoted(configuration.getScriptFile()); } }; ExternalTerminalType TERMINOLOGY = new SimplePathType("app.terminology", "terminology") { @Override - protected CommandBuilder toCommand(String name, String file) { - return CommandBuilder.of() - .add("-T") - .addQuoted(name) - .add("-2") - .add("-e") - .addQuoted(file); + public boolean supportsTabs() { + return true; } @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.LINUX); + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + return CommandBuilder.of() + .add("-T") + .addQuoted(configuration.getColoredTitle()) + .add("-2") + .add("-e") + .addQuoted(configuration.getScriptFile()); } }; ExternalTerminalType COOL_RETRO_TERM = new SimplePathType("app.coolRetroTerm", "cool-retro-term") { @Override - protected CommandBuilder toCommand(String name, String file) { - return CommandBuilder.of().add("-T").addQuoted(name).add("-e").addQuoted(file); + public boolean supportsTabs() { + return false; } @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.LINUX); + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + return CommandBuilder.of().add("-T").addQuoted(configuration.getColoredTitle()).add("-e").addQuoted(configuration.getScriptFile()); } }; ExternalTerminalType GUAKE = new SimplePathType("app.guake", "guake") { @Override - protected CommandBuilder toCommand(String name, String file) { + public boolean supportsTabs() { + return true; + } + + @Override + protected CommandBuilder toCommand(LaunchConfiguration configuration) { return CommandBuilder.of() .add("-n", "~") .add("-r") - .addQuoted(name) + .addQuoted(configuration.getColoredTitle()) .add("-e") - .addQuoted(file); - } - - @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.LINUX); + .addQuoted(configuration.getScriptFile()); } }; - ExternalTerminalType ALACRITTY = new SimplePathType("app.alacritty", "alacritty") { + ExternalTerminalType ALACRITTY_LINUX = new SimplePathType("app.alacritty", "alacritty") { @Override - protected CommandBuilder toCommand(String name, String file) { - return CommandBuilder.of().add("-t").addQuoted(name).add("-e").addQuoted(file); + public boolean supportsTabs() { + return false; } @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.LINUX); + public boolean supportsColoredTitle() { + return false; + } + + @Override + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + return CommandBuilder.of().add("-t").addQuoted(configuration.getCleanTitle()).add("-e").addQuoted(configuration.getScriptFile()); } }; ExternalTerminalType TILDA = new SimplePathType("app.tilda", "tilda") { @Override - protected CommandBuilder toCommand(String name, String file) { - return CommandBuilder.of().add("-c").addQuoted(file); + public boolean supportsTabs() { + return true; } @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.LINUX); + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + return CommandBuilder.of().add("-c").addQuoted(configuration.getScriptFile()); } }; ExternalTerminalType XTERM = new SimplePathType("app.xterm", "xterm") { @Override - protected CommandBuilder toCommand(String name, String file) { - return CommandBuilder.of().add("-title").addQuoted(name).add("-e").addQuoted(file); + public boolean supportsTabs() { + return false; } @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.LINUX); + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + return CommandBuilder.of().add("-title").addQuoted(configuration.getColoredTitle()).add("-e").addQuoted(configuration.getScriptFile()); } }; ExternalTerminalType DEEPIN_TERMINAL = new SimplePathType("app.deepinTerminal", "deepin-terminal") { @Override - protected CommandBuilder toCommand(String name, String file) { - return CommandBuilder.of().add("-C").addQuoted(file); + public boolean supportsTabs() { + return false; } @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.LINUX); + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + return CommandBuilder.of().add("-C").addQuoted(configuration.getScriptFile()); } }; @@ -508,25 +514,135 @@ public interface ExternalTerminalType extends PrefsChoiceValue { ExternalTerminalType Q_TERMINAL = new SimplePathType("app.qTerminal", "qterminal") { @Override - protected CommandBuilder toCommand(String name, String file) { - return CommandBuilder.of().add("-e").addQuoted(file); + public boolean supportsTabs() { + return false; } @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.LINUX); + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + return CommandBuilder.of().add("-e").addQuoted(configuration.getColoredTitle()); } }; - ExternalTerminalType MACOS_TERMINAL = new MacOsTerminalType(); + ExternalTerminalType MACOS_TERMINAL = new MacOsType("app.macosTerminal", "Terminal") { + @Override + public boolean supportsTabs() { + return false; + } - ExternalTerminalType ITERM2 = new ITerm2Type(); + @Override + public void launch(LaunchConfiguration configuration) throws Exception { + try (ShellControl pc = LocalShell.getShell()) { + var suffix = "\"" + configuration.getScriptFile().replaceAll("\"", "\\\\\"") + "\""; + pc.osascriptCommand(String.format( + """ + activate application "Terminal" + delay 1 + tell app "Terminal" to do script %s + """, + suffix)) + .execute(); + } + } + }; - ExternalTerminalType WARP = new WarpType(); + ExternalTerminalType ITERM2 = new MacOsType("app.iterm2", "iTerm") { + @Override + public boolean supportsTabs() { + return true; + } - ExternalTerminalType TABBY_MAC_OS = new TabbyMacOsType(); + @Override + public void launch(LaunchConfiguration configuration) throws Exception { + var app = this.getApplicationPath(); + if (app.isEmpty()) { + throw new IllegalStateException("iTerm installation not found"); + } - ExternalTerminalType ALACRITTY_MACOS = new MacOsType("app.alacrittyMacOs", "Alacritty") { + try (ShellControl pc = LocalShell.getShell()) { + var a = app.get().toString(); + pc.osascriptCommand(String.format( + """ + if application "%s" is not running then + launch application "%s" + delay 1 + tell application "%s" + tell current tab of current window + close + end tell + end tell + end if + tell application "%s" + activate + create window with default profile command "%s" + end tell + """, + a, a, a, a, configuration.getScriptFile().replaceAll("\"", "\\\\\""))) + .execute(); + } + } + }; + + ExternalTerminalType WARP = new MacOsType("app.warp", "Warp") { + @Override + public boolean supportsTabs() { + return true; + } + + @Override + public boolean shouldClear() { + return false; + } + + @Override + public void launch(LaunchConfiguration configuration) throws Exception { + if (!MacOsPermissions.waitForAccessibilityPermissions()) { + return; + } + + try (ShellControl pc = LocalShell.getShell()) { + pc.osascriptCommand(String.format( + """ + tell application "Warp" to activate + tell application "System Events" to tell process "Warp" to keystroke "t" using command down + delay 1 + tell application "System Events" + tell process "Warp" + keystroke "%s" + delay 0.01 + key code 36 + end tell + end tell + """, + configuration.getScriptFile().replaceAll("\"", "\\\\\""))) + .execute(); + } + } + }; + + ExternalTerminalType TABBY_MAC_OS = new MacOsType("app.tabby", "Tabby") { + @Override + public boolean supportsTabs() { + return true; + } + + @Override + public void launch(LaunchConfiguration configuration) throws Exception { + LocalShell.getShell() + .executeSimpleCommand(CommandBuilder.of() + .add("open", "-a") + .addQuoted("Tabby.app") + .add("-n", "--args", "run") + .addFile(configuration.getScriptFile())); + } + }; + + ExternalTerminalType ALACRITTY_MACOS = new MacOsType("app.alacritty", "Alacritty") { + + @Override + public boolean supportsTabs() { + return false; + } @Override public boolean supportsColoredTitle() { @@ -540,13 +656,18 @@ public interface ExternalTerminalType extends PrefsChoiceValue { .add("open", "-a") .addQuoted("Alacritty.app") .add("-n", "--args", "-t") - .addQuoted(configuration.getTitle()) + .addQuoted(configuration.getCleanTitle()) .add("-e") .addFile(configuration.getScriptFile())); } }; - ExternalTerminalType WEZ_MACOS = new MacOsType("app.wezMacOs", "WezTerm") { + ExternalTerminalType WEZ_MACOS = new MacOsType("app.wezterm", "WezTerm") { + + @Override + public boolean supportsTabs() { + return false; + } @Override public void launch(LaunchConfiguration configuration) throws Exception { @@ -560,7 +681,12 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } }; - ExternalTerminalType KITTY_MACOS = new MacOsType("app.kittyMacOs", "kitty") { + ExternalTerminalType KITTY_MACOS = new MacOsType("app.kitty", "kitty") { + + @Override + public boolean supportsTabs() { + return false; + } @Override public boolean supportsColoredTitle() { @@ -599,43 +725,56 @@ public interface ExternalTerminalType extends PrefsChoiceValue { ExternalTerminalType CUSTOM = new CustomType(); - List ALL = Stream.of( - TABBY_WINDOWS, - ALACRITTY_WINDOWS, - WEZ_WINDOWS, - WINDOWS_TERMINAL_PREVIEW, - WINDOWS_TERMINAL, - PWSH_WINDOWS, - POWERSHELL_WINDOWS, - CMD, - WEZ_LINUX, - KONSOLE, - XFCE, - ELEMENTARY, - GNOME_TERMINAL, - TILIX, - TERMINATOR, - KITTY, - TERMINOLOGY, - COOL_RETRO_TERM, - GUAKE, - ALACRITTY, - TILDA, - XTERM, - DEEPIN_TERMINAL, - Q_TERMINAL, - ITERM2, - TABBY_MAC_OS, - ALACRITTY_MACOS, - KITTY_MACOS, - WARP, - WEZ_MACOS, - MACOS_TERMINAL, - CUSTOM) - .filter(terminalType -> terminalType.isSelectable()) - .toList(); + List WINDOWS_TERMINALS = List.of( + TABBY_WINDOWS, + ALACRITTY_WINDOWS, + WEZ_WINDOWS, + WINDOWS_TERMINAL_PREVIEW, + WINDOWS_TERMINAL, PWSH, POWERSHELL, + CMD); + List LINUX_TERMINALS = List.of( + WEZ_LINUX, + KONSOLE, + XFCE, + ELEMENTARY, + GNOME_TERMINAL, + TILIX, + TERMINATOR, KITTY_LINUX, + TERMINOLOGY, + COOL_RETRO_TERM, + GUAKE, ALACRITTY_LINUX, + TILDA, + XTERM, + DEEPIN_TERMINAL, + Q_TERMINAL); + List MACOS_TERMINALS = List.of( + ITERM2, + TABBY_MAC_OS, + ALACRITTY_MACOS, + KITTY_MACOS, + WARP, + WEZ_MACOS, + MACOS_TERMINAL); - static ExternalTerminalType getDefault() { + @SuppressWarnings("TrivialFunctionalExpressionUsage") + List ALL = ((Supplier>) () -> { + var all = new ArrayList(); + if (OsType.getLocal().equals(OsType.WINDOWS)) { + all.addAll(WINDOWS_TERMINALS); + } + if (OsType.getLocal().equals(OsType.LINUX)) { + all.addAll(LINUX_TERMINALS); + } + if (OsType.getLocal().equals(OsType.MACOS)) { + all.addAll(MACOS_TERMINALS); + } + // Prefer with tabs + all.sort(Comparator.comparingInt(o -> (o.supportsTabs() ? -1 : 0))); + all.add(CUSTOM); + return all; + }).get(); + + static ExternalTerminalType determineDefault() { return ALL.stream() .filter(externalTerminalType -> !externalTerminalType.equals(CUSTOM)) .filter(terminalType -> terminalType.isAvailable()) @@ -645,45 +784,36 @@ public interface ExternalTerminalType extends PrefsChoiceValue { @Value class LaunchConfiguration { - DataStoreColor color; - String title; + String coloredTitle; String cleanTitle; + @With String scriptFile; + ShellDialect scriptDialect; + + public CommandBuilder getDialectLaunchCommand() { + var open = scriptDialect.getOpenScriptCommand(scriptFile); + return open; + } + + public CommandBuilder appendDialectLaunchCommand(CommandBuilder b) { + var open = getDialectLaunchCommand(); + b.add(open); + return b; + } } + boolean supportsTabs(); + default boolean supportsColoredTitle() { return true; } - default boolean shouldClear() { return true; } default void launch(LaunchConfiguration configuration) throws Exception {} - class MacOsTerminalType extends ExternalApplicationType.MacApplication implements ExternalTerminalType { - - public MacOsTerminalType() { - super("app.macosTerminal", "Terminal"); - } - - @Override - public void launch(LaunchConfiguration configuration) throws Exception { - try (ShellControl pc = LocalShell.getShell()) { - var suffix = "\"" + configuration.getScriptFile().replaceAll("\"", "\\\\\"") + "\""; - pc.osascriptCommand(String.format( - """ - activate application "Terminal" - delay 1 - tell app "Terminal" to do script %s - """, - suffix)) - .execute(); - } - } - } - class CustomType extends ExternalApplicationType implements ExternalTerminalType { public CustomType() { @@ -691,8 +821,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } @Override - public boolean supportsColoredTitle() { - return false; + public boolean supportsTabs() { + return true; } @Override @@ -707,7 +837,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { var toExecute = ApplicationHelper.replaceFileArgument(format, "CMD", configuration.getScriptFile()); // We can't be sure whether the command is blocking or not, so always make it not blocking if (pc.getOsType().equals(OsType.WINDOWS)) { - toExecute = "start \"" + configuration.getTitle() + "\" " + toExecute; + toExecute = "start \"" + configuration.getCleanTitle() + "\" " + toExecute; } else { toExecute = "nohup " + toExecute + " /dev/null & disown"; } @@ -726,97 +856,6 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } } - class ITerm2Type extends ExternalApplicationType.MacApplication implements ExternalTerminalType { - - public ITerm2Type() { - super("app.iterm2", "iTerm"); - } - - @Override - public void launch(LaunchConfiguration configuration) throws Exception { - var app = this.getApplicationPath(); - if (app.isEmpty()) { - throw new IllegalStateException("iTerm installation not found"); - } - - try (ShellControl pc = LocalShell.getShell()) { - var a = app.get().toString(); - pc.osascriptCommand(String.format( - """ - if application "%s" is not running then - launch application "%s" - delay 1 - tell application "%s" - tell current tab of current window - close - end tell - end tell - end if - tell application "%s" - activate - create window with default profile command "%s" - end tell - """, - a, a, a, a, configuration.getScriptFile().replaceAll("\"", "\\\\\""))) - .execute(); - } - } - } - - class TabbyMacOsType extends ExternalApplicationType.MacApplication implements ExternalTerminalType { - - public TabbyMacOsType() { - super("app.tabbyMacOs", "Tabby"); - } - - @Override - public void launch(LaunchConfiguration configuration) throws Exception { - LocalShell.getShell() - .executeSimpleCommand(CommandBuilder.of() - .add("open", "-a") - .addQuoted("Tabby.app") - .add("-n", "--args", "run") - .addFile(configuration.getScriptFile())); - } - } - - class WarpType extends ExternalApplicationType.MacApplication implements ExternalTerminalType { - - public WarpType() { - super("app.warp", "Warp"); - } - - @Override - public boolean shouldClear() { - return false; - } - - @Override - public void launch(LaunchConfiguration configuration) throws Exception { - if (!MacOsPermissions.waitForAccessibilityPermissions()) { - return; - } - - try (ShellControl pc = LocalShell.getShell()) { - pc.osascriptCommand(String.format( - """ - tell application "Warp" to activate - tell application "System Events" to tell process "Warp" to keystroke "t" using command down - delay 1 - tell application "System Events" - tell process "Warp" - keystroke "%s" - delay 0.01 - key code 36 - end tell - end tell - """, - configuration.getScriptFile().replaceAll("\"", "\\\\\""))) - .execute(); - } - } - } - abstract class MacOsType extends ExternalApplicationType.MacApplication implements ExternalTerminalType { public MacOsType(String id, String applicationName) { @@ -825,21 +864,12 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } @Getter - abstract class PathType extends ExternalApplicationType.PathApplication implements ExternalTerminalType { + abstract class PathCheckType extends ExternalApplicationType.PathApplication implements ExternalTerminalType { - public PathType(String id, String executable) { + public PathCheckType(String id, String executable) { super(id, executable); } - public boolean isAvailable() { - try (ShellControl pc = LocalShell.getShell()) { - return pc.executeSimpleBooleanCommand(pc.getShellDialect().getWhichCommand(executable)); - } catch (Exception e) { - ErrorEvent.fromThrowable(e).omit().handle(); - return false; - } - } - @Override public boolean isSelectable() { return true; @@ -847,7 +877,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } @Getter - abstract class SimplePathType extends PathType { + abstract class SimplePathType extends PathCheckType { public SimplePathType(String id, String executable) { super(id, executable); @@ -855,26 +885,10 @@ public interface ExternalTerminalType extends PrefsChoiceValue { @Override public void launch(LaunchConfiguration configuration) throws Exception { - try (ShellControl pc = LocalShell.getShell()) { - if (!ApplicationHelper.isInPath(pc, executable)) { - throw ErrorEvent.unreportable( - new IOException( - "Executable " + executable - + " not found in PATH. Either add it to the PATH and refresh the environment by restarting XPipe, or specify an absolute executable path using the custom terminal setting.")); - } - - var toExecute = executable + " " - + toCommand(configuration.getTitle(), configuration.getScriptFile()) - .buildString(pc); - if (pc.getOsType().equals(OsType.WINDOWS)) { - toExecute = "start \"" + configuration.getTitle() + "\" " + toExecute; - } else { - toExecute = "nohup " + toExecute + " /dev/null & disown"; - } - pc.executeSimpleCommand(toExecute); - } + var args = toCommand(configuration).buildCommandBase(LocalShell.getShell()); + launch(configuration.getColoredTitle(), args); } - protected abstract CommandBuilder toCommand(String name, String file); + protected abstract CommandBuilder toCommand(LaunchConfiguration configuration); } } diff --git a/app/src/main/java/io/xpipe/app/prefs/JsonStorageHandler.java b/app/src/main/java/io/xpipe/app/prefs/JsonStorageHandler.java deleted file mode 100644 index 13cce2a87..000000000 --- a/app/src/main/java/io/xpipe/app/prefs/JsonStorageHandler.java +++ /dev/null @@ -1,195 +0,0 @@ -package io.xpipe.app.prefs; - -import com.dlsc.preferencesfx.util.StorageHandler; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.NullNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; -import com.fasterxml.jackson.databind.type.CollectionType; -import io.xpipe.app.core.AppProperties; -import io.xpipe.app.ext.PrefsChoiceValue; -import io.xpipe.app.issue.ErrorEvent; -import io.xpipe.app.issue.TrackEvent; -import io.xpipe.app.util.JsonConfigHelper; -import io.xpipe.core.util.JacksonMapper; -import javafx.collections.ObservableList; -import org.apache.commons.io.FileUtils; - -import java.nio.file.Path; -import java.util.List; - -import static io.xpipe.app.ext.PrefsChoiceValue.getAll; -import static io.xpipe.app.ext.PrefsChoiceValue.getSupported; - -public class JsonStorageHandler implements StorageHandler { - - private final Path file = - AppProperties.get().getDataDir().resolve("settings").resolve("preferences.json"); - private ObjectNode content; - - private String getSaveId(String bc) { - return bc.split("#")[bc.split("#").length - 1]; - } - - private JsonNode getContent(String key) { - if (content == null) { - content = JsonConfigHelper.readConfigObject(file); - } - return content.get(key); - } - - private void setContent(String key, JsonNode value) { - content.set(key, value); - } - - void save() { - JsonConfigHelper.writeConfig(file, content); - } - - @Override - public void saveObject(String breadcrumb, Object object) { - var id = getSaveId(breadcrumb); - var tree = object instanceof PrefsChoiceValue prefsChoiceValue - ? new TextNode(prefsChoiceValue.getId()) - : (object != null ? JacksonMapper.getDefault().valueToTree(object) : NullNode.getInstance()); - setContent(id, tree); - } - - @Override - @SuppressWarnings("unchecked") - public Object loadObject(String breadcrumb, Object defaultObject) { - Class c = (Class) AppPrefs.get().getSettingType(breadcrumb); - return loadObject(breadcrumb, c, defaultObject); - } - - @Override - @SuppressWarnings("unchecked") - public T loadObject(String breadcrumb, Class type, T defaultObject) { - var id = getSaveId(breadcrumb); - var tree = getContent(id); - if (tree == null) { - TrackEvent.debug("Preferences value not found for key: " + breadcrumb); - return defaultObject; - } - - var all = getAll(type); - if (all != null) { - Class cast = (Class) type; - var in = tree.asText(); - var found = all.stream() - .filter(t -> ((PrefsChoiceValue) t).getId().equalsIgnoreCase(in)) - .findAny(); - if (found.isEmpty()) { - TrackEvent.withWarn("Invalid prefs value found") - .tag("key", id) - .tag("value", in) - .handle(); - return defaultObject; - } - - var supported = getSupported(cast); - if (!supported.contains(found.get())) { - TrackEvent.withWarn("Unsupported prefs value found") - .tag("key", id) - .tag("value", in) - .handle(); - return defaultObject; - } - - TrackEvent.debug("Loading preferences value for key " + breadcrumb + " from value " + found.get()); - return found.get(); - } - - try { - TrackEvent.debug("Loading preferences value for key " + breadcrumb + " from value " + tree); - return JacksonMapper.getDefault().treeToValue(tree, type); - } catch (Exception ex) { - ErrorEvent.fromThrowable(ex).omit().handle(); - return defaultObject; - } - } - - @Override - @SuppressWarnings("unchecked") - public ObservableList loadObservableList(String breadcrumb, ObservableList defaultObservableList) { - return loadObservableList(breadcrumb, defaultObservableList.get(0).getClass(), defaultObservableList); - } - - @Override - public ObservableList loadObservableList( - String breadcrumb, Class type, ObservableList defaultObservableList) { - var id = getSaveId(breadcrumb); - var tree = getContent(id); - if (tree == null) { - return defaultObservableList; - } - - try { - CollectionType javaType = - JacksonMapper.newMapper().getTypeFactory().constructCollectionType(List.class, type); - return JacksonMapper.newMapper().treeToValue(tree, javaType); - } catch (Exception ex) { - ErrorEvent.fromThrowable(ex).omit().handle(); - return defaultObservableList; - } - } - - @Override - public boolean clearPreferences() { - return FileUtils.deleteQuietly(file.toFile()); - } - - // ====== - // UNUSED - // ====== - - @Override - public void saveSelectedCategory(String breadcrumb) { - throw new AssertionError(); - } - - @Override - public String loadSelectedCategory() { - throw new AssertionError(); - } - - @Override - public void saveDividerPosition(double dividerPosition) {} - - @Override - public double loadDividerPosition() { - return 0.27; - } - - @Override - public void saveWindowWidth(double windowWidth) {} - - @Override - public double loadWindowWidth() { - return 0; - } - - @Override - public void saveWindowHeight(double windowHeight) {} - - @Override - public double loadWindowHeight() { - return 0; - } - - @Override - public void saveWindowPosX(double windowPosX) {} - - @Override - public double loadWindowPosX() { - return 0; - } - - @Override - public void saveWindowPosY(double windowPosY) {} - - @Override - public double loadWindowPosY() { - return 0; - } -} diff --git a/app/src/main/java/io/xpipe/app/prefs/LazyNodeElement.java b/app/src/main/java/io/xpipe/app/prefs/LazyNodeElement.java deleted file mode 100644 index 72aa39f74..000000000 --- a/app/src/main/java/io/xpipe/app/prefs/LazyNodeElement.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.xpipe.app.prefs; - -import com.dlsc.formsfx.model.structure.Element; -import javafx.scene.Node; - -import java.util.function.Supplier; - -public class LazyNodeElement extends Element> { - - protected Supplier node; - - public static LazyNodeElement of(Supplier node) { - return new LazyNodeElement<>(node); - } - - protected LazyNodeElement(Supplier node) { - if (node == null) { - throw new NullPointerException("Node argument must not be null"); - } - this.node = node; - } - - public N getNode() { - return node.get(); - } -} \ No newline at end of file diff --git a/app/src/main/java/io/xpipe/app/prefs/PasswordCategory.java b/app/src/main/java/io/xpipe/app/prefs/PasswordManagerCategory.java similarity index 61% rename from app/src/main/java/io/xpipe/app/prefs/PasswordCategory.java rename to app/src/main/java/io/xpipe/app/prefs/PasswordManagerCategory.java index 8a0bb2b51..110a47ed2 100644 --- a/app/src/main/java/io/xpipe/app/prefs/PasswordCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/PasswordManagerCategory.java @@ -1,33 +1,33 @@ package io.xpipe.app.prefs; import atlantafx.base.theme.Styles; -import com.dlsc.preferencesfx.model.Category; -import com.dlsc.preferencesfx.model.Group; -import com.dlsc.preferencesfx.model.Setting; import io.xpipe.app.comp.base.ButtonComp; +import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.TextFieldComp; -import io.xpipe.app.util.TerminalHelper; +import io.xpipe.app.util.OptionsBuilder; +import io.xpipe.app.util.TerminalLauncher; import io.xpipe.app.util.ThreadHelper; +import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.CommandControl; -import io.xpipe.core.process.ShellDialects; import io.xpipe.core.store.LocalStore; import javafx.beans.property.SimpleStringProperty; import javafx.geometry.Insets; import javafx.geometry.Pos; -import lombok.SneakyThrows; import org.kordamp.ikonli.javafx.FontIcon; import java.util.List; -public class PasswordCategory extends AppPrefsCategory { +public class PasswordManagerCategory extends AppPrefsCategory { - public PasswordCategory(AppPrefs prefs) { - super(prefs); + @Override + protected String getId() { + return "passwordManager"; } - @SneakyThrows - public Category create() { + @Override + protected Comp create() { + var prefs = AppPrefs.get(); var testPasswordManagerValue = new SimpleStringProperty(); Runnable test = () -> { prefs.save(); @@ -37,23 +37,20 @@ public class PasswordCategory extends AppPrefsCategory { } ThreadHelper.runFailableAsync(() -> { - TerminalHelper.open( + TerminalLauncher.open( "Password test", new LocalStore() .control() - .command(cmd + .command(CommandBuilder.ofFunction(sc -> cmd + "\n" - + ShellDialects.getPlatformDefault() - .getEchoCommand("Is this your password?", false)) + + sc.getShellDialect().getEchoCommand("Is this your password?", false))) .terminalExitMode(CommandControl.TerminalExitMode.KEEP_OPEN)); }); }; - var testPasswordManager = lazyNode( - "passwordManagerCommandTest", - new HorizontalComp(List.of( + var testPasswordManager = new HorizontalComp(List.of( new TextFieldComp(testPasswordManagerValue) - .apply(struc -> struc.get().setPromptText("Test password key")) + .apply(struc -> struc.get().setPromptText("Enter password key")) .styleClass(Styles.LEFT_PILL) .grow(false, true), new ButtonComp(null, new FontIcon("mdi2p-play"), test) @@ -61,12 +58,11 @@ public class PasswordCategory extends AppPrefsCategory { .grow(false, true))) .padding(new Insets(15, 0, 0, 0)) .apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)) - .apply(struc -> struc.get().setFillHeight(true)), - null); - return Category.of( - "passwordManager", - Group.of( - Setting.of("passwordManagerCommand", prefs.passwordManagerCommand), - testPasswordManager)); + .apply(struc -> struc.get().setFillHeight(true)); + return new OptionsBuilder().addTitle("passwordManager").sub(new OptionsBuilder() + .nameAndDescription("passwordManagerCommand") + .addComp(new TextFieldComp(prefs.passwordManagerCommand, true).apply(struc -> struc.get().setPromptText("mypassmgr get $KEY"))) + .nameAndDescription("passwordManagerCommandTest") + .addComp(testPasswordManager)).buildComp(); } } diff --git a/app/src/main/java/io/xpipe/app/prefs/PrefFields.java b/app/src/main/java/io/xpipe/app/prefs/PrefFields.java deleted file mode 100644 index 7c70e6e7a..000000000 --- a/app/src/main/java/io/xpipe/app/prefs/PrefFields.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.xpipe.app.prefs; - -import com.dlsc.formsfx.model.structure.StringField; -import com.dlsc.preferencesfx.formsfx.view.controls.SimpleChooserControl; -import io.xpipe.app.core.AppI18n; -import io.xpipe.app.fxcomps.util.BindingsHelper; -import io.xpipe.app.fxcomps.util.PlatformThread; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; - -import java.nio.file.Path; - -public class PrefFields { - - public static StringField ofPath(ObjectProperty fileProperty) { - StringProperty stringProperty = new SimpleStringProperty(fileProperty.getValue().toString()); - - // Prevent garbage collection of this due to how preferencesfx handles properties via bindings - BindingsHelper.linkPersistently(fileProperty, stringProperty); - - stringProperty.addListener((observable, oldValue, newValue) -> { - fileProperty.setValue(newValue != null ? Path.of(newValue) : null); - }); - - fileProperty.addListener((observable, oldValue, newValue) -> { - PlatformThread.runLaterIfNeeded(() -> { - stringProperty.setValue(newValue != null ? newValue.toString() : ""); - }); - }); - - return StringField.ofStringType(stringProperty) - .render(() -> { - var c = new SimpleChooserControl( - AppI18n.get("browse"), fileProperty.getValue().toFile(), true); - c.setMinWidth(600); - c.setPrefWidth(600); - return c; - }); - } -} diff --git a/app/src/main/java/io/xpipe/app/prefs/PrefsComp.java b/app/src/main/java/io/xpipe/app/prefs/PrefsComp.java deleted file mode 100644 index 65d70e9c1..000000000 --- a/app/src/main/java/io/xpipe/app/prefs/PrefsComp.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.xpipe.app.prefs; - -import io.xpipe.app.core.AppFont; -import io.xpipe.app.core.AppLayoutModel; -import io.xpipe.app.fxcomps.SimpleComp; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import org.controlsfx.control.MasterDetailPane; - -public class PrefsComp extends SimpleComp { - - private final AppLayoutModel layout; - - public PrefsComp(AppLayoutModel layout) { - this.layout = layout; - } - - @Override - protected Region createSimple() { - return createButtonOverlay(); - } - - private Region createButtonOverlay() { - var pfx = AppPrefs.get().createControls().getView(); - pfx.getStyleClass().add("prefs"); - MasterDetailPane p = (MasterDetailPane) pfx.getCenter(); - p.dividerPositionProperty().setValue(0.27); - - var stack = new StackPane(pfx); - stack.setPickOnBounds(false); - AppFont.medium(stack); - - return stack; - } -} diff --git a/app/src/main/java/io/xpipe/app/prefs/PropertiesComp.java b/app/src/main/java/io/xpipe/app/prefs/PropertiesComp.java deleted file mode 100644 index f53d2f517..000000000 --- a/app/src/main/java/io/xpipe/app/prefs/PropertiesComp.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.xpipe.app.prefs; - -import io.xpipe.app.core.AppI18n; -import io.xpipe.app.core.AppProperties; -import io.xpipe.app.fxcomps.Comp; -import io.xpipe.app.fxcomps.SimpleComp; -import io.xpipe.app.fxcomps.impl.LabelComp; -import io.xpipe.app.util.JfxHelper; -import io.xpipe.app.util.OptionsBuilder; -import javafx.scene.layout.Region; - -public class PropertiesComp extends SimpleComp { - - @Override - protected Region createSimple() { - var title = Comp.of(() -> { - return JfxHelper.createNamedEntry(AppI18n.get("xPipeClient"), "Version " + AppProperties.get().getVersion() + " (" - + AppProperties.get().getArch() + ")", "logo/logo_48x48.png"); - }); - - var section = new OptionsBuilder() - .addComp(title, null) - .addComp(Comp.vspacer(10)) - .name("build") - .addComp( - new LabelComp(AppProperties.get().getBuild()), - null) - .name("runtimeVersion") - .addComp( - new LabelComp(System.getProperty("java.vm.version")), - null) - .name("virtualMachine") - .addComp( - new LabelComp(System.getProperty("java.vm.vendor") + " " + System.getProperty("java.vm.name")), - null) - .buildComp(); - return section.styleClass("properties-comp").createRegion(); - } -} diff --git a/app/src/main/java/io/xpipe/app/prefs/QuietResourceBundleService.java b/app/src/main/java/io/xpipe/app/prefs/QuietResourceBundleService.java deleted file mode 100644 index 4b09840fb..000000000 --- a/app/src/main/java/io/xpipe/app/prefs/QuietResourceBundleService.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.xpipe.app.prefs; - -import com.dlsc.formsfx.model.util.ResourceBundleService; -import io.xpipe.app.core.AppI18n; -import lombok.NonNull; - -import java.util.*; - -public class QuietResourceBundleService extends ResourceBundleService { - - public QuietResourceBundleService() { - super(new ResourceBundle() { - @Override - protected Object handleGetObject(@NonNull String key) { - return null; - } - - @Override - public @NonNull Enumeration getKeys() { - return Collections.emptyEnumeration(); - } - }); - } - - @Override - public String translate(String key) { - return AppI18n.get(key); - } -} diff --git a/app/src/main/java/io/xpipe/app/prefs/SecurityCategory.java b/app/src/main/java/io/xpipe/app/prefs/SecurityCategory.java new file mode 100644 index 000000000..dce1dee28 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/SecurityCategory.java @@ -0,0 +1,34 @@ +package io.xpipe.app.prefs; + +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.util.ElevationAccessChoiceComp; +import io.xpipe.app.util.OptionsBuilder; + +public class SecurityCategory extends AppPrefsCategory { + + @Override + protected String getId() { + return "security"; + } + + public Comp create() { + var prefs = AppPrefs.get(); + var builder = new OptionsBuilder(); + builder.addTitle("securityPolicy") + .sub(new OptionsBuilder() + .nameAndDescription("elevationPolicy") + .addComp(new ElevationAccessChoiceComp(prefs.elevationPolicy).minWidth(250), prefs.elevationPolicy) + .nameAndDescription("dontCachePasswords") + .addToggle(prefs.dontCachePasswords) + .nameAndDescription("denyTempScriptCreation") + .addToggle(prefs.denyTempScriptCreation) + .nameAndDescription("disableCertutilUse") + .addToggle(prefs.disableCertutilUse) + .nameAndDescription("disableTerminalRemotePasswordPreparation") + .addToggle(prefs.disableTerminalRemotePasswordPreparation) + .nameAndDescription("dontAcceptNewHostKeys") + .addToggle(prefs.dontAcceptNewHostKeys) + ); + return builder.buildComp(); + } +} diff --git a/app/src/main/java/io/xpipe/app/prefs/SupportedLocale.java b/app/src/main/java/io/xpipe/app/prefs/SupportedLocale.java index da3943456..a3a3cde31 100644 --- a/app/src/main/java/io/xpipe/app/prefs/SupportedLocale.java +++ b/app/src/main/java/io/xpipe/app/prefs/SupportedLocale.java @@ -1,6 +1,8 @@ package io.xpipe.app.prefs; import io.xpipe.app.ext.PrefsChoiceValue; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ObservableValue; import lombok.AllArgsConstructor; import lombok.Getter; @@ -16,8 +18,8 @@ public enum SupportedLocale implements PrefsChoiceValue { private final String id; @Override - public String toTranslatedString() { - return locale.getDisplayName(); + public ObservableValue toTranslatedString() { + return new SimpleStringProperty(locale.getDisplayName()); } @Override diff --git a/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java b/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java new file mode 100644 index 000000000..06e76968c --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java @@ -0,0 +1,32 @@ +package io.xpipe.app.prefs; + +import io.xpipe.app.comp.base.ButtonComp; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.util.DesktopHelper; +import io.xpipe.app.util.OptionsBuilder; + +public class SyncCategory extends AppPrefsCategory { + + public Comp create() { + var prefs = AppPrefs.get(); + var builder = new OptionsBuilder(); + builder.addTitle("sync").sub(new OptionsBuilder().nameAndDescription("enableGitStorage") + .addToggle(prefs.enableGitStorage) + .nameAndDescription("storageGitRemote") + .addString(prefs.storageGitRemote, true) + .disable(prefs.enableGitStorage.not()) + .addComp(prefs.getCustomComp("gitVaultIdentityStrategy")) + .nameAndDescription("openDataDir") + .addComp(new ButtonComp(AppI18n.observable("openDataDirButton"), () -> { + DesktopHelper.browsePath(DataStorage.get().getDataDir()); + }))); + return builder.buildComp(); + } + + @Override + protected String getId() { + return "sync"; + } +} diff --git a/app/src/main/java/io/xpipe/app/prefs/SystemCategory.java b/app/src/main/java/io/xpipe/app/prefs/SystemCategory.java new file mode 100644 index 000000000..713a77f71 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/SystemCategory.java @@ -0,0 +1,34 @@ +package io.xpipe.app.prefs; + +import io.xpipe.app.ext.PrefsChoiceValue; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.impl.ChoiceComp; +import io.xpipe.app.util.OptionsBuilder; + +public class SystemCategory extends AppPrefsCategory { + + @Override + protected String getId() { + return "system"; + } + + public Comp create() { + var prefs = AppPrefs.get(); + var builder = new OptionsBuilder(); + builder.addTitle("appBehaviour") + .sub(new OptionsBuilder() + .nameAndDescription("startupBehaviour") + .addComp(ChoiceComp.ofTranslatable(prefs.startupBehaviour, PrefsChoiceValue.getSupported(StartupBehaviour.class), false).minWidth(300)) + .nameAndDescription("closeBehaviour") + .addComp(ChoiceComp.ofTranslatable(prefs.closeBehaviour, PrefsChoiceValue.getSupported(CloseBehaviour.class), false).minWidth(300)) + ) + .addTitle("advanced") + .sub(new OptionsBuilder() + .nameAndDescription("developerMode") + .addToggle(prefs.developerMode)) + .addTitle("updates") + .sub(new OptionsBuilder() + .nameAndDescription("automaticallyUpdate").addToggle(prefs.automaticallyCheckForUpdates)); + return builder.buildComp(); + } +} diff --git a/app/src/main/java/io/xpipe/app/prefs/TerminalCategory.java b/app/src/main/java/io/xpipe/app/prefs/TerminalCategory.java new file mode 100644 index 000000000..fdc7a4ad7 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/TerminalCategory.java @@ -0,0 +1,58 @@ +package io.xpipe.app.prefs; + +import io.xpipe.app.comp.base.ButtonComp; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.ext.PrefsChoiceValue; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.impl.ChoiceComp; +import io.xpipe.app.fxcomps.impl.StackComp; +import io.xpipe.app.fxcomps.impl.TextFieldComp; +import io.xpipe.app.util.OptionsBuilder; +import io.xpipe.app.util.TerminalLauncher; +import io.xpipe.app.util.ThreadHelper; +import io.xpipe.core.store.LocalStore; +import javafx.beans.binding.Bindings; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.util.List; + +public class TerminalCategory extends AppPrefsCategory { + + @Override + protected String getId() { + return "terminal"; + } + + @Override + protected Comp create() { + var prefs = AppPrefs.get(); + var terminalTest = new StackComp(List.of(new ButtonComp(AppI18n.observable("test"), new FontIcon("mdi2p-play"), () -> { + prefs.save(); + ThreadHelper.runFailableAsync(() -> { + var term = AppPrefs.get().terminalType().getValue(); + if (term != null) { + TerminalLauncher.open("Test", new LocalStore().control().command("echo Test")); + } + }); + }))).padding(new Insets(15, 0, 0, 0)).apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)); + return new OptionsBuilder().addTitle("terminalConfiguration").sub(new OptionsBuilder() + .nameAndDescription("terminalEmulator").addComp( + ChoiceComp.ofTranslatable(prefs.terminalType, PrefsChoiceValue.getSupported(ExternalTerminalType.class), false)) + .nameAndDescription( + "customTerminalCommand").addComp(new TextFieldComp(prefs.customTerminalCommand, true).apply( + struc -> struc.get().setPromptText("myterminal -e $CMD")).hide(prefs.terminalType.isNotEqualTo(ExternalTerminalType.CUSTOM))).addComp( + terminalTest) + .name("preferTerminalTabs") + .description(Bindings.createStringBinding(() -> { + var disabled = prefs.terminalType().getValue() != null && !prefs.terminalType.get().supportsTabs(); + return !disabled ? AppI18n.get("preferTerminalTabs") : + AppI18n.get("preferTerminalTabsDisabled", prefs.terminalType().getValue().toTranslatedString().getValue()); + }, prefs.terminalType())) + .addToggle(prefs.preferTerminalTabs).disable(Bindings.createBooleanBinding(() -> { + return prefs.terminalType().getValue() != null && !prefs.terminalType.get().supportsTabs(); + }, prefs.terminalType())) + .nameAndDescription("clearTerminalOnInit").addToggle(prefs.clearTerminalOnInit)).buildComp(); + } +} diff --git a/app/src/main/java/io/xpipe/app/prefs/TranslatableComboBoxControl.java b/app/src/main/java/io/xpipe/app/prefs/TranslatableComboBoxControl.java deleted file mode 100644 index 266c87c8d..000000000 --- a/app/src/main/java/io/xpipe/app/prefs/TranslatableComboBoxControl.java +++ /dev/null @@ -1,95 +0,0 @@ -package io.xpipe.app.prefs; - -import com.dlsc.formsfx.model.structure.SingleSelectionField; -import com.dlsc.preferencesfx.formsfx.view.controls.SimpleControl; -import io.xpipe.app.util.Translatable; -import javafx.geometry.Pos; -import javafx.scene.control.ComboBox; -import javafx.scene.control.Label; -import javafx.scene.layout.StackPane; - -public class TranslatableComboBoxControl - extends SimpleControl, StackPane> { - - private ComboBox comboBox; - private Label readOnlyLabel; - - /** - * {@inheritDoc} - */ - @Override - @SuppressWarnings("unchecked") - public void initializeParts() { - super.initializeParts(); - - fieldLabel = new Label(field.labelProperty().getValue()); - readOnlyLabel = new Label(); - - node = new StackPane(); - node.getStyleClass().add("simple-select-control"); - - comboBox = new ComboBox(field.getItems()); - comboBox.setConverter(Translatable.stringConverter()); - - comboBox.getSelectionModel().select(field.getItems().indexOf(field.getSelection())); - } - - /** - * {@inheritDoc} - */ - @Override - public void layoutParts() { - readOnlyLabel.getStyleClass().add("read-only-label"); - - comboBox.setMaxWidth(Double.MAX_VALUE); - comboBox.setVisibleRowCount(10); - - node.setAlignment(Pos.CENTER_LEFT); - node.getChildren().addAll(comboBox, readOnlyLabel); - } - - /** - * {@inheritDoc} - */ - @Override - public void setupBindings() { - super.setupBindings(); - - comboBox.visibleProperty().bind(field.editableProperty()); - readOnlyLabel.visibleProperty().bind(field.editableProperty().not()); - readOnlyLabel.textProperty().bind(Translatable.asTranslatedString(comboBox.valueProperty())); - } - - /** - * {@inheritDoc} - */ - @Override - @SuppressWarnings("unchecked") - public void setupValueChangedListeners() { - super.setupValueChangedListeners(); - - field.itemsProperty().addListener((observable, oldValue, newValue) -> comboBox.setItems(field.getItems())); - - field.selectionProperty().addListener((observable, oldValue, newValue) -> { - if (field.getSelection() != null) { - comboBox.getSelectionModel().select(field.getItems().indexOf(field.getSelection())); - } else { - comboBox.getSelectionModel().clearSelection(); - } - }); - - field.errorMessagesProperty().addListener((observable, oldValue, newValue) -> toggleTooltip(comboBox)); - field.tooltipProperty().addListener((observable, oldValue, newValue) -> toggleTooltip(comboBox)); - comboBox.focusedProperty().addListener((observable, oldValue, newValue) -> toggleTooltip(comboBox)); - } - - /** - * {@inheritDoc} - */ - @Override - public void setupEventHandlers() { - comboBox.valueProperty() - .addListener((observable, oldValue, newValue) -> - field.select(comboBox.getSelectionModel().getSelectedIndex())); - } -} diff --git a/app/src/main/java/io/xpipe/app/prefs/TroubleshootCategory.java b/app/src/main/java/io/xpipe/app/prefs/TroubleshootCategory.java new file mode 100644 index 000000000..96f3cdd7c --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/TroubleshootCategory.java @@ -0,0 +1,103 @@ +package io.xpipe.app.prefs; + +import io.xpipe.app.comp.base.TileButtonComp; +import io.xpipe.app.core.AppLogs; +import io.xpipe.app.core.mode.OperationMode; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.issue.UserReportComp; +import io.xpipe.app.util.*; +import io.xpipe.core.process.OsType; +import io.xpipe.core.store.FileNames; +import io.xpipe.core.store.LocalStore; +import io.xpipe.core.util.XPipeInstallation; + +public class TroubleshootCategory extends AppPrefsCategory { + + @Override + protected String getId() { + return "troubleshoot"; + } + + @Override + protected Comp create() { + OptionsBuilder b = new OptionsBuilder() + .addTitle("troubleshootingOptions") + .spacer(30) + .addComp( + new TileButtonComp("reportIssue", "reportIssueDescription", "mdal-bug_report", e -> { + var event = ErrorEvent.fromMessage("User Report"); + if (AppLogs.get().isWriteToFile()) { + event.attachment(AppLogs.get().getSessionLogsDirectory()); + } + UserReportComp.show(event.build()); + e.consume(); + }) + .grow(true, false), + null) + .separator() + .addComp( + new TileButtonComp("launchDebugMode", "launchDebugModeDescription", "mdmz-refresh", e -> { + OperationMode.executeAfterShutdown(() -> { + try (var sc = new LocalStore() + .control() + .start()) { + var script = FileNames.join( + XPipeInstallation.getCurrentInstallationBasePath() + .toString(), + XPipeInstallation.getDaemonDebugScriptPath(OsType.getLocal())); + if (sc.getOsType().equals(OsType.WINDOWS)) { + sc.executeSimpleCommand( + ApplicationHelper.createDetachCommand(sc, "\"" + script + "\"")); + } else { + TerminalLauncher.open("XPipe Debug", LocalShell.getShell().command("\"" + script + "\"")); + } + } + }); + e.consume(); + }) + .grow(true, false), + null) + .separator(); + + if (AppLogs.get().isWriteToFile()) { + b.addComp( + new TileButtonComp( + "openCurrentLogFile", + "openCurrentLogFileDescription", + "mdmz-text_snippet", + e -> { + FileOpener.openInTextEditor(AppLogs.get() + .getSessionLogsDirectory() + .resolve("xpipe.log") + .toString()); + e.consume(); + }) + .grow(true, false), + null) + .separator(); + } + + b.addComp( + new TileButtonComp( + "openInstallationDirectory", + "openInstallationDirectoryDescription", + "mdomz-snippet_folder", + e -> { + DesktopHelper.browsePath( + XPipeInstallation.getCurrentInstallationBasePath()); + e.consume(); + }) + .grow(true, false), + null) + .separator() + .addComp( + new TileButtonComp("clearCaches", "clearCachesDescription", "mdi2t-trash-can-outline", e -> { + ClearCacheAlert.show(); + e.consume(); + }) + .grow(true, false), + null); + return b.buildComp(); + } +} diff --git a/app/src/main/java/io/xpipe/app/prefs/TroubleshootComp.java b/app/src/main/java/io/xpipe/app/prefs/TroubleshootComp.java deleted file mode 100644 index 3525669fc..000000000 --- a/app/src/main/java/io/xpipe/app/prefs/TroubleshootComp.java +++ /dev/null @@ -1,132 +0,0 @@ -package io.xpipe.app.prefs; - -import io.xpipe.app.comp.base.TileButtonComp; -import io.xpipe.app.core.AppLogs; -import io.xpipe.app.core.mode.OperationMode; -import io.xpipe.app.fxcomps.Comp; -import io.xpipe.app.fxcomps.CompStructure; -import io.xpipe.app.fxcomps.impl.VerticalComp; -import io.xpipe.app.issue.ErrorEvent; -import io.xpipe.app.issue.UserReportComp; -import io.xpipe.app.util.*; -import io.xpipe.core.process.OsType; -import io.xpipe.core.store.FileNames; -import io.xpipe.core.store.LocalStore; -import io.xpipe.core.util.XPipeInstallation; - -import java.util.List; - -public class TroubleshootComp extends Comp> { - - private Comp createActions() { - OptionsBuilder b = new OptionsBuilder() - .addTitle("troubleshootingOptions") - .spacer(13) - .addComp( - new TileButtonComp("reportIssue", "reportIssueDescription", "mdal-bug_report", e -> { - var event = ErrorEvent.fromMessage("User Report"); - if (AppLogs.get().isWriteToFile()) { - event.attachment(AppLogs.get().getSessionLogsDirectory()); - } - UserReportComp.show(event.build()); - e.consume(); - }) - .grow(true, false), - null) - .separator() - // .addComp( - // new TileButtonComp("restart", "restartDescription", "mdmz-refresh", e -> { - // OperationMode.executeAfterShutdown(() -> { - // try (var sc = ShellStore.createLocal() - // .control() - // .start()) { - // var script = FileNames.join( - // XPipeInstallation.getCurrentInstallationBasePath() - // .toString(), - // - // XPipeInstallation.getDaemonExecutablePath(sc.getOsType())); - // sc.executeSimpleCommand( - // ScriptHelper.createDetachCommand(sc, "\"" + script - // + "\"")); - // } - // }); - // e.consume(); - // }) - // .grow(true, false), - // null) - // .separator() - .addComp( - new TileButtonComp("launchDebugMode", "launchDebugModeDescription", "mdmz-refresh", e -> { - OperationMode.executeAfterShutdown(() -> { - try (var sc = new LocalStore() - .control() - .start()) { - var script = FileNames.join( - XPipeInstallation.getCurrentInstallationBasePath() - .toString(), - XPipeInstallation.getDaemonDebugScriptPath(sc.getOsType())); - if (sc.getOsType().equals(OsType.WINDOWS)) { - sc.executeSimpleCommand( - ApplicationHelper.createDetachCommand(sc, "\"" + script + "\"")); - } else { - TerminalHelper.open("XPipe Debug", LocalShell.getShell().command("\"" + script + "\"")); - } - } - }); - e.consume(); - }) - .grow(true, false), - null) - .separator(); - - if (AppLogs.get().isWriteToFile()) { - b.addComp( - new TileButtonComp( - "openCurrentLogFile", - "openCurrentLogFileDescription", - "mdmz-text_snippet", - e -> { - FileOpener.openInTextEditor(AppLogs.get() - .getSessionLogsDirectory() - .resolve("xpipe.log") - .toString()); - e.consume(); - }) - .grow(true, false), - null) - .separator(); - } - - b.addComp( - new TileButtonComp( - "openInstallationDirectory", - "openInstallationDirectoryDescription", - "mdomz-snippet_folder", - e -> { - DesktopHelper.browsePath( - XPipeInstallation.getCurrentInstallationBasePath()); - e.consume(); - }) - .grow(true, false), - null) - .separator() - .addComp( - new TileButtonComp("clearCaches", "clearCachesDescription", "mdi2t-trash-can-outline", e -> { - ClearCacheAlert.show(); - e.consume(); - }) - .grow(true, false), - null); - return b.buildComp(); - } - - @Override - public CompStructure createBase() { - var box = new VerticalComp(List.of(createActions())) - .apply(s -> s.get().setFillWidth(true)) - .apply(struc -> struc.get().setSpacing(15)) - .styleClass("troubleshoot-tab") - .apply(struc -> struc.get().setPrefWidth(600)); - return box.createStructure(); - } -} diff --git a/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java b/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java index ec88b9691..505cb2b40 100644 --- a/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java @@ -1,102 +1,48 @@ package io.xpipe.app.prefs; -import com.dlsc.formsfx.model.structure.BooleanField; -import com.dlsc.formsfx.model.structure.StringField; -import com.dlsc.preferencesfx.formsfx.view.controls.SimpleControl; -import com.dlsc.preferencesfx.formsfx.view.controls.SimpleTextControl; -import com.dlsc.preferencesfx.model.Category; -import com.dlsc.preferencesfx.model.Group; -import com.dlsc.preferencesfx.model.Setting; import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.core.AppI18n; -import io.xpipe.app.storage.DataStorage; -import io.xpipe.app.util.DesktopHelper; +import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.util.LockChangeAlert; import io.xpipe.app.util.OptionsBuilder; +import io.xpipe.app.util.Validator; import io.xpipe.core.util.XPipeInstallation; import javafx.beans.binding.Bindings; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; import lombok.SneakyThrows; -import static io.xpipe.app.prefs.AppPrefs.group; - public class VaultCategory extends AppPrefsCategory { private static final boolean STORAGE_DIR_FIXED = System.getProperty(XPipeInstallation.DATA_DIR_PROP) != null; - private final StringField lockCryptControl = StringField.ofStringType(prefs.getLockCrypt()) - .render(() -> new SimpleControl() { - - private Region button; - - @Override - public void initializeParts() { - super.initializeParts(); - this.node = new StackPane(); - button = new ButtonComp( - Bindings.createStringBinding(() -> { - return prefs.getLockCrypt().getValue() != null - ? AppI18n.get("changeLock") - : AppI18n.get("createLock"); - }), - () -> LockChangeAlert.show()) - .createRegion(); - } - - @Override - public void layoutParts() { - this.node.getChildren().addAll(this.button); - this.node.setAlignment(Pos.CENTER_LEFT); - } - }); - - public VaultCategory(AppPrefs prefs) { - super(prefs); + @Override + protected String getId() { + return "vault"; } @SneakyThrows - public Category create() { - BooleanField enable = BooleanField.ofBooleanType(prefs.enableGitStorage) - .render(() -> { - return new CustomToggleControl(); - }); - StringField remote = StringField.ofStringType(prefs.storageGitRemote) - .render(() -> { - var c = new SimpleTextControl(); - c.setPrefWidth(1000); - return c; - }); - - var openDataDir = lazyNode( - "openDataDir", new OptionsBuilder().name("openDataDir").description("openDataDirDescription").addComp( - new ButtonComp(AppI18n.observable("openDataDirButton"), () -> { - DesktopHelper.browsePath(DataStorage.get().getDataDir()); - }) - ).buildComp().padding(new Insets(25, 0, 0, 0)), - null); - - return Category.of( - "vault", - group( - "sharing", - Setting.of( - "enableGitStorage", - enable, - prefs.enableGitStorage), - Setting.of( - "storageGitRemote", - remote, - prefs.storageGitRemote), - openDataDir), - group( - "storage", - STORAGE_DIR_FIXED - ? null - : Setting.of( - "storageDirectory", prefs.storageDirectoryControl, prefs.storageDirectory)), - Group.of("security", Setting.of("workspaceLock", lockCryptControl, prefs.getLockCrypt()))); + public Comp create() { + var prefs = AppPrefs.get(); + var builder = new OptionsBuilder(); + if (!STORAGE_DIR_FIXED) { + var sub = new OptionsBuilder() + .nameAndDescription("storageDirectory") + .addPath(prefs.storageDirectory); + sub.withValidator(val-> { + sub.check(Validator.absolutePath(val, prefs.storageDirectory)); + sub.check(Validator.directory(val, prefs.storageDirectory)); + }); + builder.addTitle("storage") + .sub(sub); + } + builder.addTitle("vaultSecurity").sub(new OptionsBuilder() + .nameAndDescription("encryptAllVaultData") + .addToggle(prefs.encryptAllVaultData) + .nameAndDescription("workspaceLock").addComp(new ButtonComp( + Bindings.createStringBinding(() -> { + return prefs.getLockCrypt().getValue() != null && !prefs.getLockCrypt().getValue().isEmpty() + ? AppI18n.get("changeLock") + : AppI18n.get("createLock"); + }, prefs.getLockCrypt()), LockChangeAlert::show), prefs.getLockCrypt())); + return builder.buildComp(); } } diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorage.java b/app/src/main/java/io/xpipe/app/storage/DataStorage.java index c9f458cb1..f3aa68325 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -1,5 +1,6 @@ package io.xpipe.app.storage; +import io.xpipe.app.comp.store.StoreSortMode; import io.xpipe.app.ext.DataStoreProviders; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.AppPrefs; @@ -12,7 +13,9 @@ import lombok.Getter; import lombok.NonNull; import lombok.Setter; +import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; @@ -58,12 +61,15 @@ public abstract class DataStorage { protected final ReentrantLock busyIo = new ReentrantLock(); public DataStorage() { - this.dir = AppPrefs.get().storageDirectory().getValue(); + var prefsDir = AppPrefs.get().storageDirectory().getValue(); + this.dir = !Files.exists(prefsDir) || !Files.isDirectory(prefsDir) ? AppPrefs.DEFAULT_STORAGE_DIR : prefsDir; this.storeEntries = new ConcurrentHashMap<>(); this.storeEntriesSet = storeEntries.keySet(); this.storeCategories = new CopyOnWriteArrayList<>(); } + public abstract String getVaultKey(); + public DataStoreCategory getDefaultCategory() { return getStoreCategoryIfPresent(DEFAULT_CATEGORY_UUID).orElseThrow(); } @@ -101,6 +107,12 @@ public abstract class DataStorage { }); } + public void forceRewrite() { + getStoreEntries().forEach(dataStoreEntry -> { + dataStoreEntry.reassignStore(); + }); + } + public static void reset() { if (INSTANCE == null) { return; @@ -115,6 +127,54 @@ public abstract class DataStorage { save(true); } + protected void setupBuiltinCategories() { + var categoriesDir = getCategoriesDir(); + var allConnections = getStoreCategoryIfPresent(ALL_CONNECTIONS_CATEGORY_UUID); + if (allConnections.isEmpty()) { + var cat = DataStoreCategory.createNew(null, ALL_CONNECTIONS_CATEGORY_UUID, "All connections"); + cat.setDirectory(categoriesDir.resolve(ALL_CONNECTIONS_CATEGORY_UUID.toString())); + storeCategories.add(cat); + } else { + allConnections.get().setParentCategory(null); + } + + var allScripts = getStoreCategoryIfPresent(ALL_SCRIPTS_CATEGORY_UUID); + if (allScripts.isEmpty()) { + var cat = DataStoreCategory.createNew(null, ALL_SCRIPTS_CATEGORY_UUID, "All scripts"); + cat.setDirectory(categoriesDir.resolve(ALL_SCRIPTS_CATEGORY_UUID.toString())); + storeCategories.add(cat); + } else { + allScripts.get().setParentCategory(null); + } + + if (getStoreCategoryIfPresent(PREDEFINED_SCRIPTS_CATEGORY_UUID).isEmpty()) { + var cat = DataStoreCategory.createNew(ALL_SCRIPTS_CATEGORY_UUID, PREDEFINED_SCRIPTS_CATEGORY_UUID, "Predefined"); + cat.setDirectory(categoriesDir.resolve(PREDEFINED_SCRIPTS_CATEGORY_UUID.toString())); + storeCategories.add(cat); + } + + if (getStoreCategoryIfPresent(CUSTOM_SCRIPTS_CATEGORY_UUID).isEmpty()) { + var cat = DataStoreCategory.createNew(ALL_SCRIPTS_CATEGORY_UUID, CUSTOM_SCRIPTS_CATEGORY_UUID, "Custom"); + cat.setDirectory(categoriesDir.resolve(CUSTOM_SCRIPTS_CATEGORY_UUID.toString())); + storeCategories.add(cat); + } + + if (getStoreCategoryIfPresent(DEFAULT_CATEGORY_UUID).isEmpty()) { + storeCategories.add( + new DataStoreCategory(categoriesDir.resolve(DEFAULT_CATEGORY_UUID.toString()), DEFAULT_CATEGORY_UUID, "Default", Instant.now(), + Instant.now(), true, ALL_CONNECTIONS_CATEGORY_UUID, StoreSortMode.ALPHABETICAL_ASC, false)); + } + + storeCategories.forEach(dataStoreCategory -> { + if (dataStoreCategory.getParentCategory() != null && getStoreCategoryIfPresent(dataStoreCategory.getParentCategory()).isEmpty()) { + dataStoreCategory.setParentCategory(ALL_CONNECTIONS_CATEGORY_UUID); + } else if (dataStoreCategory.getParentCategory() == null && !dataStoreCategory.getUuid().equals(ALL_CONNECTIONS_CATEGORY_UUID) && + !dataStoreCategory.getUuid().equals(ALL_SCRIPTS_CATEGORY_UUID)) { + dataStoreCategory.setParentCategory(ALL_CONNECTIONS_CATEGORY_UUID); + } + }); + } + protected void onReset() {} public static DataStorage get() { diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorageEncryption.java b/app/src/main/java/io/xpipe/app/storage/DataStorageEncryption.java new file mode 100644 index 000000000..ce2403395 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/storage/DataStorageEncryption.java @@ -0,0 +1,64 @@ +package io.xpipe.app.storage; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.core.util.InPlaceSecretValue; +import io.xpipe.core.util.JacksonMapper; + +import java.io.CharArrayReader; +import java.io.CharArrayWriter; +import java.io.IOException; + +public class DataStorageEncryption { + + public static JsonNode readPossiblyEncryptedNode(JsonNode node) { + if (!node.isObject()) { + return node; + } + + try { + var secret = JacksonMapper.getDefault().treeToValue(node, DataStoreSecret.class); + if (secret == null) { + return node; + } + + if (secret.getInternalSecret() == null) { + return node; + } + + var read = secret.getInternalSecret().mapSecretValueFailable(chars -> { + return JacksonMapper.getDefault().readTree(new CharArrayReader(chars)); + }); + if (read != null) { + return read; + } + } catch (Exception e) { + ErrorEvent.fromThrowable(e).build().handle(); + } + return JsonNodeFactory.instance.missingNode(); + } + + public static JsonNode encryptNodeIfNeeded(JsonNode node) { + if (AppPrefs.get() == null || !AppPrefs.get().encryptAllVaultData().get()) { + return node; + } + + var writer = new CharArrayWriter(); + JsonFactory f = new JsonFactory(); + try (JsonGenerator g = f.createGenerator(writer).setPrettyPrinter(new DefaultPrettyPrinter())) { + JacksonMapper.getDefault().writeTree(g, node); + } catch (IOException e) { + ErrorEvent.fromThrowable(e).build().handle(); + return node; + } + + var newContent = writer.toCharArray(); + var secret = new DataStoreSecret(InPlaceSecretValue.of(newContent)); + return JacksonMapper.getDefault().valueToTree(secret); + } +} diff --git a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java index a50543cfa..c94683ede 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java @@ -258,7 +258,7 @@ public class DataStoreEntry extends StorageElement { // Store loading is prone to errors. JsonNode storeNode = null; try { - storeNode = mapper.readTree(storeFile.toFile()); + storeNode = DataStorageEncryption.readPossiblyEncryptedNode(mapper.readTree(storeFile.toFile())); } catch (Exception e) { ErrorEvent.fromThrowable(e).handle(); } @@ -384,6 +384,11 @@ public class DataStoreEntry extends StorageElement { dirty = true; } + public void reassignStore() { + this.storeNode = DataStorageWriter.storeToNode(store); + dirty = true; + } + public void validate() { try { validateOrThrow(); @@ -513,7 +518,7 @@ public class DataStoreEntry extends StorageElement { var entryString = mapper.writeValueAsString(obj); var stateString = mapper.writeValueAsString(stateObj); - var storeString = mapper.writeValueAsString(storeNode); + var storeString = mapper.writeValueAsString(DataStorageEncryption.encryptNodeIfNeeded(storeNode)); FileUtils.forceMkdir(directory.toFile()); Files.writeString(directory.resolve("state.json"), stateString); @@ -522,14 +527,6 @@ public class DataStoreEntry extends StorageElement { dirty = false; } - public ObjectNode getResolvedNode() { - if (store == null) { - return null; - } - - return JacksonMapper.getDefault().valueToTree(store); - } - @Getter public enum Validity { @JsonProperty("loadFailed") diff --git a/app/src/main/java/io/xpipe/app/storage/DataStoreSecret.java b/app/src/main/java/io/xpipe/app/storage/DataStoreSecret.java new file mode 100644 index 000000000..f85a747ee --- /dev/null +++ b/app/src/main/java/io/xpipe/app/storage/DataStoreSecret.java @@ -0,0 +1,67 @@ +package io.xpipe.app.storage; + +import com.fasterxml.jackson.core.TreeNode; +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.util.PasswordLockSecretValue; +import io.xpipe.app.util.VaultKeySecretValue; +import io.xpipe.core.util.InPlaceSecretValue; +import io.xpipe.core.util.SecretValue; +import lombok.Setter; +import lombok.Value; +import lombok.experimental.NonFinal; + +import java.util.Arrays; +import java.util.Objects; + +@Value +public class DataStoreSecret { + + @Setter + @NonFinal + TreeNode originalNode; + InPlaceSecretValue internalSecret; + String usedPasswordLockCrypt; + + public DataStoreSecret(InPlaceSecretValue internalSecret) { + this(null, internalSecret); + } + + public DataStoreSecret(TreeNode originalNode, InPlaceSecretValue internalSecret) { + this.originalNode = originalNode; + this.internalSecret = internalSecret; + this.usedPasswordLockCrypt = AppPrefs.get() != null ? AppPrefs.get().getLockCrypt().get() : null; + } + + public boolean requiresRewrite() { + return AppPrefs.get() != null && AppPrefs.get().getLockCrypt().get() != null && !Objects.equals(AppPrefs.get().getLockCrypt().get(), + usedPasswordLockCrypt); + } + + public char[] getSecret() { + return internalSecret != null ? internalSecret.getSecret() : new char[0]; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DataStoreSecret that)) { + return false; + } + return Arrays.equals(getSecret(), that.getSecret()); + } + + @Override + public int hashCode() { + return Arrays.hashCode(getSecret()); + } + + public SecretValue getOutputSecret() { + if (AppPrefs.get() != null && AppPrefs.get().getLockPassword().getValue() != null) { + return new PasswordLockSecretValue(getSecret()); + } + + return new VaultKeySecretValue(getSecret()); + } +} diff --git a/app/src/main/java/io/xpipe/app/storage/ImpersistentStorage.java b/app/src/main/java/io/xpipe/app/storage/ImpersistentStorage.java index 99dc448e2..1a3d6894c 100644 --- a/app/src/main/java/io/xpipe/app/storage/ImpersistentStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/ImpersistentStorage.java @@ -1,15 +1,51 @@ package io.xpipe.app.storage; +import io.xpipe.app.comp.store.StoreSortMode; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.TrackEvent; +import io.xpipe.core.store.LocalStore; import org.apache.commons.io.FileUtils; import java.nio.file.Files; +import java.time.Instant; +import java.util.UUID; public class ImpersistentStorage extends DataStorage { + @Override + public String getVaultKey() { + return UUID.randomUUID().toString(); + } + @Override public void load() { + var storesDir = getStoresDir(); + var categoriesDir = getCategoriesDir(); + + { + var cat = DataStoreCategory.createNew(null, ALL_CONNECTIONS_CATEGORY_UUID, "All connections"); + cat.setDirectory(categoriesDir.resolve(ALL_CONNECTIONS_CATEGORY_UUID.toString())); + storeCategories.add(cat); + } + { + var cat = DataStoreCategory.createNew(null, ALL_SCRIPTS_CATEGORY_UUID, "All scripts"); + cat.setDirectory(categoriesDir.resolve(ALL_SCRIPTS_CATEGORY_UUID.toString())); + storeCategories.add(cat); + } + { + var cat = new DataStoreCategory(categoriesDir.resolve(DEFAULT_CATEGORY_UUID.toString()), DEFAULT_CATEGORY_UUID, "Default", Instant.now(), + Instant.now(), true, ALL_CONNECTIONS_CATEGORY_UUID, StoreSortMode.ALPHABETICAL_ASC, true); + storeCategories.add(cat); + selectedCategory = getStoreCategoryIfPresent(DEFAULT_CATEGORY_UUID).orElseThrow(); + } + + var e = DataStoreEntry.createNew( + LOCAL_ID, DataStorage.DEFAULT_CATEGORY_UUID, "Local Machine", new LocalStore()); + e.setDirectory(getStoresDir().resolve(LOCAL_ID.toString())); + e.setConfiguration( + StorageElement.Configuration.builder().deletable(false).build()); + storeEntries.put(e, e); + e.validate(); } @Override @@ -18,7 +54,7 @@ public class ImpersistentStorage extends DataStorage { } @Override - public void save(boolean dispose) { + public synchronized void save(boolean dispose) { var storesDir = getStoresDir(); TrackEvent.info("Storage persistence is disabled. Deleting storage contents ..."); diff --git a/app/src/main/java/io/xpipe/app/storage/StandardStorage.java b/app/src/main/java/io/xpipe/app/storage/StandardStorage.java index 4602647c3..50331f716 100644 --- a/app/src/main/java/io/xpipe/app/storage/StandardStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/StandardStorage.java @@ -1,18 +1,19 @@ package io.xpipe.app.storage; -import io.xpipe.app.comp.store.StoreSortMode; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.util.LocalShell; import io.xpipe.core.store.LocalStore; import lombok.Getter; import org.apache.commons.io.FileUtils; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.time.Instant; import java.util.ArrayList; +import java.util.Base64; import java.util.List; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; @@ -24,6 +25,8 @@ public class StandardStorage extends DataStorage { @Getter private final GitStorageHandler gitStorageHandler; + private String vaultKey; + @Getter private boolean disposed; @@ -32,6 +35,11 @@ public class StandardStorage extends DataStorage { this.gitStorageHandler.init(dir); } + @Override + public String getVaultKey() { + return vaultKey; + } + @Override protected void onReset() { gitStorageHandler.onReset(); @@ -60,7 +68,7 @@ public class StandardStorage extends DataStorage { var entry = getStoreEntryIfPresent(uuid); if (entry.isEmpty()) { - TrackEvent.withTrace("storage", "Deleting leftover store directory") + TrackEvent.withTrace("Deleting leftover store directory") .tag("uuid", uuid) .handle(); FileUtils.forceDelete(file.toFile()); @@ -93,7 +101,7 @@ public class StandardStorage extends DataStorage { var entry = getStoreCategoryIfPresent(uuid); if (entry.isEmpty()) { - TrackEvent.withTrace("storage", "Deleting leftover category directory") + TrackEvent.withTrace("Deleting leftover category directory") .tag("uuid", uuid) .handle(); FileUtils.forceDelete(file.toFile()); @@ -108,11 +116,52 @@ public class StandardStorage extends DataStorage { } } + private void initVaultKey() throws IOException { + var file = dir.resolve("vaultkey"); + if (Files.exists(file)) { + var s = Files.readString(file); + vaultKey = new String(Base64.getDecoder().decode(s), StandardCharsets.UTF_8); + } else { + Files.createDirectories(dir); + vaultKey = UUID.randomUUID().toString(); + Files.writeString(file,Base64.getEncoder().encodeToString(vaultKey.getBytes(StandardCharsets.UTF_8))); + } + } + + private void initSystemInfo() throws IOException { + var file = dir.resolve("systeminfo"); + if (Files.exists(file)) { + var s = Files.readString(file); + if (!LocalShell.getShell().getOsName().equals(s)) { + ErrorEvent.fromMessage("This vault was originally created on a different system running " + s + + ". Sharing connection information between systems directly might cause some problems." + + " If you want to properly synchronize connection information across many systems, you can take a look into the git vault synchronization functionality in the settings." + ).expected().handle(); + } + } else { + Files.createDirectories(dir); + var s = LocalShell.getShell().getOsName(); + Files.writeString(file, s); + } + } + public void load() { if (!busyIo.tryLock()) { return; } + try { + initSystemInfo(); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).build().handle(); + } + + try { + initVaultKey(); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).terminal(true).build().handle(); + } + this.gitStorageHandler.beforeStorageLoad(); var storesDir = getStoresDir(); @@ -156,56 +205,9 @@ public class StandardStorage extends DataStorage { ErrorEvent.fromThrowable(exception.get()).handle(); } - var allConnections = getStoreCategoryIfPresent(ALL_CONNECTIONS_CATEGORY_UUID); - if (allConnections.isEmpty()) { - var cat = DataStoreCategory.createNew(null, ALL_CONNECTIONS_CATEGORY_UUID, "All connections"); - cat.setDirectory(categoriesDir.resolve(ALL_CONNECTIONS_CATEGORY_UUID.toString())); - storeCategories.add(cat); - } else { - allConnections.get().setParentCategory(null); - } - - var allScripts = getStoreCategoryIfPresent(ALL_SCRIPTS_CATEGORY_UUID); - if (allScripts.isEmpty()) { - var cat = DataStoreCategory.createNew(null, ALL_SCRIPTS_CATEGORY_UUID, "All scripts"); - cat.setDirectory(categoriesDir.resolve(ALL_SCRIPTS_CATEGORY_UUID.toString())); - storeCategories.add(cat); - } else { - allScripts.get().setParentCategory(null); - } - - if (getStoreCategoryIfPresent(PREDEFINED_SCRIPTS_CATEGORY_UUID).isEmpty()) { - var cat = DataStoreCategory.createNew(ALL_SCRIPTS_CATEGORY_UUID, PREDEFINED_SCRIPTS_CATEGORY_UUID, "Predefined"); - cat.setDirectory(categoriesDir.resolve(PREDEFINED_SCRIPTS_CATEGORY_UUID.toString())); - storeCategories.add(cat); - } - - if (getStoreCategoryIfPresent(CUSTOM_SCRIPTS_CATEGORY_UUID).isEmpty()) { - var cat = DataStoreCategory.createNew(ALL_SCRIPTS_CATEGORY_UUID, CUSTOM_SCRIPTS_CATEGORY_UUID, "Custom"); - cat.setDirectory(categoriesDir.resolve(CUSTOM_SCRIPTS_CATEGORY_UUID.toString())); - cat.setShare(true); - storeCategories.add(cat); - } - - if (getStoreCategoryIfPresent(DEFAULT_CATEGORY_UUID).isEmpty()) { - var cat = new DataStoreCategory(categoriesDir.resolve(DEFAULT_CATEGORY_UUID.toString()), DEFAULT_CATEGORY_UUID, "Default", - Instant.now(), Instant.now(), true, ALL_CONNECTIONS_CATEGORY_UUID, StoreSortMode.ALPHABETICAL_ASC, true); - storeCategories.add(cat); - } - + setupBuiltinCategories(); selectedCategory = getStoreCategoryIfPresent(DEFAULT_CATEGORY_UUID).orElseThrow(); - storeCategories.forEach(dataStoreCategory -> { - if (dataStoreCategory.getParentCategory() != null - && getStoreCategoryIfPresent(dataStoreCategory.getParentCategory()) - .isEmpty()) { - dataStoreCategory.setParentCategory(ALL_CONNECTIONS_CATEGORY_UUID); - } else if (dataStoreCategory.getParentCategory() == null && !dataStoreCategory.getUuid().equals(ALL_CONNECTIONS_CATEGORY_UUID) && !dataStoreCategory.getUuid().equals( - ALL_SCRIPTS_CATEGORY_UUID)) { - dataStoreCategory.setParentCategory(ALL_CONNECTIONS_CATEGORY_UUID); - } - }); - try (var dirs = Files.list(storesDir)) { dirs.filter(Files::isDirectory).forEach(path -> { try { @@ -260,21 +262,35 @@ public class StandardStorage extends DataStorage { ErrorEvent.fromThrowable(ex).terminal(true).build().handle(); } - var hasFixedLocal = storeEntriesSet.stream().anyMatch(dataStoreEntry -> dataStoreEntry.getUuid().equals(LOCAL_ID)); - if (!hasFixedLocal) { - var e = DataStoreEntry.createNew( - LOCAL_ID, DataStorage.DEFAULT_CATEGORY_UUID, "Local Machine", new LocalStore()); - e.setDirectory(getStoresDir().resolve(LOCAL_ID.toString())); - e.setConfiguration( - StorageElement.Configuration.builder().deletable(false).build()); - storeEntries.put(e, e); - e.validate(); - } + var hasFixedLocal = storeEntriesSet.stream().anyMatch(dataStoreEntry -> dataStoreEntry.getUuid().equals(LOCAL_ID)); - var local = DataStorage.get().getStoreEntry(LOCAL_ID); - if (storeEntriesSet.stream().noneMatch(entry -> entry.getColor() != null)) { - local.setColor(DataStoreColor.BLUE); + if (hasFixedLocal) { + var local = getStoreEntry(LOCAL_ID); + if (local.getValidity() == DataStoreEntry.Validity.LOAD_FAILED) { + try { + storeEntries.remove(local); + local.deleteFromDisk(); + hasFixedLocal = false; + } catch (IOException ex) { + ErrorEvent.fromThrowable(ex).terminal(true).build().handle(); + } } + } + + if (!hasFixedLocal) { + var e = DataStoreEntry.createNew( + LOCAL_ID, DataStorage.DEFAULT_CATEGORY_UUID, "Local Machine", new LocalStore()); + e.setDirectory(getStoresDir().resolve(LOCAL_ID.toString())); + e.setConfiguration( + StorageElement.Configuration.builder().deletable(false).build()); + storeEntries.put(e, e); + e.validate(); + } + + var local = DataStorage.get().getStoreEntry(LOCAL_ID); + if (storeEntriesSet.stream().noneMatch(entry -> entry.getColor() != null)) { + local.setColor(DataStoreColor.BLUE); + } refreshValidities(true); storeEntriesSet.forEach(entry -> { diff --git a/app/src/main/java/io/xpipe/app/storage/StorageJacksonModule.java b/app/src/main/java/io/xpipe/app/storage/StorageJacksonModule.java index dd639c971..390ed0859 100644 --- a/app/src/main/java/io/xpipe/app/storage/StorageJacksonModule.java +++ b/app/src/main/java/io/xpipe/app/storage/StorageJacksonModule.java @@ -2,13 +2,16 @@ package io.xpipe.app.storage; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.xpipe.app.util.PasswordLockSecretValue; +import io.xpipe.app.util.VaultKeySecretValue; import io.xpipe.core.store.LocalStore; +import io.xpipe.core.util.EncryptedSecretValue; +import io.xpipe.core.util.JacksonMapper; +import io.xpipe.core.util.SecretValue; import java.io.IOException; import java.util.UUID; @@ -17,10 +20,16 @@ public class StorageJacksonModule extends SimpleModule { @Override public void setupModule(SetupContext context) { + context.registerSubtypes(VaultKeySecretValue.class); + context.registerSubtypes(PasswordLockSecretValue.class); + addSerializer(DataStoreEntryRef.class, new DataStoreEntryRefSerializer()); addDeserializer(DataStoreEntryRef.class, new DataStoreEntryRefDeserializer()); addSerializer(ContextualFileReference.class, new LocalFileReferenceSerializer()); addDeserializer(ContextualFileReference.class, new LocalFileReferenceDeserializer()); + addSerializer(DataStoreSecret.class, new DataStoreSecretSerializer()); + addDeserializer(DataStoreSecret.class, new DataStoreSecretDeserializer()); + context.addSerializers(_serializers); context.addDeserializers(_deserializers); } @@ -41,6 +50,58 @@ public class StorageJacksonModule extends SimpleModule { } } + public static class DataStoreSecretSerializer extends JsonSerializer { + + @Override + public void serialize(DataStoreSecret value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + // Preserve same output if not changed + if (value.getOriginalNode() != null && !value.requiresRewrite()) { + var tree = JsonNodeFactory.instance.objectNode(); + tree.set("secret", (JsonNode) value.getOriginalNode()); + jgen.writeTree(tree); + return; + } + + // Reencrypt + var val = value.getOutputSecret(); + var valTree = JacksonMapper.getDefault().valueToTree(val); + var tree = JsonNodeFactory.instance.objectNode(); + tree.set("secret", valTree); + jgen.writeTree(tree); + value.setOriginalNode(valTree); + } + } + + public static class DataStoreSecretDeserializer extends JsonDeserializer { + + @Override + public DataStoreSecret deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + var tree = JacksonMapper.getDefault().readTree(p); + if (!tree.isObject()) { + return null; + } + + var legacy = JacksonMapper.getDefault().treeToValue(tree, EncryptedSecretValue.class); + if (legacy != null) { + // Don't cache legacy node + return new DataStoreSecret(null, legacy.inPlace()); + } + + var obj = (ObjectNode) tree; + if (!obj.has("secret")) { + return null; + } + + var secretTree = obj.required("secret"); + var secret = JacksonMapper.getDefault().treeToValue(secretTree, SecretValue.class); + if (secret == null) { + return null; + } + + return new DataStoreSecret(secretTree, secret.inPlace()); + } + } + @SuppressWarnings("all") public static class DataStoreEntryRefSerializer extends JsonSerializer { diff --git a/app/src/main/java/io/xpipe/app/test/DaemonExtensionTest.java b/app/src/main/java/io/xpipe/app/test/DaemonExtensionTest.java index 605e4ecda..f6e077851 100644 --- a/app/src/main/java/io/xpipe/app/test/DaemonExtensionTest.java +++ b/app/src/main/java/io/xpipe/app/test/DaemonExtensionTest.java @@ -1,7 +1,6 @@ package io.xpipe.app.test; import io.xpipe.app.core.AppProperties; -import io.xpipe.app.ext.XPipeServiceProviders; import io.xpipe.app.util.XPipeSession; import io.xpipe.beacon.BeaconDaemonController; import io.xpipe.core.util.JacksonMapper; @@ -17,7 +16,6 @@ public class DaemonExtensionTest extends ExtensionTest { public static void setup() throws Exception { AppProperties.init(); JacksonMapper.initModularized(ModuleLayer.boot()); - XPipeServiceProviders.load(ModuleLayer.boot()); XPipeSession.init(UUID.randomUUID()); BeaconDaemonController.start(XPipeDaemonMode.TRAY); } diff --git a/app/src/main/java/io/xpipe/app/test/ExtensionTest.java b/app/src/main/java/io/xpipe/app/test/ExtensionTest.java index b949ea05a..ba38e695f 100644 --- a/app/src/main/java/io/xpipe/app/test/ExtensionTest.java +++ b/app/src/main/java/io/xpipe/app/test/ExtensionTest.java @@ -1,17 +1,21 @@ package io.xpipe.app.test; +import io.xpipe.core.store.FileNames; import lombok.SneakyThrows; +import java.nio.file.Files; import java.nio.file.Path; public class ExtensionTest { @SneakyThrows public static Path getResourcePath(Class c, String name) { - var url = c.getResource(name); - if (url == null) { + var loc = Path.of(c.getProtectionDomain().getCodeSource().getLocation().toURI()); + var testName = FileNames.getBaseName(loc.getFileName().toString()).split("-")[1]; + var f = loc.getParent().getParent().resolve("resources").resolve(testName).resolve(name); + if (!Files.exists(f)) { throw new IllegalArgumentException(String.format("File %s does not exist", name)); } - return Path.of(url.toURI()); + return f; } } diff --git a/app/src/main/java/io/xpipe/app/test/LocalExtensionTest.java b/app/src/main/java/io/xpipe/app/test/LocalExtensionTest.java index d0f0e0a1f..52227d7df 100644 --- a/app/src/main/java/io/xpipe/app/test/LocalExtensionTest.java +++ b/app/src/main/java/io/xpipe/app/test/LocalExtensionTest.java @@ -1,20 +1,18 @@ package io.xpipe.app.test; -import io.xpipe.app.core.AppProperties; -import io.xpipe.app.ext.XPipeServiceProviders; -import io.xpipe.app.util.XPipeSession; -import io.xpipe.core.util.JacksonMapper; +import io.xpipe.app.core.mode.OperationMode; +import lombok.SneakyThrows; import org.junit.jupiter.api.BeforeAll; -import java.util.UUID; - public class LocalExtensionTest extends ExtensionTest { @BeforeAll + @SneakyThrows public static void setup() { - JacksonMapper.initModularized(ModuleLayer.boot()); - XPipeServiceProviders.load(ModuleLayer.boot()); - AppProperties.init(); - XPipeSession.init(UUID.randomUUID()); + if (OperationMode.get() != null) { + return; + } + + OperationMode.init(new String[0]); } } diff --git a/app/src/main/java/io/xpipe/app/test/TestModule.java b/app/src/main/java/io/xpipe/app/test/TestModule.java index afbe91020..f04b6ac43 100644 --- a/app/src/main/java/io/xpipe/app/test/TestModule.java +++ b/app/src/main/java/io/xpipe/app/test/TestModule.java @@ -12,15 +12,11 @@ public abstract class TestModule { private static final Map, Map> values = new LinkedHashMap<>(); @SuppressWarnings({"unchecked", "rawtypes"}) - public static Map get(Class c, String... classes) { + public static Map get(Class c, Module module, String... classes) { if (!values.containsKey(c)) { - List> loadedClasses = (List>) Arrays.stream(classes) + List> loadedClasses = Arrays.stream(classes) .map(s -> { - try { - return Optional.of(Class.forName(s)); - } catch (ClassNotFoundException ex) { - return Optional.empty(); - } + return Optional.>of(Class.forName(module, s)); }) .flatMap(Optional::stream) .toList(); @@ -39,9 +35,9 @@ public abstract class TestModule { .collect(Collectors.toMap(o -> o.getKey(), o -> ((Supplier) o.getValue()).get())); } - public static Stream> getArguments(Class c, String... classes) { + public static Stream> getArguments(Class c, Module module, String... classes) { Stream.Builder> argumentBuilder = Stream.builder(); - for (var s : TestModule.get(c, classes).entrySet()) { + for (var s : TestModule.get(c, module, classes).entrySet()) { argumentBuilder.add(Named.of(s.getKey(), s.getValue())); } return argumentBuilder.build(); diff --git a/app/src/main/java/io/xpipe/app/update/AppDownloads.java b/app/src/main/java/io/xpipe/app/update/AppDownloads.java index 57058cdda..92f43e4a7 100644 --- a/app/src/main/java/io/xpipe/app/update/AppDownloads.java +++ b/app/src/main/java/io/xpipe/app/update/AppDownloads.java @@ -58,7 +58,7 @@ public class AppDownloads { FileUtils.getTempDirectory().toPath().resolve(asset.get().getName()); Files.write(downloadFile, bytes); - TrackEvent.withInfo("installation", "Downloaded asset") + TrackEvent.withInfo("Downloaded asset") .tag("version", version) .tag("url", url) .tag("size", FileUtils.byteCountToDisplaySize(bytes.length)) diff --git a/app/src/main/java/io/xpipe/app/update/AppInstaller.java b/app/src/main/java/io/xpipe/app/update/AppInstaller.java index b4c157e78..b442fadaf 100644 --- a/app/src/main/java/io/xpipe/app/update/AppInstaller.java +++ b/app/src/main/java/io/xpipe/app/update/AppInstaller.java @@ -5,54 +5,22 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import io.xpipe.app.core.AppProperties; import io.xpipe.app.util.ScriptHelper; -import io.xpipe.app.util.TerminalHelper; -import io.xpipe.core.process.CommandControl; -import io.xpipe.core.process.OsType; -import io.xpipe.core.process.ShellControl; -import io.xpipe.core.process.ShellDialects; +import io.xpipe.app.util.TerminalLauncher; +import io.xpipe.core.process.*; import io.xpipe.core.store.FileNames; import io.xpipe.core.store.LocalStore; import io.xpipe.core.util.XPipeInstallation; import lombok.Getter; -import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; public class AppInstaller { - public static void installOnRemoteMachine(ShellControl s, String version) throws Exception { - var asset = getSuitablePlatformAsset(s); - var file = AppDownloads.downloadInstaller(asset, version, false); - if (file.isEmpty()) { - return; - } - - installFile(s, asset, file.get()); - } - public static void installFileLocal(InstallerAssetType asset, Path localFile) throws Exception { asset.installLocal(localFile.toString()); } - public static void installFile(ShellControl s, InstallerAssetType asset, Path localFile) throws Exception { - String targetFile; - if (s.hasLocalSystemAccess()) { - targetFile = localFile.toString(); - } else { - targetFile = FileNames.join( - s.getSubTemporaryDirectory(), localFile.getFileName().toString()); - try (InputStream in = Files.newInputStream(localFile)) { - in.transferTo(s.getShellDialect() - .createStreamFileWriteCommand(s, targetFile) - .startExternalStdin()); - } - } - - asset.installRemote(s, targetFile); - } - public static InstallerAssetType getSuitablePlatformAsset() { if (OsType.getLocal().equals(OsType.WINDOWS)) { return new InstallerAssetType.Msi(); @@ -71,26 +39,6 @@ public class AppInstaller { throw new AssertionError(); } - public static InstallerAssetType getSuitablePlatformAsset(ShellControl p) throws Exception { - if (p.getOsType().equals(OsType.WINDOWS)) { - return new InstallerAssetType.Msi(); - } - - if (p.getOsType().equals(OsType.LINUX)) { - try (CommandControl c = p.getShellDialect() - .createFileExistsCommand(p, "/etc/debian_version") - .start()) { - return c.discardAndCheckExit() ? new InstallerAssetType.Debian() : new InstallerAssetType.Rpm(); - } - } - - if (p.getOsType().equals(OsType.MACOS)) { - return new InstallerAssetType.Pkg(); - } - - throw new AssertionError(); - } - @Getter @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @@ -101,8 +49,6 @@ public class AppInstaller { }) public abstract static class InstallerAssetType { - public abstract void installRemote(ShellControl pc, String file) throws Exception; - public abstract void installLocal(String file) throws Exception; public boolean isCorrectAsset(String name) { @@ -120,35 +66,10 @@ public class AppInstaller { return ".msi"; } - @Override - public void installRemote(ShellControl shellControl, String file) throws Exception { - var exec = XPipeInstallation.getInstallationExecutable( - shellControl, XPipeInstallation.getDefaultInstallationBasePath(shellControl)); - var logsDir = FileNames.join(XPipeInstallation.getDataDir(shellControl), "logs"); - var cmd = new ArrayList<>(java.util.List.of( - "start", - "/wait", - "msiexec", - "/i", - file, - "/l*", - FileNames.join(logsDir, "installer_" + FileNames.getFileName(file) + ".log"), - "/qb", - "&", - exec - // "/qf" - )); - try (CommandControl c = shellControl.command(cmd).start()) { - c.discardOrThrow(); - } - } - @Override public void installLocal(String file) throws Exception { var shellProcessControl = new LocalStore().control().start(); - var exec = XPipeInstallation.getInstallationExecutable( - shellProcessControl, - XPipeInstallation.getDefaultInstallationBasePath(shellProcessControl)); + var exec = XPipeInstallation.getCurrentInstallationBasePath().resolve(XPipeInstallation.getDaemonExecutablePath(OsType.getLocal())); var logsDir = FileNames.join(XPipeInstallation.getDataDir().toString(), "logs"); var logFile = FileNames.join(logsDir, "installer_" + FileNames.getFileName(file) + ".log"); var script = ScriptHelper.createExecScript( @@ -172,44 +93,25 @@ public class AppInstaller { return ".deb"; } - @Override - public void installRemote(ShellControl shellControl, String file) throws Exception { - try (var pc = shellControl.subShell(ShellDialects.BASH).start()) { - try (CommandControl c = pc.command("DEBIAN_FRONTEND=noninteractive apt-get remove -qy xpipe") - .elevated("xpipe") - .start()) { - c.discardOrThrow(); - } - try (CommandControl c = pc.command( - "DEBIAN_FRONTEND=noninteractive apt-get install -qy \"" + file + "\"") - .elevated("xpipe") - .start()) { - c.discardOrThrow(); - } - pc.executeSimpleCommand("xpipe open"); - } - } - @Override public void installLocal(String file) throws Exception { + var name = AppProperties.get().isStaging() ? "xpipe-ptb" : "xpipe"; var command = new LocalStore() .control() .subShell(ShellDialects.BASH) .command(String.format( """ function exec { - echo "+ sudo apt-get remove -qy xpipe" - echo "+ sudo apt-get install -qy \\"%s\\"" - DEBIAN_FRONTEND=noninteractive sudo apt-get remove -qy xpipe || return 1 + echo "+ sudo apt install \\"%s\\"" DEBIAN_FRONTEND=noninteractive sudo apt-get install -qy "%s" || return 1 - xpipe open || return 1 + %s open || return 1 } cd ~ exec || read -rsp "Update failed ..."$'\\n' -n 1 key """, - file, file)); - TerminalHelper.open("XPipe Updater", command); + file, file, name)); + TerminalLauncher.open("XPipe Updater", command); } } @@ -220,20 +122,9 @@ public class AppInstaller { return ".rpm"; } - @Override - public void installRemote(ShellControl shellControl, String file) throws Exception { - try (var pc = shellControl.subShell(ShellDialects.BASH).start()) { - try (CommandControl c = pc.command("rpm -U -v --force \"" + file + "\"") - .elevated("xpipe") - .start()) { - c.discardOrThrow(); - } - pc.executeSimpleCommand("xpipe open"); - } - } - @Override public void installLocal(String file) throws Exception { + var name = AppProperties.get().isStaging() ? "xpipe-ptb" : "xpipe"; var command = new LocalStore() .control() .subShell(ShellDialects.BASH) @@ -242,14 +133,14 @@ public class AppInstaller { function exec { echo "+ sudo rpm -U -v --force \\"%s\\"" sudo rpm -U -v --force "%s" || return 1 - xpipe open || return 1 + %s open || return 1 } cd ~ exec || read -rsp "Update failed ..."$'\\n' -n 1 key """, - file, file)); - TerminalHelper.open("XPipe Updater", command); + file, file, name)); + TerminalLauncher.open("XPipe Updater", command); } } @@ -260,21 +151,9 @@ public class AppInstaller { return ".pkg"; } - @Override - public void installRemote(ShellControl shellControl, String file) throws Exception { - try (var pc = shellControl.subShell(ShellDialects.BASH).start()) { - try (CommandControl c = pc.command( - "installer -verboseR -allowUntrusted -pkg \"" + file + "\" -target /") - .elevated("xpipe") - .start()) { - c.discardOrThrow(); - } - pc.executeSimpleCommand("xpipe open"); - } - } - @Override public void installLocal(String file) throws Exception { + var name = AppProperties.get().isStaging() ? "xpipe-ptb" : "xpipe"; var command = new LocalStore() .control() .command(String.format( @@ -282,14 +161,14 @@ public class AppInstaller { function exec { echo "+ sudo installer -verboseR -allowUntrusted -pkg \\"%s\\" -target /" sudo installer -verboseR -allowUntrusted -pkg "%s" -target / || return 1 - xpipe open || return 1 + %s open || return 1 } cd ~ exec || echo "Update failed ..." && read -rs -k 1 key """, - file, file)); - TerminalHelper.open("XPipe Updater", command); + file, file, name)); + TerminalLauncher.open("XPipe Updater", command); } } } diff --git a/app/src/main/java/io/xpipe/app/update/GitHubUpdater.java b/app/src/main/java/io/xpipe/app/update/GitHubUpdater.java index 4f92d9b72..2ce52a278 100644 --- a/app/src/main/java/io/xpipe/app/update/GitHubUpdater.java +++ b/app/src/main/java/io/xpipe/app/update/GitHubUpdater.java @@ -42,7 +42,7 @@ public class GitHubUpdater extends UpdateHandler { preparedUpdate.setValue(rel); } - public void executeUpdateAndCloseImpl() throws Exception { + public void executeUpdateOnCloseImpl() throws Exception { var downloadFile = preparedUpdate.getValue().getFile(); if (!Files.exists(downloadFile)) { return; diff --git a/app/src/main/java/io/xpipe/app/update/PortableUpdater.java b/app/src/main/java/io/xpipe/app/update/PortableUpdater.java index 0b11472b2..8e5285083 100644 --- a/app/src/main/java/io/xpipe/app/update/PortableUpdater.java +++ b/app/src/main/java/io/xpipe/app/update/PortableUpdater.java @@ -27,7 +27,7 @@ public class PortableUpdater extends UpdateHandler { .createRegion(); } - public void executeUpdateAndCloseImpl() { + public void executeUpdateOnCloseImpl() { throw new UnsupportedOperationException(); } diff --git a/app/src/main/java/io/xpipe/app/update/UpdateChangelogAlert.java b/app/src/main/java/io/xpipe/app/update/UpdateChangelogAlert.java index 5af6461e6..045dc333b 100644 --- a/app/src/main/java/io/xpipe/app/update/UpdateChangelogAlert.java +++ b/app/src/main/java/io/xpipe/app/update/UpdateChangelogAlert.java @@ -4,6 +4,7 @@ import io.xpipe.app.comp.base.MarkdownComp; import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppWindowHelper; import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.util.Hyperlinks; import javafx.scene.control.Alert; import javafx.scene.control.ButtonBar; import javafx.scene.control.ButtonType; @@ -20,7 +21,13 @@ public class UpdateChangelogAlert { public static void showIfNeeded() { var update = XPipeDistributionType.get().getUpdateHandler().getPerformedUpdate(); if (update != null && !XPipeDistributionType.get().getUpdateHandler().isUpdateSucceeded()) { - ErrorEvent.fromMessage("Update did not succeed").handle(); + ErrorEvent.fromMessage(""" + Update installation did not succeed. + + Note that you can also install the latest version manually from %s + if there are any problems with the automatic update installation. + """.formatted(Hyperlinks.GITHUB + "/releases/latest") + ).handle(); return; } diff --git a/app/src/main/java/io/xpipe/app/update/UpdateHandler.java b/app/src/main/java/io/xpipe/app/update/UpdateHandler.java index 09fac1f7b..6cf02bcb3 100644 --- a/app/src/main/java/io/xpipe/app/update/UpdateHandler.java +++ b/app/src/main/java/io/xpipe/app/update/UpdateHandler.java @@ -107,7 +107,7 @@ public abstract class UpdateHandler { } protected void event(String msg) { - TrackEvent.builder().category("updater").type("info").message(msg).handle(); + TrackEvent.builder().type("info").message(msg).handle(); } protected final boolean isUpdate(String releaseVersion) { @@ -220,21 +220,26 @@ public abstract class UpdateHandler { event("Executing update ..."); OperationMode.executeAfterShutdown(() -> { try { - executeUpdateAndCloseImpl(); - } catch (Throwable ex) { - ex.printStackTrace(); - } finally { var performedUpdate = new PerformedUpdate( preparedUpdate.getValue().getVersion(), preparedUpdate.getValue().getBody(), preparedUpdate.getValue().getVersion(), preparedUpdate.getValue().getReleaseDate()); AppCache.update("performedUpdate", performedUpdate); + + executeUpdateOnCloseImpl(); + + // In case we perform any operations such as opening a terminal + // give it some time to open while this process is still alive + // Otherwise it might quit because the parent process is dead already + ThreadHelper.sleep(1000); + } catch (Throwable ex) { + ex.printStackTrace(); } }); } - public void executeUpdateAndCloseImpl() throws Exception { + public void executeUpdateOnCloseImpl() throws Exception { throw new UnsupportedOperationException(); } diff --git a/app/src/main/java/io/xpipe/app/update/XPipeInstanceHelper.java b/app/src/main/java/io/xpipe/app/update/XPipeInstanceHelper.java index b0aa71de2..bbe311d7e 100644 --- a/app/src/main/java/io/xpipe/app/update/XPipeInstanceHelper.java +++ b/app/src/main/java/io/xpipe/app/update/XPipeInstanceHelper.java @@ -35,7 +35,7 @@ public class XPipeInstanceHelper { public static boolean isSupported(ShellStore host) { try (var pc = host.control().start(); - var cmd = pc.command(List.of("xpipe"))) { + var cmd = pc.command("xpipe")) { cmd.discardOrThrow(); return true; } catch (Exception e) { diff --git a/app/src/main/java/io/xpipe/app/util/ApplicationHelper.java b/app/src/main/java/io/xpipe/app/util/ApplicationHelper.java index cbd3b0a05..fa9ac0c09 100644 --- a/app/src/main/java/io/xpipe/app/util/ApplicationHelper.java +++ b/app/src/main/java/io/xpipe/app/util/ApplicationHelper.java @@ -3,6 +3,7 @@ package io.xpipe.app.util; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.OsType; import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellDialects; @@ -10,7 +11,6 @@ import io.xpipe.core.util.FailableSupplier; import java.io.IOException; import java.util.Locale; -import java.util.function.Function; public class ApplicationHelper { @@ -24,10 +24,10 @@ public class ApplicationHelper { return format.replace("\"$" + variable + "\"", fileString).replace("$" + variable, fileString); } - public static void executeLocalApplication(Function s, boolean detach) throws Exception { + public static void executeLocalApplication(CommandBuilder b, boolean detach) throws Exception { try (var sc = LocalShell.getShell().start()) { - var cmd = detach ? createDetachCommand(sc, s.apply(sc)) : s.apply(sc); - TrackEvent.withDebug("proc", "Executing local application") + var cmd = detach ? createDetachCommand(sc, b.buildString(sc)) : b.buildString(sc); + TrackEvent.withDebug("Executing local application") .tag("command", cmd) .handle(); try (var c = sc.command(cmd).start()) { diff --git a/app/src/main/java/io/xpipe/app/util/AskpassAlert.java b/app/src/main/java/io/xpipe/app/util/AskpassAlert.java index 619e62c30..704c40e57 100644 --- a/app/src/main/java/io/xpipe/app/util/AskpassAlert.java +++ b/app/src/main/java/io/xpipe/app/util/AskpassAlert.java @@ -3,36 +3,17 @@ package io.xpipe.app.util; import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppWindowHelper; import io.xpipe.app.fxcomps.impl.SecretFieldComp; -import io.xpipe.core.util.SecretValue; +import io.xpipe.core.util.InPlaceSecretValue; import javafx.application.Platform; import javafx.beans.property.SimpleObjectProperty; import javafx.scene.control.Alert; import javafx.scene.layout.StackPane; - -import java.util.*; +import javafx.stage.Stage; public class AskpassAlert { - private static final Set cancelledRequests = new HashSet<>(); - private static final Map requests = new HashMap<>(); - - public static SecretValue query(String prompt, UUID requestId, UUID secretId, int sub) { - if (cancelledRequests.contains(requestId)) { - return null; - } - - var ref = new SecretManager.SecretReference(secretId, sub); - if (SecretManager.get(ref).isPresent() && ref.equals(requests.get(requestId))) { - SecretManager.clear(ref); - } - - var found = SecretManager.get(ref); - if (found.isPresent()) { - requests.put(requestId, ref); - return found.get(); - } - - var prop = new SimpleObjectProperty(); + public static SecretQueryResult queryRaw(String prompt) { + var prop = new SimpleObjectProperty(); var r = AppWindowHelper.showBlockingAlert(alert -> { alert.setTitle(AppI18n.get("askpassAlertTitle")); alert.setHeaderText(prompt); @@ -40,28 +21,22 @@ public class AskpassAlert { var text = new SecretFieldComp(prop).createRegion(); alert.getDialogPane().setContent(new StackPane(text)); + var stage = (Stage) alert.getDialogPane().getScene().getWindow(); + stage.setAlwaysOnTop(true); alert.setOnShown(event -> { + stage.requestFocus(); // Wait 1 pulse before focus so that the scene can be assigned to text Platform.runLater(text::requestFocus); event.consume(); }); }) - .filter(b -> b.getButtonData().isDefaultButton() && prop.getValue() != null) + .filter(b -> b.getButtonData().isDefaultButton()) .map(t -> { - return prop.getValue() != null ? prop.getValue() : SecretHelper.encryptInPlace(""); + return prop.getValue() != null ? prop.getValue() : InPlaceSecretValue.of(""); }) .orElse(null); - - // If the result is null, assume that the operation was aborted by the user - if (r != null && SecretManager.shouldCacheForPrompt(prompt)) { - requests.put(requestId,ref); - SecretManager.set(ref, r); - } else { - cancelledRequests.add(requestId); - } - - return r; + return new SecretQueryResult(r,r == null); } } diff --git a/app/src/main/java/io/xpipe/app/util/ElevationAccess.java b/app/src/main/java/io/xpipe/app/util/ElevationAccess.java new file mode 100644 index 000000000..39da269bc --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/ElevationAccess.java @@ -0,0 +1,47 @@ +package io.xpipe.app.util; + +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.core.AppWindowHelper; +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.core.process.ShellControl; + +public enum ElevationAccess { + + ALLOW { + @Override + public boolean requestElevationUsage(ShellControl shellControl) { + return true; + } + }, + ASK { + @Override + public boolean requestElevationUsage(ShellControl shellControl) { + var name = shellControl.getSourceStore().flatMap(shellStore -> DataStorage.get().getStoreEntryIfPresent(shellStore)) + .map(entry -> entry.getName()).orElse("a system"); + return AppWindowHelper.showConfirmationAlert( + AppI18n.observable("elevationRequestTitle"), + AppI18n.observable("elevationRequestHeader", name), + AppI18n.observable("elevationRequestDescription") + ); + } + }, + DENY { + @Override + public boolean requestElevationUsage(ShellControl shellControl) { + return false; + } + }; + + public boolean requestElevationUsage(ShellControl shellControl) { + return false; + } + + public static boolean request(ShellControl shellControl) { + if (AppPrefs.get() == null) { + return true; + } + + return AppPrefs.get().elevationPolicy().getValue().requestElevationUsage(shellControl); + } +} diff --git a/app/src/main/java/io/xpipe/app/util/ElevationAccessChoiceComp.java b/app/src/main/java/io/xpipe/app/util/ElevationAccessChoiceComp.java new file mode 100644 index 000000000..3da475844 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/ElevationAccessChoiceComp.java @@ -0,0 +1,27 @@ +package io.xpipe.app.util; + +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.fxcomps.SimpleComp; +import io.xpipe.app.fxcomps.impl.ChoiceComp; +import javafx.beans.property.Property; +import javafx.beans.value.ObservableValue; +import javafx.scene.layout.Region; + +import java.util.LinkedHashMap; + +public class ElevationAccessChoiceComp extends SimpleComp { + + private final Property value; + + public ElevationAccessChoiceComp(Property value) {this.value = value;} + + @Override + protected Region createSimple() { + var map = new LinkedHashMap>(); + map.put(ElevationAccess.ALLOW, AppI18n.observable("allow")); + map.put(ElevationAccess.ASK, AppI18n.observable("ask")); + map.put(ElevationAccess.DENY, AppI18n.observable("deny")); + var c = new ChoiceComp<>(value, map, false); + return c.createRegion(); + } +} diff --git a/app/src/main/java/io/xpipe/app/util/FileBridge.java b/app/src/main/java/io/xpipe/app/util/FileBridge.java index 54edabd29..757165371 100644 --- a/app/src/main/java/io/xpipe/app/util/FileBridge.java +++ b/app/src/main/java/io/xpipe/app/util/FileBridge.java @@ -5,6 +5,7 @@ import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.core.process.OsType; +import io.xpipe.core.util.FailableFunction; import io.xpipe.core.util.FailableSupplier; import lombok.Getter; import org.apache.commons.io.FileUtils; @@ -14,12 +15,10 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; import java.time.Instant; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.CopyOnWriteArraySet; +import java.util.*; +import java.util.function.BiConsumer; import java.util.function.Consumer; public class FileBridge { @@ -27,18 +26,14 @@ public class FileBridge { private static final Path TEMP = FileUtils.getTempDirectory().toPath().resolve("xpipe").resolve("bridge"); private static FileBridge INSTANCE; - private final Set openEntries = new CopyOnWriteArraySet<>(); + private final Set openEntries = new HashSet<>(); public static FileBridge get() { return INSTANCE; } - private static TrackEvent.TrackEventBuilder event() { - return TrackEvent.builder().category("editor").type("debug"); - } - private static void event(String msg) { - TrackEvent.builder().category("editor").type("debug").message(msg).handle(); + TrackEvent.builder().type("debug").message(msg).handle(); } private static String getFileSystemCompatibleName(String name) { @@ -61,57 +56,59 @@ public class FileBridge { } AppFileWatcher.getInstance().startWatchersInDirectories(List.of(TEMP), (changed, kind) -> { - if (kind == StandardWatchEventKinds.ENTRY_DELETE) { - event("Editor entry file " + changed.toString() + " has been removed"); - INSTANCE.removeForFile(changed); - } else { - INSTANCE.getForFile(changed).ifPresent(e -> { - // Wait for edit to finish in case external editor has write lock - if (!Files.exists(changed)) { - event("File " + TEMP.relativize(e.file) + " is probably still writing ..."); - ThreadHelper.sleep( - AppPrefs.get().editorReloadTimeout().getValue()); - - // If still no read lock after 500ms, just don't parse it - if (!Files.exists(changed)) { - event("Could not obtain read lock even after timeout. Ignoring change ..."); - return; - } - } - - try { - event("Registering modification for file " + TEMP.relativize(e.file)); - event("Last modification for file: " + e.lastModified.toString() + " vs current one: " - + e.getLastModified()); - if (e.hasChanged()) { - event("Registering change for file " + TEMP.relativize(e.file) + " for editor node " - + e.getName()); - boolean valid = - get().openEntries.stream().anyMatch(entry -> entry.file.equals(changed)); - event("Editor node " + e.getName() + " validity: " + valid); - if (valid) { - e.registerChange(); - try (var in = Files.newInputStream(e.file)) { - e.writer.accept(in); - } - } - } - } catch (Exception ex) { - ErrorEvent.fromThrowable(ex).omit().handle(); - } - }); - } + INSTANCE.handleWatchEvent(changed,kind); }); } catch (IOException e) { ErrorEvent.fromThrowable(e).handle(); } } - private void removeForFile(Path file) { + private synchronized void handleWatchEvent(Path changed, WatchEvent.Kind kind) { + if (kind == StandardWatchEventKinds.ENTRY_DELETE) { + event("Editor entry file " + changed.toString() + " has been removed"); + removeForFile(changed); + return; + } + + var entry = getForFile(changed); + if (entry.isEmpty()) { + return; + } + + var e = entry.get(); + // Wait for edit to finish in case external editor has write lock + if (!Files.exists(changed)) { + event("File " + TEMP.relativize(e.file) + " is probably still writing ..."); + ThreadHelper.sleep( + AppPrefs.get().editorReloadTimeout().getValue()); + + // If still no read lock after 500ms, just don't parse it + if (!Files.exists(changed)) { + event("Could not obtain read lock even after timeout. Ignoring change ..."); + return; + } + } + + try { + event("Registering modification for file " + TEMP.relativize(e.file)); + event("Last modification for file: " + e.lastModified.toString() + " vs current one: " + e.getLastModified()); + if (e.hasChanged()) { + event("Registering change for file " + TEMP.relativize(e.file) + " for editor entry " + e.getName()); + e.registerChange(); + try (var in = Files.newInputStream(e.file)) { + e.writer.accept(in, (long) in.available()); + } + } + } catch (Exception ex) { + ErrorEvent.fromThrowable(ex).omit().handle(); + } + } + + private synchronized void removeForFile(Path file) { openEntries.removeIf(es -> es.file.equals(file)); } - private Optional getForKey(Object node) { + private synchronized Optional getForKey(Object node) { for (var es : openEntries) { if (es.key.equals(node)) { return Optional.of(es); @@ -120,7 +117,7 @@ public class FileBridge { return Optional.empty(); } - private Optional getForFile(Path file) { + private synchronized Optional getForFile(Path file) { for (var es : openEntries) { if (es.file.equals(file)) { return Optional.of(es); @@ -156,7 +153,7 @@ public class FileBridge { keyName, key, () -> new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)), - () -> new ByteArrayOutputStream(s.length()) { + (size) -> new ByteArrayOutputStream(s.length()) { @Override public void close() throws IOException { super.close(); @@ -166,15 +163,25 @@ public class FileBridge { fileConsumer); } - public void openIO( + public synchronized void openIO( String keyName, Object key, FailableSupplier input, - FailableSupplier output, + FailableFunction output, Consumer consumer) { var ext = getForKey(key); if (ext.isPresent()) { - consumer.accept(ext.get().file.toString()); + var existingFile = ext.get().file; + try { + try (var out = Files.newOutputStream(existingFile); var in = input.get()) { + in.transferTo(out); + } + } catch (Exception ex) { + ErrorEvent.fromThrowable(ex).handle(); + return; + } + ext.get().registerChange(); + consumer.accept(existingFile.toString()); return; } @@ -190,9 +197,9 @@ public class FileBridge { return; } - var entry = new Entry(file, key, keyName, in -> { + var entry = new Entry(file, key, keyName, (in, size) -> { if (output != null) { - try (var out = output.get()) { + try (var out = output.apply(size)) { in.transferTo(out); } catch (Exception ex) { ErrorEvent.fromThrowable(ex).handle(); @@ -211,10 +218,10 @@ public class FileBridge { private final Path file; private final Object key; private final String name; - private final Consumer writer; + private final BiConsumer writer; private Instant lastModified; - public Entry(Path file, Object key, String name, Consumer writer) { + public Entry(Path file, Object key, String name, BiConsumer writer) { this.file = file; this.key = key; this.name = name; diff --git a/app/src/main/java/io/xpipe/app/util/FileOpener.java b/app/src/main/java/io/xpipe/app/util/FileOpener.java index ea53ecb13..b2ee568c3 100644 --- a/app/src/main/java/io/xpipe/app/util/FileOpener.java +++ b/app/src/main/java/io/xpipe/app/util/FileOpener.java @@ -2,9 +2,10 @@ package io.xpipe.app.util; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.AppPrefs; -import io.xpipe.core.store.FileNames; +import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.CommandControl; import io.xpipe.core.process.OsType; +import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileSystem; import lombok.SneakyThrows; @@ -14,12 +15,7 @@ import java.util.function.Consumer; public class FileOpener { - public static void openInDefaultApplication(FileSystem.FileEntry entry) { - var editor = AppPrefs.get().externalEditor().getValue(); - if (editor == null || !editor.isSelectable()) { - return; - } - + public static void openWithAnyApplication(FileSystem.FileEntry entry) { var file = entry.getPath(); var key = entry.getPath().hashCode() + entry.getFileSystem().hashCode(); FileBridge.get() @@ -29,7 +25,21 @@ public class FileOpener { () -> { return entry.getFileSystem().openInput(file); }, - () -> entry.getFileSystem().openOutput(file), + (size) -> entry.getFileSystem().openOutput(file, size), + s -> openWithAnyApplication(s)); + } + + public static void openInDefaultApplication(FileSystem.FileEntry entry) { + var file = entry.getPath(); + var key = entry.getPath().hashCode() + entry.getFileSystem().hashCode(); + FileBridge.get() + .openIO( + FileNames.getFileName(file), + key, + () -> { + return entry.getFileSystem().openInput(file); + }, + (size) -> entry.getFileSystem().openOutput(file, size), s -> openInDefaultApplication(s)); } @@ -48,18 +58,18 @@ public class FileOpener { () -> { return entry.getFileSystem().openInput(file); }, - () -> entry.getFileSystem().openOutput(file), + (size) -> entry.getFileSystem().openOutput(file, size), FileOpener::openInTextEditor); } - public static void openInTextEditor(String file) { + public static void openInTextEditor(String localFile) { var editor = AppPrefs.get().externalEditor().getValue(); if (editor == null) { return; } try { - editor.launch(Path.of(file).toRealPath()); + editor.launch(Path.of(localFile).toRealPath()); } catch (Exception e) { ErrorEvent.fromThrowable(e) .expected() @@ -67,18 +77,40 @@ public class FileOpener { } } - public static void openInDefaultApplication(String file) { - try (var pc = LocalShell.getShell().start()) { - if (pc.getOsType().equals(OsType.WINDOWS)) { - pc.executeSimpleCommand("start \"\" \"" + file + "\""); - } else if (pc.getOsType().equals(OsType.LINUX)) { - pc.executeSimpleCommand("xdg-open \"" + file + "\""); - } else { - pc.executeSimpleCommand("open \"" + file + "\""); + public static void openWithAnyApplication(String localFile) { + try { + switch (OsType.getLocal()) { + case OsType.Windows windows -> { + var cmd = CommandBuilder.of().add("rundll32.exe", "shell32.dll,OpenAs_RunDLL", localFile); + LocalShell.getShell().executeSimpleCommand(cmd); + } + case OsType.Linux linux -> { + LocalShell.getShell().executeSimpleCommand("mimeopen -a " + + LocalShell.getShell().getShellDialect().fileArgument(localFile)); + } + case OsType.MacOs macOs -> { + throw new UnsupportedOperationException(); + } } } catch (Exception e) { ErrorEvent.fromThrowable(e) - .description("Unable to open file " + file) + .description("Unable to open file " + localFile) + .handle(); + } + } + + public static void openInDefaultApplication(String localFile) { + try (var pc = LocalShell.getShell().start()) { + if (pc.getOsType().equals(OsType.WINDOWS)) { + pc.executeSimpleCommand("start \"\" \"" + localFile + "\""); + } else if (pc.getOsType().equals(OsType.LINUX)) { + pc.executeSimpleCommand("xdg-open \"" + localFile + "\""); + } else { + pc.executeSimpleCommand("open \"" + localFile + "\""); + } + } catch (Exception e) { + ErrorEvent.fromThrowable(e) + .description("Unable to open file " + localFile) .handle(); } } diff --git a/app/src/main/java/io/xpipe/app/util/Hyperlinks.java b/app/src/main/java/io/xpipe/app/util/Hyperlinks.java index 02c927562..98456973c 100644 --- a/app/src/main/java/io/xpipe/app/util/Hyperlinks.java +++ b/app/src/main/java/io/xpipe/app/util/Hyperlinks.java @@ -4,11 +4,9 @@ import io.xpipe.app.issue.ErrorEvent; public class Hyperlinks { - public static final String DOCUMENTATION = "https://docs.xpipe.io"; public static final String GITHUB = "https://github.com/xpipe-io/xpipe"; public static final String PRIVACY = "https://docs.xpipe.io/privacy-policy"; public static final String EULA = "https://docs.xpipe.io/end-user-license-agreement"; - public static final String PREVIEW = "https://docs.xpipe.io/preview"; public static final String SECURITY = "https://docs.xpipe.io/security"; public static final String DISCORD = "https://discord.gg/8y89vS8cRb"; public static final String SLACK = "https://join.slack.com/t/XPipe/shared_invite/zt-1awjq0t5j-5i4UjNJfNe1VN4b_auu6Cg"; diff --git a/app/src/main/java/io/xpipe/app/util/Indicator.java b/app/src/main/java/io/xpipe/app/util/Indicator.java new file mode 100644 index 000000000..657574597 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/Indicator.java @@ -0,0 +1,106 @@ +package io.xpipe.app.util; + +import javafx.animation.AnimationTimer; +import javafx.scene.Group; +import javafx.scene.Parent; +import javafx.scene.paint.Color; +import javafx.scene.shape.*; + +public class Indicator { + + private static final Color lColor = Color.rgb(0x66, 0x66, 0x66); + private static final Color rColor = Color.rgb(0x0f, 0x87, 0xc3); + + private static final PathElement[] ELEMS = new PathElement[] { + new MoveTo(9.2362945,19.934046), + new CubicCurveTo(-1.3360939,-0.28065, -1.9963146,-1.69366, -1.9796182,-2.95487), + new CubicCurveTo(-0.1152909,-1.41268, -0.5046634,-3.07081, -1.920768,-3.72287), + new CubicCurveTo(-1.4711631,-0.77284, -3.4574873,-0.11153, -4.69154031,-1.40244), + new CubicCurveTo(-1.30616123,-1.40422, -0.5308003,-4.1855799, 1.46313121,-4.4219799), + new CubicCurveTo(1.4290018,-0.25469, 3.1669517,-0.0875, 4.1676818,-1.36207), + new CubicCurveTo(0.9172241,-1.12206, 0.9594176,-2.63766, 1.0685793,-4.01259), + new CubicCurveTo(0.4020299,-1.95732999, 3.2823027,-2.72818999, 4.5638567,-1.15760999), + new CubicCurveTo(1.215789,1.31824999, 0.738899,3.90740999, -1.103778,4.37267999), + new CubicCurveTo(-1.3972543,0.40868, -3.0929979,0.0413, -4.2208253,1.16215), + new CubicCurveTo(-1.3524806,1.26423, -1.3178578,3.29187, -1.1086673,4.9895199), + new CubicCurveTo(0.167826,1.28946, 1.0091133,2.5347, 2.3196964,2.86608), + new CubicCurveTo(1.6253079,0.53477, 3.4876372,0.45004, 5.0294052,-0.30121), + new CubicCurveTo(1.335829,-0.81654, 1.666839,-2.49408, 1.717756,-3.9432), + new CubicCurveTo(0.08759,-1.1232899, 0.704887,-2.3061299, 1.871843,-2.5951699), + new CubicCurveTo(1.534558,-0.50726, 3.390804,0.62784, 3.467269,2.28631), + new CubicCurveTo(0.183147,1.4285099, -0.949563,2.9179999, -2.431156,2.9383699), + new CubicCurveTo(-1.390597,0.17337, -3.074035,0.18128, -3.971365,1.45069), + new CubicCurveTo(-0.99314,1.271, -0.676157,2.98683, -1.1715,4.43018), + new CubicCurveTo(-0.518248,1.11436, -1.909118,1.63902, -3.0700005,1.37803), + new ClosePath() + }; + + static { + for(int i = 1; i < ELEMS.length; ++i) { + ELEMS[i].setAbsolute(false); + } + } + + private final Path left; + private final Path right; + private final Group g; + private final int steps; + + private boolean fw = true; + private int step = 0; + + public Indicator(int ticksPerCycle, double scale) { + this.steps = ticksPerCycle; + + left = new Path(ELEMS); + right = new Path(ELEMS); + + left.setScaleX(scale); + left.setScaleY(scale); + right.setScaleX(-1 * scale); + right.setScaleY(-1 * scale); + right.setTranslateX(7.266 * scale); + right.setOpacity(0.0); + + left.setStroke(null); + right.setStroke(null); + left.setFill(lColor); + right.setFill(rColor); + + g = new Group(left, right); + + AnimationTimer timer = new AnimationTimer() { + @Override + public void handle(long l) { + step(); + } + }; + timer.start(); + } + + public Parent getNode() { + return g; + } + + private void step() { + double lOpacity, rOpacity; + + step += fw ? 1 : -1; + + if(step == steps) { + fw = false; + lOpacity = 0.0; + rOpacity = 1.0; + } else if(step == 0) { + fw = true; + lOpacity = 1.0; + rOpacity = 0.0; + } else { + lOpacity = 1.0 * (steps - step) / steps; + rOpacity = 1.0 * step / steps; + } + + left.setOpacity(lOpacity); + right.setOpacity(rOpacity); + } +} diff --git a/app/src/main/java/io/xpipe/app/util/LicenseProvider.java b/app/src/main/java/io/xpipe/app/util/LicenseProvider.java index c89c62ada..d34590ccc 100644 --- a/app/src/main/java/io/xpipe/app/util/LicenseProvider.java +++ b/app/src/main/java/io/xpipe/app/util/LicenseProvider.java @@ -21,7 +21,7 @@ public abstract class LicenseProvider { public void init(ModuleLayer layer) { INSTANCE = ServiceLoader.load(layer, LicenseProvider.class).stream() .map(ServiceLoader.Provider::get) - .findFirst().orElseThrow(() -> ExtensionException.corrupt("Missing license provider.")); + .findFirst().orElseThrow(() -> ExtensionException.corrupt("Missing license provider")); } @Override diff --git a/app/src/main/java/io/xpipe/app/util/LockChangeAlert.java b/app/src/main/java/io/xpipe/app/util/LockChangeAlert.java index b216c3b63..3a6324351 100644 --- a/app/src/main/java/io/xpipe/app/util/LockChangeAlert.java +++ b/app/src/main/java/io/xpipe/app/util/LockChangeAlert.java @@ -6,7 +6,7 @@ import io.xpipe.app.core.AppWindowHelper; import io.xpipe.app.fxcomps.impl.LabelComp; import io.xpipe.app.fxcomps.impl.SecretFieldComp; import io.xpipe.app.prefs.AppPrefs; -import io.xpipe.core.util.SecretValue; +import io.xpipe.core.util.InPlaceSecretValue; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleObjectProperty; import javafx.scene.control.Alert; @@ -18,29 +18,19 @@ import java.util.Objects; public class LockChangeAlert { public static void show() { - var prop1 = new SimpleObjectProperty(); - var prop2 = new SimpleObjectProperty(); + var prop1 = new SimpleObjectProperty(); + var prop2 = new SimpleObjectProperty(); AppWindowHelper.showBlockingAlert(alert -> { alert.setTitle(AppI18n.get("lockCreationAlertTitle")); alert.setHeaderText(AppI18n.get("lockCreationAlertHeader")); alert.setAlertType(Alert.AlertType.CONFIRMATION); - var label1 = new LabelComp(AppI18n.observable("password")).createRegion(); - var p1 = new SecretFieldComp(prop1) { - @Override - protected SecretValue encrypt(char[] c) { - return SecretHelper.encryptInPlace(c); - } - }.createRegion(); + var label1 = new LabelComp(AppI18n.observable("passphrase")).createRegion(); + var p1 = new SecretFieldComp(prop1).createRegion(); p1.setStyle("-fx-border-width: 1px"); - var label2 = new LabelComp(AppI18n.observable("repeatPassword")).createRegion(); - var p2 = new SecretFieldComp(prop2) { - @Override - protected SecretValue encrypt(char[] c) { - return SecretHelper.encryptInPlace(c); - } - }.createRegion(); + var label2 = new LabelComp(AppI18n.observable("repeatPassphrase")).createRegion(); + var p2 = new SecretFieldComp(prop2).createRegion(); p1.setStyle("-fx-border-width: 1px"); var content = new VBox(label1, p1, new Spacer(15), label2, p2); diff --git a/app/src/main/java/io/xpipe/app/util/LockedSecretValue.java b/app/src/main/java/io/xpipe/app/util/LockedSecretValue.java deleted file mode 100644 index 0b1e78ee9..000000000 --- a/app/src/main/java/io/xpipe/app/util/LockedSecretValue.java +++ /dev/null @@ -1,51 +0,0 @@ -package io.xpipe.app.util; - -import com.fasterxml.jackson.annotation.JsonTypeName; -import io.xpipe.app.prefs.AppPrefs; -import io.xpipe.core.util.AesSecretValue; -import io.xpipe.core.util.DefaultSecretValue; -import io.xpipe.core.util.SecretValue; -import lombok.EqualsAndHashCode; -import lombok.experimental.SuperBuilder; -import lombok.extern.jackson.Jacksonized; - -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.SecretKeySpec; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.KeySpec; -import java.util.Random; - -@JsonTypeName("locked") -@SuperBuilder -@Jacksonized -@EqualsAndHashCode(callSuper = true) -public class LockedSecretValue extends AesSecretValue { - - public LockedSecretValue(char[] secret) { - super(secret); - } - - @Override - public SecretValue inPlace() { - return new DefaultSecretValue(getSecret()); - } - - @Override - public String toString() { - return ""; - } - - protected SecretKey getAESKey(int keysize) throws NoSuchAlgorithmException, InvalidKeySpecException { - var chars = AppPrefs.get().getLockPassword().getValue() != null - ? AppPrefs.get().getLockPassword().getValue().getSecret() - : new char[0]; - SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); - var salt = new byte[16]; - new Random(keysize).nextBytes(salt); - KeySpec spec = new PBEKeySpec(chars, salt, 8192, keysize); - return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES"); - } -} diff --git a/app/src/main/java/io/xpipe/app/util/OptionsBuilder.java b/app/src/main/java/io/xpipe/app/util/OptionsBuilder.java index 0a0a5a593..28cbe9574 100644 --- a/app/src/main/java/io/xpipe/app/util/OptionsBuilder.java +++ b/app/src/main/java/io/xpipe/app/util/OptionsBuilder.java @@ -1,11 +1,12 @@ package io.xpipe.app.util; import atlantafx.base.controls.Spacer; +import io.xpipe.app.comp.base.ToggleSwitchComp; import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.GuiDialog; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.impl.*; -import io.xpipe.core.util.SecretValue; +import io.xpipe.core.util.InPlaceSecretValue; import javafx.beans.property.*; import javafx.beans.value.ObservableValue; import javafx.geometry.Orientation; @@ -13,10 +14,13 @@ import javafx.scene.control.Label; import javafx.scene.layout.Region; import net.synedra.validatorfx.Check; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Supplier; public class OptionsBuilder { @@ -93,6 +97,10 @@ public class OptionsBuilder { entries.add(entry); } + public OptionsBuilder nameAndDescription(String key) { + return name(key).description(key + "Description"); + } + public OptionsBuilder sub(OptionsBuilder builder) { return sub(builder, null); } @@ -134,7 +142,7 @@ public class OptionsBuilder { return this; } - public OptionsBuilder decorate(Check c) { + public OptionsBuilder check(Check c) { lastCompHeadReference.apply(s -> c.decorates(s.get())); return this; } @@ -144,6 +152,16 @@ public class OptionsBuilder { return this; } + public OptionsBuilder disable(ObservableValue b) { + lastCompHeadReference.disable(b); + return this; + } + + public OptionsBuilder hide(ObservableValue b) { + lastCompHeadReference.hide(b); + return this; + } + public OptionsBuilder disable(boolean b) { lastCompHeadReference.disable(new SimpleBooleanProperty(b)); return this; @@ -152,25 +170,30 @@ public class OptionsBuilder { public OptionsBuilder nonNull() { var e = lastNameReference; var p = props.get(props.size() - 1); - return decorate(Validator.nonNull(ownValidator, e, p)); + return check(Validator.nonNull(ownValidator, e, p)); + } + + public OptionsBuilder withValidator(Consumer val) { + val.accept(ownValidator); + return this; } public OptionsBuilder nonEmpty() { var e = lastNameReference; var p = props.get(props.size() - 1); - return decorate(Validator.nonEmpty(ownValidator, e, (ReadOnlyListProperty) p)); + return check(Validator.nonEmpty(ownValidator, e, (ReadOnlyListProperty) p)); } public OptionsBuilder validate() { var e = lastNameReference; var p = props.get(props.size() - 1); - return decorate(Validator.nonNull(ownValidator, e, p)); + return check(Validator.nonNull(ownValidator, e, p)); } public OptionsBuilder nonNull(Validator v) { var e = lastNameReference; var p = props.get(props.size() - 1); - return decorate(Validator.nonNull(v, e, p)); + return check(Validator.nonNull(v, e, p)); } private void pushComp(Comp comp) { @@ -194,6 +217,13 @@ public class OptionsBuilder { } public OptionsBuilder addToggle(Property prop) { + var comp = new ToggleSwitchComp(prop, null); + pushComp(comp); + props.add(prop); + return this; + } + + public OptionsBuilder addYesNoToggle(Property prop) { var comp = new ToggleGroupComp<>( prop, new SimpleObjectProperty<>(Map.of( @@ -207,6 +237,27 @@ public class OptionsBuilder { return addString(prop, false); } + public OptionsBuilder addPath(Property prop) { + var string = new SimpleStringProperty(prop.getValue() != null ? prop.getValue().toString() : null); + var comp = new TextFieldComp(string, true); + string.addListener((observable, oldValue, newValue) -> { + if (newValue == null) { + prop.setValue(null); + return; + } + + try { + var p = Path.of(newValue); + prop.setValue(p); + } catch (InvalidPathException ignored) { + + } + }); + pushComp(comp); + props.add(prop); + return this; + } + public OptionsBuilder addString(Property prop, boolean lazy) { var comp = new TextFieldComp(prop, lazy); pushComp(comp); @@ -263,7 +314,7 @@ public class OptionsBuilder { return this; } - public OptionsBuilder addSecret(Property prop) { + public OptionsBuilder addSecret(Property prop) { var comp = new SecretFieldComp(prop); pushComp(comp); props.add(prop); diff --git a/app/src/main/java/io/xpipe/app/util/PasswordLockSecretValue.java b/app/src/main/java/io/xpipe/app/util/PasswordLockSecretValue.java new file mode 100644 index 000000000..06e434113 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/PasswordLockSecretValue.java @@ -0,0 +1,45 @@ +package io.xpipe.app.util; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.core.util.AesSecretValue; +import io.xpipe.core.util.InPlaceSecretValue; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import javax.crypto.SecretKey; +import java.security.spec.InvalidKeySpecException; + +@JsonTypeName("locked") +@SuperBuilder +@Jacksonized +@EqualsAndHashCode(callSuper = true) +public class PasswordLockSecretValue extends AesSecretValue { + + public PasswordLockSecretValue(char[] secret) { + super(secret); + } + + @Override + protected int getIterationCount() { + return 8192; + } + + @Override + public InPlaceSecretValue inPlace() { + return new InPlaceSecretValue(getSecret()); + } + + @Override + public String toString() { + return ""; + } + + protected SecretKey getAESKey() throws InvalidKeySpecException { + var chars = AppPrefs.get().getLockPassword().getValue() != null + ? AppPrefs.get().getLockPassword().getValue().getSecret() + : new char[0]; + return getSecretKey(chars); + } +} diff --git a/app/src/main/java/io/xpipe/app/util/PlatformState.java b/app/src/main/java/io/xpipe/app/util/PlatformState.java index c03b8734b..4a117c717 100644 --- a/app/src/main/java/io/xpipe/app/util/PlatformState.java +++ b/app/src/main/java/io/xpipe/app/util/PlatformState.java @@ -3,6 +3,8 @@ package io.xpipe.app.util; import io.xpipe.app.core.AppBundledFonts; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.issue.TrackEvent; +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.core.process.OsType; import javafx.application.Platform; import javafx.scene.input.Clipboard; import lombok.Getter; @@ -80,6 +82,23 @@ public enum PlatformState { // Check if we have no fonts and set properties to load bundled ones AppBundledFonts.init(); + if (AppPrefs.get() != null) { + var s = AppPrefs.get().uiScale().getValue(); + if (s != null) { + var i = Math.min(300, Math.max(25, s)); + var value = i + "%"; + switch (OsType.getLocal()) { + case OsType.Linux linux -> { + System.setProperty("glass.gtk.uiScale",value); + } + case OsType.Windows windows -> { + System.setProperty("glass.win.uiScale",value); + } + default -> {} + } + } + } + try { CountDownLatch latch = new CountDownLatch(1); Platform.setImplicitExit(false); diff --git a/app/src/main/java/io/xpipe/app/util/ProxyManagerProviderImpl.java b/app/src/main/java/io/xpipe/app/util/ProxyManagerProviderImpl.java index 0d12f406d..df660b7ac 100644 --- a/app/src/main/java/io/xpipe/app/util/ProxyManagerProviderImpl.java +++ b/app/src/main/java/io/xpipe/app/util/ProxyManagerProviderImpl.java @@ -1,16 +1,9 @@ package io.xpipe.app.util; import io.xpipe.app.core.AppI18n; -import io.xpipe.app.core.AppProperties; import io.xpipe.app.core.AppWindowHelper; -import io.xpipe.app.prefs.AppPrefs; -import io.xpipe.app.update.AppDownloads; -import io.xpipe.app.update.AppInstaller; -import io.xpipe.core.store.FileNames; import io.xpipe.core.process.ShellControl; -import io.xpipe.core.util.ModuleHelper; import io.xpipe.core.util.ProxyManagerProvider; -import io.xpipe.core.util.XPipeInstallation; import javafx.scene.control.Alert; import java.util.Optional; @@ -31,43 +24,11 @@ public class ProxyManagerProviderImpl extends ProxyManagerProvider { @Override public Optional checkCompatibility(ShellControl s) throws Exception { - var version = ModuleHelper.isImage() ? AppProperties.get().getVersion() : AppDownloads.getLatestVersion(); - - if (AppPrefs.get().developerDisableConnectorInstallationVersionCheck().get()) { - return Optional.of(AppI18n.get("versionCheckOverride")); - } - - var defaultInstallationExecutable = FileNames.join( - XPipeInstallation.getDefaultInstallationBasePath(s), - XPipeInstallation.getDaemonExecutablePath(s.getOsType())); - if (!s.getShellDialect() - .createFileExistsCommand(s, defaultInstallationExecutable) - .executeAndCheck()) { - return Optional.of(AppI18n.get("noInstallationFound")); - } - - var installationVersion = XPipeInstallation.queryInstallationVersion(s, defaultInstallationExecutable); - if (!version.equals(installationVersion)) { - return Optional.of(AppI18n.get("installationVersionMismatch", version, installationVersion)); - } - return Optional.empty(); } @Override public boolean setup(ShellControl s) throws Exception { - var message = checkCompatibility(s); - if (message.isPresent()) { - if (showAlert()) { - var version = - ModuleHelper.isImage() ? AppProperties.get().getVersion() : AppDownloads.getLatestVersion(); - AppInstaller.installOnRemoteMachine(s, version); - return true; - } - - return false; - } else { - return true; - } + return true; } } diff --git a/app/src/main/java/io/xpipe/app/util/ScanAlert.java b/app/src/main/java/io/xpipe/app/util/ScanAlert.java index daeaa9192..412c31813 100644 --- a/app/src/main/java/io/xpipe/app/util/ScanAlert.java +++ b/app/src/main/java/io/xpipe/app/util/ScanAlert.java @@ -1,27 +1,26 @@ package io.xpipe.app.util; +import io.xpipe.app.comp.base.DialogComp; import io.xpipe.app.comp.base.ListSelectorComp; -import io.xpipe.app.comp.base.MultiStepComp; import io.xpipe.app.comp.store.StoreViewState; import io.xpipe.app.core.AppI18n; -import io.xpipe.app.core.AppWindowHelper; import io.xpipe.app.ext.ScanProvider; import io.xpipe.app.fxcomps.Comp; -import io.xpipe.app.fxcomps.CompStructure; -import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.impl.DataStoreChoiceComp; +import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.core.store.ShellStore; import javafx.application.Platform; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleListProperty; -import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.*; +import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; +import javafx.geometry.Insets; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; +import javafx.stage.Stage; import java.util.ArrayList; import java.util.List; @@ -42,7 +41,7 @@ public class ScanAlert { private static void showForShellStore(DataStoreEntry initial) { show(initial, (DataStoreEntry entry) -> { try (var sc = ((ShellStore) entry.getStore()).control().start()) { - if (!sc.getShellDialect().isSupportedShell()) { + if (!sc.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) { return null; } @@ -66,106 +65,111 @@ public class ScanAlert { }); } - private static void show( - DataStoreEntry initialStore, Function> applicable - ) { - var entry = new SimpleObjectProperty>(); - var selected = new SimpleListProperty(FXCollections.observableArrayList()); + private static class Dialog extends DialogComp { - var loading = new SimpleBooleanProperty(); - Platform.runLater(() -> { - var stage = AppWindowHelper.sideWindow(AppI18n.get("scanAlertTitle"), window -> { - return new MultiStepComp() { + private final DataStoreEntryRef initialStore; + private final Function> applicable; + private final Stage window; + private final ObjectProperty> entry; + private final ListProperty selected = new SimpleListProperty(FXCollections.observableArrayList()); + private final BooleanProperty busy = new SimpleBooleanProperty(); - private final StackPane stackPane = new StackPane(); + private Dialog(Stage window, DataStoreEntryRef entry, Function> applicable) { + this.window = window; + this.initialStore = entry; + this.entry = new SimpleObjectProperty<>(entry); + this.applicable = applicable; + } - { - stackPane.getStyleClass().add("scan-list"); + @Override + protected void finish() { + ThreadHelper.runAsync(() -> { + if (entry.get() == null) { + return; + } + + Platform.runLater(() -> { + window.close(); + }); + + BooleanScope.execute(busy, () -> { + entry.get().get().setExpanded(true); + + var copy = new ArrayList<>(selected); + for (var a : copy) { + // If the user decided to remove the selected entry + // while the scan is running, just return instantly + if (!DataStorage.get().getStoreEntriesSet().contains(entry.get().get())) { + return; + } + + try { + a.getScanner().run(); + } catch (Exception ex) { + ErrorEvent.fromThrowable(ex).handle(); + } } + }); + }); + } - @Override - protected List setup() { - return List.of(new Entry(AppI18n.observable("a"), new Step<>() { - @Override - public CompStructure createBase() { - var b = new OptionsBuilder().name("scanAlertChoiceHeader").description("scanAlertChoiceHeaderDescription").addComp( - new DataStoreChoiceComp<>(DataStoreChoiceComp.Mode.OTHER, null, entry, ShellStore.class, store1 -> true, - StoreViewState.get().getAllConnectionsCategory()).disable( - new SimpleBooleanProperty(initialStore != null))).name("scanAlertHeader").description( - "scanAlertHeaderDescription").addComp(Comp.of(() -> stackPane).vgrow()).buildComp().prefWidth(500).prefHeight( - 600).styleClass("window-content").apply(struc -> { - VBox.setVgrow(struc.get().getChildren().get(1), ALWAYS); - }).createStructure().get(); + @Override + protected ObservableValue busy() { + return busy; + } - entry.addListener((observable, oldValue, newValue) -> { - selected.clear(); - stackPane.getChildren().clear(); + @Override + public Comp content() { + StackPane stackPane = new StackPane(); + stackPane.getStyleClass().add("scan-list"); - if (newValue == null) { - return; - } + var b = new OptionsBuilder().name("scanAlertChoiceHeader").description("scanAlertChoiceHeaderDescription").addComp( + new DataStoreChoiceComp<>(DataStoreChoiceComp.Mode.OTHER, null, entry, ShellStore.class, store1 -> true, + StoreViewState.get().getAllConnectionsCategory()).disable( + new SimpleBooleanProperty(initialStore != null))).name("scanAlertHeader").description( + "scanAlertHeaderDescription").addComp(Comp.of(() -> stackPane).vgrow()).buildComp().prefWidth(500).prefHeight( + 650).apply(struc -> { + VBox.setVgrow(struc.get().getChildren().get(1), ALWAYS); + }).padding(new Insets(20)); - ThreadHelper.runAsync(() -> { - BooleanScope.execute(loading, () -> { - var a = applicable.apply(entry.get().get()); + SimpleChangeListener.apply(entry, newValue -> { + selected.clear(); + stackPane.getChildren().clear(); - Platform.runLater(() -> { - if (a == null) { - window.close(); - return; - } + if (newValue == null) { + return; + } - selected.setAll(a.stream().filter(scanOperation -> scanOperation.isDefaultSelected() && !scanOperation.isDisabled()).toList()); - var r = new ListSelectorComp(a, - scanOperation -> AppI18n.get(scanOperation.getNameKey()), - selected,scanOperation -> scanOperation.isDisabled(), - a.size() > 3).createRegion(); - stackPane.getChildren().add(r); - }); - }); - }); - }); + ThreadHelper.runAsync(() -> { + BooleanScope.execute(busy, () -> { + var a = applicable.apply(entry.get().get()); - entry.set(initialStore != null ? initialStore.ref() : null); - return new SimpleCompStructure<>(b); - } - })); - } - - @Override - protected void finish() { - ThreadHelper.runAsync(() -> { - if (entry.get() == null) { + Platform.runLater(() -> { + if (a == null) { + window.close(); return; } - Platform.runLater(() -> { - window.close(); - }); - - BooleanScope.execute(loading, () -> { - entry.get().get().setExpanded(true); - - var copy = new ArrayList<>(selected); - for (var a : copy) { - // If the user decided to remove the selected entry - // while the scan is running, just return instantly - if (!DataStorage.get().getStoreEntriesSet().contains(entry.get().get())) { - return; - } - - try { - a.getScanner().run(); - } catch (Exception ex) { - ErrorEvent.fromThrowable(ex).handle(); - } - } - }); + selected.setAll(a.stream().filter(scanOperation -> scanOperation.isDefaultSelected() && !scanOperation.isDisabled()).toList()); + var r = new ListSelectorComp(a, + scanOperation -> AppI18n.get(scanOperation.getNameKey()), + selected,scanOperation -> scanOperation.isDisabled(), + a.size() > 3).createRegion(); + stackPane.getChildren().add(r); }); - } - }; - }, false, loading); - stage.show(); - }); + }); + }); + }); + + return b; + } + } + + private static void show( + DataStoreEntry initialStore, Function> applicable + ) { + DialogComp.showWindow("scanAlertTitle", stage -> + new Dialog(stage, initialStore != null ? initialStore.ref() : null, + applicable)); } } diff --git a/app/src/main/java/io/xpipe/app/util/ScriptHelper.java b/app/src/main/java/io/xpipe/app/util/ScriptHelper.java index 4403c0a7f..d6ed7da0a 100644 --- a/app/src/main/java/io/xpipe/app/util/ScriptHelper.java +++ b/app/src/main/java/io/xpipe/app/util/ScriptHelper.java @@ -3,11 +3,13 @@ package io.xpipe.app.util; import io.xpipe.app.issue.TrackEvent; import io.xpipe.core.process.*; import io.xpipe.core.store.FileNames; +import io.xpipe.core.util.FailableFunction; import io.xpipe.core.util.SecretValue; import lombok.SneakyThrows; import java.util.List; import java.util.Random; +import java.util.UUID; public class ScriptHelper { @@ -25,12 +27,7 @@ public class ScriptHelper { } } - public static String constructInitFile(ShellControl processControl, List init, String toExecuteInShell, TerminalInitScriptConfig config) - throws Exception { - return constructInitFile(processControl.getShellDialect(), processControl, init, toExecuteInShell, config); - } - - public static String constructInitFile(ShellDialect t, ShellControl processControl, List init, String toExecuteInShell, TerminalInitScriptConfig config) + public static String constructTerminalInitFile(ShellDialect t, ShellControl processControl, FailableFunction workingDirectory, List init, String toExecuteInShell, TerminalInitScriptConfig config) throws Exception { String nl = t.getNewLine().getNewLineString(); var content = ""; @@ -56,6 +53,13 @@ public class ScriptHelper { content += nl + t.changeTitleCommand(config.getDisplayName()) + nl; } + if (workingDirectory != null) { + var wd = workingDirectory.apply(processControl); + if (wd != null) { + content += t.getCdCommand(wd) + nl; + } + } + content += nl + String.join(nl, init.stream().filter(s -> s != null).toList()) + nl; if (toExecuteInShell != null) { @@ -76,7 +80,7 @@ public class ScriptHelper { @SneakyThrows public static String getExecScriptFile(ShellControl processControl, String fileEnding) { var fileName = "exec-" + getScriptId(); - var temp = processControl.getSubTemporaryDirectory(); + var temp = processControl.getSystemTemporaryDirectory(); return FileNames.join(temp, fileName + "." + fileEnding); } @@ -88,7 +92,7 @@ public class ScriptHelper { @SneakyThrows public static String createExecScript(ShellDialect type, ShellControl processControl, String content) { var fileName = "exec-" + getScriptId(); - var temp = processControl.getSubTemporaryDirectory(); + var temp = processControl.getSystemTemporaryDirectory(); var file = FileNames.join(temp, fileName + "." + type.getScriptFileEnding()); return createExecScript(type, processControl, file, content); } @@ -97,7 +101,7 @@ public class ScriptHelper { public static String createExecScript(ShellDialect type, ShellControl processControl, String file, String content) { content = type.prepareScriptContent(content); - TrackEvent.withTrace("proc", "Writing exec script") + TrackEvent.withTrace("Writing exec script") .tag("file", file) .tag("content", content) .handle(); @@ -109,12 +113,35 @@ public class ScriptHelper { return file; } - public static String createAskpassScript(SecretValue pass, ShellControl parent, boolean forceExecutable, String errorMessage) + public static String createRemoteAskpassScript(ShellControl parent, UUID requestId, String prefix) throws Exception { - return createAskpassScript(pass != null ? List.of(pass) : List.of(), parent, forceExecutable, errorMessage); + var type = parent.getShellDialect(); + + // Fix for powershell as there are permission issues when executing a powershell askpass script + if (parent.getShellDialect().equals(ShellDialects.POWERSHELL)) { + type = parent.getOsType().equals(OsType.WINDOWS) ? ShellDialects.CMD : ShellDialects.SH; + } + + var fileName = "exec-" + getScriptId() + "." + type.getScriptFileEnding(); + var temp = parent.getSystemTemporaryDirectory(); + var file = FileNames.join(temp, fileName); + if (type != parent.getShellDialect()) { + try (var sub = parent.subShell(type).start()) { + var content = sub.getShellDialect().getAskpass().prepareStderrPassthroughContent(sub, requestId, prefix); + return createExecScript(sub.getShellDialect(), sub, file, content); + } + } else { + var content = parent.getShellDialect().getAskpass().prepareStderrPassthroughContent(parent, requestId, prefix); + return createExecScript(parent.getShellDialect(), parent, file, content); + } } - public static String createAskpassScript(List pass, ShellControl parent, boolean forceExecutable, String errorMessage) + public static String createAskpassPreparedScript(SecretValue pass, ShellControl parent, boolean forceExecutable, String errorMessage) + throws Exception { + return createAskpassPreparedScript(pass != null ? List.of(pass) : List.of(), parent, forceExecutable, errorMessage); + } + + public static String createAskpassPreparedScript(List pass, ShellControl parent, boolean forceExecutable, String errorMessage) throws Exception { var scriptType = parent.getShellDialect(); @@ -123,18 +150,18 @@ public class ScriptHelper { scriptType = parent.getOsType().equals(OsType.WINDOWS) ? ShellDialects.CMD : ShellDialects.SH; } - return createAskpassScript(pass, parent, scriptType, errorMessage); + return createAskpassPreparedScript(pass, parent, scriptType, errorMessage); } - private static String createAskpassScript(List pass, ShellControl parent, ShellDialect type, String errorMessage) + private static String createAskpassPreparedScript(List pass, ShellControl parent, ShellDialect type, String errorMessage) throws Exception { var fileName = "exec-" + getScriptId() + "." + type.getScriptFileEnding(); - var temp = parent.getSubTemporaryDirectory(); + var temp = parent.getSystemTemporaryDirectory(); var file = FileNames.join(temp, fileName); if (type != parent.getShellDialect()) { try (var sub = parent.subShell(type).start()) { - var content = sub.getShellDialect() - .prepareAskpassContent( + var content = sub.getShellDialect().getAskpass() + .prepareFixedContent( sub, file, pass.stream() @@ -143,8 +170,8 @@ public class ScriptHelper { return createExecScript(sub.getShellDialect(), sub, file, content); } } else { - var content = parent.getShellDialect() - .prepareAskpassContent( + var content = parent.getShellDialect().getAskpass() + .prepareFixedContent( parent, file, pass.stream() diff --git a/app/src/main/java/io/xpipe/app/util/SecretHelper.java b/app/src/main/java/io/xpipe/app/util/SecretHelper.java deleted file mode 100644 index 07d20cb39..000000000 --- a/app/src/main/java/io/xpipe/app/util/SecretHelper.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.xpipe.app.util; - -import io.xpipe.app.prefs.AppPrefs; -import io.xpipe.core.util.DefaultSecretValue; -import io.xpipe.core.util.EncryptedSecretValue; - -public class SecretHelper { - - public static EncryptedSecretValue encryptInPlace(char[] c) { - if (c == null) { - return null; - } - - return new DefaultSecretValue(c); - } - - public static EncryptedSecretValue encryptInPlace(String s) { - if (s == null) { - return null; - } - - return encryptInPlace(s.toCharArray()); - } - - public static EncryptedSecretValue encrypt(char[] c) { - if (c == null) { - return null; - } - - if (AppPrefs.get() != null && AppPrefs.get().getLockPassword().getValue() != null) { - return new LockedSecretValue(c); - } - - return new DefaultSecretValue(c); - } - - public static EncryptedSecretValue encrypt(String s) { - if (s == null) { - return null; - } - - return encrypt(s.toCharArray()); - } -} diff --git a/app/src/main/java/io/xpipe/app/util/SecretManager.java b/app/src/main/java/io/xpipe/app/util/SecretManager.java index a5999bf65..84196f526 100644 --- a/app/src/main/java/io/xpipe/app/util/SecretManager.java +++ b/app/src/main/java/io/xpipe/app/util/SecretManager.java @@ -1,33 +1,41 @@ package io.xpipe.app.util; +import io.xpipe.core.process.CountDown; +import io.xpipe.core.util.SecretReference; import io.xpipe.core.util.SecretValue; import io.xpipe.core.util.UuidHelper; -import lombok.AllArgsConstructor; -import lombok.Value; import java.util.*; public class SecretManager { - @Value - @AllArgsConstructor - public static class SecretReference { + private static final Map secrets = new HashMap<>(); + private static final Set progress = new HashSet<>(); - UUID secretId; - int subId; - - public SecretReference(Object store) { - this.secretId = UuidHelper.generateFromObject(store); - this.subId = 0; - } - - public SecretReference(Object store, int sub) { - this.secretId = UuidHelper.generateFromObject(store); - this.subId = sub; - } + public static Optional getProgress(UUID requestId, UUID storeId) { + return progress.stream() + .filter(secretQueryProgress -> secretQueryProgress.getRequestId().equals(requestId) && + secretQueryProgress.getStoreId().equals(storeId)) + .findFirst(); } - private static final Map passwords = new HashMap<>(); + public static Optional getProgress(UUID requestId) { + return progress.stream() + .filter(secretQueryProgress -> secretQueryProgress.getRequestId().equals(requestId)) + .findFirst(); + } + + public static SecretQueryProgress expectCacheablePrompt(UUID request, UUID storeId, CountDown countDown) { + var p = new SecretQueryProgress(request, storeId, List.of(SecretQuery.prompt(true)), SecretQuery.prompt(false), countDown); + progress.add(p); + return p; + } + + public static SecretQueryProgress expectAskpass(UUID request, UUID storeId, List suppliers, SecretQuery fallback, CountDown countDown) { + var p = new SecretQueryProgress(request, storeId, suppliers, fallback, countDown); + progress.add(p); + return p; + } public static boolean shouldCacheForPrompt(String prompt) { var l = prompt.toLowerCase(Locale.ROOT); @@ -38,44 +46,29 @@ public class SecretManager { return true; } - public static SecretValue retrieve(SecretRetrievalStrategy strategy, String prompt, Object key) throws Exception { - return retrieve(strategy, prompt,key, 0); - } - public static SecretValue retrieve(SecretRetrievalStrategy strategy, String prompt, Object key, int sub) throws Exception { - var ref = new SecretReference(key, sub); - if (strategy == null) { + public static SecretValue retrieve(SecretRetrievalStrategy strategy, String prompt, Object store, int sub) { + if (!strategy.expectsPrompt()) { return null; } - if (strategy.shouldCache() && passwords.containsKey(ref)) { - return passwords.get(ref); - } - - var pass = strategy.retrieve(prompt, ref.getSecretId(), ref.getSubId()); - if (pass == null) { - return null; - } - - if (strategy.shouldCache()) { - passwords.put(ref, pass); - } - return pass; + var p = expectAskpass(UUID.randomUUID(), UuidHelper.generateFromObject(store, sub), List.of(strategy.query()), SecretQuery.prompt(false), CountDown.of()); + return p.process(prompt); } public static void clearAll(Object store) { var id = UuidHelper.generateFromObject(store); - passwords.entrySet().removeIf(secretReferenceSecretValueEntry -> secretReferenceSecretValueEntry.getKey().getSecretId().equals(id)); + secrets.entrySet().removeIf(secretReferenceSecretValueEntry -> secretReferenceSecretValueEntry.getKey().getSecretId().equals(id)); } public static void clear(SecretReference ref) { - passwords.remove(ref); + secrets.remove(ref); } public static void set(SecretReference ref, SecretValue value) { - passwords.put(ref, value); + secrets.put(ref, value); } public static Optional get(SecretReference ref) { - return Optional.ofNullable(passwords.get(ref)); + return Optional.ofNullable(secrets.get(ref)); } } diff --git a/app/src/main/java/io/xpipe/app/util/SecretQuery.java b/app/src/main/java/io/xpipe/app/util/SecretQuery.java new file mode 100644 index 000000000..e193d91ac --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/SecretQuery.java @@ -0,0 +1,29 @@ +package io.xpipe.app.util; + +public interface SecretQuery { + + static SecretQuery prompt(boolean cache) { + return new SecretQuery() { + @Override + public SecretQueryResult query(String prompt) { + return AskpassAlert.queryRaw(prompt); + } + + @Override + public boolean cache() { + return cache; + } + + @Override + public boolean retryOnFail() { + return true; + } + }; + } + + SecretQueryResult query(String prompt); + + boolean cache(); + + boolean retryOnFail(); +} diff --git a/app/src/main/java/io/xpipe/app/util/SecretQueryProgress.java b/app/src/main/java/io/xpipe/app/util/SecretQueryProgress.java new file mode 100644 index 000000000..d077d14ac --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/SecretQueryProgress.java @@ -0,0 +1,93 @@ +package io.xpipe.app.util; + +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.core.process.CountDown; +import io.xpipe.core.util.SecretReference; +import io.xpipe.core.util.SecretValue; +import lombok.Getter; + +import java.util.*; + +@Getter +public class SecretQueryProgress { + + private final UUID requestId; + private final UUID storeId; + private final List suppliers; + private final SecretQuery fallback; + private final List seenPrompts; + private final CountDown countDown; + private boolean requestCancelled; + + public SecretQueryProgress(UUID requestId, UUID storeId, List suppliers, SecretQuery fallback, CountDown countDown) { + this.requestId = requestId; + this.storeId = storeId; + this.suppliers = suppliers; + this.fallback = fallback; + this.countDown = countDown; + this.seenPrompts = new ArrayList<>(); + } + + public SecretValue process(String prompt) { + // Cancel early + if (requestCancelled) { + return null; + } + + var seenBefore = seenPrompts.contains(prompt); + if (!seenBefore) { + seenPrompts.add(prompt); + } + + var firstSeenIndex = seenPrompts.indexOf(prompt); + if (firstSeenIndex >= suppliers.size()) { + countDown.pause(); + var r = fallback.query(prompt); + countDown.resume(); + if (r.isCancelled()) { + requestCancelled = true; + return null; + } + return r.getSecret(); + } + + var ref = new SecretReference(storeId, firstSeenIndex); + var sup = suppliers.get(firstSeenIndex); + var shouldCache = sup.cache() && SecretManager.shouldCacheForPrompt(prompt) && !AppPrefs.get().dontCachePasswords().get(); + var wasLastPrompt = firstSeenIndex == seenPrompts.size() - 1; + + // Clear cache if secret was wrong/queried again + // Check whether this is actually the last prompt seen as it might happen that + // previous prompts get rolled back again when one further down is wrong + if (seenBefore && shouldCache && wasLastPrompt) { + SecretManager.clear(ref); + } + + // If we supplied a wrong secret and cannot retry, cancel the entire request + if (seenBefore && wasLastPrompt && !sup.retryOnFail()) { + requestCancelled = true; + return null; + } + + if (shouldCache) { + var cached = SecretManager.get(ref); + if (cached.isPresent()) { + return cached.get(); + } + } + + countDown.pause(); + var r = sup.query(prompt); + countDown.resume(); + + if (r.isCancelled()) { + requestCancelled = true; + return null; + } + + if (shouldCache) { + SecretManager.set(ref, r.getSecret()); + } + return r.getSecret(); + } +} diff --git a/app/src/main/java/io/xpipe/app/util/SecretQueryResult.java b/app/src/main/java/io/xpipe/app/util/SecretQueryResult.java new file mode 100644 index 000000000..a617dcb98 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/SecretQueryResult.java @@ -0,0 +1,11 @@ +package io.xpipe.app.util; + +import io.xpipe.core.util.SecretValue; +import lombok.Value; + +@Value +public class SecretQueryResult { + + SecretValue secret; + boolean cancelled; +} diff --git a/app/src/main/java/io/xpipe/app/util/SecretRetrievalStrategy.java b/app/src/main/java/io/xpipe/app/util/SecretRetrievalStrategy.java index 646f36355..69e3e21b8 100644 --- a/app/src/main/java/io/xpipe/app/util/SecretRetrievalStrategy.java +++ b/app/src/main/java/io/xpipe/app/util/SecretRetrievalStrategy.java @@ -1,26 +1,20 @@ package io.xpipe.app.util; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.storage.DataStoreSecret; import io.xpipe.core.store.LocalStore; -import io.xpipe.core.process.ProcessOutputException; -import io.xpipe.core.util.SecretValue; +import io.xpipe.core.util.InPlaceSecretValue; import lombok.Builder; -import lombok.Getter; import lombok.Value; import lombok.extern.jackson.Jacksonized; -import java.util.UUID; -import java.util.function.Supplier; - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = SecretRetrievalStrategy.None.class), - @JsonSubTypes.Type(value = SecretRetrievalStrategy.Reference.class), @JsonSubTypes.Type(value = SecretRetrievalStrategy.InPlace.class), @JsonSubTypes.Type(value = SecretRetrievalStrategy.Prompt.class), @JsonSubTypes.Type(value = SecretRetrievalStrategy.CustomCommand.class), @@ -28,82 +22,56 @@ import java.util.function.Supplier; }) public interface SecretRetrievalStrategy { - SecretValue retrieve(String displayName, UUID id, int sub) throws Exception; + SecretQuery query(); - boolean isLocalAskpassCompatible(); + default boolean expectsPrompt() { + return true; + } - boolean shouldCache(); @JsonTypeName("none") class None implements SecretRetrievalStrategy { @Override - public SecretValue retrieve(String displayName, UUID id, int sub) { + public SecretQuery query() { return null; } - @Override - public boolean isLocalAskpassCompatible() { - return true; - } - - @Override - public boolean shouldCache() { - return false; - } - } - - @JsonTypeName("reference") - class Reference implements SecretRetrievalStrategy { - - @JsonIgnore - private final Supplier supplier; - - public Reference(Supplier supplier) { - this.supplier = supplier; - } - - @Override - public SecretValue retrieve(String displayName, UUID id, int sub) { - return supplier.get(); - } - - @Override - public boolean isLocalAskpassCompatible() { - return false; - } - - @Override - public boolean shouldCache() { + public boolean expectsPrompt() { return false; } } @JsonTypeName("inPlace") - @Getter @Builder @Value @Jacksonized class InPlace implements SecretRetrievalStrategy { - SecretValue value; + DataStoreSecret value; - public InPlace(SecretValue value) { + public InPlace(DataStoreSecret value) { this.value = value; } @Override - public SecretValue retrieve(String displayName, UUID id, int sub) { - return value; - } + public SecretQuery query() { + return new SecretQuery() { + @Override + public SecretQueryResult query(String prompt) { + return new SecretQueryResult(value != null ? value.getInternalSecret() : InPlaceSecretValue.of(""), false); + } - @Override - public boolean shouldCache() { - return false; - } - @Override - public boolean isLocalAskpassCompatible() { - return false; + @Override + public boolean cache() { + return false; + } + + @Override + public boolean retryOnFail() { + return false; + } + }; } } @@ -111,17 +79,23 @@ public interface SecretRetrievalStrategy { class Prompt implements SecretRetrievalStrategy { @Override - public SecretValue retrieve(String displayName, UUID id, int sub) { - return AskpassAlert.query(displayName, UUID.randomUUID(), id, sub); - } + public SecretQuery query() { + return new SecretQuery() { + @Override + public SecretQueryResult query(String prompt) { + return AskpassAlert.queryRaw(prompt); + } - @Override - public boolean shouldCache() { - return true; - } - @Override - public boolean isLocalAskpassCompatible() { - return true; + @Override + public boolean cache() { + return true; + } + + @Override + public boolean retryOnFail() { + return true; + } + }; } } @@ -134,27 +108,33 @@ public interface SecretRetrievalStrategy { String key; @Override - public SecretValue retrieve(String displayName, UUID id, int sub) throws Exception { - var cmd = AppPrefs.get().passwordManagerString(key); - if (cmd == null) { - return null; - } + public SecretQuery query() { + return new SecretQuery() { + @Override + public SecretQueryResult query(String prompt) { + var cmd = AppPrefs.get().passwordManagerString(key); + if (cmd == null) { + return null; + } - try (var cc = new LocalStore().control().command(cmd).start()) { - return SecretHelper.encrypt(cc.readStdoutOrThrow()); - } catch (ProcessOutputException ex) { - throw ErrorEvent.unreportable(ProcessOutputException.withPrefix("Unable to retrieve password with command " + cmd, ex)); - } - } + try (var cc = new LocalStore().control().command(cmd).start()) { + return new SecretQueryResult(InPlaceSecretValue.of(cc.readStdoutOrThrow()), false); + } catch (Exception ex) { + ErrorEvent.fromThrowable("Unable to retrieve password with command " + cmd, ex).handle(); + return new SecretQueryResult(null, true); + } + } - @Override - public boolean shouldCache() { - return false; - } + @Override + public boolean cache() { + return false; + } - @Override - public boolean isLocalAskpassCompatible() { - return false; + @Override + public boolean retryOnFail() { + return false; + } + }; } } @@ -167,20 +147,28 @@ public interface SecretRetrievalStrategy { String command; @Override - public SecretValue retrieve(String displayName, UUID id, int sub) throws Exception { - try (var cc = new LocalStore().control().command(command).start()) { - return SecretHelper.encrypt(cc.readStdoutOrThrow()); - } - } + public SecretQuery query() { + return new SecretQuery() { + @Override + public SecretQueryResult query(String prompt) { + try (var cc = new LocalStore().control().command(command).start()) { + return new SecretQueryResult(InPlaceSecretValue.of(cc.readStdoutOrThrow()), false); + } catch (Exception ex) { + ErrorEvent.fromThrowable("Unable to retrieve password with command " + command, ex).handle(); + return new SecretQueryResult(null, true); + } + } - @Override - public boolean shouldCache() { - return false; - } + @Override + public boolean cache() { + return false; + } - @Override - public boolean isLocalAskpassCompatible() { - return false; + @Override + public boolean retryOnFail() { + return false; + } + }; } } } diff --git a/app/src/main/java/io/xpipe/app/util/SecretRetrievalStrategyHelper.java b/app/src/main/java/io/xpipe/app/util/SecretRetrievalStrategyHelper.java index 44d0a33b7..8471a337b 100644 --- a/app/src/main/java/io/xpipe/app/util/SecretRetrievalStrategyHelper.java +++ b/app/src/main/java/io/xpipe/app/util/SecretRetrievalStrategyHelper.java @@ -6,24 +6,30 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.SecretFieldComp; import io.xpipe.app.fxcomps.impl.TextFieldComp; import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.storage.DataStoreSecret; import javafx.beans.property.Property; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import org.kordamp.ikonli.javafx.FontIcon; +import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; public class SecretRetrievalStrategyHelper { private static OptionsBuilder inPlace(Property p) { - var secretProperty = - new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getValue() : null); + var original = p.getValue() != null ? p.getValue().getValue() : null; + var secretProperty = new SimpleObjectProperty<>(p.getValue() != null && p.getValue().getValue() != null ? p.getValue().getValue().getInternalSecret() : null); return new OptionsBuilder() .addComp(new SecretFieldComp(secretProperty), secretProperty) .bind( () -> { - return new SecretRetrievalStrategy.InPlace(secretProperty.getValue()); + var newSecret = secretProperty.get(); + var changed = !Arrays.equals( + newSecret != null ? newSecret.getSecret() : new char[0], + original != null ? original.getSecret() : new char[0]); + return new SecretRetrievalStrategy.InPlace(changed ? new DataStoreSecret(secretProperty.getValue()) : original); }, p); } @@ -34,7 +40,7 @@ public class SecretRetrievalStrategyHelper { var content = new HorizontalComp(List.of( new TextFieldComp(keyProperty).apply(struc -> struc.get().setPromptText("Password key")).hgrow(), new ButtonComp(null, new FontIcon("mdomz-settings"), () -> { - AppPrefs.get().selectCategory(5); + AppPrefs.get().selectCategory(9); App.getApp().getStage().requestFocus(); }) .grow(false, true))) diff --git a/app/src/main/java/io/xpipe/app/util/SmoothScroll.java b/app/src/main/java/io/xpipe/app/util/SmoothScroll.java deleted file mode 100644 index 5c19f6d28..000000000 --- a/app/src/main/java/io/xpipe/app/util/SmoothScroll.java +++ /dev/null @@ -1,107 +0,0 @@ -package io.xpipe.app.util; - -import javafx.animation.Animation; -import javafx.animation.KeyFrame; -import javafx.animation.Timeline; -import javafx.event.EventHandler; -import javafx.geometry.Bounds; -import javafx.geometry.Orientation; -import javafx.scene.Node; -import javafx.scene.control.ListView; -import javafx.scene.control.ScrollBar; -import javafx.scene.control.TableView; -import javafx.scene.input.MouseEvent; -import javafx.scene.input.ScrollEvent; -import javafx.util.Duration; - -import java.util.function.Function; - -public class SmoothScroll { - - private static ScrollBar getScrollbarComponent(Node no, Orientation orientation) { - Node n = no.lookup(".scroll-bar"); - if (n instanceof final ScrollBar bar) { - if (bar.getOrientation().equals(orientation)) { - return bar; - } - } - - return null; - } - - public static void smoothScrollingListView(Node n, double speed) { - smoothScrollingListView(n, speed, Orientation.VERTICAL, bounds -> bounds.getHeight()); - } - - public static void smoothHScrollingListView(ListView listView, double speed) { - smoothScrollingListView(listView, speed, Orientation.HORIZONTAL, bounds -> bounds.getHeight()); - } - - private static void smoothScrollingListView( - Node n, double speed, Orientation orientation, Function sizeFunc) { - ((TableView) n).skinProperty().addListener((observable, oldValue, newValue) -> { - if (newValue != null) { - ScrollBar scrollBar = getScrollbarComponent(n, orientation); - if (scrollBar == null) { - return; - } - scrollBar.setUnitIncrement(1); - final double[] frictions = { - 0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, - 0.00003, 0.00001 - }; - final double[] pushes = {speed}; - final double[] derivatives = new double[frictions.length]; - final double[] lastVPos = {0}; - Timeline timeline = new Timeline(); - final EventHandler dragHandler = event -> timeline.stop(); - final EventHandler scrollHandler = event -> { - scrollBar.valueProperty().set(lastVPos[0]); - if (event.getEventType() == ScrollEvent.SCROLL) { - double direction = event.getDeltaY() > 0 ? -1 : 1; - for (int i = 0; i < pushes.length; i++) { - derivatives[i] += direction * pushes[i]; - } - if (timeline.getStatus() == Animation.Status.STOPPED) { - timeline.play(); - } - } - event.consume(); - }; - if (scrollBar.getParent() != null) { - scrollBar.getParent().addEventHandler(MouseEvent.DRAG_DETECTED, dragHandler); - scrollBar.getParent().addEventHandler(ScrollEvent.ANY, scrollHandler); - } - scrollBar.parentProperty().addListener((o, oldVal, newVal) -> { - if (oldVal != null) { - oldVal.removeEventHandler(MouseEvent.DRAG_DETECTED, dragHandler); - oldVal.removeEventHandler(ScrollEvent.ANY, scrollHandler); - } - if (newVal != null) { - newVal.addEventHandler(MouseEvent.DRAG_DETECTED, dragHandler); - newVal.addEventHandler(ScrollEvent.ANY, scrollHandler); - } - }); - - timeline.getKeyFrames().add(new KeyFrame(Duration.millis(3), (event) -> { - for (int i = 0; i < derivatives.length; i++) { - derivatives[i] *= frictions[i]; - } - for (int i = 1; i < derivatives.length; i++) { - derivatives[i] += derivatives[i - 1]; - } - double dy = derivatives[derivatives.length - 1]; - double size = sizeFunc.apply(scrollBar.getLayoutBounds()); - scrollBar.valueProperty().set(Math.min(Math.max(scrollBar.getValue() + dy / size, 0), 1)); - lastVPos[0] = scrollBar.getValue(); - if (Math.abs(dy) < 1) { - if (Math.abs(dy) < 0.001) { - timeline.stop(); - } - } - })); - timeline.setCycleCount(Animation.INDEFINITE); - } - }); - } -} \ No newline at end of file diff --git a/app/src/main/java/io/xpipe/app/util/TerminalHelper.java b/app/src/main/java/io/xpipe/app/util/TerminalLauncher.java similarity index 57% rename from app/src/main/java/io/xpipe/app/util/TerminalHelper.java rename to app/src/main/java/io/xpipe/app/util/TerminalLauncher.java index 75c18a334..ba4835dc7 100644 --- a/app/src/main/java/io/xpipe/app/util/TerminalHelper.java +++ b/app/src/main/java/io/xpipe/app/util/TerminalLauncher.java @@ -6,16 +6,17 @@ import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.ExternalTerminalType; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; -import io.xpipe.core.process.ProcessControl; -import io.xpipe.core.process.TerminalInitScriptConfig; +import io.xpipe.core.process.*; -public class TerminalHelper { +import java.io.IOException; +import java.util.UUID; +public class TerminalLauncher { public static void open(String title, ProcessControl cc) throws Exception { - open(null, title, cc); + open(null, title, null, cc); } - public static void open(DataStoreEntry entry, String title, ProcessControl cc) throws Exception { + public static void open(DataStoreEntry entry, String title, String directory, ProcessControl cc) throws Exception { var type = AppPrefs.get().terminalType().getValue(); if (type == null) { throw ErrorEvent.unreportable(new IllegalStateException(AppI18n.get("noTerminalSet"))); @@ -27,10 +28,21 @@ public class TerminalHelper { : ""; var cleanTitle = (title != null ? title : entry != null ? entry.getName() : "?"); var adjustedTitle = prefix + cleanTitle; - var file = ScriptHelper.createLocalExecScript(cc.prepareTerminalOpen(new TerminalInitScriptConfig(adjustedTitle, type.shouldClear(), color != null))); - var config = new ExternalTerminalType.LaunchConfiguration(entry != null ? color : null, adjustedTitle, cleanTitle, file); + var terminalConfig = new TerminalInitScriptConfig( + adjustedTitle, + type.shouldClear() && AppPrefs.get().clearTerminalOnInit().get(), + color != null); + + var request = UUID.randomUUID(); + var d = LocalShell.getShell().getShellDialect(); + var launcherScript = d.terminalLauncherScript(request, adjustedTitle); + var preparationScript = ScriptHelper.createLocalExecScript(launcherScript); + var config = new ExternalTerminalType.LaunchConfiguration(entry != null ? color : null, adjustedTitle, cleanTitle, preparationScript, + d); + var latch = TerminalLauncherManager.submitAsync(request,cc,terminalConfig,directory); try { type.launch(config); + latch.await(); } catch (Exception ex) { throw ErrorEvent.unreportable(ex); } diff --git a/app/src/main/java/io/xpipe/app/util/TerminalLauncherManager.java b/app/src/main/java/io/xpipe/app/util/TerminalLauncherManager.java new file mode 100644 index 000000000..b9cbc26d8 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/TerminalLauncherManager.java @@ -0,0 +1,111 @@ +package io.xpipe.app.util; + +import io.xpipe.beacon.ClientException; +import io.xpipe.beacon.ServerException; +import io.xpipe.core.process.ProcessControl; +import io.xpipe.core.process.TerminalInitScriptConfig; +import lombok.Setter; +import lombok.Value; +import lombok.experimental.NonFinal; + +import java.nio.file.Path; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; + +public class TerminalLauncherManager { + + private static final Map entries = new ConcurrentHashMap<>(); + + private static void prepare(ProcessControl processControl, TerminalInitScriptConfig config, String directory, Entry entry) { + try { + var file = ScriptHelper.createLocalExecScript( + processControl.prepareTerminalOpen(config, directory != null ? var1 -> directory : null)); + entry.setResult(new ResultSuccess(Path.of(file))); + } catch (Exception e) { + entry.setResult(new ResultFailure(e)); + } + } + + public static void submitSync(UUID request, + ProcessControl processControl, TerminalInitScriptConfig config, String directory + ) { + var entry = new Entry(request, processControl, config, directory, null); + entries.put(request, entry); + prepare(processControl, config, directory, entry); + } + + public static CountDownLatch submitAsync(UUID request, + ProcessControl processControl, TerminalInitScriptConfig config, String directory + ) { + var entry = new Entry(request, processControl, config, directory, null); + entries.put(request, entry); + var latch = new CountDownLatch(1); + ThreadHelper.runAsync(() -> { + prepare(processControl, config, directory, entry); + latch.countDown(); + }); + return latch; + } + + public static Path waitForCompletion(UUID request) throws ClientException, ServerException { + var e = entries.get(request); + if (e == null) { + throw new ClientException("Unknown launch request " + request); + } + + while (true) { + if (e.result == null) { + ThreadHelper.sleep(10); + continue; + } + + var r = e.getResult(); + if (r instanceof ResultFailure failure) { + entries.remove(request); + throw new ServerException(failure.getThrowable()); + } + + return ((ResultSuccess) r).getTargetScript(); + } + } + + public static Path performLaunch(UUID request) throws ClientException { + var e = entries.remove(request); + if (e == null) { + throw new ClientException("Unknown launch request " + request); + } + + if (!(e.result instanceof ResultSuccess)) { + throw new ClientException("Invalid launch request state " + request); + } + + return ((ResultSuccess) e.getResult()).getTargetScript(); + } + + public static interface Result {} + + @Value + public static class Entry { + + UUID request; + ProcessControl processControl; + TerminalInitScriptConfig config; + String workingDirectory; + + @Setter + @NonFinal + Result result; + } + + @Value + public static class ResultSuccess implements Result { + Path targetScript; + } + + @Value + public static class ResultFailure implements Result { + Throwable throwable; + } +} diff --git a/app/src/main/java/io/xpipe/app/util/Translatable.java b/app/src/main/java/io/xpipe/app/util/Translatable.java index c3ff046ef..a24f81b02 100644 --- a/app/src/main/java/io/xpipe/app/util/Translatable.java +++ b/app/src/main/java/io/xpipe/app/util/Translatable.java @@ -1,51 +1,8 @@ package io.xpipe.app.util; -import javafx.beans.binding.StringBinding; -import javafx.beans.binding.StringExpression; import javafx.beans.value.ObservableValue; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.util.StringConverter; public interface Translatable { - static StringConverter stringConverter() { - return new StringConverter<>() { - @Override - public String toString(T t) { - return t == null ? null : t.toTranslatedString(); - } - - @Override - public T fromString(String string) { - throw new AssertionError(); - } - }; - } - - static StringExpression asTranslatedString(ObservableValue observableValue) { - return new StringBinding() { - { - super.bind(observableValue); - } - - @Override - public void dispose() { - super.unbind(observableValue); - } - - @Override - protected String computeValue() { - final T value = observableValue.getValue(); - return (value == null) ? "null" : value.toTranslatedString(); - } - - @Override - public ObservableList> getDependencies() { - return FXCollections.singletonObservableList(observableValue); - } - }; - } - - String toTranslatedString(); + ObservableValue toTranslatedString(); } diff --git a/app/src/main/java/io/xpipe/app/util/UnlockAlert.java b/app/src/main/java/io/xpipe/app/util/UnlockAlert.java index 4f16a3520..b1c49b17a 100644 --- a/app/src/main/java/io/xpipe/app/util/UnlockAlert.java +++ b/app/src/main/java/io/xpipe/app/util/UnlockAlert.java @@ -1,19 +1,20 @@ package io.xpipe.app.util; -import io.xpipe.app.core.AppI18n; -import io.xpipe.app.core.AppWindowHelper; +import io.xpipe.app.core.*; import io.xpipe.app.fxcomps.impl.SecretFieldComp; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.AppPrefs; -import io.xpipe.core.util.SecretValue; +import io.xpipe.core.util.InPlaceSecretValue; +import javafx.application.Platform; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.scene.control.Alert; import javafx.scene.layout.VBox; +import javafx.stage.Stage; public class UnlockAlert { - public static void showIfNeeded() { + public static void showIfNeeded() throws Exception { if (AppPrefs.get().getLockCrypt().getValue() == null || AppPrefs.get().getLockCrypt().getValue().isEmpty()) { return; @@ -23,25 +24,35 @@ public class UnlockAlert { return; } + PlatformState.initPlatformOrThrow(); + AppI18n.init(); + AppStyle.init(); + AppTheme.init(); + while (true) { - var pw = new SimpleObjectProperty(); + var pw = new SimpleObjectProperty(); var canceled = new SimpleBooleanProperty(); AppWindowHelper.showBlockingAlert(alert -> { alert.setTitle(AppI18n.get("unlockAlertTitle")); alert.setHeaderText(AppI18n.get("unlockAlertHeader")); alert.setAlertType(Alert.AlertType.CONFIRMATION); - var p1 = new SecretFieldComp(pw) { - @Override - protected SecretValue encrypt(char[] c) { - return SecretHelper.encryptInPlace(c); - } - }.createRegion(); - p1.setStyle("-fx-border-width: 1px"); + var text = new SecretFieldComp(pw).createRegion(); + text.setStyle("-fx-border-width: 1px"); - var content = new VBox(p1); + var content = new VBox(text); content.setSpacing(5); alert.getDialogPane().setContent(content); + + var stage = (Stage) alert.getDialogPane().getScene().getWindow(); + stage.setAlwaysOnTop(true); + + alert.setOnShown(event -> { + stage.requestFocus(); + // Wait 1 pulse before focus so that the scene can be assigned to text + Platform.runLater(text::requestFocus); + event.consume(); + }); }) .filter(b -> b.getButtonData().isDefaultButton()) .ifPresentOrElse(t -> {}, () -> canceled.set(true)); diff --git a/app/src/main/java/io/xpipe/app/util/Validator.java b/app/src/main/java/io/xpipe/app/util/Validator.java index 93a209552..98a197303 100644 --- a/app/src/main/java/io/xpipe/app/util/Validator.java +++ b/app/src/main/java/io/xpipe/app/util/Validator.java @@ -11,8 +11,27 @@ import javafx.collections.ObservableList; import net.synedra.validatorfx.Check; import net.synedra.validatorfx.ValidationResult; +import java.nio.file.Files; +import java.nio.file.Path; + public interface Validator { + public static Check absolutePath(Validator v, ObservableValue s) { + return v.createCheck().dependsOn("val", s).withMethod(c -> { + if (c.get("val") == null || !((Path) c.get("val")).isAbsolute()) { + c.error(AppI18n.get("app.notAnAbsolutePath")); + } + }); + } + + public static Check directory(Validator v, ObservableValue s) { + return v.createCheck().dependsOn("val", s).withMethod(c -> { + if (c.get("val") instanceof Path p && (!Files.exists(p) || !Files.isDirectory(p))) { + c.error(AppI18n.get("app.notADirectory")); + } + }); + } + static Check nonNull(Validator v, ObservableValue name, ObservableValue s) { return v.createCheck().dependsOn("val", s).withMethod(c -> { if (c.get("val") == null) { diff --git a/app/src/main/java/io/xpipe/app/util/VaultKeySecretValue.java b/app/src/main/java/io/xpipe/app/util/VaultKeySecretValue.java new file mode 100644 index 000000000..43fd437ab --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/VaultKeySecretValue.java @@ -0,0 +1,45 @@ +package io.xpipe.app.util; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.core.util.AesSecretValue; +import io.xpipe.core.util.InPlaceSecretValue; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import javax.crypto.SecretKey; +import java.security.spec.InvalidKeySpecException; + +@JsonTypeName("vault") +@SuperBuilder +@Jacksonized +@EqualsAndHashCode(callSuper = true) +public class VaultKeySecretValue extends AesSecretValue { + + public VaultKeySecretValue(char[] secret) { + super(secret); + } + + @Override + protected int getIterationCount() { + return 8192; + } + + @Override + public InPlaceSecretValue inPlace() { + return new InPlaceSecretValue(getSecret()); + } + + @Override + public String toString() { + return ""; + } + + protected SecretKey getAESKey() throws InvalidKeySpecException { + var chars = DataStorage.get() != null + ? DataStorage.get().getVaultKey().toCharArray() + : new char[0]; + return getSecretKey(chars); + } +} diff --git a/app/src/main/java/module-info.java b/app/src/main/java/module-info.java index c44e94fb7..26a388e9a 100644 --- a/app/src/main/java/module-info.java +++ b/app/src/main/java/module-info.java @@ -10,7 +10,7 @@ import io.xpipe.app.storage.DataStateProviderImpl; import io.xpipe.app.storage.StorageJacksonModule; import io.xpipe.app.util.LicenseProvider; import io.xpipe.app.util.ProxyManagerProviderImpl; -import io.xpipe.app.util.TerminalHelper; +import io.xpipe.app.util.TerminalLauncher; import io.xpipe.core.util.DataStateProvider; import io.xpipe.core.util.ModuleLayerLoader; import io.xpipe.core.util.ProxyFunction; @@ -44,20 +44,11 @@ open module io.xpipe.app { requires org.slf4j; requires atlantafx.base; requires org.ocpsoft.prettytime; - requires com.dlsc.preferencesfx; requires com.vladsch.flexmark; - requires com.vladsch.flexmark_util_data; - requires com.vladsch.flexmark_util_ast; - requires com.vladsch.flexmark_util_sequence; requires com.fasterxml.jackson.core; requires com.fasterxml.jackson.databind; - requires org.fxmisc.richtext; - requires org.fxmisc.flowless; requires net.synedra.validatorfx; - requires org.fxmisc.undofx; - requires org.fxmisc.wellbehavedfx; requires org.kordamp.ikonli.feather; - requires org.reactfx; requires io.xpipe.modulefs; requires io.xpipe.core; requires static lombok; @@ -70,10 +61,8 @@ open module io.xpipe.app { requires javafx.media; requires javafx.web; requires javafx.graphics; - requires com.jfoenix; requires org.kordamp.ikonli.javafx; requires org.kordamp.ikonli.material; - requires org.controlsfx.controls; requires io.sentry; requires io.xpipe.beacon; requires org.kohsuke.github; @@ -82,8 +71,6 @@ open module io.xpipe.app { requires java.management; requires jdk.management; requires jdk.management.agent; - requires com.jthemedetector; - requires versioncompare; requires net.steppschuh.markdowngenerator; // Required by extensions @@ -107,10 +94,9 @@ open module io.xpipe.app { // For debugging requires jdk.jdwp.agent; requires org.kordamp.ikonli.core; - requires static io.xpipe.api; uses MessageExchangeImpl; - uses TerminalHelper; + uses TerminalLauncher; uses io.xpipe.app.ext.ActionProvider; uses EventHandler; uses PrefsProvider; @@ -154,7 +140,8 @@ open module io.xpipe.app { RenameStoreExchangeImpl, ListStoresExchangeImpl, StoreAddExchangeImpl, - AskpassExchangeImpl, + AskpassExchangeImpl, TerminalWaitExchangeImpl, + TerminalLaunchExchangeImpl, QueryStoreExchangeImpl, WriteStreamExchangeImpl, ReadStreamExchangeImpl, diff --git a/app/src/main/resources/io/xpipe/app/resources/img/Hips-dark.svg b/app/src/main/resources/io/xpipe/app/resources/img/Hips-dark.svg new file mode 100644 index 000000000..836aa17b7 --- /dev/null +++ b/app/src/main/resources/io/xpipe/app/resources/img/Hips-dark.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + diff --git a/app/src/main/resources/io/xpipe/app/resources/lang/dscreation_en.properties b/app/src/main/resources/io/xpipe/app/resources/lang/dscreation_en.properties index 1f14f08ac..11c7daa6e 100644 --- a/app/src/main/resources/io/xpipe/app/resources/lang/dscreation_en.properties +++ b/app/src/main/resources/io/xpipe/app/resources/lang/dscreation_en.properties @@ -29,7 +29,7 @@ reportIssue=Report Issue reportIssueDescription=Open the integrated issue reporter usefulActions=Useful actions stored=Saved -troubleshootingOptions=Troubleshooting options +troubleshootingOptions=Troubleshooting tools troubleshoot=Troubleshoot other=Other remote=Remote File diff --git a/app/src/main/resources/io/xpipe/app/resources/lang/intro_en.properties b/app/src/main/resources/io/xpipe/app/resources/lang/intro_en.properties index 590baeafe..e6acb9a6c 100644 --- a/app/src/main/resources/io/xpipe/app/resources/lang/intro_en.properties +++ b/app/src/main/resources/io/xpipe/app/resources/lang/intro_en.properties @@ -13,7 +13,7 @@ dataSourceIntroText=Text data sources contain readable text that can\ncome in a dataSourceIntroBinary=Binary data sources contain binary data. They\ncan be used when the data should be handled and preserved byte by byte. dataSourceIntroCollection=Collection data sources contain multiple sub data sources. \nExamples are zip files or file system directories. storeIntroTitle=Connection Hub -storeIntroDescription=Here you can manage all your local and remote shell connections in one place +storeIntroDescription=Here you can manage all your local and remote shell connections in one place. To start off, you can quickly detect available connections automatically and choose which ones to add. storeStreamDescription=Stream connections produce raw byte data\nthat can be used to construct data sources from. storeMachineDescription=To start off, here you can quickly detect available\nconnections automatically and choose which ones to add. detectConnections=Search for connections diff --git a/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties b/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties index bd72e263c..7ef2c0c91 100644 --- a/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties +++ b/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties @@ -1,16 +1,23 @@ -connectionTimeout=Connection timeout +connectionTimeout=Connection start timeout connectionTimeoutDescription=The time in seconds to wait for a response before considering a connection to be timed out. If some of your remote systems take long to connect, you can try to increase this value. +useBundledTools=Use bundled OpenSSH tools +useBundledToolsDescription=Prefer to use bundled version of the openssh client instead of your locally installed one.\n\nThis version is usually more up-to-date than the ones shipped on your system and might support additional features. This also removes the requirement to have these tools installed in the first place.\n\nRequires restart to apply. connections=Connections appearance=Appearance integrations=Integrations uiOptions=UI Options theme=Theme +themeDescription=You preferred theme confirmGitShareTitle=Confirm git sharing confirmGitShareHeader=This will copy the file into your git vault and commit your changes. Do you want to continue? gitShareFileTooltip=Add file to the git vault data directory so that it is automatically synced.\n\nThis action can only be used when the git vault is enabled in the settings. performanceMode=Performance mode performanceModeDescription=Disables all visual effects that are not required to improve the application performance. -editorProgram=Default Program +dontAcceptNewHostKeys=Don't accept new SSH host keys automatically +dontAcceptNewHostKeysDescription=XPipe will automatically accept host keys by default from systems where your SSH client has no known host key already saved. If any known host key has changed however, it will refuse to connect unless you accept the new one.\n\nDisabling this behavior allows you to check all host keys, even if there is no conflict initially. +uiScale=UI Scale +uiScaleDescription=A custom scaling value that can be set independently of your system-wide display scale. Values are in percent, so e.g. value of 150 will result in a UI scale of 150%.\n\nRequires a restart to apply. +editorProgram=Editor Program editorProgramDescription=The default text editor to use when editing any kind of text data. windowOpacity=Window opacity customTerminalPlaceholder=myterminal -e $CMD @@ -29,9 +36,10 @@ thirdParty=Open source notices eulaDescription=Read the End User License Agreement for the XPipe application thirdPartyDescription=View the open source licenses of third-party libraries workspaceLock=Master passphrase -enableGitStorage=Enable git storage +enableGitStorage=Enable git synchronization sharing=Sharing -enableGitStorageDescription=When enabled, XPipe will initialize a git repository for the storage and commit any changes to it. Note that this requires git to be installed and might slow down loading and saving operations.\n\nAny categories that should be synced must be explicitly designated as shared.\n\nRequires a restart to apply. +sync=Synchronization +enableGitStorageDescription=When enabled, XPipe will initialize a git repository for the connection data storage and commit any changes to it. Note that this requires git to be installed and might slow down loading and saving operations.\n\nAny categories that should be synced must be explicitly designated as shared.\n\nRequires a restart to apply. storageGitRemote=Git remote URL storageGitRemoteDescription=When set, XPipe will automatically pull any changes when loading and push any changes to the remote repository when saving.\n\nThis allows you to share your configuration data between multiple XPipe installations. Both HTTP and SSH URLs are supported. Note that this might slow down loading and saving operations.\n\nRequires a restart to apply. vault=Vault @@ -44,6 +52,10 @@ windowOptions=Window Options saveWindowLocation=Save window location saveWindowLocationDescription=Controls whether the window coordinates should be saved and restored on restarts. startupShutdown=Startup / Shutdown +condenseConnectionDisplay=Condense connection display +condenseConnectionDisplayDescription=Make every top level connection take a less vertical space to allow for a more condensed connection list. +enforceWindowModality=Enforce window modality +enforceWindowModalityDescription=Makes secondary windows, such the connection creation dialog, block all input for the main window while they are open. This is useful if you sometimes misclick. system=System application=Application updateToPrereleases=Include prereleases @@ -90,13 +102,19 @@ developerModeDescription=When enabled, you will have access to a variety of addi editor=Editor custom=Custom passwordManagerCommand=Password manager command -passwordManagerCommandDescription=The command to execute to fetch passwords. The placeholder string $KEY will be replaced by the quoted password key when called. This should call your password manager CLI to print the password to stdout, e.g. mypassmgr get $KEY.\n\nYou can test below whether the output is correct. The command should only output the password itself, no other formatting should be included in the output. +passwordManagerCommandDescription=The command to execute to fetch passwords. The placeholder string $KEY will be replaced by the quoted password key when called. This should call your password manager CLI to print the password to stdout, e.g. mypassmgr get $KEY.\n\nYou can then set the key to be retrieved whenever you set up a connection which requires a password. +passwordManagerCommandTest=Test password manager +passwordManagerCommandTestDescription=You can test here whether the output looks correct if you have set up a password manager command. The command should only output the password itself to stdout, no other formatting should be included in the output. preferEditorTabs=Prefer to open new tabs preferEditorTabsDescription=Controls whether XPipe will try to open new tabs in your chosen editor instead of new windows.\n\nNote that not every editor supports this. customEditorCommand=Custom editor command -customEditorCommandDescription=The command to execute to open the custom editor. The placeholder string $FILE will be replaced by the quoted absolute file name when called. Remember to quote your editor executable path if it contains spaces. +customEditorCommandDescription=The command to execute to start the custom editor.\n\nThe placeholder string $FILE will be replaced by the quoted absolute file name when called. Remember to quote your editor executable path if it contains spaces. editorReloadTimeout=Editor reload timeout editorReloadTimeoutDescription=The amount of milliseconds to wait before reading a file after it has been updated. This avoids issues in cases where your editor is slow at writing or releasing file locks. +encryptAllVaultData=Encrypt all vault data +encryptAllVaultDataDescription=When enabled, every part of the vault connection data will be encrypted as opposed to only secrets within in that data. This adds another layer of security for other parameters like usernames, hostnames, etc., that are not encrypted by default in the vault.\n\nThis option will render your git vault history and diffs useless as you can't see the original changes anymore, only binary changes. +vaultSecurity=Vault security +securityPolicy=Security policy notepad++=Notepad++ notepad++Windows=Notepad++ notepad++Linux=Notepad++ @@ -114,14 +132,15 @@ developerShowHiddenProvidersDescription=Controls whether hidden and internal con developerDisableConnectorInstallationVersionCheck=Disable Connector Version Check developerDisableConnectorInstallationVersionCheckDescription=Controls whether the update checker will ignore the version number when inspecting the version of an XPipe connector installed on a remote machine. shellCommandTest=Shell Command Test +shellCommandTestDescription=Run a command in the shell session used internally by XPipe. konsole=Konsole xfce=Xfce 4 elementaryTerminal=Elementary Terminal macosTerminal=Terminal.app iterm2=iTerm2 warp=Warp -tabbyWindows=Tabby -alacrittyWindows=Alacritty +tabby=Tabby +alacritty=Alacritty alacrittyMacOs=Alacritty kittyMacOs=Kitty bbedit=BBEdit @@ -132,15 +151,31 @@ webstorm=WebStorm clion=CLion tabbyMacOs=Tabby terminal=Terminal -terminalProgram=Default program -terminalProgramDescription=The default terminal to use when opening any kind of shell connection. This application is only used for display purposes, the started shell program depends on the shell connection itself. +terminalEmulator=Terminal emulator +terminalConfiguration=Terminal configuration +editorConfiguration=Editor configuration +defaultApplication=Default application +terminalEmulatorDescription=The default terminal to use when opening any kind of shell connection. This application is only used for display purposes, the started shell program depends on the shell connection itself. program=Program customTerminalCommand=Custom terminal command -customTerminalCommandDescription=The command to execute to open the custom terminal with a given command. The placeholder string $CMD will be replaced by the actual command when called. Remember to quote your terminal executable path if it contains spaces. +customTerminalCommandDescription=The command to execute to open the custom terminal with a given command.\n\nXPipe will create a temporary launcher shell script for your terminal to execute. The placeholder string $CMD in the command you supply will be replaced by the actual launcher script when called. Remember to quote your terminal executable path if it contains spaces. preferTerminalTabs=Prefer to open new tabs +preferTerminalTabsDisabled=Prefer to open new tabs.\n\nThe currently selected terminal $TERM$ does not support opening tabs from the CLI. preferTerminalTabsDescription=Controls whether XPipe will try to open new tabs in your chosen terminal instead of new windows.\n\nNote that not every terminal supports this. +clearTerminalOnInit=Clear terminal on init +clearTerminalOnInitDescription=When enabled, XPipe will run a clear command when a new terminal session is launched to remove any unnecessary output. enableFastTerminalStartup=Enable fast terminal startup enableFastTerminalStartupDescription=When enabled, terminal sessions are attempted to be started quicker when possible.\n\nThis will skip several startup checks and won't update any displayed system information. Any connection errors will only be shown in the terminal. +dontCachePasswords=Don't cache prompted passwords +dontCachePasswordsDescription=Controls whether queried passwords should be cached internally by XPipe so you don't have to enter them again in the current session.\n\nIf this behavior is disabled, you have to reenter any prompted credentials every time they are required by the system. +denyTempScriptCreation=Deny temporary script creation +denyTempScriptCreationDescription=To realize some of its functionality, XPipe sometimes creates temporary shell scripts on a target system to allow for an easy execution of simple commands. These do not contain any sensitive information and are just created for implementation purposes.\n\nIf this behavior is disabled, XPipe will not create any temporary files on a remote system. This option is useful in high-security contexts where every file system change is monitored. If this is disabled, some functionality, e.g. shell environments and scripts, will not work as intended. +disableCertutilUse=Disable certutil use on Windows +useLocalFallbackShell=Use local fallback shell +useLocalFallbackShellDescription=Switch to using another local shell to handle local operations. This would be PowerShell on Windows and bourne shell on other systems.\n\nThis option can be used in case the normal local default shell is disabled or broken to some degree. Some features might not work as expected though when this is option is enabled.\n\nRequires a restart to apply. +disableCertutilUseDescription=Due to several shortcomings and bugs in cmd.exe, temporary shell scripts are created with certutil by using it to decode base64 input as cmd.exe breaks on non-ASCII input. XPipe can also use PowerShell for that but this will be slower.\n\nThis disables any use of certutil on Windows systems to realize some functionality and fall back to PowerShell instead. This might please some AVs as some of them block certutil usage. +disableTerminalRemotePasswordPreparation=Disable terminal remote password preparation +disableTerminalRemotePasswordPreparationDescription=In situations where a remote shell connection that goes through multiple intermediate systems should be established in the terminal, there might be a requirement to prepare any required passwords on one of the intermediate systems to allow for an automatic filling of any prompts.\n\nIf you don't want the passwords to ever be transferred to any intermediate system, you can disable this behavior. Any required intermediate password will then be queried in the terminal itself when opened. cmd=cmd.exe powershell=Powershell pwsh=Powershell Core @@ -149,6 +184,4 @@ windowsTerminalPreview=Windows Terminal Preview gnomeTerminal=Gnome Terminal createLock=Create lock tilix=Tilix -wezWindows=WezTerm -wezLinux=WezTerm -wezMacOs=WezTerm +wezterm=WezTerm diff --git a/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties b/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties index df22f20e9..fe491aa77 100644 --- a/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties +++ b/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties @@ -7,6 +7,14 @@ common=Common key=Key color=Color roadmap=Roadmap and feature requests +elevationPolicy=Elevation policy +elevationPolicyDescription=Controls how to handle cases when elevated access might be required to run a command on a system, e.g. with sudo.\n\nThis can be overridden by connection-specific settings. +allow=Allow +ask=Ask +deny=Deny +elevationRequestTitle=Elevation request +elevationRequestHeader=A command on $SYSTEM$ requires elevation. Do you want to allow this? +elevationRequestDescription=Continuing from here XPipe will try to elevate commands when needed. Aborting will cancel the operation. share=Add to git repository unshare=Remove from git repository remove=Remove @@ -28,14 +36,20 @@ ok=Ok search=Search newFile=New file newDirectory=New directory +passphrase=Passphrase +repeatPassphrase=Repeat passphrase password=Password unlockAlertTitle=Unlock workspace -unlockAlertHeader=Enter your lock password to continue +unlockAlertHeader=Enter your vault passphrase to continue enterLockPassword=Enter lock password repeatPassword=Repeat password askpassAlertTitle=Askpass nullPointer=Null Pointer unsupportedOperation=Unsupported operation: $MSG$ +fileConflictAlertTitle=Resolve conflict +fileConflictAlertHeader=A conflict was encountered. How would you like to proceed? +fileConflictAlertContent=The file $FILE$ does already exist on the target system. +fileConflictAlertContentMultiple=The file $FILE$ already exists. There might be more conflicts that you can automatically resolve by choosing an option that applies to all. moveAlertTitle=Confirm move moveAlertHeader=Do you want to move the ($COUNT$) selected elements into $TARGET$? deleteAlertTitle=Confirm deletion diff --git a/app/src/main/resources/io/xpipe/app/resources/style/alert.css b/app/src/main/resources/io/xpipe/app/resources/style/alert.css index c15d0c204..8dee7ae2b 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/alert.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/alert.css @@ -1,7 +1,3 @@ -.dialog-pane:no-header { - -fx-padding: 0; -} - .dialog-pane:header { -fx-pref-height: 2em; } @@ -23,6 +19,7 @@ .dialog-pane:header .header-panel { -fx-background-color: -color-bg-default; + -fx-padding: 1.5em; } .dialog-pane:header .header-panel .graphic-container { @@ -33,8 +30,8 @@ -fx-border-width: 2px 0 0 0; -fx-border-color:-color-accent-fg; -fx-background-color: -color-bg-default; - -fx-border-insets: 0 1.333em 1em 1em; - -fx-padding: 0.7em 0 0 0; + -fx-border-insets: 0 1.5em 1.5em 1.5em; + -fx-padding: 1.5em 0 0 0; } .content-text { diff --git a/app/src/main/resources/io/xpipe/app/resources/style/bookmark.css b/app/src/main/resources/io/xpipe/app/resources/style/bookmark.css index 9603bbd9a..e67897b10 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/bookmark.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/bookmark.css @@ -13,10 +13,5 @@ } .bookmark-list .store-section-mini-comp .item:selected { --fx-border-width: 1px; --fx-border-radius: 4px; --fx-background-color: transparent, -color-accent-emphasis, -color-bg-overlay; --fx-background-radius: 4px; --fx-background-insets: 1 11 1 1, 2 12 2 2, 3 13 3 3; --fx-padding: 0.25em 0.4em 0.25em 0.4em; + -fx-font-weight: BOLD; } diff --git a/app/src/main/resources/io/xpipe/app/resources/style/browser.css b/app/src/main/resources/io/xpipe/app/resources/style/browser.css index c6c3798f4..b7ebe2e96 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/browser.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/browser.css @@ -123,12 +123,12 @@ .browser .top-bar > .button { -fx-background-insets: 0; - -fx-background-color: 0; + -fx-background-color: transparent; } .browser .top-bar > .menu-button { -fx-background-insets: 0; - -fx-background-color: 0; + -fx-background-color: transparent; } .browser .top-bar > .button:hover { diff --git a/app/src/main/resources/io/xpipe/app/resources/style/category.css b/app/src/main/resources/io/xpipe/app/resources/style/category.css index 6963da1f5..28a74c1ea 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/category.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/category.css @@ -6,6 +6,10 @@ -fx-background-insets: 0; } +.category-button .text-field { + -fx-background-color: transparent; +} + .category-button .settings { -fx-opacity: 1.0; } diff --git a/app/src/main/resources/io/xpipe/app/resources/style/data-store-step.css b/app/src/main/resources/io/xpipe/app/resources/style/data-store-step.css deleted file mode 100644 index cafd6e18e..000000000 --- a/app/src/main/resources/io/xpipe/app/resources/style/data-store-step.css +++ /dev/null @@ -1,25 +0,0 @@ -.step { --fx-padding: 0; -} - -.spacer { --fx-padding: 1em 0 1em 0; -} - -.data-input-creation-step .jfx-tab-pane { --fx-spacing: 1em; --fx-padding: 1.5em; -} - -.data-input-creation-step .jfx-tab-pane { --fx-padding: 0.4em 0 0 0; -} - -.data-input-type { --fx-padding: 0 0 1.25em 0; -} - -.data-input-creation-step .store-local-file-chooser { --fx-padding: 1em 0 1em 0; --fx-spacing: 1em; -} diff --git a/app/src/main/resources/io/xpipe/app/resources/style/dialog-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/dialog-comp.css new file mode 100644 index 000000000..196a8efa8 --- /dev/null +++ b/app/src/main/resources/io/xpipe/app/resources/style/dialog-comp.css @@ -0,0 +1,21 @@ +.dialog-comp { + -fx-background-color: -color-bg-default; +} + +.dialog-comp .scroll-pane { + -fx-padding: 0; +} + +.dialog-comp .buttons .button { + -fx-padding: 6 12 6 12; + -fx-border-width: 1px; + -fx-border-radius: 2px; + -fx-background-radius: 2px; +} + +.dialog-comp .buttons { + -fx-padding: 10; + -fx-border-color: -color-border-default; + -fx-background-color: -color-bg-subtle; + -fx-border-width: 1 0 0 0; +} \ No newline at end of file diff --git a/app/src/main/resources/io/xpipe/app/resources/style/intro.css b/app/src/main/resources/io/xpipe/app/resources/style/intro.css index c89e75980..d4367d0cb 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/intro.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/intro.css @@ -1,19 +1,12 @@ .intro .label { --fx-line-spacing: 5px; --fx-graphic-text-gap: 8px; +-fx-line-spacing: -1px; } .intro .title { -fx-padding: 0 0 0.2em 0; + -fx-text-fill: -color-fg-emphasis; } .intro .separator { -fx-padding: 0.75em 0 0.75em 0; } - -.intro .label .ikonli-font-icon { --fx-line-spacing: 5px; -} - -.intro-add-collection-button { -} \ No newline at end of file diff --git a/app/src/main/resources/io/xpipe/app/resources/style/lazy-text-field-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/lazy-text-field-comp.css index c0e3564ff..f75082ca1 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/lazy-text-field-comp.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/lazy-text-field-comp.css @@ -1,3 +1,7 @@ +.lazy-text-field-comp { + -fx-padding: 5px; +} + .lazy-text-field-comp:disabled { -fx-opacity: 1.0; } diff --git a/app/src/main/resources/io/xpipe/app/resources/style/multi-step-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/multi-step-comp.css deleted file mode 100644 index 7be412111..000000000 --- a/app/src/main/resources/io/xpipe/app/resources/style/multi-step-comp.css +++ /dev/null @@ -1,90 +0,0 @@ -.multi-step-comp > .top .line { --fx-max-height: 3px; --fx-background-color: -color-accent-fg; -} - -.multi-step-comp > .entry { --fx-spacing: 0.2em; --fx-padding: 0.6em 0 1em 0; -} - -.multi-step-comp > .top { - -fx-border-color: -color-neutral-emphasis; --fx-border-width: 0 0 0.1em 0; --fx-background-color: -color-neutral-muted; -} - -.multi-step-comp > .top .name { --fx-text-fill: -color-accent-fg; -} - -.multi-step-comp .entry:next .name { --fx-text-fill: -color-accent-fg; -} - -.multi-step-comp .buttons { --fx-border-color: -color-neutral-emphasis; --fx-border-width: 0.1em 0 0 0; --fx-padding: 1em; --fx-background-color: -color-neutral-muted; -} - -.multi-step-comp .circle { --fx-max-width: 17px; --fx-max-height: 17px; --fx-min-height: 17px; --fx-background-radius: 4px; --fx-background-color: #073B4C; --fx-border-width: 1px; --fx-border-color:-color-accent-fg; --fx-border-radius: 4px; -} - -.multi-step-comp { --fx-background-color: -color-bg-default; -} - -.multi-step-comp .entry:completed .circle { --fx-background-color: #0B9F9B; -} - -.multi-step-comp .top .entry:completed .line { --fx-background-color: #0B9F9B; -} - -.multi-step-comp .entry:current .circle { --fx-background-color: #118AB2; -} - -.multi-step-comp .top .entry:current .line { --fx-background-color: #118AB2; -} - -.multi-step-comp .entry:next .circle { --fx-background-color: #0B566F; -} - -.multi-step-comp .top .entry:next .line { --fx-background-color: #0B566F; -} - -.multi-step-comp > .top .entry .number { --fx-text-fill: white; -} - -.multi-step-comp > .top .entry:next .number { --fx-text-fill: white; -} - -.multi-step-comp > .top { --fx-height: 4em; -} - -.multi-step-comp > .jfx-tab-pane .tab-header-area { --fx-pref-height: 0; -} - -.multi-step-comp > .jfx-tab-pane .tab-content-area { --fx-padding: 0; -} - diff --git a/app/src/main/resources/io/xpipe/app/resources/style/prefs.css b/app/src/main/resources/io/xpipe/app/resources/style/prefs.css index 3e49e5354..e54008b65 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/prefs.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/prefs.css @@ -1,73 +1,55 @@ - -.undo-redo-box * { -visibility: hidden; +.prefs-container .options-comp .name { + -fx-padding: 3em 0 0 0; + -fx-font-weight: BOLD; + -fx-font-size: 1.1em; } -.prefs .group-title { --fx-font-size: 1.1em; +.prefs-container .options-comp .description { + -fx-padding: 0.5em 0 0.5em 0; } -.prefs .formsfx-form * { --fx-font-size: 0.7em; +.prefs-container > .title-header.first { + -fx-padding: 0 0 -1em 0; } -.prefs .formsfx-group * { --fx-font-size: 0.7em; +.prefs-container > .title-header { + -fx-padding: 2em 0 -1em 0; + -fx-font-weight: BOLD; + -fx-font-size: 1.5em; } -.prefs .simple-control { --fx-pref-height: 2em; +.prefs-container.options-comp > .options-comp { + -fx-padding: 0 0 0 1em; } -.prefs .simple-select-control { --fx-pref-height: 2em; -} - -.root:light .prefs .tree-view { - -color-cell-bg: #9992; -} - -.root:dark .prefs .tree-view { - -color-cell-bg: #1114; -} - -.integer-slider-control { --fx-alignment: CENTER; -} - -.prefs .bread-crumb-bar { -visibility: hidden; --fx-font-size: 0.1em; -} - -.prefs .custom-text-field { --fx-border-width: 1em; -} - -.prefs .tree-view { --fx-border-width: 0 1px 0 0; -} - -.prefs .grid { --fx-padding: 1em 2.3em 2em 2.3em; +.prefs-container.options-comp .name.first { + -fx-padding: 0; } .prefs { --fx-border-width: 0; --fx-padding: 0; --fx-border-color: transparent; - -fx-background-insets: 0; + -fx-background-color: transparent; } -.prefs .custom-text-field { --fx-border-width: 5px; --fx-padding: 0; --fx-border-color: transparent; --fx-border-radius: 0; --fx-background-radius: 0; +.prefs .sidebar { + -fx-spacing: 0; + -fx-background-color: -color-bg-subtle; + -fx-border-width: 0 1 0 0; + -fx-border-color: -color-border-default; + -fx-padding: 0.7em 0 0 0; } -.prefs .split-pane-divider { --fx-padding: 0 ; +.prefs .sidebar .button { + -fx-background-color: transparent; + -fx-padding: 0.5em 1em 0.5em 1.2em; + -fx-border-radius: 0; + -fx-background-radius: 0; + -fx-border-width: 1; + -fx-background-insets: 0; + -fx-border-insets: 0; } +.prefs .sidebar .button:selected { + -fx-background-color: -color-accent-subtle; + -fx-border-color: -color-accent-emphasis; + -fx-border-width: 1 0 1 0; +} diff --git a/app/src/main/resources/io/xpipe/app/resources/style/section-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/section-comp.css index 1a6d8de93..866160b5d 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/section-comp.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/section-comp.css @@ -1,8 +1,3 @@ -.section-comp { --fx-vgap: 7px; --fx-hgap: 10px; -} - .title-header { -fx-font-size: 1.2em; -fx-padding: 15px 0 3px 0; diff --git a/app/src/main/resources/io/xpipe/app/resources/style/sidebar-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/sidebar-comp.css index 1502ae6c1..619fec830 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/sidebar-comp.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/sidebar-comp.css @@ -5,11 +5,6 @@ -fx-background-insets: 0; } -.sidebar-comp .icon-button-comp { --fx-border-width: 0 3px 0 0; --fx-border-color: -color-border-default; -} - .sidebar-comp .icon-button-comp, .sidebar-comp .button { -fx-background-radius: 0; -fx-background-insets: 0; @@ -21,12 +16,10 @@ } .sidebar-comp .icon-button-comp:hover, .sidebar-comp .icon-button-comp:focused { --fx-border-color: -color-accent-muted; -fx-background-color: -color-neutral-muted; } .sidebar-comp .icon-button-comp:selected { --fx-border-color: -color-accent-emphasis; -fx-background-color: -color-neutral-muted; } diff --git a/app/src/main/resources/io/xpipe/app/resources/style/store-creator.css b/app/src/main/resources/io/xpipe/app/resources/style/store-creator.css deleted file mode 100644 index d0518153a..000000000 --- a/app/src/main/resources/io/xpipe/app/resources/style/store-creator.css +++ /dev/null @@ -1,3 +0,0 @@ -.store-creator > .scroll-pane { - -fx-padding: 0 10px 0 0; - } diff --git a/app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css index 430ff6500..7731818a8 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css @@ -37,14 +37,6 @@ -fx-background-radius: 3; } -.root:pretty .store-entry-grid .icon { --fx-effect: dropshadow(three-pass-box, -color-shadow-default, 2px, 0.5, 0, 1); -} - -.root:pretty .store-entry-grid .icon > * { --fx-effect: dropshadow(three-pass-box, -color-accent-muted, 2px, 0.2, 0, 1); -} - .store-entry-grid { -fx-padding: 6px 6px 6px 6px; } diff --git a/app/src/main/resources/io/xpipe/app/resources/style/style.css b/app/src/main/resources/io/xpipe/app/resources/style/style.css index 37de01011..07ce369cc 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/style.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/style.css @@ -1,18 +1,3 @@ -.prefs * { --fx-text-fill: -color-fg-default; --fx-highlight-text-fill: -color-fg-default; --fx-highlight-fill: -color-neutral-muted; --fx-prompt-text-fill: -color-fg-subtle; --fx-text-box-border:-color-neutral-muted; --fx-control-inner-background: -color-neutral-muted; --fx-body-color: -color-neutral-muted; --fx-inner-border: -color-accent-fg; -} - -.root .window-content { --fx-padding: 1.2em; -} - .store-layout .split-pane-divider { -fx-background-color: transparent; } @@ -71,11 +56,6 @@ -fx-background-color:transparent; } -.jfx-list-view { - -fx-background-insets: 0; - -fx-padding: 0; -} - .root:light .loading-comp { -fx-background-color: rgba(100,100,100,0.5); } diff --git a/app/src/main/resources/io/xpipe/app/resources/style/tab-pane-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/tab-pane-comp.css deleted file mode 100644 index 093ec23aa..000000000 --- a/app/src/main/resources/io/xpipe/app/resources/style/tab-pane-comp.css +++ /dev/null @@ -1,16 +0,0 @@ -.tab-pane-comp .tab-header-area .jfx-rippler { - -jfx-rippler-fill: #118AB210; -} - -.tab-pane-comp .tab-header-background { --fx-background-color: transparent; -} - -.tab-pane-comp .tab-header-area .tab-selected-line { - -fx-background-color: #073B4C43; - -fx-pref-height: 1px; -} - -.tab-pane-comp .headers-region * { - -fx-font-size: 1em; -} \ No newline at end of file diff --git a/app/src/main/resources/io/xpipe/app/resources/style/tab-pane.css b/app/src/main/resources/io/xpipe/app/resources/style/tab-pane.css deleted file mode 100644 index c8f3e5215..000000000 --- a/app/src/main/resources/io/xpipe/app/resources/style/tab-pane.css +++ /dev/null @@ -1,13 +0,0 @@ -.jfx-tab-pane .tab-header-background { -fx-background-color: transparent; } - -.jfx-tab-pane .tab-header-area .tab-selected-line { - -fx-pref-height: 1px; -} - -.jfx-tab-pane .headers-region * { - -fx-font-size: 1em; -} - -.jfx-tab-pane .tab-content-area { --fx-padding: 0.5em, 0, 0, 0; -} \ No newline at end of file diff --git a/app/src/test/java/Test.java b/app/src/test/java/Test.java new file mode 100644 index 000000000..d5ac1298a --- /dev/null +++ b/app/src/test/java/Test.java @@ -0,0 +1,10 @@ +import io.xpipe.app.util.ThreadHelper; + +public class Test { + + @org.junit.jupiter.api.Test + public void test() { + System.out.println("a"); + ThreadHelper.sleep(1000); + } +} diff --git a/beacon/build.gradle b/beacon/build.gradle index 8b14e6f70..c45a7e5a8 100644 --- a/beacon/build.gradle +++ b/beacon/build.gradle @@ -2,7 +2,6 @@ plugins { id 'java-library' id 'maven-publish' id 'signing' - id "org.moditect.gradleplugin" version "1.0.0-rc3" } apply from: "$rootDir/gradle/gradle_scripts/java.gradle" diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/AskpassExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/AskpassExchange.java index 581b82e14..6e5334474 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/AskpassExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/AskpassExchange.java @@ -21,10 +21,7 @@ public class AskpassExchange implements MessageExchange { @Builder @Value public static class Request implements RequestMessage { - @NonNull - UUID storeId; - - int subId; + UUID secretId; @NonNull UUID request; diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/TerminalLaunchExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/TerminalLaunchExchange.java new file mode 100644 index 000000000..cce8c89c4 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/TerminalLaunchExchange.java @@ -0,0 +1,34 @@ +package io.xpipe.beacon.exchange; + +import io.xpipe.beacon.RequestMessage; +import io.xpipe.beacon.ResponseMessage; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.nio.file.Path; +import java.util.UUID; + +public class TerminalLaunchExchange implements MessageExchange { + + @Override + public String getId() { + return "terminalLaunch"; + } + + @Jacksonized + @Builder + @Value + public static class Request implements RequestMessage { + @NonNull + UUID request; + } + + @Jacksonized + @Builder + @Value + public static class Response implements ResponseMessage { + @NonNull Path targetFile; + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/TerminalWaitExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/TerminalWaitExchange.java new file mode 100644 index 000000000..a341003a4 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/TerminalWaitExchange.java @@ -0,0 +1,32 @@ +package io.xpipe.beacon.exchange; + +import io.xpipe.beacon.RequestMessage; +import io.xpipe.beacon.ResponseMessage; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.UUID; + +public class TerminalWaitExchange implements MessageExchange { + + @Override + public String getId() { + return "terminalWait"; + } + + @Jacksonized + @Builder + @Value + public static class Request implements RequestMessage { + @NonNull + UUID request; + } + + @Jacksonized + @Builder + @Value + public static class Response implements ResponseMessage { + } +} diff --git a/beacon/src/main/java/module-info.java b/beacon/src/main/java/module-info.java index 7bdb970cf..8c3d50e3c 100644 --- a/beacon/src/main/java/module-info.java +++ b/beacon/src/main/java/module-info.java @@ -48,7 +48,8 @@ module io.xpipe.beacon { RemoveStoreExchange, StoreAddExchange, ReadDrainExchange, - AskpassExchange, + AskpassExchange, TerminalWaitExchange, + TerminalLaunchExchange, ListStoresExchange, DialogExchange, VersionExchange; diff --git a/build.gradle b/build.gradle index b23abd335..76d5b9136 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,18 @@ +import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform + import java.util.stream.Stream plugins { id "io.codearte.nexus-staging" version "0.30.0" + id 'org.gradlex.extra-java-module-info' version '1.8' apply false +} + +allprojects { subproject -> + apply plugin: 'org.gradlex.extra-java-module-info' + extraJavaModuleInfo { + failOnMissingModuleInfo.set(false) + } + apply from: "$rootDir/modules.gradle" } def getArchName() { @@ -21,6 +32,19 @@ def getArchName() { return arch } +def getPlatformName() { + def currentOS = DefaultNativePlatform.currentOperatingSystem; + def platform = null + if (currentOS.isWindows()) { + platform = 'windows' + } else if (currentOS.isLinux()) { + platform = 'linux' + } else if (currentOS.isMacOsX()) { + platform = 'osx' + } + return platform; +} + project.ext { ci = System.getenv('CI') != null os = org.gradle.internal.os.OperatingSystem.current() @@ -49,7 +73,23 @@ project.ext { website = 'https://xpipe.io' sourceWebsite = 'https://github.com/xpipe-io/xpipe' authors = 'Christopher Schnick' - javafxVersion = '20.0.2' + javafxVersion = '22-ea+27' + platformName = getPlatformName() + artifactChecksums = new HashMap() + jvmRunArgs = [ + "--add-opens", "java.base/java.lang=io.xpipe.app", + "--add-opens", "java.base/java.lang=io.xpipe.core", + "--add-opens", "java.desktop/java.awt=io.xpipe.app", + "--add-opens", "net.synedra.validatorfx/net.synedra.validatorfx=io.xpipe.app", + "--add-opens", "java.base/java.nio.file=io.xpipe.app", + "-Xmx8g", + "-Dio.xpipe.app.arch=$rootProject.arch", + "-Dfile.encoding=UTF-8", + // Disable this for now as it requires Windows 10+ + // '-XX:+UseZGC', + "-Dvisualvm.display.name=XPipe" + ] + useBundledJavaFx = fullVersion && !(platformName == 'linux' && arch == 'arm64') } if (isFullRelease && rawVersion.contains("-")) { @@ -73,3 +113,32 @@ def replaceVariablesInFile(String f, Map replacements) { file(temp).text = replaced return file(temp) } + +def testTasks = [ + project(':core').getTasksByName('test', true), + project(':api').getTasksByName('test', true), + project(':app').getTasksByName('test', true), + project(':base').getTasksByName('localTest', true), + project(':jdbc').getTasksByName('localTest', true), + project(':proc').getTasksByName('localTest', true), + project(':cli').getTasksByName('remoteTest', true), +] + +tasks.register('testReport', TestReport) { + getDestinationDirectory().set(file("$rootProject.buildDir/reports/all")) + getTestResults().from(testTasks.stream().map { + file("${it.project.buildDir.get(0)}/test-results/${it.name.get(0)}/binary") + }.toList()) +} + +task testAll(type: DefaultTask) { + for (final def t in testTasks) { + t.forEach {dependsOn(it.getTaskDependencies())} + } + doFirst { + for (final def t in testTasks) { + t.forEach {it.executeTests()} + } + } + finalizedBy(testReport) +} diff --git a/core/build.gradle b/core/build.gradle index a0dc39673..a6b64d35e 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -2,7 +2,6 @@ plugins { id 'java-library' id 'maven-publish' id 'signing' - id "org.moditect.gradleplugin" version "1.0.0-rc3" } apply from: "$rootDir/gradle/gradle_scripts/java.gradle" @@ -14,10 +13,10 @@ compileJava { } dependencies { - api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.15.2" - implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: "2.15.2" - implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.15.2" - implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jdk8', version: "2.15.2" + api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.16.1" + implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: "2.16.1" + implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.16.1" + implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jdk8', version: "2.16.1" } version = rootProject.versionString @@ -28,6 +27,10 @@ repositories { mavenCentral() } +dependencies { + testImplementation project(':core') +} + task dist(type: Copy) { from jar.archiveFile into "${project(':dist').buildDir}/dist/libraries" diff --git a/core/src/main/java/io/xpipe/core/dialog/QueryConverter.java b/core/src/main/java/io/xpipe/core/dialog/QueryConverter.java index 819dbc745..f41c77cda 100644 --- a/core/src/main/java/io/xpipe/core/dialog/QueryConverter.java +++ b/core/src/main/java/io/xpipe/core/dialog/QueryConverter.java @@ -56,7 +56,7 @@ public abstract class QueryConverter { @Override protected String toString(SecretValue value) { - return value.getSecretValue(); + return new String(value.getSecret()); } }; diff --git a/core/src/main/java/io/xpipe/core/process/AskpassSetup.java b/core/src/main/java/io/xpipe/core/process/AskpassSetup.java new file mode 100644 index 000000000..4af327031 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/process/AskpassSetup.java @@ -0,0 +1,3 @@ +package io.xpipe.core.process; + +public class AskpassSetup {} diff --git a/core/src/main/java/io/xpipe/core/process/CommandBuilder.java b/core/src/main/java/io/xpipe/core/process/CommandBuilder.java index 146c071f6..ec5e680bd 100644 --- a/core/src/main/java/io/xpipe/core/process/CommandBuilder.java +++ b/core/src/main/java/io/xpipe/core/process/CommandBuilder.java @@ -1,5 +1,7 @@ package io.xpipe.core.process; +import io.xpipe.core.util.FailableConsumer; +import io.xpipe.core.util.FailableFunction; import lombok.Getter; import lombok.SneakyThrows; @@ -12,11 +14,27 @@ public class CommandBuilder { return new CommandBuilder(); } + public static CommandBuilder ofString(String s) { + return new CommandBuilder().add(s); + } + + public static CommandBuilder ofFunction(FailableFunction command) { + return CommandBuilder.of().add(sc -> command.apply(sc)); + } + private CommandBuilder() {} + @Getter + private final CountDown countDown = CountDown.of(); private final List elements = new ArrayList<>(); @Getter private final Map environmentVariables = new LinkedHashMap<>(); + private final List> setup = new ArrayList<>(); + + public CommandBuilder setup(FailableConsumer consumer) { + setup.add(consumer); + return this; + } public CommandBuilder fixedEnvrironment(String k, String v) { environmentVariables.put(k, new Fixed(v)); @@ -126,14 +144,9 @@ public class CommandBuilder { return this; } - public CommandBuilder addSub(CommandBuilder sub) { - elements.add(sc -> { - if (sc == null) { - return sub.buildSimple(); - } - - return sub.buildString(sc); - }); + public CommandBuilder add(CommandBuilder sub) { + elements.addAll(sub.elements); + environmentVariables.putAll(sub.environmentVariables); return this; } @@ -156,21 +169,6 @@ public class CommandBuilder { return prepend("\"" + s + "\""); } - public CommandBuilder addFile(String s) { - elements.add(sc -> { - if (s == null) { - return null; - } - - if (sc == null) { - return "\"" + s + "\""; - } - - return sc.getShellDialect().fileArgument(s); - }); - return this; - } - public CommandBuilder addFile(Function f) { elements.add(sc -> { if (f == null) { @@ -186,13 +184,46 @@ public class CommandBuilder { return this; } + public CommandBuilder addFile(String s) { + elements.add(sc -> { + if (s == null) { + return null; + } + + if (sc == null) { + return "\"" + s + "\""; + } + + return sc.getShellDialect().fileArgument(s); + }); + return this; + } + + public CommandBuilder addLiteral(String s) { + elements.add(sc -> { + if (s == null) { + return null; + } + + if (sc == null) { + return "\"" + s + "\""; + } + + return sc.getShellDialect().literalArgument(s); + }); + return this; + } + public CommandBuilder addFiles(SequencedCollection s) { s.forEach(this::addFile); return this; } - public String buildBase(ShellControl sc) throws Exception { - sc.getShellDialect().prepareCommandForShell(this); + public String buildCommandBase(ShellControl sc) throws Exception { + for (FailableConsumer s : setup) { + s.accept(sc); + } + List list = new ArrayList<>(); for (Element element : elements) { String evaluate = element.evaluate(sc); @@ -206,7 +237,7 @@ public class CommandBuilder { } public String buildString(ShellControl sc) throws Exception { - var s = buildBase(sc); + var s = buildCommandBase(sc); LinkedHashMap map = new LinkedHashMap<>(); for (var e : environmentVariables.entrySet()) { var v = e.getValue().evaluate(sc); diff --git a/core/src/main/java/io/xpipe/core/process/CommandControl.java b/core/src/main/java/io/xpipe/core/process/CommandControl.java index 06aa18773..41d2ff827 100644 --- a/core/src/main/java/io/xpipe/core/process/CommandControl.java +++ b/core/src/main/java/io/xpipe/core/process/CommandControl.java @@ -75,8 +75,6 @@ public interface CommandControl extends ProcessControl { @Override CommandControl start() throws Exception; - CommandControl exitTimeout(Integer timeout); - void withStdoutOrThrow(FailableConsumer c); String readStdoutDiscardErr() throws Exception; diff --git a/core/src/main/java/io/xpipe/core/process/CountDown.java b/core/src/main/java/io/xpipe/core/process/CountDown.java new file mode 100644 index 000000000..255ec629f --- /dev/null +++ b/core/src/main/java/io/xpipe/core/process/CountDown.java @@ -0,0 +1,55 @@ +package io.xpipe.core.process; + +import lombok.Getter; +import lombok.Setter; + +public class CountDown { + + public static CountDown of() { + return new CountDown(); + } + + private long lastMillis = -1; + private long millisecondsLeft; + @Setter + private boolean active; + @Getter + private long maxMillis; + + private CountDown() { + } + + public synchronized void start(long millisecondsLeft) { + this.millisecondsLeft = millisecondsLeft; + this.maxMillis = millisecondsLeft; + lastMillis = System.currentTimeMillis(); + active = true; + + } + + public void pause() { + lastMillis = System.currentTimeMillis(); + setActive(false); + } + + public void resume() { + lastMillis = System.currentTimeMillis(); + setActive(true); + } + + public synchronized boolean countDown() { + var ml = System.currentTimeMillis(); + if (!active) { + lastMillis = ml; + return true; + } + + var diff = ml - lastMillis; + lastMillis = ml; + millisecondsLeft -= diff; + if (millisecondsLeft < 0) { + return false; + } + return true; + } +} diff --git a/core/src/main/java/io/xpipe/core/process/OsType.java b/core/src/main/java/io/xpipe/core/process/OsType.java index 3ad5e0999..823544d2b 100644 --- a/core/src/main/java/io/xpipe/core/process/OsType.java +++ b/core/src/main/java/io/xpipe/core/process/OsType.java @@ -7,13 +7,26 @@ import java.util.Locale; import java.util.Map; import java.util.stream.Collectors; -public sealed interface OsType permits OsType.Windows, OsType.Linux, OsType.MacOs { +public interface OsType { + + sealed interface Local extends OsType permits OsType.Windows, OsType.Linux, OsType.MacOs { + + default Any toAny() { + return (Any) this; + } + } + + sealed interface Any extends OsType permits OsType.Windows, OsType.Linux, OsType.MacOs, OsType.Solaris, OsType.Bsd { + + } Windows WINDOWS = new Windows(); Linux LINUX = new Linux(); MacOs MACOS = new MacOs(); + Bsd BSD = new Bsd(); + Solaris SOLARIS = new Solaris(); - static OsType getLocal() { + static Local getLocal() { String osName = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH); if ((osName.contains("mac")) || (osName.contains("darwin"))) { return MACOS; @@ -40,7 +53,7 @@ public sealed interface OsType permits OsType.Windows, OsType.Linux, OsType.MacO String determineOperatingSystemName(ShellControl pc) throws Exception; - final class Windows implements OsType { + final class Windows implements OsType, Local, Any { @Override public List determineInterestingPaths(ShellControl pc) throws Exception { @@ -102,7 +115,7 @@ public sealed interface OsType permits OsType.Windows, OsType.Linux, OsType.MacO } } - final class Linux implements OsType { + class Unix implements OsType { @Override public List determineInterestingPaths(ShellControl pc) throws Exception { @@ -138,20 +151,6 @@ public sealed interface OsType permits OsType.Windows, OsType.Linux, OsType.MacO @Override public String determineOperatingSystemName(ShellControl pc) throws Exception { - try (CommandControl c = pc.command("lsb_release -a").start()) { - var text = c.readStdoutDiscardErr(); - if (c.getExitCode() == 0) { - return PropertiesFormatsParser.parse(text, ":").getOrDefault("Description", "Unknown"); - } - } - - try (CommandControl c = pc.command("cat /etc/*release").start()) { - var text = c.readStdoutDiscardErr(); - if (c.getExitCode() == 0) { - return PropertiesFormatsParser.parse(text, "=").getOrDefault("PRETTY_NAME", "Unknown"); - } - } - String type = "Unknown"; try (CommandControl c = pc.command("uname -o").start()) { var text = c.readStdoutDiscardErr(); @@ -172,7 +171,38 @@ public sealed interface OsType permits OsType.Windows, OsType.Linux, OsType.MacO } } - final class MacOs implements OsType { + + final class Linux extends Unix implements OsType, Local, Any { + + @Override + public String determineOperatingSystemName(ShellControl pc) throws Exception { + try (CommandControl c = pc.command("lsb_release -a").start()) { + var text = c.readStdoutDiscardErr(); + if (c.getExitCode() == 0) { + return PropertiesFormatsParser.parse(text, ":").getOrDefault("Description", "Unknown"); + } + } + + try (CommandControl c = pc.command("cat /etc/*release").start()) { + var text = c.readStdoutDiscardErr(); + if (c.getExitCode() == 0) { + return PropertiesFormatsParser.parse(text, "=").getOrDefault("PRETTY_NAME", "Unknown"); + } + } + + return super.determineOperatingSystemName(pc); + } + } + + final class Solaris extends Unix implements Any { + + } + + final class Bsd extends Unix implements Any { + + } + + final class MacOs implements OsType, Local, Any { @Override public List determineInterestingPaths(ShellControl pc) throws Exception { diff --git a/core/src/main/java/io/xpipe/core/process/ParentSystemAccess.java b/core/src/main/java/io/xpipe/core/process/ParentSystemAccess.java new file mode 100644 index 000000000..843f129c9 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/process/ParentSystemAccess.java @@ -0,0 +1,90 @@ +package io.xpipe.core.process; + +public interface ParentSystemAccess { + + + static ParentSystemAccess none() { + return new ParentSystemAccess() { + @Override + public boolean supportsFileSystemAccess() { + return false; + } + + @Override + public boolean supportsExecutables() { + return false; + } + + @Override + public String translateFromLocalSystemPath(String path) { + throw new UnsupportedOperationException(); + } + + @Override + public String translateToLocalSystemPath(String path) { + throw new UnsupportedOperationException(); + } + }; + } + + static ParentSystemAccess identity() { + return new ParentSystemAccess() { + @Override + public boolean supportsFileSystemAccess() { + return true; + } + + @Override + public boolean supportsExecutables() { + return true; + } + + @Override + public String translateFromLocalSystemPath(String path) { + return path; + } + + @Override + public String translateToLocalSystemPath(String path) { + return path; + } + }; + } + + static ParentSystemAccess combine(ParentSystemAccess a1, ParentSystemAccess a2) { + return new ParentSystemAccess() { + + @Override + public boolean supportsFileSystemAccess() { + return a1.supportsFileSystemAccess() && a2.supportsFileSystemAccess(); + } + + @Override + public boolean supportsExecutables() { + return a1.supportsExecutables() && a2.supportsExecutables(); + } + + @Override + public String translateFromLocalSystemPath(String path) throws Exception { + return a2.translateFromLocalSystemPath(a1.translateFromLocalSystemPath(path)); + } + + @Override + public String translateToLocalSystemPath(String path) throws Exception { + return a1.translateToLocalSystemPath(a2.translateToLocalSystemPath(path)); + } + }; + } + + default boolean supportsAnyAccess() { + return supportsFileSystemAccess(); + } + + boolean supportsFileSystemAccess(); + + boolean supportsExecutables(); + + String translateFromLocalSystemPath(String path) throws Exception; + + String translateToLocalSystemPath(String path) throws Exception; +} diff --git a/core/src/main/java/io/xpipe/core/process/ProcessControl.java b/core/src/main/java/io/xpipe/core/process/ProcessControl.java index af04754c8..b6911ab2b 100644 --- a/core/src/main/java/io/xpipe/core/process/ProcessControl.java +++ b/core/src/main/java/io/xpipe/core/process/ProcessControl.java @@ -1,5 +1,7 @@ package io.xpipe.core.process; +import io.xpipe.core.util.FailableFunction; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -12,11 +14,13 @@ public interface ProcessControl extends AutoCloseable { T convert(T t); } + ProcessControl withExceptionConverter(ExceptionConverter converter); void resetData(boolean cache); - String prepareTerminalOpen(TerminalInitScriptConfig config) throws Exception; + String prepareTerminalOpen(TerminalInitScriptConfig config, + FailableFunction workingDirectory) throws Exception; void closeStdin() throws IOException; diff --git a/core/src/main/java/io/xpipe/core/process/ProcessControlProvider.java b/core/src/main/java/io/xpipe/core/process/ProcessControlProvider.java index 718f72be6..00954d159 100644 --- a/core/src/main/java/io/xpipe/core/process/ProcessControlProvider.java +++ b/core/src/main/java/io/xpipe/core/process/ProcessControlProvider.java @@ -1,6 +1,5 @@ package io.xpipe.core.process; -import io.xpipe.core.util.FailableFunction; import lombok.NonNull; import java.util.ServiceLoader; @@ -22,15 +21,23 @@ public abstract class ProcessControlProvider { public abstract ShellControl sub( ShellControl parent, - @NonNull FailableFunction commandFunction, - ShellControl.TerminalOpenFunction terminalCommand); + @NonNull ShellOpenFunction commandFunction, + ShellOpenFunction terminalCommand); public abstract CommandControl command( ShellControl parent, - @NonNull FailableFunction command, - FailableFunction terminalCommand); + CommandBuilder command, + CommandBuilder terminalCommand); public abstract ShellControl createLocalProcessControl(boolean stoppable); public abstract Object createStorageHandler(); + + public abstract ShellDialect getEffectiveLocalDialect(); + + public abstract void toggleFallbackShell(); + + public abstract ShellDialect getDefaultLocalDialect(); + + public abstract ShellDialect getFallbackDialect(); } diff --git a/core/src/main/java/io/xpipe/core/process/ShellControl.java b/core/src/main/java/io/xpipe/core/process/ShellControl.java index 17678db01..1591af454 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellControl.java +++ b/core/src/main/java/io/xpipe/core/process/ShellControl.java @@ -2,11 +2,11 @@ package io.xpipe.core.process; import io.xpipe.core.store.ShellStore; import io.xpipe.core.store.StatefulDataStore; -import io.xpipe.core.util.*; +import io.xpipe.core.util.FailableConsumer; +import io.xpipe.core.util.FailableFunction; import lombok.NonNull; import java.io.IOException; -import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -16,6 +16,10 @@ import java.util.function.Function; public interface ShellControl extends ProcessControl { + UUID getElevationSecretId(); + + void setParentSystemAccess(ParentSystemAccess access); + List getExitUuids(); Optional getSourceStore(); @@ -24,18 +28,12 @@ public interface ShellControl extends ProcessControl { List getInitCommands(); - ShellControl withTargetTerminalShellDialect(ShellDialect d); + ParentSystemAccess getParentSystemAccess(); - ShellDialect getTargetTerminalShellDialect(); - - default boolean hasLocalSystemAccess() { - return getSystemId() != null && getSystemId().equals(XPipeSystemId.getLocal()); - } + ParentSystemAccess getLocalSystemAccess(); boolean isLocal(); - ShellControl changesHosts(); - ShellControl getMachineRootSession(); ShellControl withoutLicenseCheck(); @@ -44,19 +42,19 @@ public interface ShellControl extends ProcessControl { boolean isLicenseCheck(); - UUID getSystemId(); - ReentrantLock getLock(); - ShellControl onInit(FailableConsumer pc); + void setOriginalShellDialect(ShellDialect dialect); - ShellControl onPreInit(FailableConsumer pc); + ShellDialect getOriginalShellDialect(); + + ShellControl onInit(FailableConsumer pc); default ShellControl withShellStateInit(StatefulDataStore store) { return onInit(shellControl -> { var s = store.getState(); s.setOsType(shellControl.getOsType()); - s.setShellDialect(shellControl.getShellDialect()); + s.setShellDialect(shellControl.getOriginalShellDialect()); s.setRunning(true); s.setOsName(shellControl.getOsName()); store.setState(s); @@ -79,16 +77,10 @@ public interface ShellControl extends ProcessControl { ShellControl withErrorFormatter(Function formatter); - String prepareTerminalOpen(TerminalInitScriptConfig config) throws Exception; - - String prepareIntermediateTerminalOpen(String content, TerminalInitScriptConfig config) throws Exception; + String prepareIntermediateTerminalOpen(String content, TerminalInitScriptConfig config, FailableFunction workingDirectory) throws Exception; String getSystemTemporaryDirectory(); - String getSubTemporaryDirectory(); - - void checkRunning(); - default CommandControl osascriptCommand(String script) { return command(String.format( """ @@ -144,49 +136,58 @@ public interface ShellControl extends ProcessControl { } } - ElevationResult buildElevatedCommand(CommandConfiguration input, String prefix) throws Exception; - - void restart() throws Exception; - - OsType getOsType(); - - ElevationConfig getElevationConfig() throws Exception; - - ShellControl elevated(String message, FailableFunction elevationFunction); - - default ShellControl elevationPassword(SecretValue value) { - return elevationPassword(() -> value); - } - ShellControl elevationPassword(FailableSupplier value); - - ShellControl withInitSnippet(ScriptSnippet snippet); - - ShellControl additionalTimeout(int ms); - - default ShellControl disableTimeout() { - return additionalTimeout(Integer.MAX_VALUE); - } - - FailableSupplier getElevationPassword(); - - default ShellControl subShell(@NonNull ShellDialect type) { - return subShell(p -> type.getLoginOpenCommand(), (sc) -> type.getLoginOpenCommand()) - .elevationPassword(getElevationPassword()); - } - - interface TerminalOpenFunction { - - String prepareWithoutInitCommand(ShellControl sc) throws Exception; - - default String prepareWithInitCommand(ShellControl sc, @NonNull String command) throws Exception { - return command; + default String executeSimpleStringCommand(ShellDialect type, String command) throws Exception { + try (var sub = subShell(type).start()) { + return sub.executeSimpleStringCommand(command); } } + ShellControl withSecurityPolicy(ShellSecurityPolicy policy); + + ShellSecurityPolicy getEffectiveSecurityPolicy(); + + String buildElevatedCommand(CommandConfiguration input, String prefix, CountDown countDown) throws Exception; + + void restart() throws Exception; + + OsType.Any getOsType(); + + ShellControl elevated(String message, FailableFunction elevationFunction); + + ShellControl withInitSnippet(ScriptSnippet snippet); + + default ShellControl subShell(@NonNull ShellDialect type) { + var o = new ShellOpenFunction() { + + @Override + public CommandBuilder prepareWithoutInitCommand() throws Exception { + return CommandBuilder.of().add(sc -> type.getLoginOpenCommand(sc)); + } + + @Override + public CommandBuilder prepareWithInitCommand(@NonNull String command) throws Exception { + return CommandBuilder.ofString(command); + } + }; + var s = singularSubShell(o); + s.setParentSystemAccess(ParentSystemAccess.identity()); + return s; + } + default ShellControl identicalSubShell() { - return subShell(p -> p.getShellDialect().getLoginOpenCommand(), - (sc) -> sc.getShellDialect().getLoginOpenCommand() - ).elevationPassword(getElevationPassword()); + var o = new ShellOpenFunction() { + + @Override + public CommandBuilder prepareWithoutInitCommand() throws Exception { + return CommandBuilder.of().add(sc -> sc.getShellDialect().getLoginOpenCommand(sc)); + } + + @Override + public CommandBuilder prepareWithInitCommand(@NonNull String command) throws Exception { + return CommandBuilder.ofString(command); + } + }; + return singularSubShell(o); } default T enforceDialect(@NonNull ShellDialect type, FailableFunction sc) throws Exception { @@ -200,7 +201,9 @@ public interface ShellControl extends ProcessControl { } ShellControl subShell( - FailableFunction command, TerminalOpenFunction terminalCommand); + ShellOpenFunction command, ShellOpenFunction terminalCommand); + + ShellControl singularSubShell(ShellOpenFunction command); void writeLineAndReadEcho(String command) throws Exception; @@ -211,31 +214,19 @@ public interface ShellControl extends ProcessControl { @Override ShellControl start(); - CommandControl command(FailableFunction command); - - CommandControl command( - FailableFunction command, - FailableFunction terminalCommand); - - default CommandControl command(String... command) { - var c = Arrays.stream(command).filter(s -> s != null).toArray(String[]::new); - return command(shellProcessControl -> String.join("\n", c)); + default CommandControl command(String command) { + return command(CommandBuilder.ofFunction(shellProcessControl -> command)); } - default CommandControl buildCommand(Consumer builder) { - return command(sc-> { - var b = CommandBuilder.of(); - builder.accept(b); - return b.buildString(sc); - }); - } - - default CommandControl command(List command) { - return command(shellProcessControl -> ShellDialect.flatten(command)); + default CommandControl command(Consumer builder) { + var b = CommandBuilder.of(); + builder.accept(b); + return command(b); } default CommandControl command(CommandBuilder builder) { - return command(shellProcessControl -> builder.buildString(shellProcessControl)); + var sc = ProcessControlProvider.get().command(this, builder, builder); + return sc; } void exitAndWait() throws IOException; diff --git a/core/src/main/java/io/xpipe/core/process/ShellDialect.java b/core/src/main/java/io/xpipe/core/process/ShellDialect.java index cd9d9b8f8..a4c4b6a9b 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellDialect.java +++ b/core/src/main/java/io/xpipe/core/process/ShellDialect.java @@ -6,32 +6,19 @@ import io.xpipe.core.charsetter.StreamCharset; import io.xpipe.core.store.FileSystem; import io.xpipe.core.util.SecretValue; -import java.io.IOException; import java.nio.charset.Charset; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; +import java.util.UUID; import java.util.stream.Stream; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") public interface ShellDialect { - static String flatten(List command) { - return command.stream() - .map(s -> s.contains(" ") - && !(s.startsWith("\"") && s.endsWith("\"")) - && !(s.startsWith("'") && s.endsWith("'")) - ? "\"" + s + "\"" - : s) - .collect(Collectors.joining(" ")); - } + String terminalLauncherScript(UUID request, String name); String getExecutableName(); - default boolean isSupportedShell() { - return true; - } - default boolean isSelectable() { return true; } @@ -40,10 +27,6 @@ public interface ShellDialect { return other.equals(this); } - default ShellDialect getDumbReplacementDialect(ShellControl parent) { - return this; - } - String getCatchAllVariable(); String queryVersion(ShellControl shellControl) throws Exception; @@ -58,17 +41,15 @@ public interface ShellDialect { CommandControl resolveDirectory(ShellControl shellControl, String directory); + String literalArgument(String s); + String fileArgument(String s); String quoteArgument(String s); - String executeWithNoInitFiles(ShellDialect parentDialect, String file); - - void prepareDumbTerminalCommands(ShellControl sc) throws Exception; - String prepareTerminalEnvironmentCommands(); - String appendToPathVariableCommand(String entry); + String addToPathVariableCommand(List entry, boolean append); default String applyRcFileCommand() { return null; @@ -80,7 +61,7 @@ public interface ShellDialect { return null; } - CommandControl createStreamFileWriteCommand(ShellControl shellControl, String file); + CommandControl createStreamFileWriteCommand(ShellControl shellControl, String file, long totalBytes); default String getCdCommand(String directory) { return "cd \"" + directory + "\""; @@ -98,10 +79,6 @@ public interface ShellDialect { String prepareScriptContent(String content); - default void exit(ShellControl sc) throws IOException { - sc.writeLine("exit"); - } - default String getPassthroughExitCommand() { return "exit"; } @@ -110,13 +87,6 @@ public interface ShellDialect { return "exit 0"; } - ElevationConfig determineElevationConfig(ShellControl shellControl) throws Exception; - - - ElevationResult elevateDumbCommand(ShellControl shellControl, CommandConfiguration command, String message) throws Exception; - - String elevateTerminalCommand(ShellControl shellControl, String command, String message) throws Exception; - String environmentVariable(String name); default String getConcatenationOperator() { @@ -125,13 +95,9 @@ public interface ShellDialect { String getDiscardOperator(); - default String getOrConcatenationOperator() { - return "||"; - } - String getScriptPermissionsCommand(String file); - String prepareAskpassContent(ShellControl sc, String fileName, List s, String errorMessage) throws Exception; + ShellDialectAskpass getAskpass(); String getSetEnvironmentVariableCommand(String variable, String value); @@ -151,10 +117,12 @@ public interface ShellDialect { return getPrintVariableCommand(name); } - String getOpenCommand(); + String getOpenCommand(ShellControl shellControl); - default String getLoginOpenCommand() { - return getOpenCommand(); + CommandBuilder getOpenScriptCommand(String file); + + default String getLoginOpenCommand(ShellControl shellControl) { + return getOpenCommand(shellControl); } default void prepareCommandForShell(CommandBuilder b) {} @@ -163,8 +131,6 @@ public interface ShellDialect { String runScriptCommand(ShellControl parent, String file); - String runScriptSilentlyCommand(ShellControl parent, String file); - String sourceScriptCommand(ShellControl parent, String file); String executeCommandWithShell(String cmd); @@ -193,6 +159,10 @@ public interface ShellDialect { String clearDisplayCommand(); + String[] getLocalLaunchCommand(); + + ShellDumbMode getDumbMode(); + CommandControl createFileExistsCommand(ShellControl sc, String file); CommandControl symbolicLink(ShellControl sc, String linkFile, String targetFile); diff --git a/core/src/main/java/io/xpipe/core/process/ShellDialectAskpass.java b/core/src/main/java/io/xpipe/core/process/ShellDialectAskpass.java new file mode 100644 index 000000000..7c1b7f6e6 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/process/ShellDialectAskpass.java @@ -0,0 +1,17 @@ +package io.xpipe.core.process; + +import java.util.List; +import java.util.UUID; + +public interface ShellDialectAskpass { + + String prepareLocalPassthroughContent(ShellControl sc, UUID requestId, String prefix) throws Exception; + + String prepareStderrPassthroughContent(ShellControl sc, UUID requestId, String prefix) throws Exception; + + String prepareFixedContent(ShellControl sc, String fileName, List s, String errorMessage) throws Exception; + + String elevateDumbCommand(ShellControl shellControl, CommandConfiguration command, UUID requestId, String message) throws Exception; + + String elevateTerminalCommandWithPreparedAskpass(ShellControl shellControl, UUID request, String command, String prefix) throws Exception; +} diff --git a/core/src/main/java/io/xpipe/core/process/ShellDialects.java b/core/src/main/java/io/xpipe/core/process/ShellDialects.java index 1cf9fbd4c..0e6984e50 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellDialects.java +++ b/core/src/main/java/io/xpipe/core/process/ShellDialects.java @@ -4,6 +4,7 @@ import io.xpipe.core.util.ModuleLayerLoader; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.ServiceLoader; public class ShellDialects { @@ -16,7 +17,6 @@ public class ShellDialects { public static ShellDialect CMD; public static ShellDialect ASH; public static ShellDialect SH; - public static ShellDialect SH_BSD; public static ShellDialect DASH; public static ShellDialect BASH; public static ShellDialect ZSH; @@ -27,7 +27,7 @@ public class ShellDialects { public static ShellDialect RBASH; public static List getStartableDialects() { - return ALL.stream().filter(dialect -> dialect.getOpenCommand() != null).filter(dialect -> dialect != SH_BSD).toList(); + return ALL.stream().filter(dialect -> dialect.getOpenCommand(null) != null).toList(); } public static class Loader implements ModuleLayerLoader { @@ -50,7 +50,6 @@ public class ShellDialects { CSH = byId("csh"); ASH = byId("ash"); SH = byId("sh"); - SH_BSD = byId("shBsd"); CISCO = byId("cisco"); RBASH = byId("rbash"); } @@ -71,15 +70,19 @@ public class ShellDialects { .filter(shellType -> shellType.getId().equals(name)) .findFirst() .orElseThrow(); + } + + public static boolean isPowershell(ShellControl sc) { + return sc.getShellDialect().equals(POWERSHELL) || sc.getShellDialect().equals(POWERSHELL_CORE); } - public static ShellDialect getPlatformDefault() { - if (OsType.getLocal().equals(OsType.WINDOWS)) { - return CMD; - } else if (OsType.getLocal().equals(OsType.LINUX)) { - return BASH; - } else { - return ZSH; - } + public static ShellDialect byName(String name) { + return byNameIfPresent(name).orElseThrow(); + } + + public static Optional byNameIfPresent(String name) { + return ALL.stream() + .filter(shellType -> shellType.getId().equals(name)) + .findFirst(); } } diff --git a/core/src/main/java/io/xpipe/core/process/ShellDumbMode.java b/core/src/main/java/io/xpipe/core/process/ShellDumbMode.java new file mode 100644 index 000000000..1fd7991c9 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/process/ShellDumbMode.java @@ -0,0 +1,74 @@ +package io.xpipe.core.process; + +import java.io.IOException; + +public interface ShellDumbMode { + + default boolean supportsAnyPossibleInteraction() { + return true; + } + + default ShellDialect getSwitchDialect() { + return null; + } + + default String getUnsupportedMessage() { + return null; + } + + default CommandBuilder prepareInlineDumbCommand(ShellControl self, ShellControl parent, ShellOpenFunction function) throws Exception { + return function.prepareWithoutInitCommand(); + } + + default void prepareDumbInit(ShellControl shellControl) throws Exception {} + + default void prepareDumbExit(ShellControl shellControl) throws IOException { + shellControl.writeLine("exit"); + } + + class Adjusted implements ShellDumbMode { + + private final ShellDialect replacementDialect; + + public Adjusted(ShellDialect replacementDialect) { + this.replacementDialect = replacementDialect; + } + + @Override + public CommandBuilder prepareInlineDumbCommand(ShellControl self, ShellControl parent, ShellOpenFunction function) throws Exception { + return function.prepareWithInitCommand(replacementDialect.getLoginOpenCommand(null)); + } + } + + class Unsupported implements ShellDumbMode { + + private final String message; + + public Unsupported(String message) {this.message = message;} + + @Override + public boolean supportsAnyPossibleInteraction() { + return false; + } + + @Override + public void prepareDumbInit(ShellControl shellControl) throws Exception { + throw new UnsupportedOperationException(); + } + + @Override + public void prepareDumbExit(ShellControl shellControl) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public CommandBuilder prepareInlineDumbCommand(ShellControl self, ShellControl parent, ShellOpenFunction function) { + throw new UnsupportedOperationException(); + } + + @Override + public String getUnsupportedMessage() { + return message; + } + } +} diff --git a/core/src/main/java/io/xpipe/core/process/ShellOpenFunction.java b/core/src/main/java/io/xpipe/core/process/ShellOpenFunction.java new file mode 100644 index 000000000..94307a151 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/process/ShellOpenFunction.java @@ -0,0 +1,38 @@ +package io.xpipe.core.process; + +import lombok.NonNull; + +public interface ShellOpenFunction { + + static ShellOpenFunction of(String b) { + return new ShellOpenFunction() { + @Override + public CommandBuilder prepareWithoutInitCommand() throws Exception { + return CommandBuilder.of().add(b); + } + + @Override + public CommandBuilder prepareWithInitCommand(@NonNull String command) throws Exception { + throw new UnsupportedOperationException(); + } + }; + } + + static ShellOpenFunction of(CommandBuilder b) { + return new ShellOpenFunction() { + @Override + public CommandBuilder prepareWithoutInitCommand() throws Exception { + return b; + } + + @Override + public CommandBuilder prepareWithInitCommand(@NonNull String command) throws Exception { + return CommandBuilder.ofString(command); + } + }; + } + + CommandBuilder prepareWithoutInitCommand() throws Exception; + + CommandBuilder prepareWithInitCommand(@NonNull String command) throws Exception; +} diff --git a/core/src/main/java/io/xpipe/core/process/ShellSecurityPolicy.java b/core/src/main/java/io/xpipe/core/process/ShellSecurityPolicy.java new file mode 100644 index 000000000..9fed33653 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/process/ShellSecurityPolicy.java @@ -0,0 +1,14 @@ +package io.xpipe.core.process; + +public interface ShellSecurityPolicy { + + boolean checkElevate(ShellControl shellControl); + + default void elevateOrThrow(ShellControl shellControl) { + if (!checkElevate(shellControl)) { + throw new UnsupportedOperationException("Elevation is not allowed for this system"); + } + } + + boolean permitTempScriptCreation(); +} diff --git a/core/src/main/java/io/xpipe/core/process/ShellStoreState.java b/core/src/main/java/io/xpipe/core/process/ShellStoreState.java index 3857d8054..3b45daaa7 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellStoreState.java +++ b/core/src/main/java/io/xpipe/core/process/ShellStoreState.java @@ -15,7 +15,7 @@ import lombok.extern.jackson.Jacksonized; @SuperBuilder public class ShellStoreState extends DataStoreState implements OsNameState { - OsType osType; + OsType.Any osType; String osName; ShellDialect shellDialect; Boolean running; diff --git a/core/src/main/java/io/xpipe/core/store/ConnectionFileSystem.java b/core/src/main/java/io/xpipe/core/store/ConnectionFileSystem.java index c781f36f4..5a4f0d368 100644 --- a/core/src/main/java/io/xpipe/core/store/ConnectionFileSystem.java +++ b/core/src/main/java/io/xpipe/core/store/ConnectionFileSystem.java @@ -1,6 +1,7 @@ package io.xpipe.core.store; import com.fasterxml.jackson.annotation.JsonIgnore; +import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.ShellControl; import lombok.Getter; @@ -72,10 +73,10 @@ public class ConnectionFileSystem implements FileSystem { } @Override - public OutputStream openOutput(String file) throws Exception { + public OutputStream openOutput(String file, long totalBytes) throws Exception { return shellControl .getShellDialect() - .createStreamFileWriteCommand(shellControl, file) + .createStreamFileWriteCommand(shellControl, file, totalBytes) .startExternalStdin(); } @@ -122,7 +123,7 @@ public class ConnectionFileSystem implements FileSystem { @Override public void mkdirs(String file) throws Exception { try (var pc = shellControl - .command(proc -> proc.getShellDialect().getMkdirsCommand(file)) + .command(CommandBuilder.ofFunction(proc -> proc.getShellDialect().getMkdirsCommand(file))) .start()) { pc.discardOrThrow(); } diff --git a/core/src/main/java/io/xpipe/core/store/DataStore.java b/core/src/main/java/io/xpipe/core/store/DataStore.java index eccacb693..3fd37278d 100644 --- a/core/src/main/java/io/xpipe/core/store/DataStore.java +++ b/core/src/main/java/io/xpipe/core/store/DataStore.java @@ -3,11 +3,6 @@ package io.xpipe.core.store; import com.fasterxml.jackson.annotation.JsonTypeInfo; import io.xpipe.core.util.DataStateProvider; -/** - * A data store represents some form of a location where data is stored, e.g. a file or a database. - * It does not contain any information on what data is stored, - * how the data is stored inside, or what part of the data store makes up the actual data source. - */ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") public interface DataStore { diff --git a/core/src/main/java/io/xpipe/core/store/DataStoreId.java b/core/src/main/java/io/xpipe/core/store/DataStoreId.java index d6cda7d27..a717c3b84 100644 --- a/core/src/main/java/io/xpipe/core/store/DataStoreId.java +++ b/core/src/main/java/io/xpipe/core/store/DataStoreId.java @@ -43,6 +43,10 @@ public class DataStoreId { throw new IllegalArgumentException("Names are null"); } + if (Arrays.stream(names).anyMatch(s -> s == null)) { + throw new IllegalArgumentException("Name is null"); + } + if (Arrays.stream(names).anyMatch(s -> s.contains("" + SEPARATOR))) { throw new IllegalArgumentException( "Separator character " + SEPARATOR + " is not allowed in the names"); @@ -67,9 +71,9 @@ public class DataStoreId { throw new IllegalArgumentException("String is null"); } - var split = s.split(String.valueOf(SEPARATOR)); + var split = s.split(String.valueOf(SEPARATOR), -1); - var names = Arrays.stream(split).toList(); + var names = Arrays.stream(split).map(String::trim).map(String::toLowerCase).toList(); if (names.stream().anyMatch(s1 -> s1.isEmpty())) { throw new IllegalArgumentException("Name must not be empty"); } diff --git a/core/src/main/java/io/xpipe/core/store/FileSystem.java b/core/src/main/java/io/xpipe/core/store/FileSystem.java index c445e8ade..78bc1701b 100644 --- a/core/src/main/java/io/xpipe/core/store/FileSystem.java +++ b/core/src/main/java/io/xpipe/core/store/FileSystem.java @@ -104,7 +104,7 @@ public interface FileSystem extends Closeable, AutoCloseable { InputStream openInput(String file) throws Exception; - OutputStream openOutput(String file) throws Exception; + OutputStream openOutput(String file, long totalBytes) throws Exception; boolean fileExists(String file) throws Exception; diff --git a/core/src/main/java/io/xpipe/core/store/LocalStore.java b/core/src/main/java/io/xpipe/core/store/LocalStore.java index 64bfee1df..0e9ac8d69 100644 --- a/core/src/main/java/io/xpipe/core/store/LocalStore.java +++ b/core/src/main/java/io/xpipe/core/store/LocalStore.java @@ -17,6 +17,7 @@ public class LocalStore extends JacksonizedValue implements ShellStore, Stateful @Override public ShellControl control() { var pc = ProcessControlProvider.get().createLocalProcessControl(true); + pc.withSourceStore(this); pc.withShellStateInit(this); pc.withShellStateFail(this); return pc; diff --git a/core/src/main/java/io/xpipe/core/util/AesSecretValue.java b/core/src/main/java/io/xpipe/core/util/AesSecretValue.java index c5c38b284..db390afe9 100644 --- a/core/src/main/java/io/xpipe/core/util/AesSecretValue.java +++ b/core/src/main/java/io/xpipe/core/util/AesSecretValue.java @@ -6,11 +6,16 @@ import lombok.experimental.SuperBuilder; import javax.crypto.Cipher; import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; import java.util.Random; @SuperBuilder @@ -21,32 +26,51 @@ public abstract class AesSecretValue extends EncryptedSecretValue { private static final int TAG_LENGTH_BIT = 128; private static final int IV_LENGTH_BYTE = 12; private static final int AES_KEY_BIT = 128; - private static final byte[] IV = getFixedNonce(IV_LENGTH_BYTE); + private static final int SALT_BIT = 16; + private static final SecretKeyFactory SECRET_FACTORY; + + static { + try { + SECRET_FACTORY = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } public AesSecretValue(char[] secret) { super(secret); } - private static byte[] getFixedNonce(int numBytes) { + protected abstract int getIterationCount(); + + protected byte[] getNonce(int numBytes) { byte[] nonce = new byte[numBytes]; - new Random(1 - 28 + 213213).nextBytes(nonce); + new SecureRandom().nextBytes(nonce); return nonce; } - protected SecretKey getAESKey(int keysize) throws NoSuchAlgorithmException, InvalidKeySpecException { + protected SecretKey getSecretKey(char[] chars) throws InvalidKeySpecException { + var salt = new byte[SALT_BIT]; + new Random(AES_KEY_BIT).nextBytes(salt); + KeySpec spec = new PBEKeySpec(chars, salt, getIterationCount(), AES_KEY_BIT); + return new SecretKeySpec(SECRET_FACTORY.generateSecret(spec).getEncoded(), "AES"); + } + + protected SecretKey getAESKey() throws InvalidKeySpecException { throw new UnsupportedOperationException(); } @Override @SneakyThrows public byte[] encrypt(byte[] c) { - SecretKey secretKey = getAESKey(AES_KEY_BIT); + SecretKey secretKey = getAESKey(); Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); - cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(TAG_LENGTH_BIT, IV)); + var iv = getNonce(IV_LENGTH_BYTE); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); var bytes = cipher.doFinal(c); - bytes = ByteBuffer.allocate(IV.length + bytes.length) + bytes = ByteBuffer.allocate(iv.length + bytes.length) .order(ByteOrder.LITTLE_ENDIAN) - .put(IV) + .put(iv) .put(bytes) .array(); return bytes; @@ -61,7 +85,7 @@ public abstract class AesSecretValue extends EncryptedSecretValue { byte[] cipherText = new byte[bb.remaining()]; bb.get(cipherText); - SecretKey secretKey = getAESKey(AES_KEY_BIT); + SecretKey secretKey = getAESKey(); Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); return cipher.doFinal(cipherText); diff --git a/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java b/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java index 82f4d87b4..8877b47d9 100644 --- a/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java +++ b/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java @@ -36,7 +36,7 @@ public class CoreJacksonModule extends SimpleModule { @Override public void setupModule(SetupContext context) { context.registerSubtypes( - new NamedType(DefaultSecretValue.class), + new NamedType(InPlaceSecretValue.class), new NamedType(StdinDataStore.class), new NamedType(StdoutDataStore.class), new NamedType(LocalStore.class), @@ -64,7 +64,8 @@ public class CoreJacksonModule extends SimpleModule { addDeserializer(Path.class, new LocalPathDeserializer()); addSerializer(OsType.class, new OsTypeSerializer()); - addDeserializer(OsType.class, new OsTypeDeserializer()); + addDeserializer(OsType.Local.class, new OsTypeLocalDeserializer()); + addDeserializer(OsType.Any.class, new OsTypeAnyDeserializer()); context.setMixInAnnotations(Throwable.class, ThrowableTypeMixIn.class); @@ -144,12 +145,23 @@ public class CoreJacksonModule extends SimpleModule { } } - public static class OsTypeDeserializer extends JsonDeserializer { + public static class OsTypeLocalDeserializer extends JsonDeserializer { @Override - public OsType deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + public OsType.Local deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + var stream = Stream.of(OsType.WINDOWS, OsType.LINUX, OsType.MACOS); var n = p.getValueAsString(); - return Stream.of(OsType.WINDOWS, OsType.LINUX, OsType.MACOS).filter(osType -> osType.getName().equals(n)).findFirst().orElse(null); + return stream.filter(osType -> osType.getName().equals(n)).findFirst().orElse(null); + } + } + + public static class OsTypeAnyDeserializer extends JsonDeserializer { + + @Override + public OsType.Any deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + var stream = Stream.of(OsType.WINDOWS, OsType.LINUX, OsType.BSD, OsType.SOLARIS, OsType.MACOS); + var n = p.getValueAsString(); + return stream.filter(osType -> osType.getName().equals(n)).findFirst().orElse(null); } } diff --git a/core/src/main/java/io/xpipe/core/util/DefaultSecretValue.java b/core/src/main/java/io/xpipe/core/util/DefaultSecretValue.java deleted file mode 100644 index aa54f87c4..000000000 --- a/core/src/main/java/io/xpipe/core/util/DefaultSecretValue.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.xpipe.core.util; - -import com.fasterxml.jackson.annotation.JsonTypeName; -import lombok.EqualsAndHashCode; -import lombok.experimental.SuperBuilder; -import lombok.extern.jackson.Jacksonized; - -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.SecretKeySpec; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.KeySpec; -import java.util.Random; - -@JsonTypeName("default") -@SuperBuilder -@Jacksonized -@EqualsAndHashCode(callSuper = true) -public class DefaultSecretValue extends AesSecretValue { - - public DefaultSecretValue(char[] secret) { - super(secret); - } - - @Override - public SecretValue inPlace() { - return this; - } - - protected SecretKey getAESKey(int keysize) throws NoSuchAlgorithmException, InvalidKeySpecException { - SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); - var salt = new byte[16]; - new Random(keysize).nextBytes(salt); - KeySpec spec = new PBEKeySpec(new char[] {'X', 'P', 'E' << 1}, salt, 2048, keysize); - return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES"); - } -} diff --git a/core/src/main/java/io/xpipe/core/util/InPlaceSecretValue.java b/core/src/main/java/io/xpipe/core/util/InPlaceSecretValue.java new file mode 100644 index 000000000..1c1e5228f --- /dev/null +++ b/core/src/main/java/io/xpipe/core/util/InPlaceSecretValue.java @@ -0,0 +1,54 @@ +package io.xpipe.core.util; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import javax.crypto.SecretKey; +import java.security.spec.InvalidKeySpecException; +import java.util.Random; + +@JsonTypeName("default") +@SuperBuilder +@Jacksonized +@EqualsAndHashCode(callSuper = true) +public class InPlaceSecretValue extends AesSecretValue { + + public static InPlaceSecretValue of(String s) { + return new InPlaceSecretValue(s.toCharArray()); + } + + public static InPlaceSecretValue of(char[] c) { + return new InPlaceSecretValue(c); + } + + public InPlaceSecretValue(char[] secret) { + super(secret); + } + + @Override + protected int getIterationCount() { + return 2048; + } + + @Override + public InPlaceSecretValue inPlace() { + return this; + } + + @Override + public String toString() { + return ""; + } + + protected byte[] getNonce(int numBytes) { + byte[] nonce = new byte[numBytes]; + new Random(1 - 28 + 213213).nextBytes(nonce); + return nonce; + } + + protected SecretKey getAESKey() throws InvalidKeySpecException { + return getSecretKey(new char[] {'X', 'P', 'E' << 1}); + } +} diff --git a/core/src/main/java/io/xpipe/core/util/SecretReference.java b/core/src/main/java/io/xpipe/core/util/SecretReference.java new file mode 100644 index 000000000..2e0203dd3 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/util/SecretReference.java @@ -0,0 +1,28 @@ +package io.xpipe.core.util; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.UUID; + +@Value +@AllArgsConstructor +public class SecretReference { + + public static SecretReference ofUuid(UUID secretId) { + return new SecretReference(secretId, 0); + } + + UUID secretId; + int subId; + + public SecretReference(Object store) { + this.secretId = UuidHelper.generateFromObject(store); + this.subId = 0; + } + + public SecretReference(Object store, int sub) { + this.secretId = UuidHelper.generateFromObject(store); + this.subId = sub; + } +} diff --git a/core/src/main/java/io/xpipe/core/util/SecretValue.java b/core/src/main/java/io/xpipe/core/util/SecretValue.java index ea7d1516d..1559577d5 100644 --- a/core/src/main/java/io/xpipe/core/util/SecretValue.java +++ b/core/src/main/java/io/xpipe/core/util/SecretValue.java @@ -5,11 +5,12 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import java.util.Arrays; import java.util.Base64; import java.util.function.Consumer; +import java.util.function.Function; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") public interface SecretValue { - SecretValue inPlace(); + InPlaceSecretValue inPlace(); static String toBase64e(byte[] b) { var base64 = Base64.getEncoder().encodeToString(b); @@ -26,6 +27,20 @@ public interface SecretValue { Arrays.fill(chars, (char) 0); } + default T mapSecretValue(Function con) { + var chars = getSecret(); + var r = con.apply(chars); + Arrays.fill(chars, (char) 0); + return r; + } + + default T mapSecretValueFailable(FailableFunction con) throws Exception { + var chars = getSecret(); + var r = con.apply(chars); + Arrays.fill(chars, (char) 0); + return r; + } + char[] getSecret(); default String getSecretValue() { diff --git a/core/src/main/java/io/xpipe/core/util/XPipeExecTempDirectory.java b/core/src/main/java/io/xpipe/core/util/XPipeExecTempDirectory.java deleted file mode 100644 index 2d115bb43..000000000 --- a/core/src/main/java/io/xpipe/core/util/XPipeExecTempDirectory.java +++ /dev/null @@ -1,88 +0,0 @@ -package io.xpipe.core.util; - -import io.xpipe.core.store.FileNames; -import io.xpipe.core.process.OsType; -import io.xpipe.core.process.ShellControl; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.stream.Stream; - -public class XPipeExecTempDirectory { - - private static final Set usedSystems = new CopyOnWriteArraySet<>(); - - public static String getSystemTempDirectory(ShellControl proc) throws Exception { - return proc.getOsType().getTempDirectory(proc); - } - - public static synchronized String initExecTempDirectory(ShellControl proc) throws Exception { - var d = proc.getShellDialect(); - - // We assume that this exists now as the systemid should have been created in this - var xpipeHome = XPipeInstallation.getDataDir(proc); - var targetTemp = FileNames.join(xpipeHome, "temp"); - - var systemTemp = proc.getOsType().getTempDirectory(proc); - var legacyTemp = FileNames.join(systemTemp, "xpipe"); - var legacyExecTemp = FileNames.join(legacyTemp, "exec"); - - // Always delete legacy directory and do not care whether it partially fails - d.deleteFileOrDirectory(proc, legacyExecTemp).executeAndCheck(); - - // Check permissions for home directory - // If this is somehow messed up, we can still default back to the system directory - if (!checkDirectoryPermissions(proc, xpipeHome)) { - if (!d.directoryExists(proc, systemTemp).executeAndCheck() || !checkDirectoryPermissions(proc, systemTemp)) { - throw new IOException("No permissions to create scripts in either %s or %s".formatted(systemTemp, targetTemp)); - } - - targetTemp = systemTemp; - } else { - // Create and set all access permissions if not existent - if (!d.directoryExists(proc, targetTemp).executeAndCheck()) { - d.prepareUserTempDirectory(proc, targetTemp).execute(); - } else if (!usedSystems.contains(proc.getSystemId())) { - // Try to clear directory and do not care about errors - d.deleteFileOrDirectory(proc, targetTemp).executeAndCheck(); - d.prepareUserTempDirectory(proc, targetTemp).executeAndCheck(); - } else { - // Still attempt to properly set permissions every time - d.prepareUserTempDirectory(proc, targetTemp).executeAndCheck(); - } - - } - - usedSystems.add(proc.getSystemId()); - return targetTemp; - } - - private static boolean checkDirectoryPermissions(ShellControl proc, String dir) throws Exception { - if (proc.getOsType().equals(OsType.WINDOWS)) { - return true; - } - - var d = proc.getShellDialect(); - return proc.executeSimpleBooleanCommand("test -r %s && test -w %s && test -x %s" - .formatted(d.fileArgument(dir), d.fileArgument(dir), d.fileArgument(dir))); - } - - public static synchronized void occupyXPipeTempDirectory(ShellControl proc) { - usedSystems.add(proc.getSystemId()); - } - - public static String getSubDirectory(ShellControl proc, String... sub) throws Exception { - var base = proc.getSubTemporaryDirectory(); - var arr = Stream.concat(Stream.of(base), Arrays.stream(sub)) - .toArray(String[]::new); - var dir = FileNames.join(arr); - - // We assume that this directory does not exist yet and therefore don't perform any checks - proc.getShellDialect().prepareUserTempDirectory(proc, dir).execute(); - - return dir; - } -} diff --git a/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java b/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java index 7f351ba98..52f365924 100644 --- a/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java +++ b/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java @@ -10,7 +10,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; -import java.util.List; import java.util.Optional; public class XPipeInstallation { @@ -212,16 +211,34 @@ public class XPipeInstallation { } public static String queryInstallationVersion(ShellControl p, String exec) throws Exception { - try (CommandControl c = p.command(List.of(exec, "version")).start()) { + try (CommandControl c = p.command(CommandBuilder.of().addFile(exec).add("version")).start()) { return c.readStdoutOrThrow(); } catch (ProcessOutputException ex) { return "?"; } } - public static String getInstallationExecutable(ShellControl p, String installation) { - var executable = getDaemonExecutablePath(p.getOsType()); - return FileNames.join(installation, executable); + public static Path getLocalBundledToolsDirectory() { + Path path = getCurrentInstallationBasePath(); + + // Check for development environment + if (!ModuleHelper.isImage()) { + if (OsType.getLocal().equals(OsType.WINDOWS)) { + return path.resolve("dist").resolve("bundled_bin").resolve("windows"); + } else if (OsType.getLocal().equals(OsType.LINUX)) { + return path.resolve("dist").resolve("bundled_bin").resolve("linux"); + } else { + return path.resolve("dist").resolve("bundled_bin").resolve("osx"); + } + } + + if (OsType.getLocal().equals(OsType.WINDOWS)) { + return path.resolve("app").resolve("bundled"); + } else if (OsType.getLocal().equals(OsType.LINUX)) { + return path.resolve("app").resolve("bundled"); + } else { + return path.resolve("Contents").resolve("Resources").resolve("bundled"); + } } public static String getLocalDefaultCliExecutable() { @@ -298,7 +315,7 @@ public class XPipeInstallation { } } - public static String getDaemonDebugScriptPath(OsType type) { + public static String getDaemonDebugScriptPath(OsType.Local type) { if (type.equals(OsType.WINDOWS)) { return FileNames.join("app", "scripts", "xpiped_debug.bat"); } else if (type.equals(OsType.LINUX)) { @@ -308,7 +325,7 @@ public class XPipeInstallation { } } - public static String getDaemonDebugAttachScriptPath(OsType type) { + public static String getDaemonDebugAttachScriptPath(OsType.Local type) { if (type.equals(OsType.WINDOWS)) { return FileNames.join("app", "scripts", "xpiped_debug_attach.bat"); } else if (type.equals(OsType.LINUX)) { @@ -318,7 +335,7 @@ public class XPipeInstallation { } } - public static String getDaemonExecutablePath(OsType type) { + public static String getDaemonExecutablePath(OsType.Local type) { if (type.equals(OsType.WINDOWS)) { return FileNames.join("app", "xpiped.exe"); } else if (type.equals(OsType.LINUX)) { @@ -328,7 +345,7 @@ public class XPipeInstallation { } } - public static String getRelativeCliExecutablePath(OsType type) { + public static String getRelativeCliExecutablePath(OsType.Local type) { if (type.equals(OsType.WINDOWS)) { return FileNames.join("cli", "bin", "xpipe.exe"); } else if (type.equals(OsType.LINUX)) { diff --git a/core/src/main/java/io/xpipe/core/util/XPipeSystemId.java b/core/src/main/java/io/xpipe/core/util/XPipeSystemId.java deleted file mode 100644 index 903d5c6a2..000000000 --- a/core/src/main/java/io/xpipe/core/util/XPipeSystemId.java +++ /dev/null @@ -1,59 +0,0 @@ -package io.xpipe.core.util; - -import io.xpipe.core.store.FileNames; -import io.xpipe.core.process.ShellControl; - -import java.nio.file.Files; -import java.util.UUID; - -public class XPipeSystemId { - - private static UUID localId; - - public static void init() { - try { - var file = - XPipeInstallation.getDataDir().resolve("system_id"); - if (!Files.exists(file)) { - Files.writeString(file, UUID.randomUUID().toString()); - } - localId = UUID.fromString(Files.readString(file).trim()); - } catch (Exception ex) { - localId = UUID.randomUUID(); - } - } - - public static UUID getLocal() { - return localId; - } - - public static UUID getSystemId(ShellControl proc) throws Exception { - var file = FileNames.join(XPipeInstallation.getDataDir(proc), "system_id"); - if (file == null) { - return UUID.randomUUID(); - } - - if (!proc.getShellDialect().createFileExistsCommand(proc, file).executeAndCheck()) { - return writeRandom(proc, file); - } - - try { - return UUID.fromString(proc.getShellDialect().getFileReadCommand(proc, file).readStdoutOrThrow() - .trim()); - } catch (IllegalArgumentException ex) { - // Handle invalid UUID content case - return writeRandom(proc, file); - } - } - - private static UUID writeRandom(ShellControl proc, String file) throws Exception { - proc.executeSimpleCommand( - proc.getShellDialect().getMkdirsCommand(FileNames.getParent(file)), - "Unable to access or create directory " + file); - var id = UUID.randomUUID(); - proc.getShellDialect() - .createTextFileWriteCommand(proc, id.toString(), file) - .execute(); - return id; - } -} diff --git a/core/src/test/java/io/xpipe/core/test/DataSourceReferenceTest.java b/core/src/test/java/io/xpipe/core/test/DataSourceReferenceTest.java deleted file mode 100644 index 031666354..000000000 --- a/core/src/test/java/io/xpipe/core/test/DataSourceReferenceTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.xpipe.core.test; - -import io.xpipe.core.store.DataStoreId; -import io.xpipe.core.source.DataSourceReference; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -public class DataSourceReferenceTest { - - @Test - public void parseValidParameters() { - Assertions.assertEquals(DataSourceReference.parse(" ").getType(), DataSourceReference.Type.LATEST); - Assertions.assertEquals(DataSourceReference.parse(null).getType(), DataSourceReference.Type.LATEST); - - Assertions.assertEquals(DataSourceReference.parse("abc").getType(), DataSourceReference.Type.NAME); - Assertions.assertEquals(DataSourceReference.parse(" abc_ d e").getName(), "abc_ d e"); - - Assertions.assertEquals(DataSourceReference.parse("ab:c").getId(), DataStoreId.fromString(" AB: C ")); - Assertions.assertEquals(DataSourceReference.parse(" ab:c ").getId(), DataStoreId.fromString("ab:c ")); - } - - @ParameterizedTest - @ValueSource(strings = {"abc:", "ab::c", "::abc"}) - public void parseInvalidParameters(String arg) { - Assertions.assertThrows(IllegalArgumentException.class, () -> { - DataSourceReference.parse(arg); - }); - } -} diff --git a/core/src/test/java/io/xpipe/core/test/DataStoreIdTest.java b/core/src/test/java/io/xpipe/core/test/DataStoreIdTest.java index 5381915c4..9a1f963ce 100644 --- a/core/src/test/java/io/xpipe/core/test/DataStoreIdTest.java +++ b/core/src/test/java/io/xpipe/core/test/DataStoreIdTest.java @@ -42,7 +42,7 @@ public class DataStoreIdTest { } @ParameterizedTest - @ValueSource(strings = {"abc", "abc:", "ab::c", "::abc", " ab", "::::", "", " "}) + @ValueSource(strings = {"abc:", "ab::c", "::abc", "::::", "", " "}) public void testFromStringInvalidParameters(String arg) { Assertions.assertThrows(IllegalArgumentException.class, () -> { DataStoreId.fromString(arg); diff --git a/core/src/test/java/io/xpipe/core/test/DataStructureTest.java b/core/src/test/java/io/xpipe/core/test/DataStructureTest.java deleted file mode 100644 index 0c62daafd..000000000 --- a/core/src/test/java/io/xpipe/core/test/DataStructureTest.java +++ /dev/null @@ -1,108 +0,0 @@ -package io.xpipe.core.test; - -import io.xpipe.core.data.generic.GenericDataStreamParser; -import io.xpipe.core.data.generic.GenericDataStreamWriter; -import io.xpipe.core.data.generic.GenericDataStructureNodeReader; -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.node.ValueNode; -import io.xpipe.core.data.typed.TypedDataStreamParser; -import io.xpipe.core.data.typed.TypedDataStreamWriter; -import io.xpipe.core.data.typed.TypedDataStructureNodeReader; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.List; - -public class DataStructureTest { - - public static DataStructureNode createTestData() { - var val = ValueNode.of("value"); - var flatArray = ArrayNode.of(List.of(ValueNode.of(1), ValueNode.of(2))); - var flatTuple = TupleNode.builder().add("key1", val).build(); - var nestedArray = ArrayNode.of(List.of(flatArray, flatTuple)); - return TupleNode.builder() - .add("key1", val) - .add("key2", flatArray) - .add("key3", flatTuple) - .add("key4", nestedArray) - .build(); - } - - @Test - public void testBasicOperations() { - var obj = createTestData(); - Assertions.assertEquals(obj.size(), 4); - Assertions.assertTrue(obj.isTuple()); - - var objCopy = createTestData(); - Assertions.assertEquals(obj, objCopy); - - var key1 = obj.forKey("key1").asString(); - Assertions.assertEquals(key1, "value"); - - var key2 = obj.forKey("key2"); - Assertions.assertTrue(key2.isArray()); - Assertions.assertEquals(key2.at(0), ValueNode.of(1)); - Assertions.assertEquals(key2.at(0).asString(), "1"); - Assertions.assertEquals(key2.at(0).asInt(), 1); - - var key3 = obj.forKey("key3"); - Assertions.assertTrue(key3.isTuple()); - Assertions.assertEquals(key3.forKey("key1"), ValueNode.of("value")); - Assertions.assertEquals(key3.forKey("key1").asString(), "value"); - - var key4 = obj.forKey("key4"); - Assertions.assertTrue(key4.isArray()); - Assertions.assertEquals(key4.at(0), ArrayNode.of(ValueNode.of(1), ValueNode.of(2))); - Assertions.assertEquals(key4.at(0).at(0).asInt(), 1); - } - - @ParameterizedTest - @EnumSource(DataStructureTests.TypedDataset.class) - public void testTypes(DataStructureTests.TypedDataset ds) throws IOException { - for (var el : ds.nodes) { - Assertions.assertTrue(ds.type.matches(el)); - } - } - - @ParameterizedTest - @EnumSource(DataStructureTests.TypedDataset.class) - public void testGenericIo(DataStructureTests.TypedDataset ds) throws IOException { - for (var el : ds.nodes) { - var dataOut = new ByteArrayOutputStream(); - GenericDataStreamWriter.writeStructure(dataOut, el); - var data = dataOut.toByteArray(); - var reader = new GenericDataStructureNodeReader(); - GenericDataStreamParser.parse(new ByteArrayInputStream(data), reader); - var readNode = reader.create(); - - Assertions.assertEquals(el, readNode); - } - } - - @ParameterizedTest - @EnumSource(DataStructureTests.TypedDataset.class) - public void testTypedIo(DataStructureTests.TypedDataset ds) throws IOException { - for (var node : ds.nodes) { - var dataOut = new ByteArrayOutputStream(); - TypedDataStreamWriter.writeStructure(dataOut, node, ds.type); - var data = dataOut.toByteArray(); - - var reader = TypedDataStructureNodeReader.of(ds.type); - new TypedDataStreamParser(ds.type).parseStructure(new ByteArrayInputStream(data), reader); - var readNode = reader.create(); - - Assertions.assertEquals(node, readNode); - if (readNode.isTuple() || readNode.isArray()) { - Assertions.assertEquals(readNode.size(), node.size()); - } - } - } -} diff --git a/core/src/test/java/io/xpipe/core/test/DataStructureTests.java b/core/src/test/java/io/xpipe/core/test/DataStructureTests.java deleted file mode 100644 index 39e4e6eda..000000000 --- a/core/src/test/java/io/xpipe/core/test/DataStructureTests.java +++ /dev/null @@ -1,184 +0,0 @@ -package io.xpipe.core.test; - -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.node.ValueNode; -import io.xpipe.core.data.type.*; -import lombok.AllArgsConstructor; - -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; - -public class DataStructureTests { - - private static DataStructureNode createTestData11() { - var val = ValueNode.of("value".getBytes(StandardCharsets.UTF_8)); - var flatArray = ArrayNode.of(List.of(ValueNode.of(1), ValueNode.of(2))); - var flatTuple = TupleNode.builder().add("key1", val).build(); - var nestedArray = ArrayNode.of(List.of(flatArray, flatTuple)); - return TupleNode.builder() - .add("key1", val) - .add("key2", flatArray) - .add("key3", flatTuple) - .add("key4", nestedArray) - .build(); - } - - private static DataStructureNode createTestData12() { - var val = ValueNode.nullValue(); - var flatArray = ArrayNode.of(); - var flatTuple = TupleNode.builder().add("key1", val).build(); - var nestedArray = ArrayNode.of(List.of(flatArray, flatTuple)); - return TupleNode.builder() - .add("key1", val) - .add("key2", flatArray) - .add("key3", flatTuple) - .add("key4", nestedArray) - .build(); - } - - private static DataType createTestDataType1() { - return createTestData11().determineDataType(); - } - - public static DataStructureNode createTestData21() { - var val = ValueNode.of("value".getBytes(StandardCharsets.UTF_8)); - var flatArray = ArrayNode.of(List.of(ValueNode.of(1), ValueNode.of(2))); - var flatTuple = TupleNode.builder().add("key1", val).build(); - var nestedArray = ArrayNode.of(List.of(flatArray, flatTuple)); - var doubleNested = ArrayNode.of(val, flatArray, flatTuple, nestedArray); - return doubleNested; - } - - public static DataStructureNode createTestData22() { - var val = ValueNode.of("value".getBytes(StandardCharsets.UTF_8)); - return ArrayNode.of(val); - } - - public static DataType createTestDataType2() { - return ArrayType.of(WildcardType.of()); - } - - public static DataStructureNode createTestData31() { - var val = ValueNode.of("value".getBytes(StandardCharsets.UTF_8)); - var flatTuple = TupleNode.builder().add("key1", val).build(); - var flatArray = ArrayNode.of(List.of(val, flatTuple)); - return flatArray; - } - - public static DataStructureNode createTestData32() { - var val = ValueNode.of("value2".getBytes(StandardCharsets.UTF_8)); - var flatTuple = TupleNode.builder() - .add("key1", ValueNode.nullValue()) - .add("key2", ValueNode.nullValue()) - .build(); - var flatArray = ArrayNode.of(List.of(val, flatTuple)); - return flatArray; - } - - public static DataStructureNode createTestData41() { - var val = ValueNode.of("value".getBytes(StandardCharsets.UTF_8)); - return val; - } - - public static DataStructureNode createTestData42() { - var val = ValueNode.nullValue(); - return val; - } - - public static DataStructureNode createTestData51() { - var val = ValueNode.of("value".getBytes(StandardCharsets.UTF_8)); - var flatArray = ArrayNode.of(List.of(val, ValueNode.nullValue())); - var array1 = ArrayNode.of(List.of(flatArray)); - var array2 = ArrayNode.of(List.of(array1, array1)); - return array2; - } - - public static DataStructureNode createTestData52() { - var val = ValueNode.of("value2".getBytes(StandardCharsets.UTF_8)); - var flatArray = ArrayNode.of(List.of(val)); - return flatArray; - } - - public static DataStructureNode createTestData53() { - var val = ValueNode.of("value2".getBytes(StandardCharsets.UTF_8)); - var flatTuple = TupleNode.builder().add("key1", val).build(); - var flatArray = ArrayNode.of(List.of(flatTuple, val)); - return flatArray; - } - - public static DataType createTestDataType5() { - return ArrayType.of(WildcardType.of()); - } - - public static DataStructureNode createTestData61() { - var val = ValueNode.of("value".getBytes(StandardCharsets.UTF_8)); - var array = ArrayNode.of(List.of(val, ValueNode.nullValue())); - var tuple = TupleNode.builder().add(val).add("key2", array).build(); - return tuple; - } - - public static DataStructureNode createTestData62() { - var val = ValueNode.of("value2".getBytes(StandardCharsets.UTF_8)); - var flatTuple = TupleNode.builder().add("key1", val).build(); - - var tuple = TupleNode.builder().add(flatTuple).add("key2", val).build(); - return tuple; - } - - public static DataType createTestDataType6() { - var keys = new ArrayList(); - keys.add(null); - keys.add("key2"); - return TupleType.of(keys, List.of(WildcardType.of(), WildcardType.of())); - } - - public static DataStructureNode createTestData71() { - return ValueNode.of("value".getBytes(StandardCharsets.UTF_8)); - } - - public static DataStructureNode createTestData72() { - var val = ValueNode.of("value2".getBytes(StandardCharsets.UTF_8)); - return TupleNode.builder().add("key1", val).build(); - } - - public static DataStructureNode createTestData73() { - var val = ValueNode.of("value".getBytes(StandardCharsets.UTF_8)); - var array = ArrayNode.of(List.of(val, ValueNode.nullValue())); - return array; - } - - public static DataType createTestDataType7() { - return WildcardType.of(); - } - - @AllArgsConstructor - public static enum TypedDataset { - - // Variety - DATA_1(createTestDataType1(), List.of(createTestData11(), createTestData12())), - - // Multiple nested arrays - DATA_2(createTestDataType2(), List.of(createTestData21(), createTestData22())), - - // Array with wildcard type - DATA_3(createTestData31().determineDataType(), List.of(createTestData31(), createTestData32())), - - // Simple values - DATA_4(ValueType.of(), List.of(createTestData41(), createTestData42())), - - // Array with wildcard type - DATA_5(createTestDataType5(), List.of(createTestData51(), createTestData52(), createTestData53())), - - // Tuple with wildcard type - DATA_6(createTestDataType6(), List.of(createTestData61(), createTestData62())), - - // Wildcard type - DATA_7(createTestDataType7(), List.of(createTestData71(), createTestData72(), createTestData73())); - - public DataType type; - public List nodes; - } -} diff --git a/dist/base.gradle b/dist/base.gradle index f2a245496..ce1b70f52 100644 --- a/dist/base.gradle +++ b/dist/base.gradle @@ -57,6 +57,10 @@ if (org.gradle.internal.os.OperatingSystem.current().isWindows()) { from "$distDir/licenses" into "$distDir/base/licenses" } + copy { + from "$projectDir/bundled_bin/$platformName" + into "$distDir/base/app/bundled" + } copy { from "$distDir/docs/html5" into "$distDir/base/cli/docs" @@ -133,6 +137,10 @@ if (org.gradle.internal.os.OperatingSystem.current().isWindows()) { from "$distDir/docs/manpage" into "$distDir/base/cli/man" } + copy { + from "$projectDir/bundled_bin/$platformName" + into "$distDir/base/app/bundled" + } } } } else { @@ -171,6 +179,10 @@ if (org.gradle.internal.os.OperatingSystem.current().isWindows()) { from "$projectDir/fonts" into "$distDir/$app/Contents/Resources/fonts" } + copy { + from "$projectDir/bundled_bin/$platformName" + into "$distDir/$app/Contents/Resources/bundled" + } copy { from "$projectDir/PkgInstaller/darwin/Resources/uninstall.sh" diff --git a/dist/build.gradle b/dist/build.gradle index d22e0091f..615ef7585 100644 --- a/dist/build.gradle +++ b/dist/build.gradle @@ -1,12 +1,10 @@ plugins { - id 'org.beryx.jlink' version '2.26.0' - id "org.moditect.gradleplugin" version "1.0.0-rc3" + id 'org.beryx.jlink' version '3.0.1' id "org.asciidoctor.jvm.convert" version "3.3.2" id 'org.jreleaser' version '1.8.0' id("com.netflix.nebula.ospackage") version "11.4.0" id 'org.gradle.crypto.checksum' version '1.4.0' - id 'de.undercouch.download' version '5.5.0' } repositories { @@ -90,5 +88,5 @@ if (rootProject.fullVersion) { apply from: 'aur.gradle' apply from: 'nix.gradle' apply from: 'choco.gradle' - apply from: 'test.gradle' + apply from: 'install.gradle' } diff --git a/dist/changelogs/8.0.md b/dist/changelogs/8.0.md new file mode 100644 index 000000000..75fc85f92 --- /dev/null +++ b/dist/changelogs/8.0.md @@ -0,0 +1,69 @@ +This is update is primarily focused on internal reworks. +It includes many changes are necessary going forward to allow for many future features to come. +These new implementations can take into account everything learned so far and are more robust, especially when considering the long-term timeline. + +The versioning scheme has also been changed to simplify version numbers. So we are going straight from 1.7 to 8.0! + +If you're interested, make sure to check out the PTB repository at https://github.com/xpipe-io/xpipe-ptb to download an early version. +The regular releases and PTB releases are designed to not interfere with each other and can therefore be installed and used side by side. +If you are planning to use the PTB version, please don't try to link it up to your existing git vault though if you're using that feature. You can use a separate repository for that. +It is intended to start out from zero with the connections in this PTB version to have a good coverage of all the workflows. +Also, please don't use this test version for your production environments as it is not considered stable yet. + +Judging from experience, there will be broken features initially. +It will definitely take a while until XPipe 8.0 will be fully released. +You can help the development effort by testing the PTB version and reporting any issues that you can find. + +## New terminal launcher + +The terminal launcher functionality got completely reworked with the goal to make it more flexible and improve the terminal startup performance. You will quickly notice the new implementation whenever you launch any connection in your terminal. + +## Proxmox integration + +There is now support to directly query all VMs and containers located on a Proxmox system via the `pct` and `qm` tools. The containers can be accessed directly as any other containers while the VMs can be accessed via SSH. In case no SSH server is running in a vm, you can also choose to set one up automatically within XPipe. + +This feature will be available in the professional version in the full release. + +## Git For Windows environments + +The git installation on windows it comes with its own posix environment, which some people use to make use of standard Linux functionalities on Windows if they have not moved to WSL yet. This update brings full support to add this shell environment as well via the automatic detection functionality. + +## Per-Vault settings + +Previously all settings were stored on a per-system basis. This caused some problems with git vaults, as all relevant settings that should persist across systems were not synced. From now on, all options that should be the same on all synced systems are automatically included in the git vault. + +## Fish and dumb shells + +Up until now, connecting to fish shells or various dumb shells you typically find in devices like routers and links, did not work as there was no proper support for them. The shell handling implementation has now been rewritten to support fish login shells (after some timeout). For SSH connections, there is now a toggle available in the professional version to designate the connection as dumb, i.e. it will only support terminal launching, nothing else. + +## File browser improvements + +The file browser has been reworked in terms of performance and reliability. Transferring many files should now be faster. Any errors that can a curr are now handled better. + +In terms of the interface, there is also now a progress indicator for files being transferred. For any file conflicts, there is now a new dialog to choose how to resolve any conflict when copying or moving files. + + +- Fix files in file browser not reloading content when trying to edit them multiple times in the session +- Add Open with ... action to open files with an arbitrary program + +## Settings rework + +This update comes with a complete rework of the settings menu. Many options have been added and existing ones have been improved, with a focus on providing more control over security settings. Make sure to give them a read to discover new options. + +## PowerShell fallback + +Some Windows admins disable cmd on their systems for some security reasons. Previously this would have caused XPipe to not function on these systems as it relied on cmd. From now on, it can also dynamically fall back to PowerShell if needed without utilizing cmd at all. + +## Bundled OpenSSH on Windows + +One common problem in the past has been to fact that Microsoft ships relatively outdated OpenSSH versions on Windows, which do not support newer features like FIDO2 keys. Due to the permissive license of OpenSSH and its Windows fork, XPipe can bundle the latest OpenSSH versions on Windows. +There is now an option the preferences menu to use the bundled OpenSSH version. + +## Dependency upgrades + +All dependencies have been upgraded to the latest version, coming with a few fixes and some new features. In particular, the JavaFX version has been bumped, which now allows for native system theme observation and the usage of accent colors. Around 10 dependency libraries have been removed as they are no longer necessary. + +## Fixes + +- Fix scaling issues on Linux by providing a separate scaling option +- Fix possible encoding issues on Windows with passwords that contained non-ASCII characters \ No newline at end of file diff --git a/dist/jpackage.gradle b/dist/jpackage.gradle index c4df627ed..1b657a0ce 100644 --- a/dist/jpackage.gradle +++ b/dist/jpackage.gradle @@ -1,5 +1,4 @@ -import java.util.stream.Collectors -import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform +apply from: "$rootDir/gradle/gradle_scripts/javafx.gradle" def distDir = "${project.layout.buildDirectory.get()}/dist" @@ -24,17 +23,13 @@ application { mainClass = 'io.xpipe.app.Main' } -def appDependencies = project(':app').configurations.findByName('runtimeClasspath').getFiles().stream() - .filter(f -> !fullVersion || !f.name.startsWith('javafx')) // Remove JavaFX dependencies - .collect(Collectors.toMap(f -> f.toPath().getFileName().toString(), f -> f, (f1, f2) -> f1)) - .values() -def appModuleNames = ['app'] -appDependencies.removeIf(f -> appModuleNames.stream() - .anyMatch(m -> f.toPath().getFileName().toString().contains("${m}.jar"))) -def appModuleOutputFiles = ["${project(':app').buildDir}/libs/app.jar"] dependencies { - implementation files(appDependencies.toArray()) - implementation files(appModuleOutputFiles.toArray()) + implementation project(':app') + if (!useBundledJavaFx) { + configurations.javafx.getAsFileTree().getFiles().forEach { + implementation files(it) + } + } } // Mac does not like a zero major version @@ -53,17 +48,8 @@ jlink { // '--strip-native-commands' ] - if (fullVersion) { - def currentOS = DefaultNativePlatform.currentOperatingSystem; - def platform = null - if (currentOS.isWindows()) { - platform = 'windows' - } else if (currentOS.isLinux()) { - platform = 'linux' - } else if (currentOS.isMacOsX()) { - platform = 'osx' - } - addExtraModulePath(layout.projectDirectory.dir("javafx/${platform}/${arch}").toString()) + if (useBundledJavaFx) { + addExtraModulePath(layout.projectDirectory.dir("javafx/${platformName}/${arch}").toString()) } launcher { diff --git a/dist/licenses/checkerqual.license b/dist/licenses/checkerqual.license deleted file mode 100644 index 7b59b5c98..000000000 --- a/dist/licenses/checkerqual.license +++ /dev/null @@ -1,22 +0,0 @@ -Checker Framework qualifiers -Copyright 2004-present by the Checker Framework developers - -MIT License: - -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. \ No newline at end of file diff --git a/dist/licenses/checkerqual.properties b/dist/licenses/checkerqual.properties deleted file mode 100644 index 1db41928b..000000000 --- a/dist/licenses/checkerqual.properties +++ /dev/null @@ -1,4 +0,0 @@ -name=checker-qual -version=3.5.0 -license=MIT License -link=https://github.com/typetools/checker-framework/blob/master/checker-qual/ \ No newline at end of file diff --git a/dist/licenses/commons-lang.license b/dist/licenses/commons-lang.license deleted file mode 100644 index f433b1a53..000000000 --- a/dist/licenses/commons-lang.license +++ /dev/null @@ -1,177 +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 diff --git a/dist/licenses/commons-lang.properties b/dist/licenses/commons-lang.properties deleted file mode 100644 index e0160ecee..000000000 --- a/dist/licenses/commons-lang.properties +++ /dev/null @@ -1,4 +0,0 @@ -name=Commons Lang -version=3.12.0 -license=Apache License 2.0 -link=https://commons.apache.org/proper/commons-lang/ \ No newline at end of file diff --git a/dist/licenses/formsfx.license b/dist/licenses/formsfx.license deleted file mode 100644 index 8dada3eda..000000000 --- a/dist/licenses/formsfx.license +++ /dev/null @@ -1,201 +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 {yyyy} {name of copyright owner} - - 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. diff --git a/dist/licenses/formsfx.properties b/dist/licenses/formsfx.properties deleted file mode 100644 index b999e1eb6..000000000 --- a/dist/licenses/formsfx.properties +++ /dev/null @@ -1,4 +0,0 @@ -name=FormsFX -version=11.5.0 -license=Apache License 2.0 -link=https://github.com/dlsc-software-consulting-gmbh/FormsFX \ No newline at end of file diff --git a/dist/licenses/graalvm.properties b/dist/licenses/graalvm.properties index 29b49333c..0f02199ec 100644 --- a/dist/licenses/graalvm.properties +++ b/dist/licenses/graalvm.properties @@ -1,4 +1,4 @@ name=GraalVM Community -version=21.3 +version=21.0.2 license=GPL2 with the Classpath Exception link=https://www.graalvm.org/ \ No newline at end of file diff --git a/dist/licenses/jfa.license b/dist/licenses/jfa.license deleted file mode 100644 index f433b1a53..000000000 --- a/dist/licenses/jfa.license +++ /dev/null @@ -1,177 +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 diff --git a/dist/licenses/jfa.properties b/dist/licenses/jfa.properties deleted file mode 100644 index 40f52a0fa..000000000 --- a/dist/licenses/jfa.properties +++ /dev/null @@ -1,4 +0,0 @@ -name=Java Foundation Access -version=1.2.0 -license=Apache License 2.0 -link=https://github.com/0x4a616e/jfa \ No newline at end of file diff --git a/dist/licenses/jfoenix.license b/dist/licenses/jfoenix.license deleted file mode 100644 index 6567f3fcf..000000000 --- a/dist/licenses/jfoenix.license +++ /dev/null @@ -1,18 +0,0 @@ -Copyright (c) 2016 JFoenix - -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. diff --git a/dist/licenses/jfoenix.properties b/dist/licenses/jfoenix.properties deleted file mode 100644 index ecc247486..000000000 --- a/dist/licenses/jfoenix.properties +++ /dev/null @@ -1,4 +0,0 @@ -name=JFoenix -version=9.0.10 -license=MIT License -link=https://github.com/sshahine/JFoenix \ No newline at end of file diff --git a/dist/licenses/jsystemthemedetector.license b/dist/licenses/jsystemthemedetector.license deleted file mode 100644 index f433b1a53..000000000 --- a/dist/licenses/jsystemthemedetector.license +++ /dev/null @@ -1,177 +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 diff --git a/dist/licenses/jsystemthemedetector.properties b/dist/licenses/jsystemthemedetector.properties deleted file mode 100644 index 3c10ec313..000000000 --- a/dist/licenses/jsystemthemedetector.properties +++ /dev/null @@ -1,4 +0,0 @@ -name=jSystemThemeDetector -version=3.8 -license=Apache License 2.0 -link=https://github.com/Dansoftowner/jSystemThemeDetector \ No newline at end of file diff --git a/dist/licenses/openjdk.license b/dist/licenses/openjdk.license deleted file mode 100644 index 2c55750db..000000000 --- a/dist/licenses/openjdk.license +++ /dev/null @@ -1,347 +0,0 @@ -The GNU General Public License (GPL) - -Version 2, June 1991 - -Copyright (C) 1989, 1991 Free Software Foundation, Inc. -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -Everyone is permitted to copy and distribute verbatim copies of this license -document, but changing it is not allowed. - -Preamble - -The licenses for most software are designed to take away your freedom to share -and change it. By contrast, the GNU General Public License is intended to -guarantee your freedom to share and change free software--to make sure the -software is free for all its users. This General Public License applies to -most of the Free Software Foundation's software and to any other program whose -authors commit to using it. (Some other Free Software Foundation software is -covered by the GNU Library General Public License instead.) You can apply it to -your programs, too. - -When we speak of free software, we are referring to freedom, not price. Our -General Public Licenses are designed to make sure that you have the freedom to -distribute copies of free software (and charge for this service if you wish), -that you receive source code or can get it if you want it, that you can change -the software or use pieces of it in new free programs; and that you know you -can do these things. - -To protect your rights, we need to make restrictions that forbid anyone to deny -you these rights or to ask you to surrender the rights. These restrictions -translate to certain responsibilities for you if you distribute copies of the -software, or if you modify it. - -For example, if you distribute copies of such a program, whether gratis or for -a fee, you must give the recipients all the rights that you have. You must -make sure that they, too, receive or can get the source code. And you must -show them these terms so they know their rights. - -We protect your rights with two steps: (1) copyright the software, and (2) -offer you this license which gives you legal permission to copy, distribute -and/or modify the software. - -Also, for each author's protection and ours, we want to make certain that -everyone understands that there is no warranty for this free software. If the -software is modified by someone else and passed on, we want its recipients to -know that what they have is not the original, so that any problems introduced -by others will not reflect on the original authors' reputations. - -Finally, any free program is threatened constantly by software patents. We -wish to avoid the danger that redistributors of a free program will -individually obtain patent licenses, in effect making the program proprietary. -To prevent this, we have made it clear that any patent must be licensed for -everyone's free use or not licensed at all. - -The precise terms and conditions for copying, distribution and modification -follow. - -TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - -0. This License applies to any program or other work which contains a notice -placed by the copyright holder saying it may be distributed under the terms of -this General Public License. The "Program", below, refers to any such program -or work, and a "work based on the Program" means either the Program or any -derivative work under copyright law: that is to say, a work containing the -Program or a portion of it, either verbatim or with modifications and/or -translated into another language. (Hereinafter, translation is included -without limitation in the term "modification".) Each licensee is addressed as -"you". - -Activities other than copying, distribution and modification are not covered by -this License; they are outside its scope. The act of running the Program is -not restricted, and the output from the Program is covered only if its contents -constitute a work based on the Program (independent of having been made by -running the Program). Whether that is true depends on what the Program does. - -1. You may copy and distribute verbatim copies of the Program's source code as -you receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice and -disclaimer of warranty; keep intact all the notices that refer to this License -and to the absence of any warranty; and give any other recipients of the -Program a copy of this License along with the Program. - -You may charge a fee for the physical act of transferring a copy, and you may -at your option offer warranty protection in exchange for a fee. - -2. You may modify your copy or copies of the Program or any portion of it, thus -forming a work based on the Program, and copy and distribute such modifications -or work under the terms of Section 1 above, provided that you also meet all of -these conditions: - - a) You must cause the modified files to carry prominent notices stating - that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in whole or - in part contains or is derived from the Program or any part thereof, to be - licensed as a whole at no charge to all third parties under the terms of - this License. - - c) If the modified program normally reads commands interactively when run, - you must cause it, when started running for such interactive use in the - most ordinary way, to print or display an announcement including an - appropriate copyright notice and a notice that there is no warranty (or - else, saying that you provide a warranty) and that users may redistribute - the program under these conditions, and telling the user how to view a copy - of this License. (Exception: if the Program itself is interactive but does - not normally print such an announcement, your work based on the Program is - not required to print an announcement.) - -These requirements apply to the modified work as a whole. If identifiable -sections of that work are not derived from the Program, and can be reasonably -considered independent and separate works in themselves, then this License, and -its terms, do not apply to those sections when you distribute them as separate -works. But when you distribute the same sections as part of a whole which is a -work based on the Program, the distribution of the whole must be on the terms -of this License, whose permissions for other licensees extend to the entire -whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest your -rights to work written entirely by you; rather, the intent is to exercise the -right to control the distribution of derivative or collective works based on -the Program. - -In addition, mere aggregation of another work not based on the Program with the -Program (or with a work based on the Program) on a volume of a storage or -distribution medium does not bring the other work under the scope of this -License. - -3. You may copy and distribute the Program (or a work based on it, under -Section 2) in object code or executable form under the terms of Sections 1 and -2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable source - code, which must be distributed under the terms of Sections 1 and 2 above - on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three years, to - give any third party, for a charge no more than your cost of physically - performing source distribution, a complete machine-readable copy of the - corresponding source code, to be distributed under the terms of Sections 1 - and 2 above on a medium customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer to - distribute corresponding source code. (This alternative is allowed only - for noncommercial distribution and only if you received the program in - object code or executable form with such an offer, in accord with - Subsection b above.) - -The source code for a work means the preferred form of the work for making -modifications to it. For an executable work, complete source code means all -the source code for all modules it contains, plus any associated interface -definition files, plus the scripts used to control compilation and installation -of the executable. However, as a special exception, the source code -distributed need not include anything that is normally distributed (in either -source or binary form) with the major components (compiler, kernel, and so on) -of the operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering access to copy -from a designated place, then offering equivalent access to copy the source -code from the same place counts as distribution of the source code, even though -third parties are not compelled to copy the source along with the object code. - -4. You may not copy, modify, sublicense, or distribute the Program except as -expressly provided under this License. Any attempt otherwise to copy, modify, -sublicense or distribute the Program is void, and will automatically terminate -your rights under this License. However, parties who have received copies, or -rights, from you under this License will not have their licenses terminated so -long as such parties remain in full compliance. - -5. You are not required to accept this License, since you have not signed it. -However, nothing else grants you permission to modify or distribute the Program -or its derivative works. These actions are prohibited by law if you do not -accept this License. Therefore, by modifying or distributing the Program (or -any work based on the Program), you indicate your acceptance of this License to -do so, and all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - -6. Each time you redistribute the Program (or any work based on the Program), -the recipient automatically receives a license from the original licensor to -copy, distribute or modify the Program subject to these terms and conditions. -You may not impose any further restrictions on the recipients' exercise of the -rights granted herein. You are not responsible for enforcing compliance by -third parties to this License. - -7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), conditions -are imposed on you (whether by court order, agreement or otherwise) that -contradict the conditions of this License, they do not excuse you from the -conditions of this License. If you cannot distribute so as to satisfy -simultaneously your obligations under this License and any other pertinent -obligations, then as a consequence you may not distribute the Program at all. -For example, if a patent license would not permit royalty-free redistribution -of the Program by all those who receive copies directly or indirectly through -you, then the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under any -particular circumstance, the balance of the section is intended to apply and -the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any patents or -other property right claims or to contest validity of any such claims; this -section has the sole purpose of protecting the integrity of the free software -distribution system, which is implemented by public license practices. Many -people have made generous contributions to the wide range of software -distributed through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing to -distribute software through any other system and a licensee cannot impose that -choiceElement. - -This section is intended to make thoroughly clear what is believed to be a -consequence of the rest of this License. - -8. If the distribution and/or use of the Program is restricted in certain -countries either by patents or by copyrighted interfaces, the original -copyright holder who places the Program under this License may add an explicit -geographical distribution limitation excluding those countries, so that -distribution is permitted only in or among countries not thus excluded. In -such case, this License incorporates the limitation as if written in the body -of this License. - -9. The Free Software Foundation may publish revised and/or new versions of the -General Public License from time to time. Such new versions will be similar in -spirit to the present version, but may differ in detail to address new problems -or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any later -version", you have the option of following the terms and conditions either of -that version or of any later version published by the Free Software Foundation. -If the Program does not specify a version number of this License, you may -choose any version ever published by the Free Software Foundation. - -10. If you wish to incorporate parts of the Program into other free programs -whose distribution conditions are different, write to the author to ask for -permission. For software which is copyrighted by the Free Software Foundation, -write to the Free Software Foundation; we sometimes make exceptions for this. -Our decision will be guided by the two goals of preserving the free status of -all derivatives of our free software and of promoting the sharing and reuse of -software generally. - -NO WARRANTY - -11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR -THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE -STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE -PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, -INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND -PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, -YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL -ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE -PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR -INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA -BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A -FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER -OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -END OF TERMS AND CONDITIONS - -How to Apply These Terms to Your New Programs - -If you develop a new program, and you want it to be of the greatest possible -use to the public, the best way to achieve this is to make it free software -which everyone can redistribute and change under these terms. - -To do so, attach the following notices to the program. It is safest to attach -them to the start of each source file to most effectively convey the exclusion -of warranty; and each file should have at least the "copyright" line and a -pointer to where the full notice is found. - - One line to give the program's name and a brief idea of what it does. - - Copyright (C) - - This program is free software; you can redistribute it and/or modify it - under the terms of the GNU General Public License as published by the Free - Software Foundation; either version 2 of the License, or (at your option) - any later version. - - This program is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this when it -starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author Gnomovision comes - with ABSOLUTELY NO WARRANTY; for details type 'show w'. This is free - software, and you are welcome to redistribute it under certain conditions; - type 'show c' for details. - -The hypothetical commands 'show w' and 'show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may be -called something other than 'show w' and 'show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your school, -if any, to sign a "copyright disclaimer" for the program, if necessary. Here -is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - 'Gnomovision' (which makes passes at compilers) written by James Hacker. - - signature of Ty Coon, 1 April 1989 - - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Library General Public -License instead of this License. - - -"CLASSPATH" EXCEPTION TO THE GPL - -Certain source files distributed by Oracle America and/or its affiliates are -subject to the following clarification and special exception to the GPL, but -only where Oracle has expressly included in the particular source file's header -the words "Oracle designates this particular file as subject to the "Classpath" -exception as provided by Oracle in the LICENSE file that accompanied this code." - - Linking this library statically or dynamically with other modules is making - a combined work based on this library. Thus, the terms and conditions of - the GNU General Public License cover the whole combination. - - As a special exception, the copyright holders of this library give you - permission to link this library with independent modules to produce an - executable, regardless of the license terms of these independent modules, - and to copy and distribute the resulting executable under terms of your - choiceElement, provided that you also meet, for each linked independent module, - the terms and conditions of the license of that module. An independent - module is a module which is not derived from or based on this library. If - you modify this library, you may extend this exception to your version of - the library, but you are not obligated to do so. If you do not wish to do - so, delete this exception statement from your version. diff --git a/dist/licenses/openjdk.properties b/dist/licenses/openjdk.properties deleted file mode 100644 index 013cbb219..000000000 --- a/dist/licenses/openjdk.properties +++ /dev/null @@ -1,4 +0,0 @@ -name=OpenJDK -version=21.0.1 -license=GPL2 with the Classpath Exception -link=https://jdk.java.net/21/ \ No newline at end of file diff --git a/dist/licenses/openjfx.properties b/dist/licenses/openjfx.properties index e915f84bb..ed137f4ce 100644 --- a/dist/licenses/openjfx.properties +++ b/dist/licenses/openjfx.properties @@ -1,4 +1,4 @@ name=OpenJFX -version=21.0.1 +version=22.0.1 license=GPL2 with the Classpath Exception link=https://github.com/openjdk/jfx \ No newline at end of file diff --git a/dist/licenses/oshi.license b/dist/licenses/oshi.license deleted file mode 100644 index c8d628e57..000000000 --- a/dist/licenses/oshi.license +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2010-2023 The OSHI Project Contributors: https://github.com/oshi/oshi/graphs/contributors - -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. diff --git a/dist/licenses/oshi.properties b/dist/licenses/oshi.properties deleted file mode 100644 index e7db3fdff..000000000 --- a/dist/licenses/oshi.properties +++ /dev/null @@ -1,4 +0,0 @@ -name=oshi -version=6.4.2 -license=MIT License -link=https://github.com/oshi/oshi \ No newline at end of file diff --git a/dist/licenses/preferencesfx.license b/dist/licenses/preferencesfx.license deleted file mode 100644 index 8dada3eda..000000000 --- a/dist/licenses/preferencesfx.license +++ /dev/null @@ -1,201 +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 {yyyy} {name of copyright owner} - - 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. diff --git a/dist/licenses/preferencesfx.properties b/dist/licenses/preferencesfx.properties deleted file mode 100644 index bd79f00c6..000000000 --- a/dist/licenses/preferencesfx.properties +++ /dev/null @@ -1,4 +0,0 @@ -name=PreferencesFX -version=11.9.0 -license=Apache License 2.0 -link=https://github.com/dlsc-software-consulting-gmbh/PreferencesFX \ No newline at end of file diff --git a/dist/licenses/win32-openssh.license b/dist/licenses/win32-openssh.license new file mode 100644 index 000000000..4b0db548a --- /dev/null +++ b/dist/licenses/win32-openssh.license @@ -0,0 +1,371 @@ +This file is part of the OpenSSH software. + +The licences which components of this software fall under are as +follows. First, we will summarize and say that all components +are under a BSD licence, or a licence more free than that. + +OpenSSH contains no GPL code. + +1) + * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland + * All rights reserved + * + * As far as I am concerned, the code I have written for this software + * can be used freely for any purpose. Any derived versions of this + * software must be clearly marked as such, and if the derived work is + * incompatible with the protocol description in the RFC file, it must be + * called by a name other than "ssh" or "Secure Shell". + + [Tatu continues] + * However, I am not implying to give any licenses to any patents or + * copyrights held by third parties, and the software includes parts that + * are not under my direct control. As far as I know, all included + * source code is used in accordance with the relevant license agreements + * and can be used freely for any purpose (the GNU license being the most + * restrictive); see below for details. + + [However, none of that term is relevant at this point in time. All of + these restrictively licenced software components which he talks about + have been removed from OpenSSH, i.e., + + - RSA is no longer included, found in the OpenSSL library + - IDEA is no longer included, its use is deprecated + - DES is now external, in the OpenSSL library + - GMP is no longer used, and instead we call BN code from OpenSSL + - Zlib is now external, in a library + - The make-ssh-known-hosts script is no longer included + - TSS has been removed + - MD5 is now external, in the OpenSSL library + - RC4 support has been replaced with ARC4 support from OpenSSL + - Blowfish is now external, in the OpenSSL library + + [The licence continues] + + Note that any information and cryptographic algorithms used in this + software are publicly available on the Internet and at any major + bookstore, scientific library, and patent office worldwide. More + information can be found e.g. at "http://www.cs.hut.fi/crypto". + + The legal status of this program is some combination of all these + permissions and restrictions. Use only at your own responsibility. + You will be responsible for any legal consequences yourself; I am not + making any claims whether possessing or using this is legal or not in + your country, and I am not taking any responsibility on your behalf. + + + NO WARRANTY + + BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY + FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN + OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES + PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED + OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS + TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE + PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, + REPAIR OR CORRECTION. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING + WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR + REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, + INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING + OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED + TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY + YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER + PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE + POSSIBILITY OF SUCH DAMAGES. + +3) + ssh-keyscan was contributed by David Mazieres under a BSD-style + license. + + * Copyright 1995, 1996 by David Mazieres . + * + * Modification and redistribution in source and binary forms is + * permitted provided that due credit is given to the author and the + * OpenBSD project by leaving this copyright notice intact. + +4) + The Rijndael implementation by Vincent Rijmen, Antoon Bosselaers + and Paulo Barreto is in the public domain and distributed + with the following license: + + * @version 3.0 (December 2000) + * + * Optimised ANSI C code for the Rijndael cipher (now AES) + * + * @author Vincent Rijmen + * @author Antoon Bosselaers + * @author Paulo Barreto + * + * This code is hereby placed in the public domain. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ''AS IS'' AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +5) + One component of the ssh source code is under a 3-clause BSD license, + held by the University of California, since we pulled these parts from + original Berkeley code. + + * Copyright (c) 1983, 1990, 1992, 1993, 1995 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + +6) + Remaining components of the software are provided under a standard + 2-term BSD licence with the following names as copyright holders: + + Markus Friedl + Theo de Raadt + Niels Provos + Dug Song + Aaron Campbell + Damien Miller + Kevin Steves + Daniel Kouril + Wesley Griffin + Per Allansson + Nils Nordman + Simon Wilkinson + + Portable OpenSSH additionally includes code from the following copyright + holders, also under the 2-term BSD license: + + Ben Lindstrom + Tim Rice + Andre Lucas + Chris Adams + Corinna Vinschen + Cray Inc. + Denis Parker + Gert Doering + Jakob Schlyter + Jason Downs + Juha Yrjölä + Michael Stone + Networks Associates Technology, Inc. + Solar Designer + Todd C. Miller + Wayne Schroeder + William Jones + Darren Tucker + Sun Microsystems + The SCO Group + Daniel Walsh + Red Hat, Inc + Simon Vallet / Genoscope + + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +8) Portable OpenSSH contains the following additional licenses: + + a) snprintf replacement + + * Copyright Patrick Powell 1995 + * This code is based on code written by Patrick Powell + * (papowell@astart.com) It may be used for any purpose as long as this + * notice remains intact on all source code distributions + + b) Compatibility code (openbsd-compat) + + Apart from the previously mentioned licenses, various pieces of code + in the openbsd-compat/ subdirectory are licensed as follows: + + Some code is licensed under a 3-term BSD license, to the following + copyright holders: + + Todd C. Miller + Theo de Raadt + Damien Miller + Eric P. Allman + The Regents of the University of California + Constantin S. Svintsoff + Kungliga Tekniska Högskolan + + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + + Some code is licensed under an ISC-style license, to the following + copyright holders: + + Internet Software Consortium. + Todd C. Miller + Reyk Floeter + Chad Mynhier + + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND TODD C. MILLER DISCLAIMS ALL + * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL TODD C. MILLER BE LIABLE + * FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + Some code is licensed under a MIT-style license to the following + copyright holders: + + Free Software Foundation, Inc. + + * 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, distribute with modifications, 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 ABOVE 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. * + * * + * Except as contained in this notice, the name(s) of the above copyright * + * holders shall not be used in advertising or otherwise to promote the * + * sale, use or other dealings in this Software without prior written * + * authorization. * + ****************************************************************************/ + + The Blowfish cipher implementation is licensed by Niels Provos under + a 3-clause BSD license: + + * Blowfish - a fast block cipher designed by Bruce Schneier + * + * Copyright 1997 Niels Provos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + Some replacement code is licensed by the NetBSD foundation under a + 2-clause BSD license: + + * Copyright (c) 2001 The NetBSD Foundation, Inc. + * All rights reserved. + * + * This code is derived from software contributed to The NetBSD Foundation + * by Todd Vierling. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + +------ +$OpenBSD: LICENCE,v 1.20 2017/04/30 23:26:16 djm Exp $ diff --git a/dist/licenses/win32-openssh.properties b/dist/licenses/win32-openssh.properties new file mode 100644 index 000000000..1ec7eb589 --- /dev/null +++ b/dist/licenses/win32-openssh.properties @@ -0,0 +1,4 @@ +name=Win32-OpenSSH +version=9.5.0.0p1 +license=BSD licence, or a licence more free than that +link=https://github.com/PowerShell/openssh-portable \ No newline at end of file diff --git a/ext/base/build.gradle b/ext/base/build.gradle index eca31e1d5..c34d808e0 100644 --- a/ext/base/build.gradle +++ b/ext/base/build.gradle @@ -1,36 +1,5 @@ plugins { id 'java' - id "org.moditect.gradleplugin" version "1.0.0-rc3" } -apply from: "$rootDir/gradle/gradle_scripts/java.gradle" -apply from: "$rootDir/gradle/gradle_scripts/commons.gradle" -apply from: "$rootDir/gradle/gradle_scripts/lombok.gradle" apply from: "$rootDir/gradle/gradle_scripts/extension.gradle" - -dependencies { - compileOnly group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0" - compileOnly 'net.java.dev.jna:jna-jpms:5.12.1' - compileOnly 'net.java.dev.jna:jna-platform-jpms:5.12.1' -} - -compileJava { - doFirst { - options.compilerArgs += [ - '--module-path', classpath.asPath - ] - classpath = files() - } -} - -configurations { - compileOnly.extendsFrom(dep) -} - -dependencies { - compileOnly project(':app') -} - -test { - enabled = false -} diff --git a/ext/base/src/test/java/module-info.java b/ext/base/src/localTest/java/module-info.java similarity index 73% rename from ext/base/src/test/java/module-info.java rename to ext/base/src/localTest/java/module-info.java index 12f080180..f5e0d8e6c 100644 --- a/ext/base/src/test/java/module-info.java +++ b/ext/base/src/localTest/java/module-info.java @@ -1,9 +1,9 @@ -module io.xpipe.ext.base.test { +open module io.xpipe.ext.base.test { requires io.xpipe.ext.base; requires org.junit.jupiter.api; requires org.junit.jupiter.params; requires io.xpipe.core; - requires io.xpipe.api; + requires static lombok; requires io.xpipe.app; exports test; diff --git a/ext/base/src/localTest/java/test/Test.java b/ext/base/src/localTest/java/test/Test.java new file mode 100644 index 000000000..c11a10b62 --- /dev/null +++ b/ext/base/src/localTest/java/test/Test.java @@ -0,0 +1,14 @@ +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()); + + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchAction.java index 3c70f2f3b..d1e357b9f 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchAction.java @@ -5,7 +5,7 @@ import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntryRef; -import io.xpipe.app.util.TerminalHelper; +import io.xpipe.app.util.TerminalLauncher; import io.xpipe.core.store.LaunchableStore; import io.xpipe.core.store.ShellStore; import io.xpipe.ext.base.script.ScriptStore; @@ -28,7 +28,7 @@ public class LaunchAction implements ActionProvider { public void execute() throws Exception { var storeName = DataStorage.get().getStoreDisplayName(entry); if (entry.getStore() instanceof ShellStore s) { - TerminalHelper.open(entry, storeName, ScriptStore.controlWithDefaultScripts(s.control())); + TerminalLauncher.open(entry, storeName, null, ScriptStore.controlWithDefaultScripts(s.control())); return; } @@ -38,7 +38,7 @@ public class LaunchAction implements ActionProvider { return; } - TerminalHelper.open(entry, storeName, command); + TerminalLauncher.open(entry, storeName, null, command); } } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/ShareStoreAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/ShareStoreAction.java index 95270f245..5e2e9951e 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/ShareStoreAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/ShareStoreAction.java @@ -6,8 +6,8 @@ import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.util.ClipboardHelper; -import io.xpipe.app.util.SecretHelper; import io.xpipe.core.store.DataStore; +import io.xpipe.core.util.InPlaceSecretValue; import javafx.beans.value.ObservableValue; import lombok.Value; @@ -25,7 +25,7 @@ public class ShareStoreAction implements ActionProvider { public static String create(DataStore store) { return "xpipe://addStore/" - + SecretHelper.encryptInPlace(store.toString()).getEncryptedValue(); + + InPlaceSecretValue.of(store.toString()).getEncryptedValue(); } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/XPipeUrlAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/XPipeUrlAction.java index 29eaf460b..36d777781 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/XPipeUrlAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/XPipeUrlAction.java @@ -5,9 +5,9 @@ import io.xpipe.app.comp.store.StoreCreationComp; import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; -import io.xpipe.app.util.TerminalHelper; +import io.xpipe.app.util.TerminalLauncher; import io.xpipe.core.store.LaunchableStore; -import io.xpipe.core.util.DefaultSecretValue; +import io.xpipe.core.util.InPlaceSecretValue; import io.xpipe.core.store.DataStore; import io.xpipe.core.util.JacksonMapper; import lombok.Value; @@ -53,7 +53,7 @@ public class XPipeUrlAction implements ActionProvider { return; } - TerminalHelper.open(storeName, command); + TerminalLauncher.open(storeName, command); } } } @@ -92,7 +92,7 @@ public class XPipeUrlAction implements ActionProvider { public Action createAction(List args) throws Exception { switch (args.get(0)) { case "addStore" -> { - var storeString = DefaultSecretValue.builder() + var storeString = InPlaceSecretValue.builder() .encryptedValue(args.get(1)) .build(); var store = JacksonMapper.parse(storeString.getSecretValue(), DataStore.class); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/BrowseInNativeManagerAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/BrowseInNativeManagerAction.java index 1dca415a1..0c6d38e49 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/BrowseInNativeManagerAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/BrowseInNativeManagerAction.java @@ -18,12 +18,13 @@ public class BrowseInNativeManagerAction implements LeafAction { ShellDialect d = sc.getShellDialect(); for (BrowserEntry entry : entries) { var e = entry.getRawFileEntry().getPath(); + var localFile = sc.getLocalSystemAccess().translateToLocalSystemPath(e); switch (OsType.getLocal()) { case OsType.Windows windows -> { if (entry.getRawFileEntry().getKind() == FileKind.DIRECTORY) { - sc.executeSimpleCommand("explorer " + d.fileArgument(e)); + sc.executeSimpleCommand("explorer " + d.fileArgument(localFile)); } else { - sc.executeSimpleCommand("explorer /select," + d.fileArgument(e)); + sc.executeSimpleCommand("explorer /select," + d.fileArgument(localFile)); } } case OsType.Linux linux -> { @@ -34,13 +35,13 @@ public class BrowseInNativeManagerAction implements LeafAction { """ dbus-send --session --print-reply --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 %s array:string:"file://%s" string:"" """, - action, entry.getRawFileEntry().getPath()); + action, localFile); sc.executeSimpleCommand(dbus); } case OsType.MacOs macOs -> { sc.executeSimpleCommand( "open " + (entry.getRawFileEntry().getKind() == FileKind.DIRECTORY ? "" : "-R ") - + d.fileArgument(entry.getRawFileEntry().getPath())); + + d.fileArgument(localFile)); } } } @@ -58,7 +59,7 @@ public class BrowseInNativeManagerAction implements LeafAction { @Override public boolean isApplicable(OpenFileSystemModel model, List entries) { - return model.isLocal(); + return model.getFileSystem().getShell().orElseThrow().getLocalSystemAccess().supportsFileSystemAccess(); } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/EditFileAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/EditFileAction.java index abff2fd17..4f1e86c2c 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/EditFileAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/EditFileAction.java @@ -44,6 +44,6 @@ public class EditFileAction implements LeafAction { @Override public String getName(OpenFileSystemModel model, List entries) { var e = AppPrefs.get().externalEditor().getValue(); - return "Edit with " + (e != null ? e.toTranslatedString() : "?"); + return "Edit with " + (e != null ? e.toTranslatedString().getValue() : "?"); } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileWithAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileWithAction.java index ccb8c5941..7bf5abe20 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileWithAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileWithAction.java @@ -1,13 +1,10 @@ package io.xpipe.ext.base.browser; -import com.sun.jna.platform.win32.Shell32; -import com.sun.jna.platform.win32.WinUser; import io.xpipe.app.browser.BrowserEntry; import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.browser.action.LeafAction; +import io.xpipe.app.util.FileOpener; import io.xpipe.core.process.OsType; -import io.xpipe.core.process.ShellControl; -import io.xpipe.core.process.ShellDialect; import io.xpipe.core.store.FileKind; import javafx.scene.Node; import javafx.scene.input.KeyCode; @@ -21,27 +18,8 @@ public class OpenFileWithAction implements LeafAction { @Override public void execute(OpenFileSystemModel model, List entries) throws Exception { - switch (OsType.getLocal()) { - case OsType.Windows windows -> { - Shell32.INSTANCE.ShellExecute( - null, - "open", - "rundll32.exe", - "shell32.dll,OpenAs_RunDLL " - + entries.get(0).getRawFileEntry().getPath(), - null, - WinUser.SW_SHOWNORMAL); - } - case OsType.Linux linux -> { - ShellControl sc = model.getFileSystem().getShell().get(); - ShellDialect d = sc.getShellDialect(); - sc.executeSimpleCommand("mimeopen -a " - + d.fileArgument(entries.get(0).getRawFileEntry().getPath())); - } - case OsType.MacOs macOs -> { - throw new UnsupportedOperationException(); - } - } + var e = entries.getFirst(); + FileOpener.openWithAnyApplication(e.getRawFileEntry()); } @Override @@ -56,9 +34,7 @@ public class OpenFileWithAction implements LeafAction { @Override public boolean isApplicable(OpenFileSystemModel model, List entries) { - var os = model.getFileSystem().getShell(); - return os.isPresent() - && os.get().getOsType().equals(OsType.WINDOWS) + return !OsType.getLocal().equals(OsType.MACOS) && entries.size() == 1 && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE); } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenNativeFileDetailsAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenNativeFileDetailsAction.java index 3679931ac..6f2198125 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenNativeFileDetailsAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenNativeFileDetailsAction.java @@ -17,17 +17,18 @@ public class OpenNativeFileDetailsAction implements LeafAction { ShellControl sc = model.getFileSystem().getShell().get(); for (BrowserEntry entry : entries) { var e = entry.getRawFileEntry().getPath(); + var localFile = sc.getLocalSystemAccess().translateToLocalSystemPath(e); switch (OsType.getLocal()) { case OsType.Windows windows -> { - var parent = FileNames.getParent(e); + var parent = FileNames.getParent(localFile); // If we execute this on a drive root there will be no parent, so we have to check for that! var content = parent != null ? String.format( "$shell = New-Object -ComObject Shell.Application; $shell.NameSpace('%s').ParseName('%s').InvokeVerb('Properties')", - FileNames.getParent(e), FileNames.getFileName(e)) + FileNames.getParent(localFile), FileNames.getFileName(localFile)) : String.format( "$shell = New-Object -ComObject Shell.Application; $shell.NameSpace('%s').Self.InvokeVerb('Properties')", - e); + localFile); // The Windows shell invoke verb functionality behaves kinda weirdly and only shows the window as // long as the parent process is running. @@ -39,16 +40,15 @@ public class OpenNativeFileDetailsAction implements LeafAction { """ dbus-send --session --print-reply --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItemProperties array:string:"file://%s" string:"" """, - entry.getRawFileEntry().getPath()); + localFile); sc.executeSimpleCommand(dbus); } case OsType.MacOs macOs -> { sc.osascriptCommand(String.format( """ set fileEntry to (POSIX file "%s") as text - tell application "Finder" to open information window of file fileEntry - """, - entry.getRawFileEntry().getPath())) + tell application "Finder" to open information window of alias fileEntry + """, localFile)) .execute(); } } @@ -67,8 +67,8 @@ public class OpenNativeFileDetailsAction implements LeafAction { @Override public boolean isApplicable(OpenFileSystemModel model, List entries) { - var sc = model.getFileSystem().getShell(); - return model.isLocal(); + var sc = model.getFileSystem().getShell().orElseThrow(); + return sc.getLocalSystemAccess().supportsFileSystemAccess(); } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStore.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStore.java index 442ba63fc..e27623d69 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStore.java @@ -45,7 +45,7 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore, var dir = initScriptsDirectory(shellControl, bringFlattened); if (dir != null) { - shellControl.withInitSnippet(new SimpleScriptSnippet(shellControl.getShellDialect().appendToPathVariableCommand(dir), + shellControl.withInitSnippet(new SimpleScriptSnippet(shellControl.getShellDialect().addToPathVariableCommand(List.of(dir), true), ScriptSnippet.ExecutionType.TERMINAL_ONLY)); } }); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStore.java b/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStore.java index 254e59d12..dc16f5027 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStore.java @@ -24,7 +24,7 @@ public class SimpleScriptStore extends ScriptStore implements ScriptSnippet { private String assemble(ShellControl shellControl, ExecutionType type) { var targetType = type == ExecutionType.TERMINAL_ONLY - ? shellControl.getTargetTerminalShellDialect() + ? shellControl.getOriginalShellDialect() : shellControl.getShellDialect(); if ((executionType == type || executionType == ExecutionType.BOTH) && minimumDialect.isCompatibleTo(targetType)) { diff --git a/ext/base/src/main/java/io/xpipe/ext/base/store/PauseableStore.java b/ext/base/src/main/java/io/xpipe/ext/base/store/PauseableStore.java new file mode 100644 index 000000000..8bb3a9d71 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/store/PauseableStore.java @@ -0,0 +1,8 @@ +package io.xpipe.ext.base.store; + +import io.xpipe.core.store.DataStore; + +public interface PauseableStore extends DataStore { + + void pause() throws Exception; +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/store/StartableStore.java b/ext/base/src/main/java/io/xpipe/ext/base/store/StartableStore.java new file mode 100644 index 000000000..219d112fd --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/store/StartableStore.java @@ -0,0 +1,8 @@ +package io.xpipe.ext.base.store; + +import io.xpipe.core.store.DataStore; + +public interface StartableStore extends DataStore { + + void start() throws Exception; +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/store/StoppableStore.java b/ext/base/src/main/java/io/xpipe/ext/base/store/StoppableStore.java new file mode 100644 index 000000000..59cdf59b1 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/store/StoppableStore.java @@ -0,0 +1,8 @@ +package io.xpipe.ext.base.store; + +import io.xpipe.core.store.DataStore; + +public interface StoppableStore extends DataStore { + + void stop() throws Exception; +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/store/StorePauseAction.java b/ext/base/src/main/java/io/xpipe/ext/base/store/StorePauseAction.java new file mode 100644 index 000000000..27f4f2caf --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/store/StorePauseAction.java @@ -0,0 +1,57 @@ +package io.xpipe.ext.base.store; + +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.ext.ActionProvider; +import io.xpipe.app.storage.DataStoreEntryRef; +import javafx.beans.value.ObservableValue; +import lombok.Value; + +public class StorePauseAction implements ActionProvider { + + @Value + static class Action implements ActionProvider.Action { + + DataStoreEntryRef entry; + + @Override + public boolean requiresJavaFXPlatform() { + return false; + } + + @Override + public void execute() throws Exception { + entry.getStore().pause(); + } + } + + @Override + public DataStoreCallSite getDataStoreCallSite() { + return new DataStoreCallSite() { + + @Override + public boolean isMajor(DataStoreEntryRef o) { + return true; + } + + @Override + public ObservableValue getName(DataStoreEntryRef store) { + return AppI18n.observable("pause"); + } + + @Override + public String getIcon(DataStoreEntryRef store) { + return "mdi2p-pause"; + } + + @Override + public ActionProvider.Action createAction(DataStoreEntryRef store) { + return new Action(store); + } + + @Override + public Class getApplicableClass() { + return PauseableStore.class; + } + }; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStartAction.java b/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStartAction.java new file mode 100644 index 000000000..c39a17ebc --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStartAction.java @@ -0,0 +1,57 @@ +package io.xpipe.ext.base.store; + +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.ext.ActionProvider; +import io.xpipe.app.storage.DataStoreEntryRef; +import javafx.beans.value.ObservableValue; +import lombok.Value; + +public class StoreStartAction implements ActionProvider { + + @Value + static class Action implements ActionProvider.Action { + + DataStoreEntryRef entry; + + @Override + public boolean requiresJavaFXPlatform() { + return false; + } + + @Override + public void execute() throws Exception { + entry.getStore().start(); + } + } + + @Override + public DataStoreCallSite getDataStoreCallSite() { + return new DataStoreCallSite() { + + @Override + public boolean isMajor(DataStoreEntryRef o) { + return true; + } + + @Override + public ObservableValue getName(DataStoreEntryRef store) { + return AppI18n.observable("start"); + } + + @Override + public String getIcon(DataStoreEntryRef store) { + return "mdi2p-play"; + } + + @Override + public ActionProvider.Action createAction(DataStoreEntryRef store) { + return new Action(store); + } + + @Override + public Class getApplicableClass() { + return StartableStore.class; + } + }; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStopAction.java b/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStopAction.java new file mode 100644 index 000000000..306abca3f --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStopAction.java @@ -0,0 +1,57 @@ +package io.xpipe.ext.base.store; + +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.ext.ActionProvider; +import io.xpipe.app.storage.DataStoreEntryRef; +import javafx.beans.value.ObservableValue; +import lombok.Value; + +public class StoreStopAction implements ActionProvider { + + @Value + static class Action implements ActionProvider.Action { + + DataStoreEntryRef entry; + + @Override + public boolean requiresJavaFXPlatform() { + return false; + } + + @Override + public void execute() throws Exception { + entry.getStore().stop(); + } + } + + @Override + public DataStoreCallSite getDataStoreCallSite() { + return new DataStoreCallSite() { + + @Override + public boolean isMajor(DataStoreEntryRef o) { + return true; + } + + @Override + public ObservableValue getName(DataStoreEntryRef store) { + return AppI18n.observable("stop"); + } + + @Override + public String getIcon(DataStoreEntryRef store) { + return "mdi2s-stop"; + } + + @Override + public ActionProvider.Action createAction(DataStoreEntryRef store) { + return new Action(store); + } + + @Override + public Class getApplicableClass() { + return StoppableStore.class; + } + }; + } +} diff --git a/ext/base/src/main/java/module-info.java b/ext/base/src/main/java/module-info.java index 8007ac0ef..b84c524bf 100644 --- a/ext/base/src/main/java/module-info.java +++ b/ext/base/src/main/java/module-info.java @@ -6,11 +6,15 @@ import io.xpipe.ext.base.action.*; import io.xpipe.ext.base.browser.*; import io.xpipe.ext.base.script.ScriptGroupStoreProvider; import io.xpipe.ext.base.script.SimpleScriptStoreProvider; +import io.xpipe.ext.base.store.StorePauseAction; +import io.xpipe.ext.base.store.StoreStartAction; +import io.xpipe.ext.base.store.StoreStopAction; open module io.xpipe.ext.base { exports io.xpipe.ext.base; exports io.xpipe.ext.base.action; exports io.xpipe.ext.base.script; + exports io.xpipe.ext.base.store; requires java.desktop; requires io.xpipe.core; @@ -21,8 +25,6 @@ open module io.xpipe.ext.base { requires static net.synedra.validatorfx; requires static io.xpipe.app; requires org.kordamp.ikonli.javafx; - requires com.sun.jna; - requires com.sun.jna.platform; requires atlantafx.base; provides BrowserAction with @@ -49,7 +51,7 @@ open module io.xpipe.ext.base { UnzipAction, JavapAction, JarAction; - provides ActionProvider with + provides ActionProvider with StoreStopAction, StoreStartAction, StorePauseAction, CloneStoreAction, RefreshStoreAction, ScanAction, diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_en.properties b/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_en.properties index c7c2b9564..acac0433c 100644 --- a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_en.properties +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_en.properties @@ -9,6 +9,9 @@ destination=Destination configuration=Configuration selectOutput=Select Output launch=Launch +start=Start +stop=Stop +pause=Pause refresh=Refresh options=Options newFile=New file diff --git a/ext/base/src/test/java/test/TextFileTest.java b/ext/base/src/test/java/test/TextFileTest.java deleted file mode 100644 index 09161a71a..000000000 --- a/ext/base/src/test/java/test/TextFileTest.java +++ /dev/null @@ -1,128 +0,0 @@ -package test; - -import io.xpipe.api.DataSource; -import io.xpipe.app.test.DaemonExtensionTest; -import io.xpipe.core.charsetter.NewLine; -import io.xpipe.core.charsetter.StreamCharset; -import io.xpipe.app.util.FileReference; -import io.xpipe.core.impl.TextSource; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; - -public class TextFileTest extends DaemonExtensionTest { - - static Path utf8File = - Path.of("ext/base/src/test/resources/utf8-bom-lf.txt").toAbsolutePath(); - static Path utf16File = - Path.of("ext/base/src/test/resources/utf16-crlf.txt").toAbsolutePath(); - static Path appendReferenceFile = - Path.of("ext/base/src/test/resources/append-reference.txt").toAbsolutePath(); - static Path appendOutputFile; - static Path writeReferenceFile = - Path.of("ext/base/src/test/resources/write-reference.txt").toAbsolutePath(); - static Path writeOutputFile; - - static DataSource utf8; - static DataSource utf16; - static DataSource appendReference; - static DataSource writeReference; - static DataSource appendOutput; - static DataSource writeOutput; - - @BeforeAll - public static void setupStorage() throws Exception { - utf8 = getSource("text", "utf8-bom-lf.txt"); - utf16 = DataSource.create(null, "text", FileReference.local(utf16File)); - appendReference = DataSource.create(null, "text", FileReference.local(appendReferenceFile)); - writeReference = DataSource.create(null, "text", FileReference.local(writeReferenceFile)); - - appendOutputFile = Files.createTempFile(null, null); - appendOutput = DataSource.create( - null, - TextSource.builder() - .store(FileReference.local(appendOutputFile)) - .charset(StreamCharset.get("windows-1252")) - .newLine(NewLine.LF) - .build()); - - writeOutputFile = Files.createTempFile(null, null); - writeOutput = DataSource.create( - null, - TextSource.builder() - .store(FileReference.local(writeOutputFile)) - .charset(StreamCharset.UTF16_LE_BOM) - .newLine(NewLine.CRLF) - .build()); - } - - @Test - public void testDetection() throws IOException { - TextSource first = (TextSource) utf8.getInternalSource(); - Assertions.assertEquals(StreamCharset.UTF8_BOM, first.getCharset()); - } - - @Test - public void testRead() throws IOException { - var first = utf8.asText(); - var firstText = first.readAll(); - var firstTextLines = first.readLines(5); - - Assertions.assertEquals(firstText, "hello\nworld"); - Assertions.assertEquals(firstTextLines, List.of("hello", "world")); - - var second = utf16.asText(); - var secondText = second.readAll(); - var secondTextLines = second.readLines(5); - - Assertions.assertEquals(secondText, "how\nis\nit\ngoing"); - Assertions.assertEquals(secondTextLines, List.of("how", "is", "it", "going")); - } - - @Test - public void testWrite() throws IOException { - var empty = Files.createTempFile(null, null); - var emptySource = DataSource.create( - null, - TextSource.builder() - .store(FileReference.local(empty)) - .charset(StreamCharset.UTF32_BE) - .newLine(NewLine.CRLF) - .build()); - emptySource.asText().forwardTo(writeOutput); - - var first = utf8.asText(); - first.forwardTo(writeOutput); - var second = utf16.asText(); - second.forwardTo(writeOutput); - - var text = writeOutput.asText().readAll(); - var referenceText = writeReference.asText().readAll(); - Assertions.assertEquals(referenceText, text); - - var bytes = Files.readAllBytes(writeOutputFile); - var referenceBytes = Files.readAllBytes(writeReferenceFile); - Assertions.assertArrayEquals(bytes, referenceBytes); - } - - @Test - public void testAppend() throws IOException { - var first = utf8.asText(); - first.appendTo(appendOutput); - var second = utf16.asText(); - second.appendTo(appendOutput); - - var text = appendOutput.asText().readAll(); - var referenceText = appendReference.asText().readAll(); - Assertions.assertEquals(referenceText, text); - - var bytes = Files.readAllBytes(appendOutputFile); - var referenceBytes = Files.readAllBytes(appendReferenceFile); - Assertions.assertArrayEquals(bytes, referenceBytes); - } -} diff --git a/ext/base/src/test/resources/append-reference.txt b/ext/base/src/test/resources/append-reference.txt deleted file mode 100644 index 90aad4d76..000000000 --- a/ext/base/src/test/resources/append-reference.txt +++ /dev/null @@ -1,6 +0,0 @@ -hello -world -how -is -it -going diff --git a/ext/base/src/test/resources/utf16-crlf.txt b/ext/base/src/test/resources/utf16-crlf.txt deleted file mode 100644 index d5b3f2207..000000000 Binary files a/ext/base/src/test/resources/utf16-crlf.txt and /dev/null differ diff --git a/ext/base/src/test/resources/utf8-bom-lf.txt b/ext/base/src/test/resources/utf8-bom-lf.txt deleted file mode 100644 index 5761cea7b..000000000 --- a/ext/base/src/test/resources/utf8-bom-lf.txt +++ /dev/null @@ -1,2 +0,0 @@ -hello -world \ No newline at end of file diff --git a/ext/base/src/test/resources/write-reference.txt b/ext/base/src/test/resources/write-reference.txt deleted file mode 100644 index a9c51463f..000000000 Binary files a/ext/base/src/test/resources/write-reference.txt and /dev/null differ diff --git a/ext/jdbc/build.gradle b/ext/jdbc/build.gradle index ff4aadf52..3e834dfae 100644 --- a/ext/jdbc/build.gradle +++ b/ext/jdbc/build.gradle @@ -1,5 +1,4 @@ plugins { id 'java' -id 'org.moditect.gradleplugin' version '1.0.0-rc3' } apply from: "$rootDir/gradle/gradle_scripts/java.gradle" apply from: "$rootDir/gradle/gradle_scripts/extension.gradle" diff --git a/ext/proc/build.gradle b/ext/proc/build.gradle index ff4aadf52..3e834dfae 100644 --- a/ext/proc/build.gradle +++ b/ext/proc/build.gradle @@ -1,5 +1,4 @@ plugins { id 'java' -id 'org.moditect.gradleplugin' version '1.0.0-rc3' } apply from: "$rootDir/gradle/gradle_scripts/java.gradle" apply from: "$rootDir/gradle/gradle_scripts/extension.gradle" diff --git a/ext/uacc/build.gradle b/ext/uacc/build.gradle index ff4aadf52..3e834dfae 100644 --- a/ext/uacc/build.gradle +++ b/ext/uacc/build.gradle @@ -1,5 +1,4 @@ plugins { id 'java' -id 'org.moditect.gradleplugin' version '1.0.0-rc3' } apply from: "$rootDir/gradle/gradle_scripts/java.gradle" apply from: "$rootDir/gradle/gradle_scripts/extension.gradle" diff --git a/get-xpipe.sh b/get-xpipe.sh index c25a8c3d8..9a7ad4f18 100644 --- a/get-xpipe.sh +++ b/get-xpipe.sh @@ -104,18 +104,18 @@ install() { if [ -f "/etc/debian_version" ]; then info "Installing file $file with apt" sudo apt update - DEBIAN_FRONTEND=noninteractive sudo apt install -qy "$file" + DEBIAN_FRONTEND=noninteractive sudo apt install "$file" elif [ -x "$(command -v zypper)" ]; then info "Installing file $file with zypper" sudo zypper refresh - sudo zypper install --allow-unsigned-rpm -y "$file" + sudo zypper install --allow-unsigned-rpm "$file" elif [ -x "$(command -v dnf)" ]; then info "Installing file $file with dnf" - sudo dnf install -y --refresh "$file" + sudo dnf install --refresh "$file" elif [ -x "$(command -v yum)" ]; then info "Installing file $file with yum" sudo yum clean expire-cache - sudo yum install -y "$file" + sudo yum install "$file" else info "Installing file $file with rpm" sudo rpm -U -v --force "$file" diff --git a/gradle/gradle_scripts/README.md b/gradle/gradle_scripts/README.md index 0b7e133ac..19032f615 100644 --- a/gradle/gradle_scripts/README.md +++ b/gradle/gradle_scripts/README.md @@ -6,7 +6,7 @@ It also contains various other types of shared build script components that are As the [jlink](https://docs.oracle.com/en/java/javase/17/docs/specs/man/jlink.html) tool effectively requires proper modules as inputs but many established java libraries did not add proper support yet, using an approach like this is required. -The modules are generated with the help of [moditect](https://github.com/moditect/moditect-gradle-plugin). +The modules are generated with the help of [extra-java-module-info](https://github.com/gradlex-org/extra-java-module-info). The generated `module-info.java` file contains the necessary declarations to make a library work. While gradle already has a [similar system](https://docs.gradle.org/current/userguide/platforms.html) to better share dependencies, this system is lacking several features. diff --git a/gradle/gradle_scripts/commons.gradle b/gradle/gradle_scripts/commons.gradle deleted file mode 100644 index bb6cd05c0..000000000 --- a/gradle/gradle_scripts/commons.gradle +++ /dev/null @@ -1,43 +0,0 @@ -configurations { - dep -} - -dependencies { - dep files("${project.layout.buildDirectory.get()}/generated-modules/commons-lang3-3.12.0.jar") - dep files("${project.layout.buildDirectory.get()}/generated-modules/commons-io-2.11.0.jar") -} - -addDependenciesModuleInfo { - overwriteExistingFiles = true - jdepsExtraArgs = ['-q'] - outputDirectory = file("${project.layout.buildDirectory.get()}/generated-modules") - modules { - module { - artifact 'org.apache.commons:commons-lang3:3.12.0' - moduleInfoSource = ''' - module org.apache.commons.lang3 { - exports org.apache.commons.lang3; - exports org.apache.commons.lang3.function; - exports org.apache.commons.lang3.arch; - exports org.apache.commons.lang3.reflect; - exports org.apache.commons.lang3.builder; - exports org.apache.commons.lang3.text; - exports org.apache.commons.lang3.tuple; - exports org.apache.commons.lang3.math; - } - ''' - } - module { - artifact 'commons-io:commons-io:2.11.0' - moduleInfoSource = ''' - module org.apache.commons.io { - exports org.apache.commons.io; - exports org.apache.commons.io.file; - exports org.apache.commons.io.input; - exports org.apache.commons.io.filefilter; - exports org.apache.commons.io.output; - } - ''' - } - } -} diff --git a/gradle/gradle_scripts/extension.gradle b/gradle/gradle_scripts/extension.gradle index 706c79db8..d172e5dcf 100644 --- a/gradle/gradle_scripts/extension.gradle +++ b/gradle/gradle_scripts/extension.gradle @@ -1,14 +1,14 @@ -task copyRuntimeLibs(type: Copy) { - into project.jar.destinationDirectory - from configurations.runtimeClasspath - exclude "${project.name}.jar" - duplicatesStrategy(DuplicatesStrategy.EXCLUDE) -} -copyRuntimeLibs.dependsOn(addDependenciesModuleInfo) -jar.dependsOn(copyRuntimeLibs) +//task copyRuntimeLibs(type: Copy) { +// into project.jar.destinationDirectory +// from configurations.runtimeClasspath +// exclude "${project.name}.jar" +// duplicatesStrategy(DuplicatesStrategy.EXCLUDE) +//} +// Do we need this? +// jar.dependsOn(copyRuntimeLibs) def dev = tasks.register('createDevOutput', Copy) { - mustRunAfter copyRuntimeLibs, jar + mustRunAfter jar if (project.allExtensions.contains(project)) { var source = "${project.jar.destinationDirectory.get()}" @@ -19,7 +19,7 @@ def dev = tasks.register('createDevOutput', Copy) { jar.finalizedBy(dev) tasks.register('createExtOutput', Copy) { - mustRunAfter copyRuntimeLibs, jar + mustRunAfter jar if (!file("${project.jar.destinationDirectory.get()}_prod").exists()) { copy { @@ -35,13 +35,33 @@ tasks.register('createExtOutput', Copy) { into "${project.jar.destinationDirectory.get()}_ext" } +project.tasks.withType(org.gradle.jvm.tasks.Jar).configureEach { + if (it.name != 'jar') { + it.destinationDirectory = project.layout.buildDirectory.dir('libs_test') + } +} + apply from: "$rootDir/gradle/gradle_scripts/java.gradle" apply from: "$rootDir/gradle/gradle_scripts/javafx.gradle" apply from: "$rootDir/gradle/gradle_scripts/lombok.gradle" -apply from: "$rootDir/gradle/gradle_scripts/extension_test.gradle" +apply from: "$rootDir/gradle/gradle_scripts/junit_suite.gradle" + +localTest { + dependencies { + if (project.name != 'base') { + implementation project(':base') + } + + testImplementation project(":$project.name") + } +} + +configurations { + compileOnly.extendsFrom(javafx) +} dependencies { - compileOnly group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.15.2" + compileOnly group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.16.1" compileOnly project(':core') compileOnly project(':beacon') compileOnly project(':app') @@ -50,12 +70,14 @@ dependencies { exclude group: 'org.openjfx', module: 'javafx-base' exclude group: 'org.openjfx', module: 'javafx-controls' } + compileOnly 'commons-io:commons-io:2.15.1' + compileOnly group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0" if (project != project(':base')) { compileOnly project(':base') - testImplementation project(':base') } - - testImplementation project(':app') } +// To fix https://github.com/gradlex-org/extra-java-module-info/issues/101#issuecomment-1934761334 +configurations.javaModulesMergeJars.shouldResolveConsistentlyWith(configurations.compileClasspath) + diff --git a/gradle/gradle_scripts/extension_test.gradle b/gradle/gradle_scripts/extension_test.gradle deleted file mode 100644 index a69e936c1..000000000 --- a/gradle/gradle_scripts/extension_test.gradle +++ /dev/null @@ -1,70 +0,0 @@ -import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform - -apply from: "$buildscript.sourceFile/../junit.gradle" - -dependencies { - testImplementation project(':api') - testImplementation project(':core') - testImplementation project(':app') - - testImplementation "org.openjfx:javafx-base:${javafxVersion}:win" - testImplementation "org.openjfx:javafx-controls:${javafxVersion}:win" - testImplementation "org.openjfx:javafx-graphics:${javafxVersion}:win" -} - -def attachDebugger = System.getProperty('idea.debugger.dispatch.addr') != null -def daemonCommand = attachDebugger ? ':app:runAttachedDebugger' : ':app:run' - -test { - workingDir = rootDir - - jvmArgs += ["-Xmx2g"] - - // Daemon properties - systemProperty "io.xpipe.beacon.daemonArgs", - " -Dio.xpipe.beacon.port=21725" + - " -Dio.xpipe.app.mode=tray" + - " -Dio.xpipe.app.dataDir=$projectDir/local/" + - " -Dio.xpipe.storage.persist=false" + - " -Dio.xpipe.app.writeSysOut=true" + - " -Dio.xpipe.app.writeLogs=false" + - " -Dio.xpipe.beacon.printMessages=false" + - " -Dio.xpipe.app.logLevel=trace" - - // Use cmd window for tests - if (!rootProject.ci && DefaultNativePlatform.currentOperatingSystem.isWindows()) { - systemProperty "io.xpipe.beacon.customDaemonCommand", - "cmd.exe /c start \"\"XPipe Debug\"\" /i \"$rootDir\\gradlew.bat\" --console=plain $daemonCommand" - } - - - // Client properties - // systemProperty 'io.xpipe.beacon.printMessages', "true" - systemProperty 'io.xpipe.beacon.printDaemonOutput', "false" - systemProperty "io.xpipe.beacon.port", "21725" - systemProperty "io.xpipe.beacon.launchDebugDaemon", "true" - systemProperty "io.xpipe.beacon.attachDebuggerToDaemon", "$daemonCommand" -} - -task productionTest(type: Test) { - classpath = sourceSets.test.runtimeClasspath - useJUnitPlatform() - workingDir = rootDir - - jvmArgs += ["-Xmx2g"] - - // Daemon properties - systemProperty "io.xpipe.beacon.daemonArgs", - " -Dio.xpipe.beacon.port=21725" + - " -Dio.xpipe.app.dataDir=$projectDir/local/" + - " -Dio.xpipe.storage.persist=false" + - " -Dio.xpipe.app.writeSysOut=true" + - " -Dio.xpipe.app.writeLogs=false" + - " -Dio.xpipe.beacon.printMessages=false" + - " -Dio.xpipe.app.logLevel=trace" - - // Client properties - systemProperty 'io.xpipe.beacon.printMessages', "true" - systemProperty 'io.xpipe.beacon.printDaemonOutput', "true" - systemProperty "io.xpipe.beacon.port", "21725" -} \ No newline at end of file diff --git a/gradle/gradle_scripts/jSystemThemeDetector-3.8.jar b/gradle/gradle_scripts/jSystemThemeDetector-3.8.jar deleted file mode 100644 index a8ac11f54..000000000 Binary files a/gradle/gradle_scripts/jSystemThemeDetector-3.8.jar and /dev/null differ diff --git a/gradle/gradle_scripts/javafx.gradle b/gradle/gradle_scripts/javafx.gradle index d302d9e9f..47bc81aee 100644 --- a/gradle/gradle_scripts/javafx.gradle +++ b/gradle/gradle_scripts/javafx.gradle @@ -10,19 +10,19 @@ if (currentOS.isWindows()) { platform = 'mac' } -def arch = System.getProperty ("os.arch"); -if (arch == 'aarch64') { +if (System.getProperty ("os.arch") == 'aarch64') { platform += '-aarch64' } configurations { - dep + javafx } +// Always use maven version for now dependencies { - dep "org.openjfx:javafx-base:${javafxVersion}:${platform}" - dep "org.openjfx:javafx-controls:${javafxVersion}:${platform}" - dep "org.openjfx:javafx-graphics:${javafxVersion}:${platform}" - dep "org.openjfx:javafx-media:${javafxVersion}:${platform}" - dep "org.openjfx:javafx-web:${javafxVersion}:${platform}" + javafx "org.openjfx:javafx-base:${javafxVersion}:${platform}" + javafx "org.openjfx:javafx-controls:${javafxVersion}:${platform}" + javafx "org.openjfx:javafx-graphics:${javafxVersion}:${platform}" + javafx "org.openjfx:javafx-media:${javafxVersion}:${platform}" + javafx "org.openjfx:javafx-web:${javafxVersion}:${platform}" } diff --git a/gradle/gradle_scripts/junit.gradle b/gradle/gradle_scripts/junit.gradle index 608132b3e..0d96ea7ae 100644 --- a/gradle/gradle_scripts/junit.gradle +++ b/gradle/gradle_scripts/junit.gradle @@ -2,19 +2,22 @@ import org.gradle.api.tasks.testing.logging.TestLogEvent dependencies { testImplementation 'org.hamcrest:hamcrest:2.2' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.3' - testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.3' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.3' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.2' testRuntimeOnly "org.junit.platform:junit-platform-launcher" } tasks.withType(Test) { + jvmArgs += ["-Xmx2g"] useJUnitPlatform() testLogging { events TestLogEvent.FAILED, TestLogEvent.PASSED, TestLogEvent.SKIPPED, - TestLogEvent.STANDARD_OUT + TestLogEvent.STANDARD_OUT, + TestLogEvent.STANDARD_ERROR, + TestLogEvent.STARTED exceptionFormat = 'full' showExceptions = true @@ -22,6 +25,8 @@ tasks.withType(Test) { showStandardStreams = true } + outputs.upToDateWhen {false} + afterSuite { desc, result -> if (!desc.parent) { // will match the outermost suite def output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)" @@ -31,15 +36,3 @@ tasks.withType(Test) { } } } - -sourceSets { - test { - // With this, the project at least compiles in eclipse (although with disabled tests) -// if (System.getProperty('idea.active') == null) { -// java { -// srcDirs = [] -// } -// } - output.resourcesDir("${project.layout.buildDirectory.get()}/classes/java/test") - } -} \ No newline at end of file diff --git a/gradle/gradle_scripts/junit_suite.gradle b/gradle/gradle_scripts/junit_suite.gradle new file mode 100644 index 000000000..52166b93b --- /dev/null +++ b/gradle/gradle_scripts/junit_suite.gradle @@ -0,0 +1,79 @@ +apply from: "$rootDir/gradle/gradle_scripts/junit.gradle" + +testing { + suites { + localTest(JvmTestSuite) { + useJUnitJupiter() + + dependencies { + implementation project(':core') + implementation project(':beacon') + implementation project(':app') + implementation project(':base') + implementation project() + } + + targets { + all { + testTask.configure { + workingDir = rootDir + + jvmArgs += ["-Xmx2g"] + jvmArgs += jvmRunArgs + + systemProperty 'io.xpipe.beacon.printDaemonOutput', "false" + systemProperty 'io.xpipe.app.useVirtualThreads', "false" + systemProperty "io.xpipe.beacon.port", "21725" + systemProperty "io.xpipe.beacon.launchDebugDaemon", "true" + systemProperty "io.xpipe.app.dataDir", "$projectDir/local/" + systemProperty "io.xpipe.app.logLevel", "trace" + systemProperty "io.xpipe.app.writeSysOut", "true" + systemProperty "io.xpipe.app.mode", "tray" + } + } + } + } + + remoteTest(JvmTestSuite) { + useJUnitJupiter() + + dependencies { + implementation project(':core') + implementation project(':beacon') + implementation project(':app') + implementation project() + } + + targets { + all { + testTask.configure { + workingDir = projectDir + + jvmArgs += ["-Xmx2g"] + jvmArgs += jvmRunArgs + + def attachDebugger = System.getProperty('idea.debugger.dispatch.addr') != null + def daemonCommand = attachDebugger ? ':app:runAttachedDebugger' : ':app:run' + if (org.gradle.internal.os.OperatingSystem.current().isWindows()) { + systemProperty "io.xpipe.beacon.customDaemonCommand", "\"$rootDir\\gradlew.bat\" --console=plain $daemonCommand" + } else { + systemProperty "io.xpipe.beacon.customDaemonCommand", "\"$rootDir/gradlew\" --console=plain $daemonCommand" + } + systemProperty "io.xpipe.beacon.daemonArgs", + " -Dio.xpipe.beacon.port=21725" + + " -Dio.xpipe.app.dataDir=$projectDir/local/" + + " -Dio.xpipe.storage.persist=false" + + " -Dio.xpipe.app.writeSysOut=true" + + " -Dio.xpipe.app.writeLogs=false" + + " -Dio.xpipe.beacon.printMessages=true" + + " -Dio.xpipe.app.logLevel=trace" + + systemProperty 'io.xpipe.beacon.printDaemonOutput', "true" + systemProperty "io.xpipe.beacon.port", "21725" + systemProperty "io.xpipe.beacon.launchDebugDaemon", "true" + } + } + } + } + } +} diff --git a/gradle/gradle_scripts/lombok.gradle b/gradle/gradle_scripts/lombok.gradle index cc8da78ba..f132bbf0b 100644 --- a/gradle/gradle_scripts/lombok.gradle +++ b/gradle/gradle_scripts/lombok.gradle @@ -3,4 +3,15 @@ dependencies { annotationProcessor 'org.projectlombok:lombok:1.18.30' testCompileOnly 'org.projectlombok:lombok:1.18.30' testAnnotationProcessor 'org.projectlombok:lombok:1.18.30' -} \ No newline at end of file +} + +testing { + suites { + configureEach { + dependencies { + compileOnly 'org.projectlombok:lombok:1.18.30' + annotationProcessor 'org.projectlombok:lombok:1.18.30' + } + } + } +} diff --git a/gradle/gradle_scripts/markdowngenerator.gradle b/gradle/gradle_scripts/markdowngenerator.gradle deleted file mode 100644 index 55d959b18..000000000 --- a/gradle/gradle_scripts/markdowngenerator.gradle +++ /dev/null @@ -1,31 +0,0 @@ -dependencies { - implementation files("${project.layout.buildDirectory.get()}/generated-modules/markdowngenerator-1.3.1.1.jar") -} - -addDependenciesModuleInfo { - overwriteExistingFiles = true - jdepsExtraArgs = ['-q'] - outputDirectory = file("${project.layout.buildDirectory.get()}/generated-modules") - modules { - module { - artifact (name: "markdowngenerator-1.3.1.1") - moduleInfoSource = ''' - module net.steppschuh.markdowngenerator { - exports net.steppschuh.markdowngenerator; - exports net.steppschuh.markdowngenerator.image; - exports net.steppschuh.markdowngenerator.link; - exports net.steppschuh.markdowngenerator.list; - exports net.steppschuh.markdowngenerator.progress; - exports net.steppschuh.markdowngenerator.rule; - exports net.steppschuh.markdowngenerator.table; - exports net.steppschuh.markdowngenerator.text; - exports net.steppschuh.markdowngenerator.text.code; - exports net.steppschuh.markdowngenerator.text.emphasis; - exports net.steppschuh.markdowngenerator.text.heading; - exports net.steppschuh.markdowngenerator.text.quote; - exports net.steppschuh.markdowngenerator.util; - } - ''' - } - } -} diff --git a/gradle/gradle_scripts/picocli.gradle b/gradle/gradle_scripts/picocli.gradle deleted file mode 100644 index 8f44ef343..000000000 --- a/gradle/gradle_scripts/picocli.gradle +++ /dev/null @@ -1,10 +0,0 @@ -compileJava { - options.compilerArgs += ["-Aproject=${project.name}"] -} - -dependencies { - implementation 'info.picocli:picocli:4.7.0' - annotationProcessor 'info.picocli:picocli-codegen:4.7.0' - testImplementation 'info.picocli:picocli:4.7.0' - testAnnotationProcessor 'info.picocli:picocli-codegen:4.7.0' -} \ No newline at end of file diff --git a/gradle/gradle_scripts/preferencesfx-core-11.15.0.jar b/gradle/gradle_scripts/preferencesfx-core-11.15.0.jar deleted file mode 100644 index 636a1c77f..000000000 Binary files a/gradle/gradle_scripts/preferencesfx-core-11.15.0.jar and /dev/null differ diff --git a/gradle/gradle_scripts/prettytime.gradle b/gradle/gradle_scripts/prettytime.gradle deleted file mode 100644 index 0de99c6a9..000000000 --- a/gradle/gradle_scripts/prettytime.gradle +++ /dev/null @@ -1,22 +0,0 @@ -dependencies { - implementation files("${project.layout.buildDirectory.get()}/generated-modules/prettytime-5.0.2.Final.jar") -} - -addDependenciesModuleInfo { - overwriteExistingFiles = true - jdepsExtraArgs = ['-q'] - outputDirectory = file("${project.layout.buildDirectory.get()}/generated-modules") - modules { - module { - artifact 'org.ocpsoft.prettytime:prettytime:5.0.2.Final' - moduleInfoSource = ''' - module org.ocpsoft.prettytime { - exports org.ocpsoft.prettytime; - exports org.ocpsoft.prettytime.format; - exports org.ocpsoft.prettytime.i18n; - exports org.ocpsoft.prettytime.units; - } - ''' - } - } -} diff --git a/gradle/gradle_scripts/versioncompare.gradle b/gradle/gradle_scripts/versioncompare.gradle deleted file mode 100644 index 1241ecaa8..000000000 --- a/gradle/gradle_scripts/versioncompare.gradle +++ /dev/null @@ -1,19 +0,0 @@ -dependencies { - implementation files("${project.layout.buildDirectory.get()}/generated-modules/versioncompare-1.5.0.jar") -} - -addDependenciesModuleInfo { - overwriteExistingFiles = true - jdepsExtraArgs = ['-q'] - outputDirectory = file("${project.layout.buildDirectory.get()}/generated-modules") - modules { - module { - artifact "io.github.g00fy2:versioncompare:1.5.0" - moduleInfoSource = ''' - module versioncompare { - exports io.github.g00fy2.versioncompare; - } - ''' - } - } -} diff --git a/modules.gradle b/modules.gradle new file mode 100644 index 000000000..41a253fba --- /dev/null +++ b/modules.gradle @@ -0,0 +1,68 @@ +extraJavaModuleInfo { + module("markdowngenerator-1.3.1.1.jar", "net.steppschuh.markdowngenerator") { + exportAllPackages() + } +} + +extraJavaModuleInfo { + module("de.vandermeer:asciitable", "de.vandermeer.asciitable") { + exportAllPackages() + requires('de.vandermeer.skb_interfaces') + requires('de.vandermeer.ascii_utf_themes') + requires('org.apache.commons.lang3') + } + + module("de.vandermeer:skb-interfaces", "de.vandermeer.skb_interfaces") { + exportAllPackages() + requires('org.apache.commons.lang3') + } + + module("de.vandermeer:ascii-utf-themes", "de.vandermeer.ascii_utf_themes") { + exportAllPackages() + requires('org.apache.commons.lang3') + requires('de.vandermeer.skb_interfaces') + } +} + +extraJavaModuleInfo { + module("org.ocpsoft.prettytime:prettytime", "org.ocpsoft.prettytime") { + exportAllPackages() + } +} + +extraJavaModuleInfo { + module("com.vladsch.flexmark:flexmark", "com.vladsch.flexmark") { + mergeJar('com.vladsch.flexmark:flexmark-util-data') + mergeJar('com.vladsch.flexmark:flexmark-util-format') + mergeJar('com.vladsch.flexmark:flexmark-util-ast') + mergeJar('com.vladsch.flexmark:flexmark-util-sequence') + mergeJar('com.vladsch.flexmark:flexmark-util-builder') + mergeJar('com.vladsch.flexmark:flexmark-util-html') + mergeJar('com.vladsch.flexmark:flexmark-util-dependency') + mergeJar('com.vladsch.flexmark:flexmark-util-collection') + mergeJar('com.vladsch.flexmark:flexmark-util-misc') + mergeJar('com.vladsch.flexmark:flexmark-util-visitor') + exportAllPackages() + } +} + +extraJavaModuleInfo { + module("org.kohsuke:github-api", "org.kohsuke.github") { + exports('org.kohsuke.github') + exports('org.kohsuke.github.function') + exports('org.kohsuke.github.authorization') + exports('org.kohsuke.github.extras') + exports('org.kohsuke.github.connector') + requires('java.logging') + requires('org.apache.commons.io') + requires('org.apache.commons.lang3') + requires('com.fasterxml.jackson.databind') + overrideModuleName() + } +} + +extraJavaModuleInfo { + module("io.sentry:sentry", "io.sentry") { + exportAllPackages() + } +} diff --git a/version b/version index 33e7e629e..cc40bca69 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.7.16 \ No newline at end of file +8.0